@immediately-run/sdk 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fs.d.cts ADDED
@@ -0,0 +1,109 @@
1
+ import { SandboxMount } from './mounts.cjs';
2
+ import './tasks.cjs';
3
+
4
+ /** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */
5
+ interface NodeFsPromises {
6
+ readFile(path: string, encoding?: any): Promise<string | Uint8Array>;
7
+ writeFile(path: string, data: string | Uint8Array): Promise<void>;
8
+ readdir(path: string, opts?: any): Promise<any[]>;
9
+ stat(path: string): Promise<any>;
10
+ mkdir(path: string, opts?: any): Promise<unknown>;
11
+ rm(path: string, opts?: any): Promise<void>;
12
+ rename(from: string, to: string): Promise<void>;
13
+ }
14
+ /** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —
15
+ * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */
16
+ interface SandboxFsPort {
17
+ promises?: NodeFsPromises;
18
+ readFile?: NodeFsPromises['readFile'];
19
+ }
20
+ /**
21
+ * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the
22
+ * resolution order previously duplicated in every app's `mountFs.ts`:
23
+ *
24
+ * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.
25
+ * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`
26
+ * whose surface has `readFile` (the bundler ZenFS-layer bound context).
27
+ * 3. else `null` (local `vite dev` / before boot).
28
+ *
29
+ * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute
30
+ * `/mnt/{hash}` paths (the file explorer / editor).
31
+ */
32
+ declare function sandboxFs(): SandboxFsPort | null;
33
+ /** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before
34
+ * boot — gate file affordances on it so an app degrades instead of throwing. */
35
+ declare function fsAvailable(): boolean;
36
+ /** A directory entry from {@link MountFs.readdir}. */
37
+ interface DirEntry {
38
+ name: string;
39
+ kind: 'file' | 'dir';
40
+ }
41
+ /** A stat result from {@link MountFs.stat}. */
42
+ interface FileStat {
43
+ kind: 'file' | 'dir';
44
+ size: number;
45
+ mtimeMs?: number;
46
+ }
47
+ /** An error from a {@link MountFs} operation, carrying a machine-readable `.code`
48
+ * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */
49
+ interface FsError extends Error {
50
+ code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
51
+ }
52
+ /** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
53
+ * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
54
+ * Obtain one with {@link openFs}. */
55
+ interface MountFs {
56
+ /** The mount this view is anchored to (read `mode`/`rules` for writability). */
57
+ readonly mount: SandboxMount;
58
+ /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
59
+ readFile(relPath: string, encoding: 'utf8'): Promise<string>;
60
+ readFile(relPath: string): Promise<Uint8Array>;
61
+ /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
62
+ writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
63
+ /** List a directory (the mount root when `relPath` is omitted). */
64
+ readdir(relPath?: string): Promise<DirEntry[]>;
65
+ /** Stat a path. Throws `not-found` if absent. */
66
+ stat(relPath: string): Promise<FileStat>;
67
+ /** Does `relPath` exist? Never throws on absence. */
68
+ exists(relPath: string): Promise<boolean>;
69
+ /** Create a directory (pass `{ recursive: true }` to make parents). */
70
+ mkdir(relPath: string, opts?: {
71
+ recursive?: boolean;
72
+ }): Promise<void>;
73
+ /** Remove a file, or a directory with `{ recursive: true }`. */
74
+ rm(relPath: string, opts?: {
75
+ recursive?: boolean;
76
+ }): Promise<void>;
77
+ /** Rename/move within the mount. */
78
+ rename(fromRel: string, toRel: string): Promise<void>;
79
+ /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),
80
+ * so an app can hide an "edit" affordance instead of catching `read-only`
81
+ * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
82
+ * flips it. EROFS from the host stays authoritative. */
83
+ canWrite(relPath?: string): boolean;
84
+ }
85
+ /**
86
+ * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
87
+ * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you
88
+ * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path
89
+ * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the
90
+ * real enforcer).
91
+ *
92
+ * ```ts
93
+ * import { mountSpace } from '@immediately-run/sdk';
94
+ * import { openFs } from '@immediately-run/sdk/fs';
95
+ * const fs = openFs(await mountSpace({ spaceId }));
96
+ * const text = await fs.readFile('notes/idea.mdx', 'utf8');
97
+ * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);
98
+ * ```
99
+ *
100
+ * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`
101
+ * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}
102
+ * with a mapped `.code` (`not-found`, `read-only`, …).
103
+ */
104
+ declare function openFs(mount: SandboxMount): MountFs;
105
+ /** Open a mount-anchored view of this app's OWN repository working tree — a convenience
106
+ * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
107
+ declare function openAppFs(): MountFs;
108
+
109
+ export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
package/dist/fs.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { SandboxMount } from './mounts.js';
2
+ import './tasks.js';
3
+
4
+ /** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */
5
+ interface NodeFsPromises {
6
+ readFile(path: string, encoding?: any): Promise<string | Uint8Array>;
7
+ writeFile(path: string, data: string | Uint8Array): Promise<void>;
8
+ readdir(path: string, opts?: any): Promise<any[]>;
9
+ stat(path: string): Promise<any>;
10
+ mkdir(path: string, opts?: any): Promise<unknown>;
11
+ rm(path: string, opts?: any): Promise<void>;
12
+ rename(from: string, to: string): Promise<void>;
13
+ }
14
+ /** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —
15
+ * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */
16
+ interface SandboxFsPort {
17
+ promises?: NodeFsPromises;
18
+ readFile?: NodeFsPromises['readFile'];
19
+ }
20
+ /**
21
+ * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the
22
+ * resolution order previously duplicated in every app's `mountFs.ts`:
23
+ *
24
+ * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.
25
+ * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`
26
+ * whose surface has `readFile` (the bundler ZenFS-layer bound context).
27
+ * 3. else `null` (local `vite dev` / before boot).
28
+ *
29
+ * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute
30
+ * `/mnt/{hash}` paths (the file explorer / editor).
31
+ */
32
+ declare function sandboxFs(): SandboxFsPort | null;
33
+ /** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before
34
+ * boot — gate file affordances on it so an app degrades instead of throwing. */
35
+ declare function fsAvailable(): boolean;
36
+ /** A directory entry from {@link MountFs.readdir}. */
37
+ interface DirEntry {
38
+ name: string;
39
+ kind: 'file' | 'dir';
40
+ }
41
+ /** A stat result from {@link MountFs.stat}. */
42
+ interface FileStat {
43
+ kind: 'file' | 'dir';
44
+ size: number;
45
+ mtimeMs?: number;
46
+ }
47
+ /** An error from a {@link MountFs} operation, carrying a machine-readable `.code`
48
+ * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */
49
+ interface FsError extends Error {
50
+ code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
51
+ }
52
+ /** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
53
+ * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
54
+ * Obtain one with {@link openFs}. */
55
+ interface MountFs {
56
+ /** The mount this view is anchored to (read `mode`/`rules` for writability). */
57
+ readonly mount: SandboxMount;
58
+ /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
59
+ readFile(relPath: string, encoding: 'utf8'): Promise<string>;
60
+ readFile(relPath: string): Promise<Uint8Array>;
61
+ /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
62
+ writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
63
+ /** List a directory (the mount root when `relPath` is omitted). */
64
+ readdir(relPath?: string): Promise<DirEntry[]>;
65
+ /** Stat a path. Throws `not-found` if absent. */
66
+ stat(relPath: string): Promise<FileStat>;
67
+ /** Does `relPath` exist? Never throws on absence. */
68
+ exists(relPath: string): Promise<boolean>;
69
+ /** Create a directory (pass `{ recursive: true }` to make parents). */
70
+ mkdir(relPath: string, opts?: {
71
+ recursive?: boolean;
72
+ }): Promise<void>;
73
+ /** Remove a file, or a directory with `{ recursive: true }`. */
74
+ rm(relPath: string, opts?: {
75
+ recursive?: boolean;
76
+ }): Promise<void>;
77
+ /** Rename/move within the mount. */
78
+ rename(fromRel: string, toRel: string): Promise<void>;
79
+ /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),
80
+ * so an app can hide an "edit" affordance instead of catching `read-only`
81
+ * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
82
+ * flips it. EROFS from the host stays authoritative. */
83
+ canWrite(relPath?: string): boolean;
84
+ }
85
+ /**
86
+ * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
87
+ * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you
88
+ * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path
89
+ * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the
90
+ * real enforcer).
91
+ *
92
+ * ```ts
93
+ * import { mountSpace } from '@immediately-run/sdk';
94
+ * import { openFs } from '@immediately-run/sdk/fs';
95
+ * const fs = openFs(await mountSpace({ spaceId }));
96
+ * const text = await fs.readFile('notes/idea.mdx', 'utf8');
97
+ * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);
98
+ * ```
99
+ *
100
+ * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`
101
+ * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}
102
+ * with a mapped `.code` (`not-found`, `read-only`, …).
103
+ */
104
+ declare function openFs(mount: SandboxMount): MountFs;
105
+ /** Open a mount-anchored view of this app's OWN repository working tree — a convenience
106
+ * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
107
+ declare function openAppFs(): MountFs;
108
+
109
+ export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
package/dist/fs.js ADDED
@@ -0,0 +1,187 @@
1
+ import { getAppMountPath } from "./mounts";
2
+ const hasFs = (fs) => typeof fs?.promises?.readFile === "function" || typeof fs?.readFile === "function";
3
+ function sandboxFs() {
4
+ try {
5
+ const shared = globalThis.__sandpackSharedFs;
6
+ if (hasFs(shared)) return shared;
7
+ } catch {
8
+ }
9
+ try {
10
+ const layers = module?.evaluation?.module?.bundler?.fs?.layers;
11
+ if (Array.isArray(layers)) {
12
+ for (const layer of layers) {
13
+ const fs = layer?.boundContext?.fs;
14
+ if (hasFs(fs)) return fs;
15
+ }
16
+ }
17
+ } catch {
18
+ }
19
+ return null;
20
+ }
21
+ function fsAvailable() {
22
+ return sandboxFs() != null;
23
+ }
24
+ const ERRNO = {
25
+ ENOENT: "not-found",
26
+ EROFS: "read-only",
27
+ EACCES: "not-permitted",
28
+ EPERM: "not-permitted",
29
+ EEXIST: "exists",
30
+ ENOTEMPTY: "not-empty"
31
+ };
32
+ const fsError = (code, message) => {
33
+ const err = new Error(message);
34
+ err.code = code;
35
+ return err;
36
+ };
37
+ const mapError = (e) => {
38
+ const errno = e?.code;
39
+ const code = (errno ? ERRNO[errno] : void 0) ?? "unknown";
40
+ const err = new Error(e?.message ?? "fs operation failed");
41
+ err.code = code;
42
+ return err;
43
+ };
44
+ const decoder = new TextDecoder();
45
+ const encoder = new TextEncoder();
46
+ const resolveUnder = (root, relPath) => {
47
+ if (relPath.startsWith("/")) {
48
+ throw fsError("invalid-path", `expected a mount-relative path, got absolute "${relPath}"`);
49
+ }
50
+ const parts = [];
51
+ for (const seg of relPath.split("/")) {
52
+ if (seg === "" || seg === ".") continue;
53
+ if (seg === "..") {
54
+ throw fsError("invalid-path", `"${relPath}" escapes the mount root`);
55
+ }
56
+ parts.push(seg);
57
+ }
58
+ const base = root.endsWith("/") ? root.slice(0, -1) : root;
59
+ return parts.length ? `${base}/${parts.join("/")}` : base;
60
+ };
61
+ const writableAt = (mount, relPath) => {
62
+ const path = "/" + relPath.split("/").filter((s) => s && s !== ".").join("/");
63
+ const rules = mount.rules;
64
+ if (rules && rules.length) {
65
+ let best;
66
+ for (const r of rules) {
67
+ const sub = r.subtree.endsWith("/") ? r.subtree : r.subtree + "/";
68
+ if (path === r.subtree || path.startsWith(sub) || r.subtree === "/") {
69
+ if (!best || r.subtree.length > best.subtree.length) best = r;
70
+ }
71
+ }
72
+ if (best) return best.mode === "rw";
73
+ }
74
+ return (mount.mode ?? "rw") === "rw";
75
+ };
76
+ const promisesOf = (port) => port.promises ?? port;
77
+ function openFs(mount) {
78
+ const root = mount.path;
79
+ const port = () => {
80
+ const p = sandboxFs();
81
+ if (!p) throw fsError("unavailable", "immediately.run: sandbox filesystem unavailable");
82
+ return promisesOf(p);
83
+ };
84
+ const api = {
85
+ mount,
86
+ async readFile(relPath, encoding) {
87
+ const p = port();
88
+ const abs = resolveUnder(root, relPath);
89
+ try {
90
+ const data = await p.readFile(abs);
91
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
92
+ return encoding === "utf8" ? decoder.decode(bytes) : bytes;
93
+ } catch (e) {
94
+ throw mapError(e);
95
+ }
96
+ },
97
+ async writeFile(relPath, data) {
98
+ const p = port();
99
+ const abs = resolveUnder(root, relPath);
100
+ try {
101
+ await p.writeFile(abs, typeof data === "string" ? encoder.encode(data) : data);
102
+ } catch (e) {
103
+ throw mapError(e);
104
+ }
105
+ },
106
+ async readdir(relPath = "") {
107
+ const p = port();
108
+ const abs = resolveUnder(root, relPath);
109
+ try {
110
+ const entries = await p.readdir(abs, { withFileTypes: true });
111
+ return entries.map(
112
+ (d) => typeof d === "string" ? { name: d, kind: "file" } : { name: d.name, kind: d.isDirectory?.() ? "dir" : "file" }
113
+ );
114
+ } catch (e) {
115
+ throw mapError(e);
116
+ }
117
+ },
118
+ async stat(relPath) {
119
+ const p = port();
120
+ const abs = resolveUnder(root, relPath);
121
+ try {
122
+ const s = await p.stat(abs);
123
+ return {
124
+ kind: s.isDirectory?.() ? "dir" : "file",
125
+ size: typeof s.size === "number" ? s.size : 0,
126
+ mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : void 0
127
+ };
128
+ } catch (e) {
129
+ throw mapError(e);
130
+ }
131
+ },
132
+ async exists(relPath) {
133
+ try {
134
+ await api.stat(relPath);
135
+ return true;
136
+ } catch (e) {
137
+ if (e.code === "not-found") return false;
138
+ if (e.code === "unavailable" || e.code === "invalid-path") {
139
+ throw e;
140
+ }
141
+ return false;
142
+ }
143
+ },
144
+ async mkdir(relPath, opts) {
145
+ const p = port();
146
+ const abs = resolveUnder(root, relPath);
147
+ try {
148
+ await p.mkdir(abs, { recursive: opts?.recursive ?? false });
149
+ } catch (e) {
150
+ throw mapError(e);
151
+ }
152
+ },
153
+ async rm(relPath, opts) {
154
+ const p = port();
155
+ const abs = resolveUnder(root, relPath);
156
+ try {
157
+ await p.rm(abs, { recursive: opts?.recursive ?? false });
158
+ } catch (e) {
159
+ throw mapError(e);
160
+ }
161
+ },
162
+ async rename(fromRel, toRel) {
163
+ const p = port();
164
+ const from = resolveUnder(root, fromRel);
165
+ const to = resolveUnder(root, toRel);
166
+ try {
167
+ await p.rename(from, to);
168
+ } catch (e) {
169
+ throw mapError(e);
170
+ }
171
+ },
172
+ canWrite(relPath = "") {
173
+ return writableAt(mount, relPath);
174
+ }
175
+ };
176
+ return api;
177
+ }
178
+ function openAppFs() {
179
+ return openFs({ path: getAppMountPath(), type: "repo" });
180
+ }
181
+ export {
182
+ fsAvailable,
183
+ openAppFs,
184
+ openFs,
185
+ sandboxFs
186
+ };
187
+ //# sourceMappingURL=fs.js.map
package/dist/fs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder.encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder.decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder.encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __reExport(index_exports, require("./secrets"), module.exports);
38
38
  __reExport(index_exports, require("./llm"), module.exports);
39
39
  __reExport(index_exports, require("./diagnostics"), module.exports);
40
40
  __reExport(index_exports, require("./onFsChange"), module.exports);
41
+ __reExport(index_exports, require("./fs"), module.exports);
41
42
  __reExport(index_exports, require("./debug"), module.exports);
42
43
  __reExport(index_exports, require("./tasks"), module.exports);
43
44
  __reExport(index_exports, require("./runtime"), module.exports);
@@ -71,6 +72,7 @@ __reExport(index_exports, require("./sandboxTypes"), module.exports);
71
72
  ...require("./llm"),
72
73
  ...require("./diagnostics"),
73
74
  ...require("./onFsChange"),
75
+ ...require("./fs"),
74
76
  ...require("./debug"),
75
77
  ...require("./tasks"),
76
78
  ...require("./runtime"),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,gCALd;AAMA,0BAAc,oBANd;AAOA,0BAAc,mBAPd;AAQA,0BAAc,oBARd;AASA,0BAAc,4BATd;AAUA,0BAAc,qBAVd;AAWA,0BAAc,yBAXd;AAYA,0BAAc,qBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,yBAdd;AAeA,0BAAc,sBAfd;AAgBA,0BAAc,kBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,uBAlBd;AAmBA,0BAAc,sBAnBd;AAoBA,0BAAc,kBApBd;AAqBA,0BAAc,0BArBd;AAsBA,0BAAc,yBAtBd;AAuBA,0BAAc,oBAvBd;AAwBA,0BAAc,oBAxBd;AAyBA,0BAAc,sBAzBd;AA0BA,0BAAc,wBA1Bd;AA2BA,0BAAc,oBA3Bd;AA4BA,0BAAc,sBA5Bd;AA6BA,0BAAc,6BA7Bd;AA8BA,0BAAc,2BA9Bd;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,gCALd;AAMA,0BAAc,oBANd;AAOA,0BAAc,mBAPd;AAQA,0BAAc,oBARd;AASA,0BAAc,4BATd;AAUA,0BAAc,qBAVd;AAWA,0BAAc,yBAXd;AAYA,0BAAc,qBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,yBAdd;AAeA,0BAAc,sBAfd;AAgBA,0BAAc,kBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,uBAlBd;AAmBA,0BAAc,sBAnBd;AAoBA,0BAAc,kBApBd;AAqBA,0BAAc,0BArBd;AAsBA,0BAAc,yBAtBd;AAuBA,0BAAc,iBAvBd;AAwBA,0BAAc,oBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,sBA1Bd;AA2BA,0BAAc,wBA3Bd;AA4BA,0BAAc,oBA5Bd;AA6BA,0BAAc,sBA7Bd;AA8BA,0BAAc,6BA9Bd;AA+BA,0BAAc,2BA/Bd;","names":[]}
package/dist/index.d.cts CHANGED
@@ -21,6 +21,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
21
21
  export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.cjs';
22
22
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.cjs';
23
23
  export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.cjs';
24
+ export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs } from './fs.cjs';
24
25
  export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.cjs';
25
26
  export { DirCap, FileCap, TaskInput, cancelTask, capDir, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.cjs';
26
27
  export { SDK_PROTOCOL_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.cjs';
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
21
21
  export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.js';
22
22
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.js';
23
23
  export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.js';
24
+ export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs } from './fs.js';
24
25
  export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.js';
25
26
  export { DirCap, FileCap, TaskInput, cancelTask, capDir, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.js';
26
27
  export { SDK_PROTOCOL_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.js';
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export * from "./secrets";
21
21
  export * from "./llm";
22
22
  export * from "./diagnostics";
23
23
  export * from "./onFsChange";
24
+ export * from "./fs";
24
25
  export * from "./debug";
25
26
  export * from "./tasks";
26
27
  export * from "./runtime";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
package/dist/llm.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5) — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string }; // data: base64, no data: URL prefix\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n return invokeStream<ChatDelta, ChatResult>('llm:chat', req as unknown as Record<string, unknown>);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: 'llm-provider',\n requestType: 'request-llm-provider',\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAmE3B,SAAS,KAAK,KAA+D;AAClF,aAAO,6BAAoC,YAAY,GAAyC;AAClG;AA0BA,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n return invokeStream<ChatDelta, ChatResult>('llm:chat', req as unknown as Record<string, unknown>);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: 'llm-provider',\n requestType: 'request-llm-provider',\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AA2E3B,SAAS,KAAK,KAA+D;AAClF,aAAO,6BAAoC,YAAY,GAAyC;AAClG;AA0BA,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
package/dist/llm.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Who authored a {@link ChatMessage}. */
2
2
  type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
