@ai-ecoverse/slicc-spectrum 1.0.0 → 1.1.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.
Files changed (80) hide show
  1. package/README.md +63 -3
  2. package/dist/chunk-24WRGUXG.js +168 -0
  3. package/dist/chunk-24WRGUXG.js.map +7 -0
  4. package/dist/chunk-364Y2XOJ.js +28 -0
  5. package/dist/chunk-364Y2XOJ.js.map +7 -0
  6. package/dist/chunk-4RGMMAR4.js +18 -0
  7. package/dist/chunk-4RGMMAR4.js.map +7 -0
  8. package/dist/chunk-5OVGH7HO.js +28 -0
  9. package/dist/chunk-5OVGH7HO.js.map +7 -0
  10. package/dist/chunk-6A6NIESE.js +9 -0
  11. package/dist/chunk-6A6NIESE.js.map +7 -0
  12. package/dist/chunk-EN3XGF2M.js +26 -0
  13. package/dist/chunk-EN3XGF2M.js.map +7 -0
  14. package/dist/chunk-ER3HU7OE.js +22 -0
  15. package/dist/chunk-ER3HU7OE.js.map +7 -0
  16. package/dist/chunk-FN3Y2AYJ.js +17 -0
  17. package/dist/chunk-FN3Y2AYJ.js.map +7 -0
  18. package/dist/chunk-H5WU2XYY.js +8 -0
  19. package/dist/chunk-H5WU2XYY.js.map +7 -0
  20. package/dist/chunk-HBUX7E54.js +234 -0
  21. package/dist/chunk-HBUX7E54.js.map +7 -0
  22. package/dist/chunk-IBQ4O4NO.js +13 -0
  23. package/dist/chunk-IBQ4O4NO.js.map +7 -0
  24. package/dist/chunk-J45KH7MF.js +180 -0
  25. package/dist/chunk-J45KH7MF.js.map +7 -0
  26. package/dist/chunk-JASEW37G.js +411 -0
  27. package/dist/chunk-JASEW37G.js.map +7 -0
  28. package/dist/chunk-MR7SU4GJ.js +59 -0
  29. package/dist/chunk-MR7SU4GJ.js.map +7 -0
  30. package/dist/chunk-MTF6JFSL.js +1093 -0
  31. package/dist/chunk-MTF6JFSL.js.map +7 -0
  32. package/dist/chunk-Q4AC2O6Z.js +13 -0
  33. package/dist/chunk-Q4AC2O6Z.js.map +7 -0
  34. package/dist/chunk-SZ33GAVH.js +2638 -0
  35. package/dist/chunk-SZ33GAVH.js.map +7 -0
  36. package/dist/chunk-T5NHW4KC.js +23 -0
  37. package/dist/chunk-T5NHW4KC.js.map +7 -0
  38. package/dist/chunk-T5QFSUYQ.js +8 -0
  39. package/dist/chunk-T5QFSUYQ.js.map +7 -0
  40. package/dist/chunk-TS7SL372.js +687 -0
  41. package/dist/chunk-TS7SL372.js.map +7 -0
  42. package/dist/chunk-Y4Z7K443.js +64 -0
  43. package/dist/chunk-Y4Z7K443.js.map +7 -0
  44. package/dist/chunk-ZMBN6APP.js +11 -0
  45. package/dist/chunk-ZMBN6APP.js.map +7 -0
  46. package/dist/chunk-ZTPYBVZL.js +181 -0
  47. package/dist/chunk-ZTPYBVZL.js.map +7 -0
  48. package/dist/fonts/source-code-pro-latin-400-normal.woff2 +0 -0
  49. package/dist/fonts/source-code-pro-latin-600-normal.woff2 +0 -0
  50. package/dist/slicc-dummy.js +1393 -0
  51. package/dist/slicc-dummy.js.map +7 -0
  52. package/dist/slicc-terminal.js +2 -27
  53. package/dist/slicc-terminal.js.map +4 -4
  54. package/dist/slicc-ui.js +22441 -0
  55. package/dist/slicc-ui.js.map +7 -0
  56. package/dist/types/app/agents.d.ts +11 -0
  57. package/dist/types/app/app.d.ts +40 -0
  58. package/dist/types/app/base.d.ts +27 -0
  59. package/dist/types/app/chat.d.ts +23 -0
  60. package/dist/types/app/fonts.d.ts +3 -0
  61. package/dist/types/app/markdown.d.ts +24 -0
  62. package/dist/types/app/panels.d.ts +31 -0
  63. package/dist/types/app/spectrum.d.ts +19 -0
  64. package/dist/types/components/dock.d.ts +24 -0
  65. package/dist/types/components/sheets.d.ts +1 -0
  66. package/dist/types/dummy/agent.d.ts +14 -0
  67. package/dist/types/dummy/browser.d.ts +15 -0
  68. package/dist/types/dummy/clock.d.ts +5 -0
  69. package/dist/types/dummy/files.d.ts +14 -0
  70. package/dist/types/dummy/fixtures.d.ts +11 -0
  71. package/dist/types/dummy/index.d.ts +6 -0
  72. package/dist/types/dummy/script.d.ts +23 -0
  73. package/dist/types/dummy/settings.d.ts +13 -0
  74. package/dist/types/dummy/shell.d.ts +16 -0
  75. package/dist/types/dummy/terminals.d.ts +15 -0
  76. package/dist/types/dummy.d.ts +2 -0
  77. package/dist/types/model/emitter.d.ts +9 -0
  78. package/dist/types/model/types.d.ts +183 -0
  79. package/dist/types/ui.d.ts +15 -0
  80. package/package.json +29 -11
