leanos/src/kernel/sdk.ts
Thomas Lutz Kolter ee203fd9fb leanOS: initial import (basiert auf NEON NOODLE OS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 14:58:54 +02:00

93 lines
2.9 KiB
TypeScript

import type { OSApi, Size } from "./types";
import { fs } from "./fs";
import { db } from "./db";
import { bus } from "./bus";
import { chat, chatStream, aiAvailable } from "./ai";
import { useOS } from "./store";
import { tokensFor, tokensAsCss } from "../design/tokens";
// Builds the sandboxed OS API handed to a single app window.
// Apps may ONLY reach the outside world through this object.
export function makeOSApi(appId: string, windowId: string): OSApi {
const store = () => useOS.getState();
return {
fs: {
read: (p) => fs.read(p),
write: (p, c) => fs.write(p, c),
list: (p) => fs.list(p),
delete: (p) => fs.delete(p),
mkdir: (p) => fs.mkdir(p),
exists: (p) => fs.exists(p),
},
window: {
setTitle: (title) => store().setWindowTitle(windowId, title),
close: () => store().closeWindow(windowId),
setSize: (size: Size) => store().resizeWindow(windowId, size),
minimize: () => store().minimizeWindow(windowId),
},
ai: {
// Getter, not a snapshot: the API key can be configured in Settings after
// an app window is already open, and the app must see the live value.
get available() {
return aiAvailable();
},
ask: (prompt, opts) => chat([{ role: "user", content: prompt }], { system: opts?.system }),
stream: (prompt, onToken, opts) =>
chatStream([{ role: "user", content: prompt }], onToken, { system: opts?.system }),
},
notify: (title, body, icon) => store().notify({ title, body, icon, appId }),
net: {
proxyUrl: (url: string) => {
const u = (url || "").trim();
if (!u) return u;
// In dev we have the /__proxy middleware; in a prod build there is none,
// so fall back to the raw URL (which the target may still block).
if (import.meta.env.DEV && /^https?:\/\//i.test(u)) {
return `/__proxy?url=${encodeURIComponent(u)}`;
}
return u;
},
},
storage: {
async get<T = unknown>(key: string): Promise<T | null> {
const d = await db();
const row = await d.get("appstore", `${appId}:${key}`);
return row ? (row.value as T) : null;
},
async set(key, value) {
const d = await db();
await d.put("appstore", { k: `${appId}:${key}`, appId, value });
},
async remove(key) {
const d = await db();
await d.delete("appstore", `${appId}:${key}`);
},
async keys() {
const d = await db();
const rows = await d.getAllFromIndex("appstore", "byApp", appId);
return rows.map((r) => r.k.slice(appId.length + 1));
},
},
theme: {
get tokens() {
return tokensAsCss(tokensFor(store().themeMode));
},
get mode() {
return store().themeMode;
},
},
bus: {
emit: (channel, payload) => bus.emit(`app:${channel}`, payload),
on: (channel, handler) => bus.on(`app:${channel}`, handler),
},
};
}