3
3
  /** A part of a message. `image` is only honored when the resolved provider
4
- * advertises `features.vision` (§2.5) branch on {@link describeChat} first. */
4
+ * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it
5
+ * advertises `features.tools` — branch on {@link describeChat} first. */
5
6
  type ContentPart = {
6
7
  type: 'text';
7
8
  text: string;
@@ -9,6 +10,16 @@ type ContentPart = {
9
10
  type: 'image';
10
11
  mimeType: string;
11
12
  data: string;
13
+ } | {
14
+ type: 'tool-use';
15
+ id: string;
16
+ name: string;
17
+ input: Record<string, unknown>;
18
+ } | {
19
+ type: 'tool-result';
20
+ toolCallId: string;
21
+ content: string;
22
+ isError?: boolean;
12
23
  };
13
24
  /** One message in a {@link ChatRequest}: a role plus its content parts. */
14
25
  interface ChatMessage {
package/dist/llm.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Who authored a {@link ChatMessage}. */
2
2
  type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
3
3
  /** A part of a message. `image` is only honored when the resolved provider
4
- * advertises `features.vision` (§2.5) branch on {@link describeChat} first. */
4
+ * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it
5
+ * advertises `features.tools` — branch on {@link describeChat} first. */
5
6
  type ContentPart = {
6
7
  type: 'text';
7
8
  text: string;
@@ -9,6 +10,16 @@ type ContentPart = {
9
10
  type: 'image';
10
11
  mimeType: string;
11
12
  data: string;
13
+ } | {
14
+ type: 'tool-use';
15
+ id: string;
16
+ name: string;
17
+ input: Record<string, unknown>;
18
+ } | {
19
+ type: 'tool-result';
20
+ toolCallId: string;
21
+ content: string;
22
+ isError?: boolean;
12
23
  };
13
24
  /** One message in a {@link ChatRequest}: a role plus its content parts. */
14
25
  interface ChatMessage {