@@ -0,0 +1,1393 @@
1
+ import {
2
+ Emitter
3
+ } from "./chunk-T5NHW4KC.js";
4
+ import "./chunk-Q4AC2O6Z.js";
5
+
6
+ // src/dummy/script.ts
7
+ var units = "/workspace/harbor/src/lib/units.ts";
8
+ var kelvin = `
9
+ export function toKelvin(celsius: number): number {
10
+ return Math.round((celsius + 273.15) * 100) / 100;
11
+ }
12
+ `;
13
+ function test() {
14
+ return [
15
+ { type: "thinking", text: "Run the whole suite first, then look at anything that fails." },
16
+ {
17
+ type: "tool",
18
+ name: "bash",
19
+ title: "Run the tests",
20
+ input: "npm test",
21
+ output: "> harbor@0.0.0 test\n> node --test test/\n\n\u2714 converts both ways (0.6ms)\n\u2714 expires entries from the previous day (0.4ms)\n\u2714 retries upstream timeouts (3.1ms)\n\u2139 tests 3\n\u2139 pass 3\n\u2139 fail 0",
22
+ paths: [],
23
+ ticks: 40
24
+ },
25
+ {
26
+ type: "text",
27
+ text: "All **3 tests pass**, including the new one for the midnight rollover. Nothing else to fix."
28
+ }
29
+ ];
30
+ }
31
+ function edit() {
32
+ return [
33
+ {
34
+ type: "thinking",
35
+ text: "The conversion helpers live in units.ts. Add the missing one next to them."
36
+ },
37
+ {
38
+ type: "tool",
39
+ name: "read_file",
40
+ title: "Read the unit helpers",
41
+ input: units,
42
+ output: "export function toCelsius(fahrenheit: number): number { \u2026 }\nexport function toFahrenheit(celsius: number): number { \u2026 }",
43
+ paths: [units],
44
+ ticks: 10
45
+ },
46
+ {
47
+ type: "tool",
48
+ name: "edit_file",
49
+ title: "Add toKelvin()",
50
+ input: units,
51
+ output: "Appended 4 lines",
52
+ paths: [units],
53
+ ticks: 20,
54
+ effect: async ({ files: files3, agentId }) => {
55
+ const before = await files3.read(units);
56
+ await files3.write(units, before + kelvin, agentId);
57
+ }
58
+ },
59
+ {
60
+ type: "text",
61
+ text: "Added `toKelvin()` to `src/lib/units.ts`. It rounds to two decimals, like the API\u2019s other values. The change is in **Changes**, ready for review."
62
+ }
63
+ ];
64
+ }
65
+ function browse() {
66
+ const url = "https://api.example.com/docs/v2/units";
67
+ return [
68
+ {
69
+ type: "tool",
70
+ name: "browser",
71
+ title: "Open the units reference",
72
+ input: `navigate ${url}`,
73
+ output: "Loaded api.example.com/docs/v2/units (200, 412 ms)",
74
+ paths: [],
75
+ ticks: 25,
76
+ effect: ({ browser, agentId }) => browser.open(url, agentId)
77
+ },
78
+ {
79
+ type: "text",
80
+ text: "The reference page is open in the **Browser** panel. Temperatures are documented in Celsius, with Fahrenheit behind `?units=imperial`."
81
+ }
82
+ ];
83
+ }
84
+ function files() {
85
+ return [
86
+ {
87
+ type: "tool",
88
+ name: "bash",
89
+ title: "List the project",
90
+ input: "ls /workspace/harbor/src",
91
+ output: "index.ts legacy lib routes",
92
+ paths: ["/workspace/harbor/src"],
93
+ ticks: 8
94
+ },
95
+ {
96
+ type: "tool",
97
+ name: "read_file",
98
+ title: "Read the README",
99
+ input: "/workspace/harbor/README.md",
100
+ output: "# harbor\n\nA small forecast API that sits in front of the upstream weather service and caches per city.",
101
+ paths: ["/workspace/harbor/README.md"],
102
+ ticks: 8
103
+ },
104
+ {
105
+ type: "text",
106
+ text: "harbor has four parts:\n\n- `index.ts`: the worker entry and routing\n- `routes/forecast.ts`: the forecast handler\n- `lib/`: cache, retry and unit helpers\n- `legacy/`: the XML output, marked for removal"
107
+ }
108
+ ];
109
+ }
110
+ function chat(prompt) {
111
+ return [
112
+ { type: "thinking", text: "Short question. Answer directly, then offer the next step." },
113
+ {
114
+ type: "tool",
115
+ name: "bash",
116
+ title: "Check the working tree",
117
+ input: "git status --short",
118
+ output: " M src/lib/cache.ts\n M src/routes/forecast.ts\n A src/lib/retry.ts\n D src/legacy/xml.ts",
119
+ paths: [],
120
+ ticks: 12
121
+ },
122
+ {
123
+ type: "text",
124
+ text: `On \u201C${prompt.slice(0, 60)}\u201D: there are **4 uncommitted changes** in harbor, from me and quiet-otter. Review them in Changes, or ask me to run the tests, edit a file or open a page.`
125
+ }
126
+ ];
127
+ }
128
+ function script(prompt) {
129
+ const text = prompt.toLowerCase();
130
+ if (/\btests?\b/.test(text)) return test();
131
+ if (/\b(fix|edit|add|change)\b/.test(text)) return edit();
132
+ if (/\b(open|browse|page|docs)\b/.test(text)) return browse();
133
+ if (/\b(files?|read|show|project)\b/.test(text)) return files();
134
+ return chat(prompt);
135
+ }
136
+
137
+ // src/dummy/agent.ts
138
+ function chunks(text) {
139
+ return text.match(/\S+\s*|\s+/g) ?? [];
140
+ }
141
+ var DummyAgent = class extends Emitter {
142
+ #agents;
143
+ #messages = /* @__PURE__ */ new Map();
144
+ #runs = /* @__PURE__ */ new Map();
145
+ #active;
146
+ #clock;
147
+ #effects;
148
+ #next = 1;
149
+ constructor(agents2, conversations2, effects, clock2) {
150
+ super();
151
+ this.#clock = clock2;
152
+ this.#effects = effects;
153
+ this.#agents = agents2.map((agent) => ({ ...agent }));
154
+ for (const agent of this.#agents) {
155
+ this.#messages.set(agent.id, structuredClone([...conversations2[agent.id] ?? []]));
156
+ }
157
+ this.#active = this.#agents[0]?.id ?? "";
158
+ }
159
+ list() {
160
+ return this.#agents.map((agent) => ({ ...agent }));
161
+ }
162
+ active() {
163
+ return this.#active;
164
+ }
165
+ #agent(id) {
166
+ return this.#agents.find((agent) => agent.id === id);
167
+ }
168
+ #changed() {
169
+ this.emit("agents", this.list());
170
+ }
171
+ select(id) {
172
+ const agent = this.#agent(id);
173
+ if (!agent) return;
174
+ this.#active = id;
175
+ agent.unread = 0;
176
+ this.emit("active", id);
177
+ this.#changed();
178
+ }
179
+ messages(agentId) {
180
+ return [...this.#messages.get(agentId) ?? []];
181
+ }
182
+ #post(agentId, message) {
183
+ this.#messages.get(agentId)?.push(message);
184
+ this.emit("message", { agentId, message });
185
+ }
186
+ #status(agentId, status) {
187
+ const agent = this.#agent(agentId);
188
+ if (!agent || agent.status === status) return;
189
+ agent.status = status;
190
+ this.#changed();
191
+ }
192
+ async #stream(agentId, reply, part, text, run) {
193
+ for (const chunk of chunks(text)) {
194
+ await this.#clock.sleep();
195
+ if (run.stopped) return;
196
+ part.text += chunk;
197
+ this.emit("message", { agentId, message: reply });
198
+ }
199
+ }
200
+ async #step(agentId, reply, step, run) {
201
+ if (step.type !== "tool") {
202
+ this.#status(agentId, "thinking");
203
+ const part = { type: step.type, text: "" };
204
+ reply.parts.push(part);
205
+ await this.#stream(agentId, reply, part, step.text, run);
206
+ return;
207
+ }
208
+ this.#status(agentId, "working");
209
+ const tool = {
210
+ id: `t-${this.#next++}`,
211
+ name: step.name,
212
+ title: step.title,
213
+ input: step.input,
214
+ output: "",
215
+ status: "running",
216
+ paths: step.paths
217
+ };
218
+ reply.parts.push({ type: "tool", tool });
219
+ this.emit("message", { agentId, message: reply });
220
+ await this.#clock.sleep(step.ticks);
221
+ const done = run.stopped ? { status: "error", output: "Stopped" } : { status: "done", output: step.output };
222
+ if (!run.stopped) await step.effect?.({ ...this.#effects, agentId });
223
+ Object.assign(tool, done);
224
+ this.emit("message", { agentId, message: reply });
225
+ }
226
+ async send(agentId, text) {
227
+ const agent = this.#agent(agentId);
228
+ const prompt = text.trim();
229
+ if (!agent || !prompt) return;
230
+ this.stop(agentId);
231
+ const run = { stopped: false };
232
+ this.#runs.set(agentId, run);
233
+ this.#post(agentId, {
234
+ id: `m-${this.#next++}`,
235
+ role: "user",
236
+ text: prompt,
237
+ createdAt: Date.now()
238
+ });
239
+ const reply = {
240
+ id: `m-${this.#next++}`,
241
+ role: "assistant",
242
+ parts: [],
243
+ status: "streaming",
244
+ createdAt: Date.now()
245
+ };
246
+ this.#post(agentId, reply);
247
+ for (const step of script(prompt)) {
248
+ if (run.stopped) break;
249
+ await this.#step(agentId, reply, step, run);
250
+ }
251
+ reply.status = run.stopped ? "stopped" : "done";
252
+ if (this.#runs.get(agentId) === run) this.#runs.delete(agentId);
253
+ agent.contextFill = Math.min(0.95, Math.round((agent.contextFill + 0.03) * 100) / 100);
254
+ if (agentId !== this.#active) agent.unread += 1;
255
+ this.emit("message", { agentId, message: reply });
256
+ agent.status = "idle";
257
+ this.#changed();
258
+ }
259
+ stop(agentId) {
260
+ const run = this.#runs.get(agentId);
261
+ if (run) run.stopped = true;
262
+ }
263
+ };
264
+
265
+ // src/dummy/browser.ts
266
+ function escape(text) {
267
+ return text.replace(/[<>&"]/g, (c) => `&#${c.charCodeAt(0)};`);
268
+ }
269
+ function hue(text) {
270
+ let sum = 0;
271
+ for (const char of text) sum = (sum * 31 + char.charCodeAt(0)) % 360;
272
+ return sum;
273
+ }
274
+ function page(tab) {
275
+ const h = hue(new URL(tab.url).host);
276
+ const rows = [0, 1, 2, 3, 4, 5].map(
277
+ (i) => `<rect x="64" y="${220 + i * 36}" width="${560 - i % 3 * 90}" height="12" rx="6" fill="#d6d6d6"/>`
278
+ ).join("");
279
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800"><rect width="1280" height="800" fill="#ffffff"/><rect width="1280" height="64" fill="hsl(${h} 45% 32%)"/><text x="64" y="40" font-family="sans-serif" font-size="20" fill="#ffffff">${escape(new URL(tab.url).host)}</text><text x="64" y="160" font-family="sans-serif" font-size="36" fill="#222222">${escape(tab.title)}</text>${rows}<rect x="720" y="200" width="496" height="280" rx="12" fill="hsl(${h} 40% 92%)"/></svg>`;
280
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
281
+ }
282
+ function titleFor(url) {
283
+ const { host, pathname } = new URL(url);
284
+ return pathname === "/" ? host : `${host}${pathname}`;
285
+ }
286
+ var DummyBrowser = class extends Emitter {
287
+ #tabs;
288
+ #active;
289
+ #clock;
290
+ #next = 1;
291
+ constructor(tabs2, clock2) {
292
+ super();
293
+ this.#clock = clock2;
294
+ this.#tabs = tabs2.map((tab) => ({ ...tab }));
295
+ this.#active = this.#tabs[0]?.id ?? null;
296
+ }
297
+ list() {
298
+ return this.#tabs.map((tab) => ({ ...tab }));
299
+ }
300
+ active() {
301
+ return this.#active;
302
+ }
303
+ #changed() {
304
+ this.emit("tabs", this.list());
305
+ this.emit("active", this.#active);
306
+ }
307
+ activate(id) {
308
+ if (!this.#tabs.some((tab) => tab.id === id)) return;
309
+ this.#active = id;
310
+ this.emit("active", id);
311
+ }
312
+ #load(id) {
313
+ void this.#clock.sleep(15).then(() => {
314
+ const tab = this.#tabs.find((candidate) => candidate.id === id);
315
+ if (!tab) return;
316
+ tab.status = "complete";
317
+ tab.title = titleFor(tab.url);
318
+ this.#changed();
319
+ });
320
+ }
321
+ open(url, agentId = null) {
322
+ const tab = {
323
+ id: `tab-new-${this.#next++}`,
324
+ title: url,
325
+ url,
326
+ status: "loading",
327
+ agentId
328
+ };
329
+ this.#tabs.push(tab);
330
+ this.#active = tab.id;
331
+ this.#changed();
332
+ this.#load(tab.id);
333
+ return { ...tab };
334
+ }
335
+ navigate(id, url) {
336
+ const tab = this.#tabs.find((candidate) => candidate.id === id);
337
+ if (!tab) return;
338
+ tab.url = url;
339
+ tab.title = url;
340
+ tab.status = "loading";
341
+ this.#changed();
342
+ this.#load(id);
343
+ }
344
+ close(id) {
345
+ const index2 = this.#tabs.findIndex((tab) => tab.id === id);
346
+ if (index2 < 0) return;
347
+ this.#tabs.splice(index2, 1);
348
+ if (this.#active === id)
349
+ this.#active = this.#tabs[Math.min(index2, this.#tabs.length - 1)]?.id ?? null;
350
+ this.#changed();
351
+ }
352
+ async screenshot(id) {
353
+ await this.#clock.sleep(2);
354
+ const tab = this.#tabs.find((candidate) => candidate.id === id);
355
+ if (!tab) throw new Error(`No tab ${id}`);
356
+ return page(tab);
357
+ }
358
+ };
359
+
360
+ // src/dummy/clock.ts
361
+ function clock(delay) {
362
+ return {
363
+ delay,
364
+ sleep: (ticks = 1) => new Promise((resolve2) => setTimeout(resolve2, delay * ticks))
365
+ };
366
+ }
367
+
368
+ // src/dummy/files.ts
369
+ function parents(path) {
370
+ const parts = path.split("/").slice(1, -1);
371
+ return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
372
+ }
373
+ var DummyFiles = class extends Emitter {
374
+ #contents = /* @__PURE__ */ new Map();
375
+ #directories = /* @__PURE__ */ new Set();
376
+ #modified = /* @__PURE__ */ new Map();
377
+ #changes = /* @__PURE__ */ new Map();
378
+ #clock;
379
+ constructor(contents, directories2, changes, clock2) {
380
+ super();
381
+ this.#clock = clock2;
382
+ for (const directory of directories2) this.#mkdirs(`${directory}/`);
383
+ for (const [path, text] of Object.entries(contents)) this.#put(path, text, 0);
384
+ for (const change of changes) this.#changes.set(change.path, { ...change });
385
+ for (const change of changes) {
386
+ if (change.after === null) this.#contents.delete(change.path);
387
+ else this.#put(change.path, change.after, 0);
388
+ }
389
+ }
390
+ #mkdirs(path) {
391
+ for (const directory of parents(path)) this.#directories.add(directory);
392
+ }
393
+ #put(path, text, now) {
394
+ this.#mkdirs(path);
395
+ this.#contents.set(path, text);
396
+ this.#modified.set(path, now);
397
+ }
398
+ #entries() {
399
+ const directories2 = [...this.#directories].map(
400
+ (path) => ({ path, kind: "directory", size: 0, modified: 0 })
401
+ );
402
+ const files3 = [...this.#contents].map(
403
+ ([path, text]) => ({
404
+ path,
405
+ kind: "file",
406
+ size: new TextEncoder().encode(text).length,
407
+ modified: this.#modified.get(path)
408
+ })
409
+ );
410
+ return [...directories2, ...files3].sort((a, b) => a.path.localeCompare(b.path));
411
+ }
412
+ async list() {
413
+ await this.#clock.sleep();
414
+ return this.#entries();
415
+ }
416
+ async read(path) {
417
+ await this.#clock.sleep();
418
+ const text = this.#contents.get(path);
419
+ if (text === void 0) throw new Error(`ENOENT: ${path}`);
420
+ return text;
421
+ }
422
+ #record(path, after, agentId) {
423
+ const previous = this.#changes.get(path);
424
+ const before = previous ? previous.before : this.#contents.get(path) ?? null;
425
+ if (before === after) {
426
+ this.#changes.delete(path);
427
+ return;
428
+ }
429
+ const status = before === null ? "added" : after === null ? "deleted" : "modified";
430
+ this.#changes.set(path, { path, status, before, after, agentId });
431
+ }
432
+ #announce(path) {
433
+ this.emit("files", this.#entries());
434
+ this.emit("file", path);
435
+ this.emit("changes", this.changes());
436
+ }
437
+ async write(path, text, agentId = null) {
438
+ await this.#clock.sleep();
439
+ this.#record(path, text, agentId);
440
+ this.#put(path, text, Date.now());
441
+ this.#announce(path);
442
+ }
443
+ async remove(path, agentId = null) {
444
+ await this.#clock.sleep();
445
+ if (!this.#contents.has(path)) throw new Error(`ENOENT: ${path}`);
446
+ this.#record(path, null, agentId);
447
+ this.#contents.delete(path);
448
+ this.#announce(path);
449
+ }
450
+ changes() {
451
+ return [...this.#changes.values()].sort((a, b) => a.path.localeCompare(b.path));
452
+ }
453
+ accept(path) {
454
+ this.#changes.delete(path);
455
+ this.emit("changes", this.changes());
456
+ }
457
+ async revert(path) {
458
+ const change = this.#changes.get(path);
459
+ if (!change) return;
460
+ await this.#clock.sleep();
461
+ this.#changes.delete(path);
462
+ if (change.before === null) this.#contents.delete(path);
463
+ else this.#put(path, change.before, Date.now());
464
+ this.#announce(path);
465
+ }
466
+ };
467
+
468
+ // src/dummy/fixtures.ts
469
+ var cacheBefore = `export interface Entry<T> {
470
+ value: T;
471
+ storedAt: number;
472
+ }
473
+
474
+ const TTL = 60 * 60 * 1000;
475
+
476
+ export class ForecastCache<T> {
477
+ #entries = new Map<string, Entry<T>>();
478
+
479
+ get(key: string, now = Date.now()): T | undefined {
480
+ const entry = this.#entries.get(key);
481
+ if (!entry) return undefined;
482
+ const day = new Date(entry.storedAt).getDate();
483
+ if (day !== new Date(now).getDate()) return entry.value;
484
+ if (now - entry.storedAt > TTL) return undefined;
485
+ return entry.value;
486
+ }
487
+
488
+ set(key: string, value: T, now = Date.now()): void {
489
+ this.#entries.set(key, { value, storedAt: now });
490
+ }
491
+ }
492
+ `;
493
+ var cacheAfter = `export interface Entry<T> {
494
+ value: T;
495
+ storedAt: number;
496
+ }
497
+
498
+ const TTL = 60 * 60 * 1000;
499
+
500
+ export class ForecastCache<T> {
501
+ #entries = new Map<string, Entry<T>>();
502
+
503
+ get(key: string, now = Date.now()): T | undefined {
504
+ const entry = this.#entries.get(key);
505
+ if (!entry) return undefined;
506
+ if (dayOf(entry.storedAt) !== dayOf(now)) {
507
+ this.#entries.delete(key);
508
+ return undefined;
509
+ }
510
+ if (now - entry.storedAt > TTL) return undefined;
511
+ return entry.value;
512
+ }
513
+
514
+ set(key: string, value: T, now = Date.now()): void {
515
+ this.#entries.set(key, { value, storedAt: now });
516
+ }
517
+ }
518
+
519
+ function dayOf(time: number): string {
520
+ return new Date(time).toISOString().slice(0, 10);
521
+ }
522
+ `;
523
+ var retry = `export async function retry<T>(task: () => Promise<T>, attempts = 3, wait = 250): Promise<T> {
524
+ let last: unknown;
525
+ for (let attempt = 0; attempt < attempts; attempt++) {
526
+ try {
527
+ return await task();
528
+ } catch (error) {
529
+ last = error;
530
+ await new Promise((resolve) => setTimeout(resolve, wait * 2 ** attempt));
531
+ }
532
+ }
533
+ throw last;
534
+ }
535
+ `;
536
+ var legacyXml = `export function toXml(forecast: { city: string; high: number; low: number }): string {
537
+ return \`<forecast city="\${forecast.city}"><high>\${forecast.high}</high><low>\${forecast.low}</low></forecast>\`;
538
+ }
539
+ `;
540
+ var forecastBefore = `import { ForecastCache } from '../lib/cache.ts';
541
+ import { toCelsius } from '../lib/units.ts';
542
+
543
+ const cache = new ForecastCache<Forecast>();
544
+
545
+ export interface Forecast {
546
+ city: string;
547
+ high: number;
548
+ low: number;
549
+ summary: string;
550
+ }
551
+
552
+ export async function forecast(city: string, upstream: typeof fetch = fetch): Promise<Forecast> {
553
+ const cached = cache.get(city);
554
+ if (cached) return cached;
555
+ const response = await upstream(\`https://api.example.com/v2/forecast?city=\${encodeURIComponent(city)}\`);
556
+ const body = await response.json();
557
+ const result = {
558
+ city,
559
+ high: toCelsius(body.daily.max),
560
+ low: toCelsius(body.daily.min),
561
+ summary: body.daily.summary,
562
+ };
563
+ cache.set(city, result);
564
+ return result;
565
+ }
566
+ `;
567
+ var forecastAfter = forecastBefore.replace(
568
+ "import { toCelsius } from '../lib/units.ts';",
569
+ "import { retry } from '../lib/retry.ts';\nimport { toCelsius } from '../lib/units.ts';"
570
+ ).replace("const response = await upstream(", "const response = await retry(() =>\n upstream(").replace("(city)}`);", "(city)}`)\n );");
571
+ var units2 = `export function toCelsius(fahrenheit: number): number {
572
+ return Math.round(((fahrenheit - 32) * 5) / 9);
573
+ }
574
+
575
+ export function toFahrenheit(celsius: number): number {
576
+ return Math.round((celsius * 9) / 5 + 32);
577
+ }
578
+ `;
579
+ var unitsTest = `import assert from 'node:assert/strict';
580
+ import { test } from 'node:test';
581
+ import { toCelsius, toFahrenheit } from '../src/lib/units.ts';
582
+
583
+ test('converts both ways', () => {
584
+ assert.equal(toCelsius(212), 100);
585
+ assert.equal(toFahrenheit(0), 32);
586
+ });
587
+ `;
588
+ var index = `import { forecast } from './routes/forecast.ts';
589
+
590
+ export default {
591
+ async fetch(request: Request): Promise<Response> {
592
+ const url = new URL(request.url);
593
+ if (url.pathname === '/health') return new Response('ok');
594
+ const city = url.searchParams.get('city');
595
+ if (!city) return Response.json({ error: 'city is required' }, { status: 400 });
596
+ return Response.json(await forecast(city));
597
+ },
598
+ };
599
+ `;
600
+ var readme = `# harbor
601
+
602
+ A small forecast API that sits in front of the upstream weather service and caches per city.
603
+
604
+ ## Development
605
+
606
+ \`\`\`sh
607
+ npm install
608
+ npm test
609
+ npm run dev
610
+ \`\`\`
611
+
612
+ The worker listens on \`http://localhost:8787\`. Try \`/forecast?city=Lisbon\`.
613
+ `;
614
+ var pkg = `{
615
+ "name": "harbor",
616
+ "private": true,
617
+ "type": "module",
618
+ "scripts": {
619
+ "dev": "wrangler dev",
620
+ "test": "node --test test/"
621
+ },
622
+ "devDependencies": {
623
+ "wrangler": "4.40.0"
624
+ }
625
+ }
626
+ `;
627
+ var skill = `---
628
+ name: release-notes
629
+ description: Draft release notes from merged pull requests since the last tag.
630
+ ---
631
+
632
+ 1. Run \`collect.jsh\` with the repository and the previous tag.
633
+ 2. Group the pull requests by label: features, fixes, chores.
634
+ 3. Write one sentence per change, in the past tense, without ticket numbers.
635
+ `;
636
+ var collect = `const [repo, since] = process.argv.slice(2);
637
+ const pulls = await gh(\`repos/\${repo}/pulls?state=closed&per_page=100\`);
638
+ for (const pull of pulls.filter((p) => p.merged_at > since)) {
639
+ console.log(\`\${pull.number}\\t\${pull.labels.map((l) => l.name).join(',')}\\t\${pull.title}\`);
640
+ }
641
+ `;
642
+ var memory = `# Shared memory
643
+
644
+ ## Preferences
645
+ - Keep replies short. Lead with the result, then the evidence.
646
+ - Run the tests before saying something is fixed.
647
+
648
+ ## Projects
649
+ - harbor: forecast API, deployed as a worker. Cache bugs show up around midnight UTC.
650
+ - release-notes: drafted every Friday from merged pull requests.
651
+ `;
652
+ var standup = `# Standup, 4 October
653
+
654
+ - harbor: retries for upstream timeouts are in review.
655
+ - Docs: the forecast endpoint page still shows Fahrenheit examples.
656
+ - Next: move the cache to KV once the TTL fix lands.
657
+ `;
658
+ var sample = `{
659
+ "city": "Lisbon",
660
+ "daily": { "max": 77, "min": 61, "summary": "Clear, light wind from the north" }
661
+ }
662
+ `;
663
+ var bashrc = `export PS1='\\u@slicc:\\w\\$ '
664
+ alias ll='ls -la'
665
+ `;
666
+ var files2 = {
667
+ "/workspace/harbor/README.md": readme,
668
+ "/workspace/harbor/package.json": pkg,
669
+ "/workspace/harbor/wrangler.toml": 'name = "harbor"\nmain = "src/index.ts"\ncompatibility_date = "2026-09-01"\n',
670
+ "/workspace/harbor/.gitignore": "node_modules/\n.wrangler/\n",
671
+ "/workspace/harbor/src/index.ts": index,
672
+ "/workspace/harbor/src/routes/forecast.ts": forecastBefore,
673
+ "/workspace/harbor/src/lib/cache.ts": cacheBefore,
674
+ "/workspace/harbor/src/lib/units.ts": units2,
675
+ "/workspace/harbor/src/legacy/xml.ts": legacyXml,
676
+ "/workspace/harbor/test/units.test.ts": unitsTest,
677
+ "/workspace/skills/release-notes/SKILL.md": skill,
678
+ "/workspace/skills/release-notes/scripts/collect.jsh": collect,
679
+ "/shared/MEMORY.md": memory,
680
+ "/shared/notes/2026-10-04-standup.md": standup,
681
+ "/home/user/.bashrc": bashrc,
682
+ "/tmp/forecast-sample.json": sample
683
+ };
684
+ var directories = ["/sessions", "/home/user/downloads"];
685
+ var pending = [
686
+ {
687
+ path: "/workspace/harbor/src/lib/cache.ts",
688
+ status: "modified",
689
+ before: cacheBefore,
690
+ after: cacheAfter,
691
+ agentId: "cone-harbor"
692
+ },
693
+ {
694
+ path: "/workspace/harbor/src/lib/retry.ts",
695
+ status: "added",
696
+ before: null,
697
+ after: retry,
698
+ agentId: "scoop-otter"
699
+ },
700
+ {
701
+ path: "/workspace/harbor/src/routes/forecast.ts",
702
+ status: "modified",
703
+ before: forecastBefore,
704
+ after: forecastAfter,
705
+ agentId: "scoop-otter"
706
+ },
707
+ {
708
+ path: "/workspace/harbor/src/legacy/xml.ts",
709
+ status: "deleted",
710
+ before: legacyXml,
711
+ after: null,
712
+ agentId: "cone-harbor"
713
+ }
714
+ ];
715
+ var models = [
716
+ { id: "claude-opus-5-5", label: "Claude Opus 5.5", provider: "Anthropic" },
717
+ { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", provider: "Anthropic" },
718
+ { id: "claude-haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic" },
719
+ { id: "local-small", label: "Local (ort-llama)", provider: "This browser" }
720
+ ];
721
+ var defaults = {
722
+ color: "system",
723
+ model: "claude-sonnet-5-5",
724
+ thinking: "medium",
725
+ sendOnEnter: true,
726
+ showThinking: true,
727
+ diffStyle: "unified"
728
+ };
729
+ var accounts = [
730
+ { id: "anthropic", provider: "Anthropic", identity: "API key ending 3f2a", status: "connected" },
731
+ { id: "github", provider: "GitHub", identity: "@example-dev", status: "connected" },
732
+ { id: "adobe", provider: "Adobe", identity: "dev@example.com", status: "expired" },
733
+ { id: "openai", provider: "OpenAI", identity: "", status: "disconnected" }
734
+ ];
735
+ var agents = [
736
+ {
737
+ id: "cone-sliccy",
738
+ name: "sliccy",
739
+ kind: "cone",
740
+ parentId: null,
741
+ status: "idle",
742
+ model: "claude-sonnet-5-5",
743
+ contextFill: 0.21,
744
+ unread: 0
745
+ },
746
+ {
747
+ id: "cone-harbor",
748
+ name: "harbor",
749
+ kind: "cone",
750
+ parentId: null,
751
+ status: "idle",
752
+ model: "claude-opus-5-5",
753
+ contextFill: 0.38,
754
+ unread: 0
755
+ },
756
+ {
757
+ id: "scoop-otter",
758
+ name: "quiet-otter",
759
+ kind: "scoop",
760
+ parentId: "cone-harbor",
761
+ status: "working",
762
+ model: "claude-haiku-4-5",
763
+ contextFill: 0.12,
764
+ unread: 0
765
+ },
766
+ {
767
+ id: "scoop-heron",
768
+ name: "amber-heron",
769
+ kind: "scoop",
770
+ parentId: "cone-harbor",
771
+ status: "idle",
772
+ model: "claude-haiku-4-5",
773
+ contextFill: 0.04,
774
+ unread: 0
775
+ },
776
+ {
777
+ id: "cone-release",
778
+ name: "release-notes",
779
+ kind: "cone",
780
+ parentId: null,
781
+ status: "idle",
782
+ model: "claude-sonnet-5-5",
783
+ contextFill: 0.09,
784
+ unread: 1
785
+ },
786
+ {
787
+ id: "cone-triage",
788
+ name: "inbox-triage",
789
+ kind: "cone",
790
+ parentId: null,
791
+ status: "waiting",
792
+ model: "claude-sonnet-5-5",
793
+ contextFill: 0.62,
794
+ unread: 2
795
+ },
796
+ {
797
+ id: "scoop-wren",
798
+ name: "tidal-wren",
799
+ kind: "scoop",
800
+ parentId: "cone-sliccy",
801
+ status: "idle",
802
+ model: "claude-haiku-4-5",
803
+ contextFill: 0.02,
804
+ unread: 0
805
+ }
806
+ ];
807
+ var start = Date.UTC(2026, 9, 5, 8, 30);
808
+ var at = (minutes) => start + minutes * 6e4;
809
+ var conversations = {
810
+ "cone-sliccy": [
811
+ {
812
+ id: "m-s1",
813
+ role: "user",
814
+ text: "What is on my plate today?",
815
+ createdAt: at(0)
816
+ },
817
+ {
818
+ id: "m-s2",
819
+ role: "assistant",
820
+ status: "done",
821
+ createdAt: at(1),
822
+ parts: [
823
+ {
824
+ type: "tool",
825
+ tool: {
826
+ id: "t-s1",
827
+ name: "read_file",
828
+ title: "Read the standup notes",
829
+ input: "/shared/notes/2026-10-04-standup.md",
830
+ output: standup,
831
+ status: "done",
832
+ paths: ["/shared/notes/2026-10-04-standup.md"]
833
+ }
834
+ },
835
+ {
836
+ type: "text",
837
+ text: "Three things carry over from yesterday:\n\n1. **harbor**: the retry change is in review, and the midnight cache bug is still open.\n2. **Docs**: the forecast page still shows Fahrenheit examples.\n3. **release-notes** has a draft waiting for you.\n\nThe `harbor` cone is the one to start with."
838
+ }
839
+ ]
840
+ }
841
+ ],
842
+ "cone-harbor": [
843
+ {
844
+ id: "m-h1",
845
+ role: "event",
846
+ kind: "webhook",
847
+ title: "GitHub \xB7 issue opened",
848
+ text: "example/harbor#57: Forecast stays on yesterday after midnight UTC",
849
+ createdAt: at(2)
850
+ },
851
+ {
852
+ id: "m-h2",
853
+ role: "user",
854
+ text: "The forecast endpoint serves stale data after midnight UTC. Find out why and fix it.",
855
+ createdAt: at(3)
856
+ },
857
+ {
858
+ id: "m-h3",
859
+ role: "assistant",
860
+ status: "done",
861
+ createdAt: at(4),
862
+ parts: [
863
+ {
864
+ type: "thinking",
865
+ text: "Stale after midnight points at the cache expiry. Check how the cache compares days."
866
+ },
867
+ {
868
+ type: "tool",
869
+ tool: {
870
+ id: "t-h1",
871
+ name: "bash",
872
+ title: "Search for the cache expiry",
873
+ input: 'rg -n "getDate|TTL" src',
874
+ output: "src/lib/cache.ts:6:const TTL = 60 * 60 * 1000;\nsrc/lib/cache.ts:14: const day = new Date(entry.storedAt).getDate();\nsrc/lib/cache.ts:16: if (now - entry.storedAt > TTL) return undefined;",
875
+ status: "done",
876
+ paths: ["/workspace/harbor/src/lib/cache.ts"]
877
+ }
878
+ },
879
+ {
880
+ type: "tool",
881
+ tool: {
882
+ id: "t-h2",
883
+ name: "read_file",
884
+ title: "Read the cache",
885
+ input: "/workspace/harbor/src/lib/cache.ts",
886
+ output: cacheBefore,
887
+ status: "done",
888
+ paths: ["/workspace/harbor/src/lib/cache.ts"]
889
+ }
890
+ },
891
+ {
892
+ type: "tool",
893
+ tool: {
894
+ id: "t-h3",
895
+ name: "edit_file",
896
+ title: "Expire entries from the previous day",
897
+ input: "/workspace/harbor/src/lib/cache.ts",
898
+ output: "Replaced 1 block (4 lines \u2192 7 lines), added dayOf()",
899
+ status: "done",
900
+ paths: ["/workspace/harbor/src/lib/cache.ts"]
901
+ }
902
+ },
903
+ {
904
+ type: "tool",
905
+ tool: {
906
+ id: "t-h4",
907
+ name: "bash",
908
+ title: "Run the tests",
909
+ input: "npm test",
910
+ output: "> harbor@0.0.0 test\n> node --test test/\n\n\u2714 converts both ways (0.6ms)\n\u2714 expires entries from the previous day (0.4ms)\n\u2139 tests 2\n\u2139 pass 2\n\u2139 fail 0",
911
+ status: "done",
912
+ paths: []
913
+ }
914
+ },
915
+ {
916
+ type: "text",
917
+ text: "Found it. `ForecastCache.get` compared **local** calendar days, and when the day changed it returned the old entry instead of dropping it:\n\n```ts\nif (day !== new Date(now).getDate()) return entry.value;\n```\n\nI changed it to compare UTC dates and delete the entry, so the first request after midnight goes upstream. The tests pass.\n\n| | Before | After |\n| --- | --- | --- |\n| Day compared | local `getDate()` | UTC `toISOString()` |\n| On a new day | returns stale entry | deletes and refetches |\n\nquiet-otter is adding retries for upstream timeouts in a scoop."
918
+ }
919
+ ]
920
+ },
921
+ {
922
+ id: "m-h4",
923
+ role: "event",
924
+ kind: "scoop",
925
+ title: "quiet-otter",
926
+ text: "Started: add retries with backoff around the upstream fetch",
927
+ createdAt: at(9)
928
+ }
929
+ ],
930
+ "scoop-otter": [
931
+ {
932
+ id: "m-o1",
933
+ role: "user",
934
+ text: "Wrap the upstream fetch in src/routes/forecast.ts with three retries and exponential backoff.",
935
+ createdAt: at(9)
936
+ },
937
+ {
938
+ id: "m-o2",
939
+ role: "assistant",
940
+ status: "done",
941
+ createdAt: at(10),
942
+ parts: [
943
+ {
944
+ type: "tool",
945
+ tool: {
946
+ id: "t-o1",
947
+ name: "write_file",
948
+ title: "Add retry()",
949
+ input: "/workspace/harbor/src/lib/retry.ts",
950
+ output: "Wrote 12 lines",
951
+ status: "done",
952
+ paths: ["/workspace/harbor/src/lib/retry.ts"]
953
+ }
954
+ },
955
+ {
956
+ type: "tool",
957
+ tool: {
958
+ id: "t-o2",
959
+ name: "edit_file",
960
+ title: "Use retry() for the upstream call",
961
+ input: "/workspace/harbor/src/routes/forecast.ts",
962
+ output: "Replaced 1 block",
963
+ status: "done",
964
+ paths: ["/workspace/harbor/src/routes/forecast.ts"]
965
+ }
966
+ },
967
+ { type: "text", text: "Both edits are in. Running the tests next." }
968
+ ]
969
+ }
970
+ ],
971
+ "scoop-heron": [],
972
+ "scoop-wren": [],
973
+ "cone-release": [
974
+ {
975
+ id: "m-r1",
976
+ role: "event",
977
+ kind: "cron",
978
+ title: "Every Friday 09:00",
979
+ text: "Draft the release notes for example/harbor",
980
+ createdAt: at(-1440)
981
+ },
982
+ {
983
+ id: "m-r2",
984
+ role: "assistant",
985
+ status: "done",
986
+ createdAt: at(-1438),
987
+ parts: [
988
+ {
989
+ type: "tool",
990
+ tool: {
991
+ id: "t-r1",
992
+ name: "bash",
993
+ title: "Collect merged pull requests",
994
+ input: "collect.jsh example/harbor v1.8.0",
995
+ output: "41 fix Return 400 when the city is missing\n43 feature Add /health\n44 chore Update wrangler to 4.40.0",
996
+ status: "done",
997
+ paths: ["/workspace/skills/release-notes/scripts/collect.jsh"]
998
+ }
999
+ },
1000
+ {
1001
+ type: "text",
1002
+ text: "## harbor 1.9.0\n\n**Features**\n- Added a `/health` endpoint.\n\n**Fixes**\n- Requests without a city now get a 400 instead of a 500.\n\n**Chores**\n- Updated wrangler to 4.40.0."
1003
+ }
1004
+ ]
1005
+ }
1006
+ ],
1007
+ "cone-triage": [
1008
+ {
1009
+ id: "m-t1",
1010
+ role: "event",
1011
+ kind: "compaction",
1012
+ title: "Context compacted",
1013
+ text: "41 earlier messages were summarized to stay under the context limit.",
1014
+ createdAt: at(-30)
1015
+ },
1016
+ {
1017
+ id: "m-t2",
1018
+ role: "assistant",
1019
+ status: "done",
1020
+ createdAt: at(-20),
1021
+ parts: [
1022
+ {
1023
+ type: "text",
1024
+ text: "Two messages need a decision from you:\n\n- A vendor asks to move Thursday\u2019s call to Friday.\n- The staging certificate expires in 6 days. Should I open a ticket?"
1025
+ }
1026
+ ]
1027
+ },
1028
+ {
1029
+ id: "m-t3",
1030
+ role: "event",
1031
+ kind: "error",
1032
+ title: "Mail connector",
1033
+ text: "The connector returned 401. Reconnect the account in Settings.",
1034
+ createdAt: at(-19)
1035
+ }
1036
+ ]
1037
+ };
1038
+ var tabs = [
1039
+ {
1040
+ id: "tab-preview",
1041
+ title: "harbor \xB7 localhost",
1042
+ url: "http://localhost:8787/forecast?city=Lisbon",
1043
+ status: "complete",
1044
+ agentId: "cone-harbor"
1045
+ },
1046
+ {
1047
+ id: "tab-docs",
1048
+ title: "Forecast API reference",
1049
+ url: "https://api.example.com/docs/v2/forecast",
1050
+ status: "complete",
1051
+ agentId: null
1052
+ },
1053
+ {
1054
+ id: "tab-pull",
1055
+ title: "Add retries for upstream timeouts \xB7 Pull request #58",
1056
+ url: "https://git.example.com/example/harbor/pull/58",
1057
+ status: "complete",
1058
+ agentId: "scoop-otter"
1059
+ }
1060
+ ];
1061
+ var terminals = [
1062
+ { id: "term-1", title: "bash", cwd: "/workspace/harbor", agentId: null }
1063
+ ];
1064
+
1065
+ // src/dummy/settings.ts
1066
+ var key = "slicc-ui.settings";
1067
+ var DummySettings = class extends Emitter {
1068
+ #settings;
1069
+ #accounts;
1070
+ #models;
1071
+ #storage;
1072
+ #clock;
1073
+ constructor(defaults2, models2, accounts2, storage, clock2) {
1074
+ super();
1075
+ this.#storage = storage;
1076
+ this.#clock = clock2;
1077
+ this.#models = models2;
1078
+ this.#accounts = accounts2.map((account) => ({ ...account }));
1079
+ this.#settings = { ...defaults2, ...this.#load() };
1080
+ }
1081
+ #load() {
1082
+ try {
1083
+ return JSON.parse(this.#storage?.getItem(key) ?? "{}");
1084
+ } catch {
1085
+ return {};
1086
+ }
1087
+ }
1088
+ get() {
1089
+ return { ...this.#settings };
1090
+ }
1091
+ update(patch) {
1092
+ this.#settings = { ...this.#settings, ...patch };
1093
+ this.#storage?.setItem(key, JSON.stringify(this.#settings));
1094
+ this.emit("settings", this.get());
1095
+ }
1096
+ models() {
1097
+ return this.#models;
1098
+ }
1099
+ accounts() {
1100
+ return this.#accounts.map((account) => ({ ...account }));
1101
+ }
1102
+ #set(id, patch) {
1103
+ this.#accounts = this.#accounts.map(
1104
+ (account) => account.id === id ? { ...account, ...patch } : account
1105
+ );
1106
+ this.emit("accounts", this.accounts());
1107
+ }
1108
+ async connect(id) {
1109
+ await this.#clock.sleep(10);
1110
+ const account = this.#accounts.find((candidate) => candidate.id === id);
1111
+ const identity = account?.identity || `${id}-user@example.com`;
1112
+ this.#set(id, { status: "connected", identity });
1113
+ }
1114
+ disconnect(id) {
1115
+ this.#set(id, { status: "disconnected" });
1116
+ }
1117
+ };
1118
+
1119
+ // src/dummy/shell.ts
1120
+ var encoder = new TextEncoder();
1121
+ var decoder = new TextDecoder();
1122
+ var home = "/home/user";
1123
+ function resolve(cwd, target) {
1124
+ const path = target === "~" || target.startsWith("~/") ? home + target.slice(1) : target;
1125
+ const parts = (path.startsWith("/") ? path : `${cwd}/${path}`).split("/");
1126
+ const out = [];
1127
+ for (const part of parts) {
1128
+ if (part === "..") out.pop();
1129
+ else if (part && part !== ".") out.push(part);
1130
+ }
1131
+ return `/${out.join("/")}`;
1132
+ }
1133
+ function shorten(cwd) {
1134
+ return cwd === home || cwd.startsWith(`${home}/`) ? `~${cwd.slice(home.length)}` : cwd;
1135
+ }
1136
+ var FakeShell = class {
1137
+ #sink;
1138
+ #files;
1139
+ #clock;
1140
+ #line = "";
1141
+ #escape = false;
1142
+ #busy = false;
1143
+ #interrupted = false;
1144
+ #history = [];
1145
+ #commands;
1146
+ cwd;
1147
+ closed = false;
1148
+ constructor(sink, files3, cwd, clock2) {
1149
+ this.#sink = sink;
1150
+ this.#files = files3;
1151
+ this.#clock = clock2;
1152
+ this.cwd = cwd;
1153
+ this.#commands = {
1154
+ help: () => this.#print("Commands: cat cd clear date echo exit git help ls npm pwd whoami"),
1155
+ pwd: () => this.#print(this.cwd),
1156
+ whoami: () => this.#print("user"),
1157
+ date: () => this.#print((/* @__PURE__ */ new Date()).toUTCString()),
1158
+ echo: (args) => this.#print(args.join(" ")),
1159
+ clear: () => this.#out("\x1B[2J\x1B[H"),
1160
+ exit: () => this.#exit(),
1161
+ cd: (args) => this.#cd(args[0] ?? "~"),
1162
+ ls: (args) => this.#ls(args.filter((arg) => !arg.startsWith("-"))[0] ?? "."),
1163
+ cat: (args) => this.#cat(args),
1164
+ git: (args) => this.#git(args[0]),
1165
+ npm: (args) => this.#npm(args[0])
1166
+ };
1167
+ this.#prompt();
1168
+ }
1169
+ #out(text) {
1170
+ this.#sink.output(encoder.encode(text));
1171
+ }
1172
+ #print(text) {
1173
+ this.#out(`${text.replace(/\n/g, "\r\n")}\r
1174
+ `);
1175
+ }
1176
+ #prompt() {
1177
+ this.#out(`\x1B[1;32muser@slicc\x1B[0m:\x1B[1;34m${shorten(this.cwd)}\x1B[0m$ `);
1178
+ }
1179
+ #exit() {
1180
+ this.closed = true;
1181
+ this.#sink.exit(0);
1182
+ }
1183
+ async #entries() {
1184
+ return new Map((await this.#files.list()).map((entry) => [entry.path, entry.kind]));
1185
+ }
1186
+ async #cd(target) {
1187
+ const path = resolve(this.cwd, target);
1188
+ if (path === "/" || (await this.#entries()).get(path) === "directory") this.cwd = path;
1189
+ else this.#print(`bash: cd: ${target}: No such file or directory`);
1190
+ }
1191
+ async #ls(target) {
1192
+ const path = resolve(this.cwd, target);
1193
+ const entries = await this.#entries();
1194
+ const prefix = path === "/" ? "/" : `${path}/`;
1195
+ const names = [...entries].filter(
1196
+ ([candidate]) => candidate.startsWith(prefix) && !candidate.slice(prefix.length).includes("/")
1197
+ ).map(([candidate, kind]) => {
1198
+ const name = candidate.slice(prefix.length);
1199
+ return kind === "directory" ? `\x1B[1;34m${name}\x1B[0m` : name;
1200
+ });
1201
+ if (path !== "/" && !entries.has(path))
1202
+ this.#print(`ls: cannot access '${target}': No such file or directory`);
1203
+ else if (names.length > 0) this.#print(names.join(" "));
1204
+ }
1205
+ async #cat(args) {
1206
+ for (const arg of args) {
1207
+ const text = await this.#files.read(resolve(this.cwd, arg)).catch(() => null);
1208
+ if (text === null) this.#print(`cat: ${arg}: No such file or directory`);
1209
+ else this.#out(text.replace(/\n/g, "\r\n"));
1210
+ }
1211
+ }
1212
+ #git(sub) {
1213
+ if (sub !== "status") {
1214
+ this.#print("usage: git status");
1215
+ return;
1216
+ }
1217
+ const changes = this.#files.changes();
1218
+ this.#print("On branch main");
1219
+ if (changes.length === 0) {
1220
+ this.#print("nothing to commit, working tree clean");
1221
+ return;
1222
+ }
1223
+ this.#print("Changes not staged for commit:");
1224
+ const colors = { added: 32, modified: 33, deleted: 31 };
1225
+ for (const change of changes) {
1226
+ this.#print(` \x1B[${colors[change.status]}m${change.status}: ${change.path}\x1B[0m`);
1227
+ }
1228
+ }
1229
+ async #npm(sub) {
1230
+ if (sub !== "test") {
1231
+ this.#print("usage: npm test");
1232
+ return;
1233
+ }
1234
+ const lines = [
1235
+ "> harbor@0.0.0 test",
1236
+ "> node --test test/",
1237
+ "",
1238
+ "\x1B[32m\u2714\x1B[0m converts both ways \x1B[90m(0.6ms)\x1B[0m",
1239
+ "\x1B[32m\u2714\x1B[0m expires entries from the previous day \x1B[90m(0.4ms)\x1B[0m",
1240
+ "\x1B[32m\u2714\x1B[0m retries upstream timeouts \x1B[90m(3.1ms)\x1B[0m",
1241
+ "\u2139 tests 3",
1242
+ "\u2139 pass 3",
1243
+ "\u2139 fail 0"
1244
+ ];
1245
+ for (const line of lines) {
1246
+ await this.#clock.sleep(2);
1247
+ if (this.closed || this.#interrupted) return;
1248
+ this.#print(line);
1249
+ }
1250
+ }
1251
+ async #run(line) {
1252
+ const [name, ...args] = line.trim().split(/\s+/);
1253
+ if (!name) return;
1254
+ this.#history.push(line);
1255
+ const command = this.#commands[name];
1256
+ if (command) await command(args);
1257
+ else this.#print(`bash: ${name}: command not found`);
1258
+ }
1259
+ #key(char) {
1260
+ if (this.#escape) {
1261
+ this.#escape = !/[A-Za-z~]/.test(char) || char === "[";
1262
+ return;
1263
+ }
1264
+ if (char === "\x1B") this.#escape = true;
1265
+ else if (char === "\r") this.#enter();
1266
+ else if (char === "\x7F") this.#backspace();
1267
+ else if (char === "") this.signal("SIGINT");
1268
+ else if (char === "\f") {
1269
+ this.#out("\x1B[2J\x1B[H");
1270
+ this.#prompt();
1271
+ this.#out(this.#line);
1272
+ } else if (char >= " ") {
1273
+ this.#line += char;
1274
+ this.#out(char);
1275
+ }
1276
+ }
1277
+ #backspace() {
1278
+ if (!this.#line) return;
1279
+ this.#line = [...this.#line].slice(0, -1).join("");
1280
+ this.#out("\b \b");
1281
+ }
1282
+ #enter() {
1283
+ const line = this.#line;
1284
+ this.#line = "";
1285
+ this.#out("\r\n");
1286
+ this.#busy = true;
1287
+ this.#interrupted = false;
1288
+ void this.#run(line).then(() => {
1289
+ this.#busy = false;
1290
+ if (!this.closed) this.#prompt();
1291
+ });
1292
+ }
1293
+ history() {
1294
+ return this.#history;
1295
+ }
1296
+ write(data) {
1297
+ if (this.closed) return;
1298
+ const text = decoder.decode(data);
1299
+ if (!this.#busy) for (const char of text) this.#key(char);
1300
+ else if (text.includes("")) this.signal("SIGINT");
1301
+ }
1302
+ resize() {
1303
+ }
1304
+ signal(name) {
1305
+ if (name === "SIGHUP") {
1306
+ this.#exit();
1307
+ return;
1308
+ }
1309
+ this.#line = "";
1310
+ this.#interrupted = this.#busy;
1311
+ this.#out("^C\r\n");
1312
+ if (!this.#busy) this.#prompt();
1313
+ }
1314
+ close() {
1315
+ this.closed = true;
1316
+ }
1317
+ };
1318
+ function shellBackend(files3, cwd, clock2) {
1319
+ return { open: (sink) => new FakeShell(sink, files3, cwd, clock2) };
1320
+ }
1321
+
1322
+ // src/dummy/terminals.ts
1323
+ var DummyTerminals = class extends Emitter {
1324
+ #terminals;
1325
+ #backends = /* @__PURE__ */ new Map();
1326
+ #files;
1327
+ #clock;
1328
+ #next;
1329
+ constructor(terminals2, files3, clock2) {
1330
+ super();
1331
+ this.#files = files3;
1332
+ this.#clock = clock2;
1333
+ this.#terminals = terminals2.map((terminal) => ({ ...terminal }));
1334
+ this.#next = this.#terminals.length + 1;
1335
+ }
1336
+ list() {
1337
+ return this.#terminals.map((terminal) => ({ ...terminal }));
1338
+ }
1339
+ open({
1340
+ cwd = "/workspace/harbor",
1341
+ agentId = null
1342
+ } = {}) {
1343
+ const terminal = {
1344
+ id: `term-${this.#next}`,
1345
+ title: `bash ${this.#next}`,
1346
+ cwd,
1347
+ agentId
1348
+ };
1349
+ this.#next += 1;
1350
+ this.#terminals.push(terminal);
1351
+ this.emit("terminals", this.list());
1352
+ return { ...terminal };
1353
+ }
1354
+ close(id) {
1355
+ this.#terminals = this.#terminals.filter((terminal) => terminal.id !== id);
1356
+ this.#backends.delete(id);
1357
+ this.emit("terminals", this.list());
1358
+ }
1359
+ backend(id) {
1360
+ const terminal = this.#terminals.find((candidate) => candidate.id === id);
1361
+ if (!terminal) throw new Error(`No terminal ${id}`);
1362
+ let backend = this.#backends.get(id);
1363
+ if (!backend) {
1364
+ backend = shellBackend(this.#files, terminal.cwd, this.#clock);
1365
+ this.#backends.set(id, backend);
1366
+ }
1367
+ return backend;
1368
+ }
1369
+ };
1370
+
1371
+ // src/dummy/index.ts
1372
+ function createDummyModel({ delay = 30, storage = null } = {}) {
1373
+ const time = clock(delay);
1374
+ const files3 = new DummyFiles(files2, directories, pending, time);
1375
+ const browser = new DummyBrowser(tabs, time);
1376
+ return {
1377
+ agent: new DummyAgent(agents, conversations, { files: files3, browser }, time),
1378
+ files: files3,
1379
+ browser,
1380
+ terminals: new DummyTerminals(terminals, files3, time),
1381
+ settings: new DummySettings(
1382
+ defaults,
1383
+ models,
1384
+ accounts,
1385
+ storage,
1386
+ time
1387
+ )
1388
+ };
1389
+ }
1390
+ export {
1391
+ createDummyModel
1392
+ };
1393
+ //# sourceMappingURL=slicc-dummy.js.map