@ai-ecoverse/slicc-agent 2.1.0 → 2.2.0

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/README.md CHANGED
@@ -31,6 +31,32 @@ await agent.prompt('What is in /home?');
31
31
  - `hostAgent(agent)` serves the agent with pi's protocol: a pi-server with one session, `agent`, whose Chord services are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state). `serveConnections(self, host)` hands it every `MessagePort` the page sends.
32
32
  - `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over.
33
33
 
34
+ ### Bash and files on slicc-kernel
35
+
36
+ The page owns the one kernel (plan decision D3), and the agent worker attaches to it as a second client ([slicc-kernel#66](https://github.com/ai-ecoverse/slicc-kernel/issues/66)), so a terminal's `ps` and `kill` reach whatever the agent started.
37
+
38
+ ```js
39
+ const owner = await startAgent({ worker, kernel: { connect: () => kernel.connect() } });
40
+ ```
41
+
42
+ ```js
43
+ import { attachKernel } from '@ai-ecoverse/slicc-kernel';
44
+ import { CodingTools } from '@earendil-works/pi-durable/tools';
45
+ import { kernelEnvironment, kernelPort } from '@ai-ecoverse/slicc-agent';
46
+
47
+ const client = await attachKernel(await kernelPort(self));
48
+ registry.install(CodingTools);
49
+ const agent = await openAgent({ models, model, storage, registry, env: kernelEnvironment(client) });
50
+ ```
51
+
52
+ `SliccKernelEnv` implements pi durable's `ExecutionEnv` on the kernel client and passes pi's environment conformance suite:
53
+ - string commands run as `bash -c`, each in its own process group; timeouts and aborts kill the group;
54
+ - output past the spill limits goes to `/tmp/slicc-agent-output-*.log`;
55
+ - files and directories go through the kernel's OPFS view and metadata sidecar;
56
+ - `watch()` polls until the kernel offers file watching.
57
+
58
+ A conversation without a `cwd` works in `/home`.
59
+
34
60
  ### In slicc-spectrum
35
61
 
36
62
  `@ai-ecoverse/slicc-agent/spectrum` turns an `AgentConnection` into slicc-spectrum's `AgentPort`, so `<slicc-app>` shows the real agent:
package/dist/agent.js CHANGED
@@ -25,7 +25,8 @@ async function openAgent(options) {
25
25
  {
26
26
  models: options.models,
27
27
  registry: options.registry ?? createRegistry(),
28
- ...options.settings ? { settings: options.settings } : {}
28
+ ...options.settings ? { settings: options.settings } : {},
29
+ ...options.env ? { env: options.env } : {}
29
30
  },
30
31
  context
31
32
  );
package/dist/agent.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/agent.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n context?: Context;\n}\n\nexport interface Agent {\n readonly harness: Harness;\n readonly root: Conversation;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry: options.registry ?? createRegistry(),\n ...(options.settings ? { settings: options.settings } : {}),\n },\n context\n );\n const root = await harness.root(context, { agent: { model: options.model } });\n harness.resume();\n return {\n harness,\n root,\n prompt: (text) => prompt(root, text, context),\n close: () => harness.close(context),\n };\n}\n"],
5
- "mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AAuBA,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ,YAAY,eAAe;AAAA,MAC7C,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IAC3D;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,SAAS,EAAE,OAAO,EAAE,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC5E,UAAQ,OAAO;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,CAAC,SAAS,OAAO,MAAM,MAAM,OAAO;AAAA,IAC5C,OAAO,MAAM,QAAQ,MAAM,OAAO;AAAA,EACpC;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n env?: HarnessOptions['env'];\n context?: Context;\n}\n\nexport interface Agent {\n readonly harness: Harness;\n readonly root: Conversation;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry: options.registry ?? createRegistry(),\n ...(options.settings ? { settings: options.settings } : {}),\n ...(options.env ? { env: options.env } : {}),\n },\n context\n );\n const root = await harness.root(context, { agent: { model: options.model } });\n harness.resume();\n return {\n harness,\n root,\n prompt: (text) => prompt(root, text, context),\n close: () => harness.close(context),\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AAwBA,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ,YAAY,eAAe;AAAA,MAC7C,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,MACzD,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,SAAS,EAAE,OAAO,EAAE,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC5E,UAAQ,OAAO;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,CAAC,SAAS,OAAO,MAAM,MAAM,OAAO;AAAA,IAC5C,OAAO,MAAM,QAAQ,MAAM,OAAO;AAAA,EACpC;AACF;",
6
6
  "names": []
7
7
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { answerText, openAgent } from "./agent.js";
2
2
  import { connectAgent } from "./client.js";
3
3
  import { createAgentControl } from "./control.js";
4
4
  import { hostAgent } from "./host.js";
5
+ import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
5
6
  import {
6
7
  AGENT_LOCK,
7
8
  startAgent
@@ -19,7 +20,7 @@ import {
19
20
  WasmSqliteDatabase
20
21
  } from "./sqlite.js";
21
22
  import { PortChannel, PortListener, withUnrefTimers } from "./wire.js";
22
- import { serveConnections } from "./worker.js";
23
+ import { kernelPort, serveConnections } from "./worker.js";
23
24
  export {
24
25
  AGENT_DIRECTORY,
25
26
  AGENT_LOCK,
@@ -27,13 +28,17 @@ export {
27
28
  AgentControl,
28
29
  AgentSessions,
29
30
  AgentTranscript,
31
+ HOME,
30
32
  PortChannel,
31
33
  PortListener,
34
+ SliccKernelEnv,
32
35
  WasmSqliteDatabase,
33
36
  answerText,
34
37
  connectAgent,
35
38
  createAgentControl,
36
39
  hostAgent,
40
+ kernelEnvironment,
41
+ kernelPort,
37
42
  openAgent,
38
43
  openMemorySqliteStorage,
39
44
  openOpfsSqliteStorage,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["export { type Agent, type AgentOptions, answerText, openAgent } from './agent.ts';\nexport { type AgentConnection, connectAgent } from './client.ts';\nexport { createAgentControl } from './control.ts';\nexport { type AgentHost, hostAgent } from './host.ts';\nexport {\n AGENT_LOCK,\n type AgentOwner,\n type AgentWorker,\n type OwnerOptions,\n startAgent,\n} from './owner.ts';\nexport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n type AgentSettingsChange,\n AgentTranscript,\n type OperationError,\n type PromptResult,\n type SendMode,\n type SendRequest,\n type SendResponse,\n} from './services.ts';\nexport {\n AGENT_DIRECTORY,\n type OpfsSqliteOptions,\n openMemorySqliteStorage,\n openOpfsSqliteStorage,\n WasmSqliteDatabase,\n} from './sqlite.ts';\nexport { PortChannel, type PortEndpoint, PortListener, withUnrefTimers } from './wire.ts';\nexport { serveConnections, type WorkerScope } from './worker.ts';\n"],
5
- "mappings": "AAAA,SAAwC,YAAY,iBAAiB;AACrE,SAA+B,oBAAoB;AACnD,SAAS,0BAA0B;AACnC,SAAyB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EAIA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAgC,cAAc,uBAAuB;AAC9E,SAAS,wBAA0C;",
4
+ "sourcesContent": ["export { type Agent, type AgentOptions, answerText, openAgent } from './agent.ts';\nexport { type AgentConnection, connectAgent } from './client.ts';\nexport { createAgentControl } from './control.ts';\nexport { type AgentHost, hostAgent } from './host.ts';\nexport type {\n KernelClient,\n KernelFs,\n KernelProcess,\n KernelStat,\n SpawnOptions,\n} from './kernel/client.ts';\nexport { HOME, type KernelEnvOptions, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nexport {\n AGENT_LOCK,\n type AgentOwner,\n type AgentWorker,\n type OwnerOptions,\n startAgent,\n} from './owner.ts';\nexport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n type AgentSettingsChange,\n AgentTranscript,\n type OperationError,\n type PromptResult,\n type SendMode,\n type SendRequest,\n type SendResponse,\n} from './services.ts';\nexport {\n AGENT_DIRECTORY,\n type OpfsSqliteOptions,\n openMemorySqliteStorage,\n openOpfsSqliteStorage,\n WasmSqliteDatabase,\n} from './sqlite.ts';\nexport { PortChannel, type PortEndpoint, PortListener, withUnrefTimers } from './wire.ts';\nexport { kernelPort, serveConnections, type WorkerScope } from './worker.ts';\n"],
5
+ "mappings": "AAAA,SAAwC,YAAY,iBAAiB;AACrE,SAA+B,oBAAoB;AACnD,SAAS,0BAA0B;AACnC,SAAyB,iBAAiB;AAQ1C,SAAS,MAA6B,mBAAmB,sBAAsB;AAC/E;AAAA,EACE;AAAA,EAIA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAgC,cAAc,uBAAuB;AAC9E,SAAS,YAAY,wBAA0C;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ err,
3
+ FileError,
4
+ ok
5
+ } from "@earendil-works/pi-durable/env";
6
+ import { execute } from "./exec.js";
7
+ import { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from "./files.js";
8
+ import { dirname, normalize, resolve } from "./paths.js";
9
+ import { pollWatch } from "./watch.js";
10
+ class SliccKernelEnv {
11
+ id;
12
+ cwd;
13
+ #client;
14
+ #watchers = /* @__PURE__ */ new Set();
15
+ #poll;
16
+ constructor(client, options) {
17
+ this.#client = client;
18
+ this.cwd = options.cwd;
19
+ this.id = options.id ?? "slicc-kernel";
20
+ this.#poll = options.watch ?? {};
21
+ }
22
+ #path(path) {
23
+ return resolve(this.cwd, path);
24
+ }
25
+ get #fs() {
26
+ return this.#client.fs;
27
+ }
28
+ async absolutePath(path) {
29
+ return ok(this.#path(path));
30
+ }
31
+ async joinPath(parts) {
32
+ return ok(normalize(parts.join("/")));
33
+ }
34
+ readTextFile(path, context) {
35
+ const resolved = this.#path(path);
36
+ return attempt(context, resolved, () => this.#fs.readText(resolved));
37
+ }
38
+ readBinaryFile(path, context) {
39
+ const resolved = this.#path(path);
40
+ return attempt(context, resolved, () => this.#fs.readFile(resolved));
41
+ }
42
+ async openTextLineReader(path, context) {
43
+ const text = await this.readTextFile(path, context);
44
+ return text.ok ? ok(new SnapshotLines(text.value)) : text;
45
+ }
46
+ async readTextLines(path, options, context) {
47
+ if (options?.maxLines !== void 0 && options.maxLines <= 0) return ok([]);
48
+ const text = await this.readTextFile(path, context);
49
+ if (!text.ok) return text;
50
+ const reader = new SnapshotLines(text.value);
51
+ const lines = [];
52
+ for (let line = await reader.readLine(); line.ok && line.value; line = await reader.readLine()) {
53
+ if (options?.maxLines !== void 0 && lines.length >= options.maxLines) break;
54
+ lines.push(line.value.text);
55
+ }
56
+ return ok(lines);
57
+ }
58
+ openBinaryReader(path, options, context) {
59
+ const resolved = this.#path(path);
60
+ return attempt(context, resolved, async () => {
61
+ if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {
62
+ throw new FileError("invalid", "Refusing to follow a symbolic link", resolved);
63
+ }
64
+ const stat = await this.#fs.stat(resolved);
65
+ if (stat.isDirectory) throw new FileError("is_directory", "Is a directory", resolved);
66
+ if (!stat.isFile) throw new FileError("invalid", "Not a regular file", resolved);
67
+ return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));
68
+ });
69
+ }
70
+ writeFile(path, content, context) {
71
+ const resolved = this.#path(path);
72
+ return attempt(context, resolved, async () => {
73
+ await this.#fs.mkdir(dirname(resolved));
74
+ await this.#fs.writeFile(resolved, content);
75
+ });
76
+ }
77
+ appendFile(path, content, context) {
78
+ const resolved = this.#path(path);
79
+ return attempt(context, resolved, async () => {
80
+ const before = await this.#fs.exists(resolved) ? await this.#fs.readFile(resolved) : new Uint8Array();
81
+ const added = typeof content === "string" ? new TextEncoder().encode(content) : content;
82
+ const all = new Uint8Array(before.length + added.length);
83
+ all.set(before);
84
+ all.set(added, before.length);
85
+ await this.#fs.mkdir(dirname(resolved));
86
+ await this.#fs.writeFile(resolved, all);
87
+ });
88
+ }
89
+ truncateFile(path, size, context) {
90
+ const resolved = this.#path(path);
91
+ if (!Number.isSafeInteger(size) || size < 0) {
92
+ return Promise.resolve(
93
+ err(new FileError("invalid", "File size must be a non-negative integer", resolved))
94
+ );
95
+ }
96
+ return attempt(context, resolved, async () => {
97
+ const before = await this.#fs.readFile(resolved);
98
+ const after = new Uint8Array(size);
99
+ after.set(before.subarray(0, size));
100
+ await this.#fs.writeFile(resolved, after);
101
+ });
102
+ }
103
+ flushFile(path, context) {
104
+ const resolved = this.#path(path);
105
+ return attempt(context, resolved, async () => {
106
+ if ((await this.#fs.stat(resolved)).isDirectory)
107
+ throw Object.assign(new Error("Is a directory"), { code: "EISDIR" });
108
+ });
109
+ }
110
+ renameFile(sourcePath, destinationPath, context) {
111
+ const source = this.#path(sourcePath);
112
+ return attempt(context, source, () => this.#fs.rename(source, this.#path(destinationPath)));
113
+ }
114
+ fileInfo(path, context) {
115
+ const resolved = this.#path(path);
116
+ return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));
117
+ }
118
+ async listDir(path, context) {
119
+ const reader = await this.openDirReader(path, context);
120
+ if (!reader.ok) return reader;
121
+ return ok(await reader.value.rest());
122
+ }
123
+ openDirReader(path, context) {
124
+ const resolved = this.#path(path);
125
+ return attempt(context, resolved, async () => {
126
+ if (!(await this.#fs.stat(resolved)).isDirectory) {
127
+ throw Object.assign(new Error("Not a directory"), { code: "ENOTDIR" });
128
+ }
129
+ return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));
130
+ });
131
+ }
132
+ watch(targets, onChange, context) {
133
+ return attempt(context, this.cwd, async () => {
134
+ const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);
135
+ this.#watchers.add(watcher);
136
+ return {
137
+ mode: watcher.mode,
138
+ close: async (closing) => {
139
+ this.#watchers.delete(watcher);
140
+ await watcher.close(closing);
141
+ }
142
+ };
143
+ });
144
+ }
145
+ canonicalPath(path, context) {
146
+ const resolved = this.#path(path);
147
+ return attempt(context, resolved, () => this.#fs.realpath(resolved));
148
+ }
149
+ async exists(path, context) {
150
+ const found = await this.fileInfo(path, context);
151
+ if (found.ok) return ok(true);
152
+ return found.error.code === "not_found" ? ok(false) : found;
153
+ }
154
+ createDir(path, _options, context) {
155
+ const resolved = this.#path(path);
156
+ return attempt(context, resolved, () => this.#fs.mkdir(resolved));
157
+ }
158
+ remove(path, options, context) {
159
+ const resolved = this.#path(path);
160
+ return attempt(context, resolved, async () => {
161
+ if (!options?.recursive) {
162
+ const stat = await this.#fs.lstat(resolved).catch((error) => {
163
+ if (options?.force) return void 0;
164
+ throw error;
165
+ });
166
+ if (stat?.isDirectory) throw Object.assign(new Error("Is a directory"), { code: "EISDIR" });
167
+ }
168
+ await this.#fs.rm(resolved, { force: options?.force ?? false });
169
+ });
170
+ }
171
+ async createTempDir(prefix, context) {
172
+ const path = `/tmp/${prefix ?? "tmp-"}${crypto.randomUUID()}`;
173
+ return attempt(context, path, async () => {
174
+ await this.#fs.mkdir(path);
175
+ return path;
176
+ });
177
+ }
178
+ async createTempFile(options, context) {
179
+ const dir = await this.createTempDir("tmp-", context);
180
+ if (!dir.ok) return dir;
181
+ const path = `${dir.value}/${options?.prefix ?? ""}${crypto.randomUUID()}${options?.suffix ?? ""}`;
182
+ return attempt(context, path, async () => {
183
+ await this.#fs.writeFile(path, "");
184
+ return path;
185
+ });
186
+ }
187
+ exec(command, options, context) {
188
+ return execute(this.#client, this.cwd, command, options, context);
189
+ }
190
+ async cleanup(context) {
191
+ for (const watcher of [...this.#watchers]) await watcher.close(context);
192
+ this.#watchers.clear();
193
+ }
194
+ }
195
+ const HOME = "/home";
196
+ function kernelEnvironment(client, options = {}) {
197
+ return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });
198
+ }
199
+ export {
200
+ HOME,
201
+ SliccKernelEnv,
202
+ kernelEnvironment
203
+ };
204
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/env.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { type PollOptions, pollWatch } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#path(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#path(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n return execute(this.#client, this.cwd, command, options, context);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ ExecutionError,
3
+ err,
4
+ ok,
5
+ StreamDecoder
6
+ } from "@earendil-works/pi-durable/env";
7
+ import { resolve } from "./paths.js";
8
+ const SHELL = ["bash", "-c"];
9
+ class Spill {
10
+ #chunks = [];
11
+ #limit;
12
+ #bytes = 0;
13
+ #lines = 0;
14
+ constructor(limit) {
15
+ this.#limit = limit;
16
+ }
17
+ add(chunk) {
18
+ if (!this.#limit) return;
19
+ this.#chunks.push(chunk);
20
+ this.#bytes += chunk.length;
21
+ for (const byte of chunk) if (byte === 10) this.#lines++;
22
+ }
23
+ get over() {
24
+ const limit = this.#limit;
25
+ return limit !== void 0 && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines);
26
+ }
27
+ async write(client) {
28
+ if (!this.over) return void 0;
29
+ const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;
30
+ const all = new Uint8Array(this.#bytes);
31
+ let offset = 0;
32
+ for (const chunk of this.#chunks) {
33
+ all.set(chunk, offset);
34
+ offset += chunk.length;
35
+ }
36
+ await client.fs.mkdir("/tmp");
37
+ await client.fs.writeFile(path, all);
38
+ return path;
39
+ }
40
+ }
41
+ function sink(stream, spill, options, context) {
42
+ const decoder = new StreamDecoder();
43
+ return {
44
+ push(bytes) {
45
+ spill.add(bytes);
46
+ const text = decoder.decode(bytes);
47
+ if (text) options?.onOutput?.(text, context, { stream });
48
+ },
49
+ end() {
50
+ const text = decoder.decode();
51
+ if (text) options?.onOutput?.(text, context, { stream });
52
+ }
53
+ };
54
+ }
55
+ function guard(process, options, context) {
56
+ let ending;
57
+ const stop = (why) => {
58
+ ending ??= why;
59
+ process.signal("SIGKILL");
60
+ };
61
+ const timer = options?.timeout === void 0 ? void 0 : setTimeout(() => stop("timeout"), options.timeout * 1e3);
62
+ const onAbort = () => stop("aborted");
63
+ context.abortSignal?.addEventListener("abort", onAbort, { once: true });
64
+ if (context.abortSignal?.aborted) onAbort();
65
+ return {
66
+ get ending() {
67
+ return ending;
68
+ },
69
+ release() {
70
+ clearTimeout(timer);
71
+ context.abortSignal?.removeEventListener("abort", onAbort);
72
+ }
73
+ };
74
+ }
75
+ async function execute(client, cwd, command, options, context) {
76
+ const argv = typeof command === "string" ? [...SHELL, command] : [...command];
77
+ if (argv.length === 0) return err(new ExecutionError("spawn_error", "No program to run"));
78
+ if (context.abortSignal?.aborted)
79
+ return err(new ExecutionError("aborted", "The command was aborted"));
80
+ const spill = new Spill(options?.spill);
81
+ const out = sink("stdout", spill, options, context);
82
+ const errors = sink("stderr", spill, options, context);
83
+ let process;
84
+ try {
85
+ process = await client.spawn(argv, {
86
+ cwd: resolve(cwd, options?.cwd ?? "."),
87
+ ...options?.env ? { env: options.env } : {},
88
+ onStdout: (bytes) => out.push(bytes),
89
+ onStderr: (bytes) => errors.push(bytes)
90
+ });
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ return err(new ExecutionError("spawn_error", message));
94
+ }
95
+ const watch = guard(process, options, context);
96
+ const exitCode = await process.exited.finally(() => watch.release());
97
+ out.end();
98
+ errors.end();
99
+ const spillPath = await spill.write(client);
100
+ const ending = watch.ending;
101
+ if (ending) {
102
+ const error = new ExecutionError(
103
+ ending,
104
+ `The command was ${ending === "timeout" ? "timed out" : "aborted"}`
105
+ );
106
+ if (spillPath) error.spillPath = spillPath;
107
+ return err(error);
108
+ }
109
+ return ok(spillPath ? { exitCode, spillPath } : { exitCode });
110
+ }
111
+ export {
112
+ SHELL,
113
+ execute
114
+ };
115
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/exec.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n ExecutionError,\n err,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n StreamDecoder,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { resolve } from './paths.ts';\n\nexport const SHELL = ['bash', '-c'] as const;\n\nclass Spill {\n readonly #chunks: Uint8Array[] = [];\n readonly #limit: { afterBytes: number; afterLines: number } | undefined;\n #bytes = 0;\n #lines = 0;\n\n constructor(limit: ShellExecOptions['spill']) {\n this.#limit = limit;\n }\n\n add(chunk: Uint8Array): void {\n if (!this.#limit) return;\n this.#chunks.push(chunk);\n this.#bytes += chunk.length;\n for (const byte of chunk) if (byte === 10) this.#lines++;\n }\n\n get over(): boolean {\n const limit = this.#limit;\n return (\n limit !== undefined && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines)\n );\n }\n\n async write(client: KernelClient): Promise<string | undefined> {\n if (!this.over) return undefined;\n const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;\n const all = new Uint8Array(this.#bytes);\n let offset = 0;\n for (const chunk of this.#chunks) {\n all.set(chunk, offset);\n offset += chunk.length;\n }\n await client.fs.mkdir('/tmp');\n await client.fs.writeFile(path, all);\n return path;\n }\n}\n\nfunction sink(\n stream: 'stdout' | 'stderr',\n spill: Spill,\n options: ShellExecOptions | undefined,\n context: Context\n) {\n const decoder = new StreamDecoder();\n return {\n push(bytes: Uint8Array) {\n spill.add(bytes);\n const text = decoder.decode(bytes);\n if (text) options?.onOutput?.(text, context, { stream });\n },\n end() {\n const text = decoder.decode();\n if (text) options?.onOutput?.(text, context, { stream });\n },\n };\n}\n\ntype Ending = 'timeout' | 'aborted' | undefined;\n\nfunction guard(process: KernelProcess, options: ShellExecOptions | undefined, context: Context) {\n let ending: Ending;\n const stop = (why: Ending) => {\n ending ??= why;\n process.signal('SIGKILL');\n };\n const timer =\n options?.timeout === undefined\n ? undefined\n : setTimeout(() => stop('timeout'), options.timeout * 1000);\n const onAbort = () => stop('aborted');\n context.abortSignal?.addEventListener('abort', onAbort, { once: true });\n if (context.abortSignal?.aborted) onAbort();\n return {\n get ending() {\n return ending;\n },\n release() {\n clearTimeout(timer);\n context.abortSignal?.removeEventListener('abort', onAbort);\n },\n };\n}\n\nexport async function execute(\n client: KernelClient,\n cwd: string,\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n): Promise<Result<ShellExecResult, ExecutionError>> {\n const argv = typeof command === 'string' ? [...SHELL, command] : [...command];\n if (argv.length === 0) return err(new ExecutionError('spawn_error', 'No program to run'));\n if (context.abortSignal?.aborted)\n return err(new ExecutionError('aborted', 'The command was aborted'));\n const spill = new Spill(options?.spill);\n const out = sink('stdout', spill, options, context);\n const errors = sink('stderr', spill, options, context);\n let process: KernelProcess;\n try {\n process = await client.spawn(argv, {\n cwd: resolve(cwd, options?.cwd ?? '.'),\n ...(options?.env ? { env: options.env } : {}),\n onStdout: (bytes) => out.push(bytes),\n onStderr: (bytes) => errors.push(bytes),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return err(new ExecutionError('spawn_error', message));\n }\n const watch = guard(process, options, context);\n const exitCode = await process.exited.finally(() => watch.release());\n out.end();\n errors.end();\n const spillPath = await spill.write(client);\n const ending = watch.ending;\n if (ending) {\n const error = new ExecutionError(\n ending,\n `The command was ${ending === 'timeout' ? 'timed out' : 'aborted'}`\n );\n if (spillPath) error.spillPath = spillPath;\n return err(error);\n }\n return ok(spillPath ? { exitCode, spillPath } : { exitCode });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,153 @@
1
+ import {
2
+ err,
3
+ FileError,
4
+ LineScanner,
5
+ ok
6
+ } from "@earendil-works/pi-durable/env";
7
+ import { basename } from "./paths.js";
8
+ const codes = {
9
+ ENOENT: "not_found",
10
+ EACCES: "permission_denied",
11
+ EPERM: "permission_denied",
12
+ ENOTDIR: "not_directory",
13
+ EISDIR: "is_directory",
14
+ EINVAL: "invalid",
15
+ ELOOP: "invalid",
16
+ ENOSYS: "not_supported",
17
+ ENOTSUP: "not_supported"
18
+ };
19
+ function fileError(error, path) {
20
+ if (error instanceof FileError) return error;
21
+ const code = error?.code;
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ const cause = error instanceof Error ? error : void 0;
24
+ return new FileError(code && codes[code] || "unknown", message, path, cause);
25
+ }
26
+ function aborted(context, path) {
27
+ if (!context.abortSignal?.aborted) return void 0;
28
+ return err(new FileError("aborted", "The operation was aborted", path));
29
+ }
30
+ async function attempt(context, path, run) {
31
+ const before = aborted(context, path);
32
+ if (before) return before;
33
+ try {
34
+ const value = await run();
35
+ return aborted(context, path) ?? ok(value);
36
+ } catch (error) {
37
+ return err(fileError(error, path));
38
+ }
39
+ }
40
+ function info(path, stat) {
41
+ const kind = stat.isSymbolicLink ? "symlink" : stat.isDirectory ? "directory" : "file";
42
+ return {
43
+ name: basename(path),
44
+ path,
45
+ kind,
46
+ size: stat.size,
47
+ mtimeMs: new Date(stat.mtime).getTime()
48
+ };
49
+ }
50
+ function closed(what, path) {
51
+ return err(new FileError("invalid", `${what} is closed`, path));
52
+ }
53
+ class SnapshotReader {
54
+ #bytes;
55
+ #info;
56
+ #closed = false;
57
+ constructor(bytes, fileInfo) {
58
+ this.#bytes = bytes;
59
+ this.#info = fileInfo;
60
+ }
61
+ async info() {
62
+ return this.#closed ? closed("Binary reader", this.#info.path) : ok(this.#info);
63
+ }
64
+ async read(offset, length, context) {
65
+ const path = this.#info.path;
66
+ const stop = aborted(context, path);
67
+ if (stop) return stop;
68
+ if (this.#closed) return closed("Binary reader", path);
69
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0) {
70
+ return err(new FileError("invalid", "Offset and length must be non-negative integers", path));
71
+ }
72
+ return ok(this.#bytes.slice(offset, offset + length));
73
+ }
74
+ async scanLines(options, context) {
75
+ const path = this.#info.path;
76
+ const stop = aborted(context, path);
77
+ if (stop) return stop;
78
+ if (this.#closed) return closed("Binary reader", path);
79
+ let scanner;
80
+ try {
81
+ scanner = new LineScanner(options.startLine, options.endLine);
82
+ } catch {
83
+ return err(new FileError("invalid", "Invalid line range", path));
84
+ }
85
+ scanner.push(this.#bytes);
86
+ return ok(scanner.finish());
87
+ }
88
+ async close() {
89
+ this.#closed = true;
90
+ }
91
+ }
92
+ class SnapshotLines {
93
+ #lines;
94
+ #next = 0;
95
+ constructor(text) {
96
+ const parts = text.split("\n");
97
+ const last = parts.pop();
98
+ this.#lines = parts.map((part) => ({ text: part, terminated: true }));
99
+ if (last !== "") this.#lines.push({ text: last, terminated: false });
100
+ }
101
+ async readLine() {
102
+ return ok(this.#lines[this.#next++]);
103
+ }
104
+ async close() {
105
+ this.#next = this.#lines.length;
106
+ }
107
+ }
108
+ class KernelDirReader {
109
+ #fs;
110
+ #path;
111
+ #names;
112
+ #closed = false;
113
+ constructor(fs, path, names) {
114
+ this.#fs = fs;
115
+ this.#path = path;
116
+ this.#names = names;
117
+ }
118
+ async next(maxEntries, context) {
119
+ if (this.#closed) return closed("Directory reader", this.#path);
120
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
121
+ return err(new FileError("invalid", "maxEntries must be a positive integer", this.#path));
122
+ }
123
+ const stop = aborted(context, this.#path);
124
+ if (stop) return stop;
125
+ const entries = await this.#take(maxEntries);
126
+ return ok({ entries, done: this.#names.length === 0 });
127
+ }
128
+ rest() {
129
+ return this.#take(Number.MAX_SAFE_INTEGER);
130
+ }
131
+ async #take(max) {
132
+ const entries = [];
133
+ while (entries.length < max && this.#names.length > 0) {
134
+ const path = `${this.#path === "/" ? "" : this.#path}/${this.#names.shift()}`;
135
+ const stat = await this.#fs.lstat(path).catch(() => void 0);
136
+ if (stat) entries.push(info(path, stat));
137
+ }
138
+ return entries;
139
+ }
140
+ async close() {
141
+ this.#closed = true;
142
+ }
143
+ }
144
+ export {
145
+ KernelDirReader,
146
+ SnapshotLines,
147
+ SnapshotReader,
148
+ aborted,
149
+ attempt,
150
+ fileError,
151
+ info
152
+ };
153
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/files.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n err,\n FileError,\n type FileErrorCode,\n type FileInfo,\n type LineScan,\n LineScanner,\n ok,\n type Result,\n type TextLine,\n type TextLineReader,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelFs, KernelStat } from './client.ts';\nimport { basename } from './paths.ts';\n\nconst codes: Record<string, FileErrorCode> = {\n ENOENT: 'not_found',\n EACCES: 'permission_denied',\n EPERM: 'permission_denied',\n ENOTDIR: 'not_directory',\n EISDIR: 'is_directory',\n EINVAL: 'invalid',\n ELOOP: 'invalid',\n ENOSYS: 'not_supported',\n ENOTSUP: 'not_supported',\n};\n\nexport function fileError(error: unknown, path?: string): FileError {\n if (error instanceof FileError) return error;\n const code = (error as { code?: string } | null)?.code;\n const message = error instanceof Error ? error.message : String(error);\n const cause = error instanceof Error ? error : undefined;\n return new FileError((code && codes[code]) || 'unknown', message, path, cause);\n}\n\nexport function aborted(context: Context, path?: string): Result<never, FileError> | undefined {\n if (!context.abortSignal?.aborted) return undefined;\n return err(new FileError('aborted', 'The operation was aborted', path));\n}\n\nexport async function attempt<T>(\n context: Context,\n path: string,\n run: () => Promise<T>\n): Promise<Result<T, FileError>> {\n const before = aborted(context, path);\n if (before) return before;\n try {\n const value = await run();\n return aborted(context, path) ?? ok(value);\n } catch (error) {\n return err(fileError(error, path));\n }\n}\n\nexport function info(path: string, stat: KernelStat): FileInfo {\n const kind = stat.isSymbolicLink ? 'symlink' : stat.isDirectory ? 'directory' : 'file';\n return {\n name: basename(path),\n path,\n kind,\n size: stat.size,\n mtimeMs: new Date(stat.mtime).getTime(),\n };\n}\n\nfunction closed<T>(what: string, path: string): Result<T, FileError> {\n return err(new FileError('invalid', `${what} is closed`, path));\n}\n\nexport class SnapshotReader implements BinaryReader {\n readonly #bytes: Uint8Array;\n readonly #info: FileInfo;\n #closed = false;\n\n constructor(bytes: Uint8Array, fileInfo: FileInfo) {\n this.#bytes = bytes;\n this.#info = fileInfo;\n }\n\n async info(): Promise<Result<FileInfo, FileError>> {\n return this.#closed ? closed('Binary reader', this.#info.path) : ok(this.#info);\n }\n\n async read(\n offset: number,\n length: number,\n context: Context\n ): Promise<Result<Uint8Array, FileError>> {\n const path = this.#info.path;\n const stop = aborted(context, path);\n if (stop) return stop;\n if (this.#closed) return closed('Binary reader', path);\n if (\n !Number.isSafeInteger(offset) ||\n offset < 0 ||\n !Number.isSafeInteger(length) ||\n length < 0\n ) {\n return err(new FileError('invalid', 'Offset and length must be non-negative integers', path));\n }\n return ok(this.#bytes.slice(offset, offset + length));\n }\n\n async scanLines(\n options: { startLine: number; endLine?: number },\n context: Context\n ): Promise<Result<LineScan, FileError>> {\n const path = this.#info.path;\n const stop = aborted(context, path);\n if (stop) return stop;\n if (this.#closed) return closed('Binary reader', path);\n let scanner: LineScanner;\n try {\n scanner = new LineScanner(options.startLine, options.endLine);\n } catch {\n return err(new FileError('invalid', 'Invalid line range', path));\n }\n scanner.push(this.#bytes);\n return ok(scanner.finish());\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n }\n}\n\nexport class SnapshotLines implements TextLineReader {\n readonly #lines: TextLine[];\n #next = 0;\n\n constructor(text: string) {\n const parts = text.split('\\n');\n const last = parts.pop() as string;\n this.#lines = parts.map((part) => ({ text: part, terminated: true }));\n if (last !== '') this.#lines.push({ text: last, terminated: false });\n }\n\n async readLine(): Promise<Result<TextLine | undefined, FileError>> {\n return ok(this.#lines[this.#next++]);\n }\n\n async close(): Promise<void> {\n this.#next = this.#lines.length;\n }\n}\n\nexport class KernelDirReader implements DirReader {\n readonly #fs: KernelFs;\n readonly #path: string;\n readonly #names: string[];\n #closed = false;\n\n constructor(fs: KernelFs, path: string, names: string[]) {\n this.#fs = fs;\n this.#path = path;\n this.#names = names;\n }\n\n async next(\n maxEntries: number,\n context: Context\n ): Promise<Result<{ entries: FileInfo[]; done: boolean }, FileError>> {\n if (this.#closed) return closed('Directory reader', this.#path);\n if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {\n return err(new FileError('invalid', 'maxEntries must be a positive integer', this.#path));\n }\n const stop = aborted(context, this.#path);\n if (stop) return stop;\n const entries = await this.#take(maxEntries);\n return ok({ entries, done: this.#names.length === 0 });\n }\n\n rest(): Promise<FileInfo[]> {\n return this.#take(Number.MAX_SAFE_INTEGER);\n }\n\n async #take(max: number): Promise<FileInfo[]> {\n const entries: FileInfo[] = [];\n while (entries.length < max && this.#names.length > 0) {\n const path = `${this.#path === '/' ? '' : this.#path}/${this.#names.shift()}`;\n const stat = await this.#fs.lstat(path).catch(() => undefined);\n if (stat) entries.push(info(path, stat));\n }\n return entries;\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,31 @@
1
+ function normalize(path) {
2
+ const absolute = path.startsWith("/");
3
+ const parts = [];
4
+ for (const part of path.split("/")) {
5
+ if (part === "" || part === ".") continue;
6
+ if (part === "..") {
7
+ if (parts.length > 0 && parts.at(-1) !== "..") parts.pop();
8
+ else if (!absolute) parts.push(part);
9
+ } else parts.push(part);
10
+ }
11
+ const joined = parts.join("/");
12
+ if (absolute) return `/${joined}`;
13
+ return joined || ".";
14
+ }
15
+ function resolve(cwd, path) {
16
+ return normalize(path.startsWith("/") ? path : `${cwd}/${path}`);
17
+ }
18
+ function dirname(path) {
19
+ const index = path.lastIndexOf("/");
20
+ return index <= 0 ? "/" : path.slice(0, index);
21
+ }
22
+ function basename(path) {
23
+ return path.slice(path.lastIndexOf("/") + 1);
24
+ }
25
+ export {
26
+ basename,
27
+ dirname,
28
+ normalize,
29
+ resolve
30
+ };
31
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/paths.ts"],
4
+ "sourcesContent": ["export function normalize(path: string): string {\n const absolute = path.startsWith('/');\n const parts: string[] = [];\n for (const part of path.split('/')) {\n if (part === '' || part === '.') continue;\n if (part === '..') {\n if (parts.length > 0 && parts.at(-1) !== '..') parts.pop();\n else if (!absolute) parts.push(part);\n } else parts.push(part);\n }\n const joined = parts.join('/');\n if (absolute) return `/${joined}`;\n return joined || '.';\n}\n\nexport function resolve(cwd: string, path: string): string {\n return normalize(path.startsWith('/') ? path : `${cwd}/${path}`);\n}\n\nexport function dirname(path: string): string {\n const index = path.lastIndexOf('/');\n return index <= 0 ? '/' : path.slice(0, index);\n}\n\nexport function basename(path: string): string {\n return path.slice(path.lastIndexOf('/') + 1);\n}\n"],
5
+ "mappings": "AAAO,SAAS,UAAU,MAAsB;AAC9C,QAAM,WAAW,KAAK,WAAW,GAAG;AACpC,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,QAAI,SAAS,MAAM,SAAS,IAAK;AACjC,QAAI,SAAS,MAAM;AACjB,UAAI,MAAM,SAAS,KAAK,MAAM,GAAG,EAAE,MAAM,KAAM,OAAM,IAAI;AAAA,eAChD,CAAC,SAAU,OAAM,KAAK,IAAI;AAAA,IACrC,MAAO,OAAM,KAAK,IAAI;AAAA,EACxB;AACA,QAAM,SAAS,MAAM,KAAK,GAAG;AAC7B,MAAI,SAAU,QAAO,IAAI,MAAM;AAC/B,SAAO,UAAU;AACnB;AAEO,SAAS,QAAQ,KAAa,MAAsB;AACzD,SAAO,UAAU,KAAK,WAAW,GAAG,IAAI,OAAO,GAAG,GAAG,IAAI,IAAI,EAAE;AACjE;AAEO,SAAS,QAAQ,MAAsB;AAC5C,QAAM,QAAQ,KAAK,YAAY,GAAG;AAClC,SAAO,SAAS,IAAI,MAAM,KAAK,MAAM,GAAG,KAAK;AAC/C;AAEO,SAAS,SAAS,MAAsB;AAC7C,SAAO,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;AAC7C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,71 @@
1
+ import { dirname, resolve } from "./paths.js";
2
+ function excluded(name, target) {
3
+ const rule = target.exclude;
4
+ if (!rule) return false;
5
+ return rule.hidden === true && name.startsWith(".") || (rule.names?.includes(name) ?? false);
6
+ }
7
+ async function record(fs, path, into) {
8
+ const stat = await fs.stat(path).catch(() => void 0);
9
+ if (!stat) return false;
10
+ into.set(
11
+ path,
12
+ `${stat.isDirectory ? "d" : "f"}:${stat.ino}:${stat.size}:${new Date(stat.mtime).getTime()}`
13
+ );
14
+ return stat.isDirectory;
15
+ }
16
+ async function scan(fs, path, target, deep, into) {
17
+ const directory = await record(fs, path, into);
18
+ if (!directory || !deep) return;
19
+ const names = await fs.readdir(path).catch(() => []);
20
+ for (const name of names) {
21
+ if (excluded(name, target)) continue;
22
+ const child = `${path === "/" ? "" : path}/${name}`;
23
+ await scan(fs, child, target, target.recursive === true, into);
24
+ }
25
+ }
26
+ async function snapshot(fs, cwd, targets) {
27
+ const all = /* @__PURE__ */ new Map();
28
+ for (const target of targets) await scan(fs, resolve(cwd, target.path), target, true, all);
29
+ return all;
30
+ }
31
+ function differences(before, after) {
32
+ const changed = /* @__PURE__ */ new Set();
33
+ const moved = (path) => {
34
+ changed.add(path);
35
+ changed.add(dirname(path));
36
+ };
37
+ for (const [path, value] of after) {
38
+ if (!before.has(path)) moved(path);
39
+ else if (before.get(path) !== value) changed.add(path);
40
+ }
41
+ for (const path of before.keys()) if (!after.has(path)) moved(path);
42
+ return [...changed];
43
+ }
44
+ async function pollWatch(fs, cwd, targets, onChange, options = {}) {
45
+ let current = await snapshot(fs, cwd, targets);
46
+ let open = true;
47
+ let running = Promise.resolve();
48
+ const tick = async () => {
49
+ const next = await snapshot(fs, cwd, targets);
50
+ const paths = differences(current, next);
51
+ current = next;
52
+ if (open && paths.length > 0) onChange({ paths });
53
+ };
54
+ const timer = setInterval(() => {
55
+ running = running.then(tick);
56
+ }, options.intervalMs ?? 100);
57
+ return {
58
+ mode: "polling",
59
+ async close(_context) {
60
+ open = false;
61
+ clearInterval(timer);
62
+ await running;
63
+ }
64
+ };
65
+ }
66
+ export {
67
+ differences,
68
+ pollWatch,
69
+ snapshot
70
+ };
71
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/watch.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { FileWatcher, WatchChange, WatchTarget } from '@earendil-works/pi-durable/env';\nimport type { KernelFs } from './client.ts';\nimport { dirname, resolve } from './paths.ts';\n\ntype Snapshot = Map<string, string>;\n\nfunction excluded(name: string, target: WatchTarget): boolean {\n const rule = target.exclude;\n if (!rule) return false;\n return (rule.hidden === true && name.startsWith('.')) || (rule.names?.includes(name) ?? false);\n}\n\nasync function record(fs: KernelFs, path: string, into: Snapshot): Promise<boolean> {\n const stat = await fs.stat(path).catch(() => undefined);\n if (!stat) return false;\n into.set(\n path,\n `${stat.isDirectory ? 'd' : 'f'}:${stat.ino}:${stat.size}:${new Date(stat.mtime).getTime()}`\n );\n return stat.isDirectory;\n}\n\nasync function scan(\n fs: KernelFs,\n path: string,\n target: WatchTarget,\n deep: boolean,\n into: Snapshot\n) {\n const directory = await record(fs, path, into);\n if (!directory || !deep) return;\n const names = await fs.readdir(path).catch(() => [] as string[]);\n for (const name of names) {\n if (excluded(name, target)) continue;\n const child = `${path === '/' ? '' : path}/${name}`;\n await scan(fs, child, target, target.recursive === true, into);\n }\n}\n\nexport async function snapshot(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[]\n): Promise<Snapshot> {\n const all: Snapshot = new Map();\n for (const target of targets) await scan(fs, resolve(cwd, target.path), target, true, all);\n return all;\n}\n\nexport function differences(before: Snapshot, after: Snapshot): string[] {\n const changed = new Set<string>();\n const moved = (path: string) => {\n changed.add(path);\n changed.add(dirname(path));\n };\n for (const [path, value] of after) {\n if (!before.has(path)) moved(path);\n else if (before.get(path) !== value) changed.add(path);\n }\n for (const path of before.keys()) if (!after.has(path)) moved(path);\n return [...changed];\n}\n\nexport interface PollOptions {\n intervalMs?: number;\n}\n\nexport async function pollWatch(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n options: PollOptions = {}\n): Promise<FileWatcher> {\n let current = await snapshot(fs, cwd, targets);\n let open = true;\n let running = Promise.resolve();\n const tick = async () => {\n const next = await snapshot(fs, cwd, targets);\n const paths = differences(current, next);\n current = next;\n if (open && paths.length > 0) onChange({ paths });\n };\n const timer = setInterval(() => {\n running = running.then(tick);\n }, options.intervalMs ?? 100);\n return {\n mode: 'polling',\n async close(_context: Context) {\n open = false;\n clearInterval(timer);\n await running;\n },\n };\n}\n"],
5
+ "mappings": "AAGA,SAAS,SAAS,eAAe;AAIjC,SAAS,SAAS,MAAc,QAA8B;AAC5D,QAAM,OAAO,OAAO;AACpB,MAAI,CAAC,KAAM,QAAO;AAClB,SAAQ,KAAK,WAAW,QAAQ,KAAK,WAAW,GAAG,MAAO,KAAK,OAAO,SAAS,IAAI,KAAK;AAC1F;AAEA,eAAe,OAAO,IAAc,MAAc,MAAkC;AAClF,QAAM,OAAO,MAAM,GAAG,KAAK,IAAI,EAAE,MAAM,MAAM,MAAS;AACtD,MAAI,CAAC,KAAM,QAAO;AAClB,OAAK;AAAA,IACH;AAAA,IACA,GAAG,KAAK,cAAc,MAAM,GAAG,IAAI,KAAK,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,EAAE,QAAQ,CAAC;AAAA,EAC5F;AACA,SAAO,KAAK;AACd;AAEA,eAAe,KACb,IACA,MACA,QACA,MACA,MACA;AACA,QAAM,YAAY,MAAM,OAAO,IAAI,MAAM,IAAI;AAC7C,MAAI,CAAC,aAAa,CAAC,KAAM;AACzB,QAAM,QAAQ,MAAM,GAAG,QAAQ,IAAI,EAAE,MAAM,MAAM,CAAC,CAAa;AAC/D,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,MAAM,MAAM,EAAG;AAC5B,UAAM,QAAQ,GAAG,SAAS,MAAM,KAAK,IAAI,IAAI,IAAI;AACjD,UAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,cAAc,MAAM,IAAI;AAAA,EAC/D;AACF;AAEA,eAAsB,SACpB,IACA,KACA,SACmB;AACnB,QAAM,MAAgB,oBAAI,IAAI;AAC9B,aAAW,UAAU,QAAS,OAAM,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI,GAAG,QAAQ,MAAM,GAAG;AACzF,SAAO;AACT;AAEO,SAAS,YAAY,QAAkB,OAA2B;AACvE,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,SAAiB;AAC9B,YAAQ,IAAI,IAAI;AAChB,YAAQ,IAAI,QAAQ,IAAI,CAAC;AAAA,EAC3B;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,QAAI,CAAC,OAAO,IAAI,IAAI,EAAG,OAAM,IAAI;AAAA,aACxB,OAAO,IAAI,IAAI,MAAM,MAAO,SAAQ,IAAI,IAAI;AAAA,EACvD;AACA,aAAW,QAAQ,OAAO,KAAK,EAAG,KAAI,CAAC,MAAM,IAAI,IAAI,EAAG,OAAM,IAAI;AAClE,SAAO,CAAC,GAAG,OAAO;AACpB;AAMA,eAAsB,UACpB,IACA,KACA,SACA,UACA,UAAuB,CAAC,GACF;AACtB,MAAI,UAAU,MAAM,SAAS,IAAI,KAAK,OAAO;AAC7C,MAAI,OAAO;AACX,MAAI,UAAU,QAAQ,QAAQ;AAC9B,QAAM,OAAO,YAAY;AACvB,UAAM,OAAO,MAAM,SAAS,IAAI,KAAK,OAAO;AAC5C,UAAM,QAAQ,YAAY,SAAS,IAAI;AACvC,cAAU;AACV,QAAI,QAAQ,MAAM,SAAS,EAAG,UAAS,EAAE,MAAM,CAAC;AAAA,EAClD;AACA,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,QAAQ,KAAK,IAAI;AAAA,EAC7B,GAAG,QAAQ,cAAc,GAAG;AAC5B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,MAAM,UAAmB;AAC7B,aAAO;AACP,oBAAc,KAAK;AACnB,YAAM;AAAA,IACR;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
package/dist/owner.js CHANGED
@@ -4,26 +4,49 @@ const AGENT_LOCK = "slicc-agent";
4
4
  async function startAgent(options) {
5
5
  const release = await holdLock(options.locks ?? navigator.locks, AGENT_LOCK);
6
6
  const channel = options.channel ?? (() => new MessageChannel());
7
- let worker;
7
+ const spawn = async () => {
8
+ const created = options.worker();
9
+ try {
10
+ if (options.kernel) {
11
+ const port = await options.kernel.connect();
12
+ created.postMessage({ kernel: port }, [port]);
13
+ }
14
+ } catch (error) {
15
+ created.terminate();
16
+ throw error;
17
+ }
18
+ return created;
19
+ };
20
+ let worker = Promise.resolve().then(spawn);
8
21
  try {
9
- worker = options.worker();
22
+ await worker;
10
23
  } catch (error) {
11
24
  release();
12
25
  throw error;
13
26
  }
14
27
  return {
15
- connect() {
28
+ async connect() {
29
+ const ready = await worker;
30
+ if (!ready) throw new Error("the agent was released");
16
31
  const { port1, port2 } = channel();
17
- worker.postMessage({ connect: port2 }, [port2]);
32
+ ready.postMessage({ connect: port2 }, [port2]);
18
33
  return connectAgent(port1);
19
34
  },
20
35
  restart() {
21
- worker.terminate();
22
- worker = options.worker();
36
+ worker = worker.catch(() => void 0).then((old) => {
37
+ old?.terminate();
38
+ return spawn();
39
+ });
40
+ return worker.then(() => void 0);
23
41
  },
24
- release() {
25
- worker.terminate();
26
- release();
42
+ async release() {
43
+ const previous = worker;
44
+ worker = Promise.resolve(void 0);
45
+ try {
46
+ (await previous.catch(() => void 0))?.terminate();
47
+ } finally {
48
+ release();
49
+ }
27
50
  }
28
51
  };
29
52
  }
package/dist/owner.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/owner.ts"],
4
- "sourcesContent": ["import { type AgentConnection, connectAgent } from './client.ts';\nimport { holdLock } from './opfs-pool.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport const AGENT_LOCK = 'slicc-agent';\n\nexport interface AgentWorker {\n postMessage(message: unknown, transfer: Transferable[]): void;\n terminate(): void;\n}\n\nexport interface AgentOwner {\n connect(): Promise<AgentConnection>;\n restart(): void;\n release(): void;\n}\n\nexport interface OwnerOptions {\n worker: () => AgentWorker;\n locks?: LockManager;\n channel?: () => { port1: PortEndpoint; port2: Transferable };\n}\n\nexport async function startAgent(options: OwnerOptions): Promise<AgentOwner> {\n const release = await holdLock(options.locks ?? navigator.locks, AGENT_LOCK);\n const channel = options.channel ?? (() => new MessageChannel());\n let worker: AgentWorker;\n try {\n worker = options.worker();\n } catch (error) {\n release();\n throw error;\n }\n return {\n connect() {\n const { port1, port2 } = channel();\n worker.postMessage({ connect: port2 }, [port2]);\n return connectAgent(port1);\n },\n restart() {\n worker.terminate();\n worker = options.worker();\n },\n release() {\n worker.terminate();\n release();\n },\n };\n}\n"],
5
- "mappings": "AAAA,SAA+B,oBAAoB;AACnD,SAAS,gBAAgB;AAGlB,MAAM,aAAa;AAmB1B,eAAsB,WAAW,SAA4C;AAC3E,QAAM,UAAU,MAAM,SAAS,QAAQ,SAAS,UAAU,OAAO,UAAU;AAC3E,QAAM,UAAU,QAAQ,YAAY,MAAM,IAAI,eAAe;AAC7D,MAAI;AACJ,MAAI;AACF,aAAS,QAAQ,OAAO;AAAA,EAC1B,SAAS,OAAO;AACd,YAAQ;AACR,UAAM;AAAA,EACR;AACA,SAAO;AAAA,IACL,UAAU;AACR,YAAM,EAAE,OAAO,MAAM,IAAI,QAAQ;AACjC,aAAO,YAAY,EAAE,SAAS,MAAM,GAAG,CAAC,KAAK,CAAC;AAC9C,aAAO,aAAa,KAAK;AAAA,IAC3B;AAAA,IACA,UAAU;AACR,aAAO,UAAU;AACjB,eAAS,QAAQ,OAAO;AAAA,IAC1B;AAAA,IACA,UAAU;AACR,aAAO,UAAU;AACjB,cAAQ;AAAA,IACV;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { type AgentConnection, connectAgent } from './client.ts';\nimport { holdLock } from './opfs-pool.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport const AGENT_LOCK = 'slicc-agent';\n\nexport interface AgentWorker {\n postMessage(message: unknown, transfer: Transferable[]): void;\n terminate(): void;\n}\n\nexport interface AgentOwner {\n connect(): Promise<AgentConnection>;\n restart(): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface KernelSource {\n connect(): Promise<Transferable>;\n}\n\nexport interface OwnerOptions {\n worker: () => AgentWorker;\n kernel?: KernelSource;\n locks?: LockManager;\n channel?: () => { port1: PortEndpoint; port2: Transferable };\n}\n\nexport async function startAgent(options: OwnerOptions): Promise<AgentOwner> {\n const release = await holdLock(options.locks ?? navigator.locks, AGENT_LOCK);\n const channel = options.channel ?? (() => new MessageChannel());\n const spawn = async (): Promise<AgentWorker> => {\n const created = options.worker();\n try {\n if (options.kernel) {\n const port = await options.kernel.connect();\n created.postMessage({ kernel: port }, [port]);\n }\n } catch (error) {\n created.terminate();\n throw error;\n }\n return created;\n };\n let worker: Promise<AgentWorker | undefined> = Promise.resolve().then(spawn);\n try {\n await worker;\n } catch (error) {\n release();\n throw error;\n }\n return {\n async connect() {\n const ready = await worker;\n if (!ready) throw new Error('the agent was released');\n const { port1, port2 } = channel();\n ready.postMessage({ connect: port2 }, [port2]);\n return connectAgent(port1);\n },\n restart() {\n worker = worker\n .catch(() => undefined)\n .then((old) => {\n old?.terminate();\n return spawn();\n });\n return worker.then(() => undefined);\n },\n async release() {\n const previous = worker;\n worker = Promise.resolve(undefined);\n try {\n (await previous.catch(() => undefined))?.terminate();\n } finally {\n release();\n }\n },\n };\n}\n"],
5
+ "mappings": "AAAA,SAA+B,oBAAoB;AACnD,SAAS,gBAAgB;AAGlB,MAAM,aAAa;AAwB1B,eAAsB,WAAW,SAA4C;AAC3E,QAAM,UAAU,MAAM,SAAS,QAAQ,SAAS,UAAU,OAAO,UAAU;AAC3E,QAAM,UAAU,QAAQ,YAAY,MAAM,IAAI,eAAe;AAC7D,QAAM,QAAQ,YAAkC;AAC9C,UAAM,UAAU,QAAQ,OAAO;AAC/B,QAAI;AACF,UAAI,QAAQ,QAAQ;AAClB,cAAM,OAAO,MAAM,QAAQ,OAAO,QAAQ;AAC1C,gBAAQ,YAAY,EAAE,QAAQ,KAAK,GAAG,CAAC,IAAI,CAAC;AAAA,MAC9C;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,UAAU;AAClB,YAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AACA,MAAI,SAA2C,QAAQ,QAAQ,EAAE,KAAK,KAAK;AAC3E,MAAI;AACF,UAAM;AAAA,EACR,SAAS,OAAO;AACd,YAAQ;AACR,UAAM;AAAA,EACR;AACA,SAAO;AAAA,IACL,MAAM,UAAU;AACd,YAAM,QAAQ,MAAM;AACpB,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,wBAAwB;AACpD,YAAM,EAAE,OAAO,MAAM,IAAI,QAAQ;AACjC,YAAM,YAAY,EAAE,SAAS,MAAM,GAAG,CAAC,KAAK,CAAC;AAC7C,aAAO,aAAa,KAAK;AAAA,IAC3B;AAAA,IACA,UAAU;AACR,eAAS,OACN,MAAM,MAAM,MAAS,EACrB,KAAK,CAAC,QAAQ;AACb,aAAK,UAAU;AACf,eAAO,MAAM;AAAA,MACf,CAAC;AACH,aAAO,OAAO,KAAK,MAAM,MAAS;AAAA,IACpC;AAAA,IACA,MAAM,UAAU;AACd,YAAM,WAAW;AACjB,eAAS,QAAQ,QAAQ,MAAS;AAClC,UAAI;AACF,SAAC,MAAM,SAAS,MAAM,MAAM,MAAS,IAAI,UAAU;AAAA,MACrD,UAAE;AACA,gBAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ export interface AgentOptions {
7
7
  storage?: Storage;
8
8
  registry?: Registry;
9
9
  settings?: HarnessOptions['settings'];
10
+ env?: HarnessOptions['env'];
10
11
  context?: Context;
11
12
  }
12
13
  export interface Agent {
@@ -2,8 +2,10 @@ export { type Agent, type AgentOptions, answerText, openAgent } from './agent.ts
2
2
  export { type AgentConnection, connectAgent } from './client.ts';
3
3
  export { createAgentControl } from './control.ts';
4
4
  export { type AgentHost, hostAgent } from './host.ts';
5
+ export type { KernelClient, KernelFs, KernelProcess, KernelStat, SpawnOptions, } from './kernel/client.ts';
6
+ export { HOME, type KernelEnvOptions, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';
5
7
  export { AGENT_LOCK, type AgentOwner, type AgentWorker, type OwnerOptions, startAgent, } from './owner.ts';
6
8
  export { AGENT_SESSION, AgentControl, AgentSessions, type AgentSettingsChange, AgentTranscript, type OperationError, type PromptResult, type SendMode, type SendRequest, type SendResponse, } from './services.ts';
7
9
  export { AGENT_DIRECTORY, type OpfsSqliteOptions, openMemorySqliteStorage, openOpfsSqliteStorage, WasmSqliteDatabase, } from './sqlite.ts';
8
10
  export { PortChannel, type PortEndpoint, PortListener, withUnrefTimers } from './wire.ts';
9
- export { serveConnections, type WorkerScope } from './worker.ts';
11
+ export { kernelPort, serveConnections, type WorkerScope } from './worker.ts';
@@ -0,0 +1,39 @@
1
+ export interface KernelStat {
2
+ isFile: boolean;
3
+ isDirectory: boolean;
4
+ isSymbolicLink: boolean;
5
+ size: number;
6
+ mtime: Date | number;
7
+ ino?: number;
8
+ }
9
+ export interface KernelFs {
10
+ readFile(path: string): Promise<Uint8Array>;
11
+ readText(path: string): Promise<string>;
12
+ writeFile(path: string, data: string | Uint8Array): Promise<void>;
13
+ stat(path: string): Promise<KernelStat>;
14
+ lstat(path: string): Promise<KernelStat>;
15
+ readdir(path: string): Promise<string[]>;
16
+ mkdir(path: string): Promise<void>;
17
+ rm(path: string, options?: {
18
+ force?: boolean;
19
+ }): Promise<void>;
20
+ rename(from: string, to: string): Promise<void>;
21
+ realpath(path: string): Promise<string>;
22
+ exists(path: string): Promise<boolean>;
23
+ }
24
+ export interface KernelProcess {
25
+ pid: number;
26
+ pgid: number;
27
+ exited: Promise<number>;
28
+ signal(name?: string): unknown;
29
+ }
30
+ export interface SpawnOptions {
31
+ cwd?: string;
32
+ env?: Record<string, string>;
33
+ onStdout?(bytes: Uint8Array): void;
34
+ onStderr?(bytes: Uint8Array): void;
35
+ }
36
+ export interface KernelClient {
37
+ fs: KernelFs;
38
+ spawn(argv: readonly string[], options: SpawnOptions): Promise<KernelProcess>;
39
+ }
@@ -0,0 +1,57 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import { type BinaryReader, type DirReader, type ExecutionEnv, type ExecutionError, FileError, type FileInfo, type FileWatcher, type Result, type ShellExecOptions, type ShellExecResult, type TextLineReader, type WatchChange, type WatchTarget } from '@earendil-works/pi-durable/env';
3
+ import type { KernelClient } from './client.ts';
4
+ import { type PollOptions } from './watch.ts';
5
+ export interface KernelEnvOptions {
6
+ cwd: string;
7
+ id?: string;
8
+ watch?: PollOptions;
9
+ }
10
+ type FileResult<T> = Promise<Result<T, FileError>>;
11
+ export declare class SliccKernelEnv implements ExecutionEnv {
12
+ #private;
13
+ readonly id: string;
14
+ cwd: string;
15
+ constructor(client: KernelClient, options: KernelEnvOptions);
16
+ absolutePath(path: string): FileResult<string>;
17
+ joinPath(parts: string[]): FileResult<string>;
18
+ readTextFile(path: string, context: Context): FileResult<string>;
19
+ readBinaryFile(path: string, context: Context): FileResult<Uint8Array>;
20
+ openTextLineReader(path: string, context: Context): FileResult<TextLineReader>;
21
+ readTextLines(path: string, options: {
22
+ maxLines?: number;
23
+ } | undefined, context: Context): FileResult<string[]>;
24
+ openBinaryReader(path: string, options: {
25
+ noFollow?: boolean;
26
+ } | undefined, context: Context): FileResult<BinaryReader>;
27
+ writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void>;
28
+ appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void>;
29
+ truncateFile(path: string, size: number, context: Context): FileResult<void>;
30
+ flushFile(path: string, context: Context): FileResult<void>;
31
+ renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void>;
32
+ fileInfo(path: string, context: Context): FileResult<FileInfo>;
33
+ listDir(path: string, context: Context): FileResult<FileInfo[]>;
34
+ openDirReader(path: string, context: Context): FileResult<DirReader>;
35
+ watch(targets: readonly WatchTarget[], onChange: (change: WatchChange) => void, context: Context): FileResult<FileWatcher>;
36
+ canonicalPath(path: string, context: Context): FileResult<string>;
37
+ exists(path: string, context: Context): FileResult<boolean>;
38
+ createDir(path: string, _options: {
39
+ recursive?: boolean;
40
+ } | undefined, context: Context): FileResult<void>;
41
+ remove(path: string, options: {
42
+ recursive?: boolean;
43
+ force?: boolean;
44
+ } | undefined, context: Context): FileResult<void>;
45
+ createTempDir(prefix: string | undefined, context: Context): FileResult<string>;
46
+ createTempFile(options: {
47
+ prefix?: string;
48
+ suffix?: string;
49
+ } | undefined, context: Context): FileResult<string>;
50
+ exec(command: string | readonly string[], options: ShellExecOptions | undefined, context: Context): Promise<Result<ShellExecResult, ExecutionError>>;
51
+ cleanup(context: Context): Promise<void>;
52
+ }
53
+ export declare const HOME = "/home";
54
+ export declare function kernelEnvironment(client: KernelClient, options?: Omit<KernelEnvOptions, 'cwd'>): (target: {
55
+ cwd?: string;
56
+ }) => SliccKernelEnv;
57
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import { ExecutionError, type Result, type ShellExecOptions, type ShellExecResult } from '@earendil-works/pi-durable/env';
3
+ import type { KernelClient } from './client.ts';
4
+ export declare const SHELL: readonly ['bash', '-c'];
5
+ export declare function execute(client: KernelClient, cwd: string, command: string | readonly string[], options: ShellExecOptions | undefined, context: Context): Promise<Result<ShellExecResult, ExecutionError>>;
@@ -0,0 +1,34 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import { type BinaryReader, type DirReader, FileError, type FileInfo, type LineScan, type Result, type TextLine, type TextLineReader } from '@earendil-works/pi-durable/env';
3
+ import type { KernelFs, KernelStat } from './client.ts';
4
+ export declare function fileError(error: unknown, path?: string): FileError;
5
+ export declare function aborted(context: Context, path?: string): Result<never, FileError> | undefined;
6
+ export declare function attempt<T>(context: Context, path: string, run: () => Promise<T>): Promise<Result<T, FileError>>;
7
+ export declare function info(path: string, stat: KernelStat): FileInfo;
8
+ export declare class SnapshotReader implements BinaryReader {
9
+ #private;
10
+ constructor(bytes: Uint8Array, fileInfo: FileInfo);
11
+ info(): Promise<Result<FileInfo, FileError>>;
12
+ read(offset: number, length: number, context: Context): Promise<Result<Uint8Array, FileError>>;
13
+ scanLines(options: {
14
+ startLine: number;
15
+ endLine?: number;
16
+ }, context: Context): Promise<Result<LineScan, FileError>>;
17
+ close(): Promise<void>;
18
+ }
19
+ export declare class SnapshotLines implements TextLineReader {
20
+ #private;
21
+ constructor(text: string);
22
+ readLine(): Promise<Result<TextLine | undefined, FileError>>;
23
+ close(): Promise<void>;
24
+ }
25
+ export declare class KernelDirReader implements DirReader {
26
+ #private;
27
+ constructor(fs: KernelFs, path: string, names: string[]);
28
+ next(maxEntries: number, context: Context): Promise<Result<{
29
+ entries: FileInfo[];
30
+ done: boolean;
31
+ }, FileError>>;
32
+ rest(): Promise<FileInfo[]>;
33
+ close(): Promise<void>;
34
+ }
@@ -0,0 +1,4 @@
1
+ export declare function normalize(path: string): string;
2
+ export declare function resolve(cwd: string, path: string): string;
3
+ export declare function dirname(path: string): string;
4
+ export declare function basename(path: string): string;
@@ -0,0 +1,10 @@
1
+ import type { FileWatcher, WatchChange, WatchTarget } from '@earendil-works/pi-durable/env';
2
+ import type { KernelFs } from './client.ts';
3
+ type Snapshot = Map<string, string>;
4
+ export declare function snapshot(fs: KernelFs, cwd: string, targets: readonly WatchTarget[]): Promise<Snapshot>;
5
+ export declare function differences(before: Snapshot, after: Snapshot): string[];
6
+ export interface PollOptions {
7
+ intervalMs?: number;
8
+ }
9
+ export declare function pollWatch(fs: KernelFs, cwd: string, targets: readonly WatchTarget[], onChange: (change: WatchChange) => void, options?: PollOptions): Promise<FileWatcher>;
10
+ export {};
@@ -7,11 +7,15 @@ export interface AgentWorker {
7
7
  }
8
8
  export interface AgentOwner {
9
9
  connect(): Promise<AgentConnection>;
10
- restart(): void;
11
- release(): void;
10
+ restart(): Promise<void>;
11
+ release(): Promise<void>;
12
+ }
13
+ export interface KernelSource {
14
+ connect(): Promise<Transferable>;
12
15
  }
13
16
  export interface OwnerOptions {
14
17
  worker: () => AgentWorker;
18
+ kernel?: KernelSource;
15
19
  locks?: LockManager;
16
20
  channel?: () => {
17
21
  port1: PortEndpoint;
@@ -3,3 +3,4 @@ export interface WorkerScope {
3
3
  addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
4
4
  }
5
5
  export declare function serveConnections(scope: WorkerScope, host: Promise<AgentHost>): void;
6
+ export declare function kernelPort(scope: WorkerScope): Promise<MessagePort>;
package/dist/worker.js CHANGED
@@ -8,7 +8,16 @@ function serveConnections(scope, host) {
8
8
  );
9
9
  });
10
10
  }
11
+ function kernelPort(scope) {
12
+ return new Promise((resolve) => {
13
+ scope.addEventListener("message", ({ data }) => {
14
+ const port = data?.kernel;
15
+ if (port) resolve(port);
16
+ });
17
+ });
18
+ }
11
19
  export {
20
+ kernelPort,
12
21
  serveConnections
13
22
  };
14
23
  //# sourceMappingURL=worker.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/worker.ts"],
4
- "sourcesContent": ["import type { AgentHost } from './host.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport interface WorkerScope {\n addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;\n}\n\nexport function serveConnections(scope: WorkerScope, host: Promise<AgentHost>): void {\n scope.addEventListener('message', ({ data }) => {\n const port = (data as { connect?: PortEndpoint } | null)?.connect;\n if (!port) return;\n host.then(\n (ready) => ready.connect(port),\n () => port.close?.()\n );\n });\n}\n"],
5
- "mappings": "AAOO,SAAS,iBAAiB,OAAoB,MAAgC;AACnF,QAAM,iBAAiB,WAAW,CAAC,EAAE,KAAK,MAAM;AAC9C,UAAM,OAAQ,MAA4C;AAC1D,QAAI,CAAC,KAAM;AACX,SAAK;AAAA,MACH,CAAC,UAAU,MAAM,QAAQ,IAAI;AAAA,MAC7B,MAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type { AgentHost } from './host.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport interface WorkerScope {\n addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;\n}\n\nexport function serveConnections(scope: WorkerScope, host: Promise<AgentHost>): void {\n scope.addEventListener('message', ({ data }) => {\n const port = (data as { connect?: PortEndpoint } | null)?.connect;\n if (!port) return;\n host.then(\n (ready) => ready.connect(port),\n () => port.close?.()\n );\n });\n}\n\nexport function kernelPort(scope: WorkerScope): Promise<MessagePort> {\n return new Promise((resolve) => {\n scope.addEventListener('message', ({ data }) => {\n const port = (data as { kernel?: MessagePort } | null)?.kernel;\n if (port) resolve(port);\n });\n });\n}\n"],
5
+ "mappings": "AAOO,SAAS,iBAAiB,OAAoB,MAAgC;AACnF,QAAM,iBAAiB,WAAW,CAAC,EAAE,KAAK,MAAM;AAC9C,UAAM,OAAQ,MAA4C;AAC1D,QAAI,CAAC,KAAM;AACX,SAAK;AAAA,MACH,CAAC,UAAU,MAAM,QAAQ,IAAI;AAAA,MAC7B,MAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF,CAAC;AACH;AAEO,SAAS,WAAW,OAA0C;AACnE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,iBAAiB,WAAW,CAAC,EAAE,KAAK,MAAM;AAC9C,YAAM,OAAQ,MAA0C;AACxD,UAAI,KAAM,SAAQ,IAAI;AAAA,IACxB,CAAC;AAAA,EACH,CAAC;AACH;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -40,7 +40,7 @@
40
40
  "prepack": "npm run build",
41
41
  "start": "npm run build && node test/integration/chrome.mjs",
42
42
  "pretest": "npm run build && node test/integration/build.mjs && playwright-core install --no-shell chromium",
43
- "test": "node --test --test-global-setup=node_modules/@ai-ecoverse/slicc-shared-web/harness/global.mjs 'test/integration/*.test.mjs'",
43
+ "test": "node --test --test-timeout=120000 --test-global-setup=node_modules/@ai-ecoverse/slicc-shared-web/harness/global.mjs 'test/integration/*.test.mjs'",
44
44
  "test:unit": "NODE_NO_WARNINGS=1 mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
45
45
  "typecheck": "tsc",
46
46
  "lint": "slicc-lint",
@@ -50,8 +50,11 @@
50
50
  "release": "semantic-release"
51
51
  },
52
52
  "devDependencies": {
53
+ "@ai-ecoverse/slicc-kernel": "1.9.0",
53
54
  "@ai-ecoverse/slicc-shared-web": "1.5.0",
54
55
  "@ai-ecoverse/slicc-spectrum": "1.7.0",
56
+ "@ai-ecoverse/wasm-bash": "5.3.0-7",
57
+ "@ai-ecoverse/wasm-coreutils": "9.7.0-3",
55
58
  "@biomejs/biome": "2.5.15",
56
59
  "@types/node": "24.19.1",
57
60
  "esbuild": "0.28.2",