@ai-ecoverse/slicc-agent 3.10.0 → 3.11.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 (49) hide show
  1. package/README.md +30 -1
  2. package/bin/agent +1 -1
  3. package/dist/client.js +9 -1
  4. package/dist/client.js.map +2 -2
  5. package/dist/control.js +11 -1
  6. package/dist/control.js.map +2 -2
  7. package/dist/host.js +5 -2
  8. package/dist/host.js.map +2 -2
  9. package/dist/main.js +15 -1
  10. package/dist/main.js.map +2 -2
  11. package/dist/scoops/cli.js +2 -0
  12. package/dist/scoops/cli.js.map +2 -2
  13. package/dist/scoops/index.js +3 -2
  14. package/dist/scoops/index.js.map +2 -2
  15. package/dist/scoops/requests.js +1 -1
  16. package/dist/scoops/requests.js.map +2 -2
  17. package/dist/services.js +3 -1
  18. package/dist/services.js.map +2 -2
  19. package/dist/skills/index.js +1 -1
  20. package/dist/skills/index.js.map +2 -2
  21. package/dist/spectrum/index.js +5 -1
  22. package/dist/spectrum/index.js.map +3 -3
  23. package/dist/spectrum/messages.js +12 -0
  24. package/dist/spectrum/messages.js.map +2 -2
  25. package/dist/spectrum/sprinkle-port.js +37 -0
  26. package/dist/spectrum/sprinkle-port.js.map +7 -0
  27. package/dist/sprinkles/index.js +216 -0
  28. package/dist/sprinkles/index.js.map +7 -0
  29. package/dist/sprinkles/kind.js +5 -0
  30. package/dist/sprinkles/kind.js.map +7 -0
  31. package/dist/sprinkles/sprinkles.js +65 -0
  32. package/dist/sprinkles/sprinkles.js.map +7 -0
  33. package/dist/types/client.d.ts +2 -0
  34. package/dist/types/control.d.ts +2 -1
  35. package/dist/types/host.d.ts +2 -0
  36. package/dist/types/scoops/cli.d.ts +1 -0
  37. package/dist/types/scoops/index.d.ts +2 -0
  38. package/dist/types/scoops/requests.d.ts +1 -1
  39. package/dist/types/services.d.ts +11 -1
  40. package/dist/types/spectrum/index.d.ts +3 -0
  41. package/dist/types/spectrum/sprinkle-port.d.ts +10 -0
  42. package/dist/types/sprinkles/index.d.ts +38 -0
  43. package/dist/types/sprinkles/kind.d.ts +10 -0
  44. package/dist/types/sprinkles/sprinkles.d.ts +24 -0
  45. package/package.json +3 -3
  46. package/packages/vfs-root/skills/sprinkles/SKILL.md +58 -0
  47. package/packages/vfs-root/skills/welcome/SKILL.md +31 -0
  48. package/packages/vfs-root/sprinkles/suggestions.shtml +391 -0
  49. package/packages/vfs-root/sprinkles/welcome.shtml +552 -0
@@ -0,0 +1,216 @@
1
+ import {
2
+ replicatedState
3
+ } from "@earendil-works/chord";
4
+ import {
5
+ describe,
6
+ homePath,
7
+ NO_EXEC,
8
+ SPRINKLE_KIND,
9
+ SPRINKLES_DIR,
10
+ SprinklesDoc,
11
+ sprinkleFiles,
12
+ WELCOME,
13
+ WELCOMED
14
+ } from "./sprinkles.js";
15
+ const USAGE = `usage:
16
+ sprinkle list every sprinkle in /home/sprinkles, with its owner
17
+ sprinkle show <name> show a sprinkle in your chat
18
+ sprinkle own <name> [<agent>] send its messages to <agent> (cone, cone-<n> or a scoop's handle)
19
+
20
+ A sprinkle is an .shtml file in /home/sprinkles, as <name>.shtml or <name>/<name>.shtml.
21
+ Every one of them has a button in the rail; edit the file and the open panel reloads.
22
+ `;
23
+ const RETIRED = {
24
+ open: "every sprinkle in /home/sprinkles already has a button in the rail; sprinkle show <name> shows one in the chat",
25
+ reload: "editing the .shtml file reloads the sprinkle",
26
+ close: "delete the .shtml file to retire a sprinkle; the user closes panels",
27
+ send: "a sprinkle reads data with slicc.readFile() or keeps it with slicc.setState(); write the file it reads",
28
+ chat: "show the sprinkle with sprinkle show <name> and wait for its lick"
29
+ };
30
+ function targetOf(agentId) {
31
+ return agentId.startsWith("scoop:") ? agentId : `cone:${agentId}`;
32
+ }
33
+ function agentOf(caller, agents) {
34
+ if (caller?.startsWith("cone:")) return caller.slice("cone:".length);
35
+ return caller ?? agents.activeCone();
36
+ }
37
+ function named(agents, value) {
38
+ const state = agents.state();
39
+ if (value in state.cones) return value;
40
+ const id = value.startsWith("scoop:") ? value : `scoop:${value}`;
41
+ return id in state.scoops ? id : void 0;
42
+ }
43
+ function setupSprinkles() {
44
+ return {
45
+ async attach(options, context) {
46
+ const { harness, agents, licks, env } = options;
47
+ const dir = options.dir ?? SPRINKLES_DIR;
48
+ const sprinkles = replicatedState([]);
49
+ const welcome = options.assets ? await options.assets("packages/vfs-root/sprinkles/welcome.shtml").catch(() => void 0) : void 0;
50
+ const owners = async (using) => (await harness.snapshot(SprinklesDoc, using))?.owners ?? {};
51
+ const reload = async (using) => {
52
+ const owned = await owners(using);
53
+ const out = [];
54
+ for (const { name, path } of await sprinkleFiles(env, dir, using)) {
55
+ if (name === WELCOME) continue;
56
+ const read = await env.readTextFile(path, using);
57
+ if (!read.ok) continue;
58
+ out.push({
59
+ id: name,
60
+ name,
61
+ ...describe(name, read.value),
62
+ agentId: owned[name] ?? agents.activeCone(),
63
+ html: read.value
64
+ });
65
+ }
66
+ if (welcome !== void 0)
67
+ out.push({
68
+ id: WELCOME,
69
+ name: WELCOME,
70
+ ...describe(WELCOME, welcome),
71
+ agentId: owned[WELCOME] ?? "cone",
72
+ html: welcome,
73
+ inline: true
74
+ });
75
+ sprinkles.replace(using, out);
76
+ };
77
+ const inside = async (path, using) => {
78
+ const asked = homePath(path);
79
+ const real = await env.canonicalPath(asked, using);
80
+ return real.ok ? homePath(real.value) : asked;
81
+ };
82
+ const offCone = agents.onCone(() => reload(context).catch(() => void 0));
83
+ const find = (id) => sprinkles.value.find((item) => item.id === id);
84
+ const post = async (id, agentId, using) => {
85
+ const conversation = await agents.conversation(agentId, using);
86
+ if (!conversation) throw new Error(`there is no agent ${agentId}`);
87
+ await conversation.submit(
88
+ { type: "write", entry: { kind: SPRINKLE_KIND, data: { sprinkle: id, at: Date.now() } } },
89
+ using
90
+ );
91
+ };
92
+ const setOwner = async (name, agentId, using) => {
93
+ await harness.commit(async (tx) => {
94
+ (await tx.doc(SprinklesDoc)).owners[name] = agentId;
95
+ }, using);
96
+ await reload(using);
97
+ };
98
+ await reload(context);
99
+ const state = await harness.snapshot(SprinklesDoc, context);
100
+ if (welcome !== void 0 && !state?.welcomed) {
101
+ await post(WELCOME, "cone", context);
102
+ await harness.commit(async (tx) => {
103
+ (await tx.doc(SprinklesDoc)).welcomed = true;
104
+ }, context);
105
+ }
106
+ let timer;
107
+ const watched = await env.watch(
108
+ [{ path: dir, recursive: true }],
109
+ () => {
110
+ clearTimeout(timer);
111
+ timer = setTimeout(() => void reload(context).catch(() => void 0), options.reloadMs);
112
+ },
113
+ context
114
+ );
115
+ const watcher = watched.ok ? watched.value : void 0;
116
+ const commands = {
117
+ async list(_argv, _caller, using) {
118
+ await reload(using);
119
+ const rows = sprinkles.value.filter((item) => !item.inline).map((item) => `${item.name} ${item.title} ${item.agentId}`);
120
+ return { code: 0, out: rows.length ? `${rows.join("\n")}
121
+ ` : "no sprinkles\n" };
122
+ },
123
+ async show(argv, caller, using) {
124
+ await reload(using);
125
+ const name = argv[1] ?? "";
126
+ if (!find(name)) return { code: 1, out: `sprinkle: there is no sprinkle ${name}
127
+ ` };
128
+ const agentId = agentOf(caller, agents);
129
+ await post(name, agentId, using);
130
+ if (!(await owners(using))[name]) await setOwner(name, agentId, using);
131
+ return { code: 0, out: `showed ${name} in the chat of ${agentId}
132
+ ` };
133
+ },
134
+ async own(argv, caller, using) {
135
+ const name = argv[1] ?? "";
136
+ if (!find(name)) return { code: 1, out: `sprinkle: there is no sprinkle ${name}
137
+ ` };
138
+ const agentId = argv[2] ? named(agents, argv[2]) : agentOf(caller, agents);
139
+ if (!agentId) return { code: 1, out: `sprinkle: there is no agent ${argv[2]}
140
+ ` };
141
+ await setOwner(name, agentId, using);
142
+ return { code: 0, out: `${name} now talks to ${agentId}
143
+ ` };
144
+ }
145
+ };
146
+ return {
147
+ sprinkles,
148
+ async send(id, payload, using) {
149
+ const sprinkle = find(id);
150
+ if (!sprinkle) return { delivered: false };
151
+ if (id === WELCOME && payload.action === "onboarding-complete")
152
+ await env.writeFile(WELCOMED, `${(/* @__PURE__ */ new Date()).toISOString()}
153
+ `, using);
154
+ await licks.deliver(
155
+ {
156
+ channel: "sprinkle",
157
+ source: `sprinkle:${id}`,
158
+ title: sprinkle.title,
159
+ text: payload.action || "(no action)",
160
+ ...payload.data === null ? {} : { body: JSON.stringify(payload.data, null, 2) },
161
+ target: targetOf((await owners(using))[id] ?? agents.activeCone())
162
+ },
163
+ using
164
+ );
165
+ return { delivered: true };
166
+ },
167
+ async call(id, method, args, using) {
168
+ if (!find(id)) throw new Error(`there is no sprinkle ${id}`);
169
+ const handlers = {
170
+ async readFile() {
171
+ const read = await env.readTextFile(await inside(args[0], using), using);
172
+ if (!read.ok) throw new Error(read.error.message);
173
+ return read.value;
174
+ },
175
+ async exists() {
176
+ const found = await env.exists(await inside(args[0], using), using);
177
+ return found.ok && found.value;
178
+ },
179
+ async getState() {
180
+ return (await harness.snapshot(SprinklesDoc, using))?.state[id] ?? null;
181
+ },
182
+ async setState() {
183
+ await harness.commit(async (tx) => {
184
+ (await tx.doc(SprinklesDoc)).state[id] = args[0] ?? null;
185
+ }, using);
186
+ return null;
187
+ }
188
+ };
189
+ const handler = handlers[method];
190
+ if (!handler) throw new Error(NO_EXEC);
191
+ return handler();
192
+ },
193
+ async command(argv, caller, using) {
194
+ const verb = argv[0] ?? "help";
195
+ const run = commands[verb];
196
+ if (run) return run(argv, caller, using);
197
+ if (verb in RETIRED)
198
+ return { code: 2, out: `sprinkle ${verb} isn't in SLICC: ${RETIRED[verb]}
199
+ ` };
200
+ return { code: verb === "help" || verb === "--help" ? 0 : 2, out: USAGE };
201
+ },
202
+ reload,
203
+ async close(using) {
204
+ offCone();
205
+ clearTimeout(timer);
206
+ await watcher?.close(using);
207
+ }
208
+ };
209
+ }
210
+ };
211
+ }
212
+ export {
213
+ USAGE,
214
+ setupSprinkles
215
+ };
216
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sprinkles/index.ts"],
4
+ "sourcesContent": ["import {\n type Context,\n type JsonValue,\n type ReplicatedState,\n replicatedState,\n} from '@earendil-works/chord';\nimport type { Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Agents } from '../agents.ts';\nimport type { LickTarget } from '../licks/lick.ts';\nimport type { Licks } from '../licks/licks.ts';\nimport {\n describe,\n homePath,\n NO_EXEC,\n SPRINKLE_KIND,\n SPRINKLES_DIR,\n type Sprinkle,\n type SprinkleMethod,\n SprinklesDoc,\n sprinkleFiles,\n WELCOME,\n WELCOMED,\n} from './sprinkles.ts';\n\nexport type Answer = { code: number; out: string };\n\nexport interface SprinklePayload {\n action: string;\n data: JsonValue | null;\n target: string | null;\n}\n\nexport interface SprinklesRuntime {\n readonly sprinkles: ReplicatedState<Sprinkle[]>;\n send(id: string, payload: SprinklePayload, context: Context): Promise<{ delivered: boolean }>;\n call(id: string, method: string, args: readonly unknown[], context: Context): Promise<JsonValue>;\n command(argv: readonly string[], caller: string | null, context: Context): Promise<Answer>;\n reload(context: Context): Promise<void>;\n close(context: Context): Promise<void>;\n}\n\nexport interface SprinklesAttach {\n harness: Harness;\n agents: Agents;\n licks: Licks;\n env: ExecutionEnv;\n assets?: (path: string) => Promise<string>;\n reloadMs: number;\n dir?: string;\n}\n\nexport const USAGE = `usage:\n sprinkle list every sprinkle in /home/sprinkles, with its owner\n sprinkle show <name> show a sprinkle in your chat\n sprinkle own <name> [<agent>] send its messages to <agent> (cone, cone-<n> or a scoop's handle)\n\nA sprinkle is an .shtml file in /home/sprinkles, as <name>.shtml or <name>/<name>.shtml.\nEvery one of them has a button in the rail; edit the file and the open panel reloads.\n`;\n\nconst RETIRED: Record<string, string> = {\n open: 'every sprinkle in /home/sprinkles already has a button in the rail; sprinkle show <name> shows one in the chat',\n reload: 'editing the .shtml file reloads the sprinkle',\n close: 'delete the .shtml file to retire a sprinkle; the user closes panels',\n send: 'a sprinkle reads data with slicc.readFile() or keeps it with slicc.setState(); write the file it reads',\n chat: 'show the sprinkle with sprinkle show <name> and wait for its lick',\n};\n\nfunction targetOf(agentId: string): LickTarget {\n return agentId.startsWith('scoop:') ? (agentId as LickTarget) : `cone:${agentId}`;\n}\n\nfunction agentOf(caller: string | null, agents: Agents): string {\n if (caller?.startsWith('cone:')) return caller.slice('cone:'.length);\n return caller ?? agents.activeCone();\n}\n\nfunction named(agents: Agents, value: string): string | undefined {\n const state = agents.state();\n if (value in state.cones) return value;\n const id = value.startsWith('scoop:') ? value : `scoop:${value}`;\n return id in state.scoops ? id : undefined;\n}\n\nexport function setupSprinkles() {\n return {\n async attach(options: SprinklesAttach, context: Context): Promise<SprinklesRuntime> {\n const { harness, agents, licks, env } = options;\n const dir = options.dir ?? SPRINKLES_DIR;\n const sprinkles = replicatedState<Sprinkle[]>([]);\n const welcome = options.assets\n ? await options.assets('packages/vfs-root/sprinkles/welcome.shtml').catch(() => undefined)\n : undefined;\n const owners = async (using: Context) =>\n (await harness.snapshot(SprinklesDoc, using))?.owners ?? {};\n const reload = async (using: Context) => {\n const owned = await owners(using);\n const out: Sprinkle[] = [];\n for (const { name, path } of await sprinkleFiles(env, dir, using)) {\n if (name === WELCOME) continue;\n const read = await env.readTextFile(path, using);\n if (!read.ok) continue;\n out.push({\n id: name,\n name,\n ...describe(name, read.value),\n agentId: owned[name] ?? agents.activeCone(),\n html: read.value,\n });\n }\n if (welcome !== undefined)\n out.push({\n id: WELCOME,\n name: WELCOME,\n ...describe(WELCOME, welcome),\n agentId: owned[WELCOME] ?? 'cone',\n html: welcome,\n inline: true,\n });\n sprinkles.replace(using, out);\n };\n const inside = async (path: unknown, using: Context) => {\n const asked = homePath(path);\n const real = await env.canonicalPath(asked, using);\n return real.ok ? homePath(real.value) : asked;\n };\n const offCone = agents.onCone(() => reload(context).catch(() => undefined));\n const find = (id: string) => sprinkles.value.find((item) => item.id === id);\n const post = async (id: string, agentId: string, using: Context) => {\n const conversation = await agents.conversation(agentId, using);\n if (!conversation) throw new Error(`there is no agent ${agentId}`);\n await conversation.submit(\n { type: 'write', entry: { kind: SPRINKLE_KIND, data: { sprinkle: id, at: Date.now() } } },\n using\n );\n };\n const setOwner = async (name: string, agentId: string, using: Context) => {\n await harness.commit(async (tx) => {\n (await tx.doc(SprinklesDoc)).owners[name] = agentId;\n }, using);\n await reload(using);\n };\n await reload(context);\n const state = await harness.snapshot(SprinklesDoc, context);\n if (welcome !== undefined && !state?.welcomed) {\n await post(WELCOME, 'cone', context);\n await harness.commit(async (tx) => {\n (await tx.doc(SprinklesDoc)).welcomed = true;\n }, context);\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const watched = await env.watch(\n [{ path: dir, recursive: true }],\n () => {\n clearTimeout(timer);\n timer = setTimeout(() => void reload(context).catch(() => undefined), options.reloadMs);\n },\n context\n );\n const watcher: FileWatcher | undefined = watched.ok ? watched.value : undefined;\n const commands: Record<\n string,\n (argv: readonly string[], caller: string | null, using: Context) => Promise<Answer>\n > = {\n async list(_argv, _caller, using) {\n await reload(using);\n const rows = sprinkles.value\n .filter((item) => !item.inline)\n .map((item) => `${item.name}\\t${item.title}\\t${item.agentId}`);\n return { code: 0, out: rows.length ? `${rows.join('\\n')}\\n` : 'no sprinkles\\n' };\n },\n async show(argv, caller, using) {\n await reload(using);\n const name = argv[1] ?? '';\n if (!find(name)) return { code: 1, out: `sprinkle: there is no sprinkle ${name}\\n` };\n const agentId = agentOf(caller, agents);\n await post(name, agentId, using);\n if (!(await owners(using))[name]) await setOwner(name, agentId, using);\n return { code: 0, out: `showed ${name} in the chat of ${agentId}\\n` };\n },\n async own(argv, caller, using) {\n const name = argv[1] ?? '';\n if (!find(name)) return { code: 1, out: `sprinkle: there is no sprinkle ${name}\\n` };\n const agentId = argv[2] ? named(agents, argv[2]) : agentOf(caller, agents);\n if (!agentId) return { code: 1, out: `sprinkle: there is no agent ${argv[2]}\\n` };\n await setOwner(name, agentId, using);\n return { code: 0, out: `${name} now talks to ${agentId}\\n` };\n },\n };\n return {\n sprinkles,\n async send(id, payload, using) {\n const sprinkle = find(id);\n if (!sprinkle) return { delivered: false };\n if (id === WELCOME && payload.action === 'onboarding-complete')\n await env.writeFile(WELCOMED, `${new Date().toISOString()}\\n`, using);\n await licks.deliver(\n {\n channel: 'sprinkle',\n source: `sprinkle:${id}`,\n title: sprinkle.title,\n text: payload.action || '(no action)',\n ...(payload.data === null ? {} : { body: JSON.stringify(payload.data, null, 2) }),\n target: targetOf((await owners(using))[id] ?? agents.activeCone()),\n },\n using\n );\n return { delivered: true };\n },\n async call(id, method, args, using) {\n if (!find(id)) throw new Error(`there is no sprinkle ${id}`);\n const handlers: Record<SprinkleMethod, () => Promise<JsonValue>> = {\n async readFile() {\n const read = await env.readTextFile(await inside(args[0], using), using);\n if (!read.ok) throw new Error(read.error.message);\n return read.value;\n },\n async exists() {\n const found = await env.exists(await inside(args[0], using), using);\n return found.ok && found.value;\n },\n async getState() {\n return (await harness.snapshot(SprinklesDoc, using))?.state[id] ?? null;\n },\n async setState() {\n await harness.commit(async (tx) => {\n (await tx.doc(SprinklesDoc)).state[id] = (args[0] ?? null) as JsonValue;\n }, using);\n return null;\n },\n };\n const handler = handlers[method as SprinkleMethod];\n if (!handler) throw new Error(NO_EXEC);\n return handler();\n },\n async command(argv, caller, using) {\n const verb = argv[0] ?? 'help';\n const run = commands[verb];\n if (run) return run(argv, caller, using);\n if (verb in RETIRED)\n return { code: 2, out: `sprinkle ${verb} isn't in SLICC: ${RETIRED[verb]}\\n` };\n return { code: verb === 'help' || verb === '--help' ? 0 : 2, out: USAGE };\n },\n reload,\n async close(using) {\n offCone();\n clearTimeout(timer);\n await watcher?.close(using);\n },\n };\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ const SPRINKLE_KIND = "slicc.sprinkle";
2
+ export {
3
+ SPRINKLE_KIND
4
+ };
5
+ //# sourceMappingURL=kind.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sprinkles/kind.ts"],
4
+ "sourcesContent": ["export const SPRINKLE_KIND = 'slicc.sprinkle';\n\nexport interface Sprinkle {\n id: string;\n name: string;\n title: string;\n icon: string;\n agentId: string;\n html: string;\n inline?: boolean;\n}\n"],
5
+ "mappings": "AAAO,MAAM,gBAAgB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,65 @@
1
+ import { defineDoc } from "@earendil-works/pi-durable";
2
+ import { normalize } from "../kernel/paths.js";
3
+ import { SPRINKLE_KIND } from "./kind.js";
4
+ const SPRINKLES_DIR = "/home/sprinkles";
5
+ const WELCOME = "welcome";
6
+ const WELCOMED = "/home/.welcomed";
7
+ const DEFAULT_ICON = "sparkles";
8
+ const SprinklesDoc = defineDoc({
9
+ kind: "slicc.sprinkles",
10
+ version: 1,
11
+ scope: "session",
12
+ initial: () => ({ owners: {}, state: {}, welcomed: false })
13
+ });
14
+ function decode(text) {
15
+ return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
16
+ }
17
+ function describe(name, html) {
18
+ const title = /<title>([\s\S]*?)<\/title>/i.exec(html)?.[1]?.trim();
19
+ const icon = /<link\b[^>]*\brel=["']icon["'][^>]*\bhref=["']([^"']+)["']/i.exec(html)?.[1] ?? /<link\b[^>]*\bhref=["']([^"']+)["'][^>]*\brel=["']icon["']/i.exec(html)?.[1];
20
+ return { title: title ? decode(title) : name, icon: icon ?? DEFAULT_ICON };
21
+ }
22
+ const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
23
+ async function sprinkleFiles(env, dir, context) {
24
+ const listed = await env.listDir(dir, context);
25
+ if (!listed.ok) return [];
26
+ const out = /* @__PURE__ */ new Map();
27
+ for (const entry of [...listed.value].sort((a, b) => a.name.localeCompare(b.name))) {
28
+ if (entry.kind !== "directory" && entry.name.endsWith(".shtml")) {
29
+ const name = entry.name.slice(0, -".shtml".length);
30
+ if (NAME.test(name)) out.set(name, `${dir}/${entry.name}`);
31
+ }
32
+ if (entry.kind === "directory" && NAME.test(entry.name) && !out.has(entry.name)) {
33
+ const inner = await env.listDir(`${dir}/${entry.name}`, context);
34
+ if (inner.ok && inner.value.some((item) => item.name === `${entry.name}.shtml`))
35
+ out.set(entry.name, `${dir}/${entry.name}/${entry.name}.shtml`);
36
+ }
37
+ }
38
+ return [...out].map(([name, path]) => ({ name, path }));
39
+ }
40
+ function homePath(path) {
41
+ if (typeof path !== "string" || !path) throw new Error("a sprinkle reads files by absolute path");
42
+ const mapped = path.startsWith("/shared/") ? `/home/${path.slice("/shared/".length)}` : path;
43
+ const segments = mapped.split("/");
44
+ if (segments.includes(".") || segments.includes(".."))
45
+ throw new Error(`a sprinkle reads paths without . or .. segments, not ${path}`);
46
+ const resolved = normalize(mapped);
47
+ if (resolved !== "/home" && !resolved.startsWith("/home/"))
48
+ throw new Error(`a sprinkle can only read files in /home, not ${path}`);
49
+ return resolved;
50
+ }
51
+ const NO_EXEC = "sprinkles can't run commands in SLICC; send the agent a message with slicc.lick() and let it run them";
52
+ export {
53
+ DEFAULT_ICON,
54
+ NAME,
55
+ NO_EXEC,
56
+ SPRINKLES_DIR,
57
+ SPRINKLE_KIND,
58
+ SprinklesDoc,
59
+ WELCOME,
60
+ WELCOMED,
61
+ describe,
62
+ homePath,
63
+ sprinkleFiles
64
+ };
65
+ //# sourceMappingURL=sprinkles.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sprinkles/sprinkles.ts"],
4
+ "sourcesContent": ["import type { Context, JsonValue } from '@earendil-works/chord';\nimport { defineDoc } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { normalize } from '../kernel/paths.ts';\n\nexport { SPRINKLE_KIND, type Sprinkle } from './kind.ts';\n\nexport const SPRINKLES_DIR = '/home/sprinkles';\nexport const WELCOME = 'welcome';\nexport const WELCOMED = '/home/.welcomed';\nexport const DEFAULT_ICON = 'sparkles';\n\nexport type SprinkleMethod = 'readFile' | 'exists' | 'getState' | 'setState';\n\nexport const SprinklesDoc = defineDoc<{\n owners: Record<string, string>;\n state: Record<string, JsonValue>;\n welcomed: boolean;\n}>({\n kind: 'slicc.sprinkles',\n version: 1,\n scope: 'session',\n initial: () => ({ owners: {}, state: {}, welcomed: false }),\n});\n\nfunction decode(text: string): string {\n return text\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&amp;/g, '&');\n}\n\nexport function describe(name: string, html: string): { title: string; icon: string } {\n const title = /<title>([\\s\\S]*?)<\\/title>/i.exec(html)?.[1]?.trim();\n const icon =\n /<link\\b[^>]*\\brel=[\"']icon[\"'][^>]*\\bhref=[\"']([^\"']+)[\"']/i.exec(html)?.[1] ??\n /<link\\b[^>]*\\bhref=[\"']([^\"']+)[\"'][^>]*\\brel=[\"']icon[\"']/i.exec(html)?.[1];\n return { title: title ? decode(title) : name, icon: icon ?? DEFAULT_ICON };\n}\n\nexport const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;\n\nexport async function sprinkleFiles(\n env: Pick<ExecutionEnv, 'listDir'>,\n dir: string,\n context: Context\n): Promise<{ name: string; path: string }[]> {\n const listed = await env.listDir(dir, context);\n if (!listed.ok) return [];\n const out = new Map<string, string>();\n for (const entry of [...listed.value].sort((a, b) => a.name.localeCompare(b.name))) {\n if (entry.kind !== 'directory' && entry.name.endsWith('.shtml')) {\n const name = entry.name.slice(0, -'.shtml'.length);\n if (NAME.test(name)) out.set(name, `${dir}/${entry.name}`);\n }\n if (entry.kind === 'directory' && NAME.test(entry.name) && !out.has(entry.name)) {\n const inner = await env.listDir(`${dir}/${entry.name}`, context);\n if (inner.ok && inner.value.some((item) => item.name === `${entry.name}.shtml`))\n out.set(entry.name, `${dir}/${entry.name}/${entry.name}.shtml`);\n }\n }\n return [...out].map(([name, path]) => ({ name, path }));\n}\n\nexport function homePath(path: unknown): string {\n if (typeof path !== 'string' || !path) throw new Error('a sprinkle reads files by absolute path');\n const mapped = path.startsWith('/shared/') ? `/home/${path.slice('/shared/'.length)}` : path;\n const segments = mapped.split('/');\n if (segments.includes('.') || segments.includes('..'))\n throw new Error(`a sprinkle reads paths without . or .. segments, not ${path}`);\n const resolved = normalize(mapped);\n if (resolved !== '/home' && !resolved.startsWith('/home/'))\n throw new Error(`a sprinkle can only read files in /home, not ${path}`);\n return resolved;\n}\n\nexport const NO_EXEC =\n \"sprinkles can't run commands in SLICC; send the agent a message with slicc.lick() and let it run them\";\n"],
5
+ "mappings": "AACA,SAAS,iBAAiB;AAE1B,SAAS,iBAAiB;AAE1B,SAAS,qBAAoC;AAEtC,MAAM,gBAAgB;AACtB,MAAM,UAAU;AAChB,MAAM,WAAW;AACjB,MAAM,eAAe;AAIrB,MAAM,eAAe,UAIzB;AAAA,EACD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,GAAG,UAAU,MAAM;AAC3D,CAAC;AAED,SAAS,OAAO,MAAsB;AACpC,SAAO,KACJ,QAAQ,SAAS,GAAG,EACpB,QAAQ,SAAS,GAAG,EACpB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,UAAU,GAAG;AAC1B;AAEO,SAAS,SAAS,MAAc,MAA+C;AACpF,QAAM,QAAQ,8BAA8B,KAAK,IAAI,IAAI,CAAC,GAAG,KAAK;AAClE,QAAM,OACJ,8DAA8D,KAAK,IAAI,IAAI,CAAC,KAC5E,8DAA8D,KAAK,IAAI,IAAI,CAAC;AAC9E,SAAO,EAAE,OAAO,QAAQ,OAAO,KAAK,IAAI,MAAM,MAAM,QAAQ,aAAa;AAC3E;AAEO,MAAM,OAAO;AAEpB,eAAsB,cACpB,KACA,KACA,SAC2C;AAC3C,QAAM,SAAS,MAAM,IAAI,QAAQ,KAAK,OAAO;AAC7C,MAAI,CAAC,OAAO,GAAI,QAAO,CAAC;AACxB,QAAM,MAAM,oBAAI,IAAoB;AACpC,aAAW,SAAS,CAAC,GAAG,OAAO,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,GAAG;AAClF,QAAI,MAAM,SAAS,eAAe,MAAM,KAAK,SAAS,QAAQ,GAAG;AAC/D,YAAM,OAAO,MAAM,KAAK,MAAM,GAAG,CAAC,SAAS,MAAM;AACjD,UAAI,KAAK,KAAK,IAAI,EAAG,KAAI,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,EAAE;AAAA,IAC3D;AACA,QAAI,MAAM,SAAS,eAAe,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,MAAM,IAAI,GAAG;AAC/E,YAAM,QAAQ,MAAM,IAAI,QAAQ,GAAG,GAAG,IAAI,MAAM,IAAI,IAAI,OAAO;AAC/D,UAAI,MAAM,MAAM,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,GAAG,MAAM,IAAI,QAAQ;AAC5E,YAAI,IAAI,MAAM,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,IAAI,MAAM,IAAI,QAAQ;AAAA,IAClE;AAAA,EACF;AACA,SAAO,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,EAAE,MAAM,KAAK,EAAE;AACxD;AAEO,SAAS,SAAS,MAAuB;AAC9C,MAAI,OAAO,SAAS,YAAY,CAAC,KAAM,OAAM,IAAI,MAAM,yCAAyC;AAChG,QAAM,SAAS,KAAK,WAAW,UAAU,IAAI,SAAS,KAAK,MAAM,WAAW,MAAM,CAAC,KAAK;AACxF,QAAM,WAAW,OAAO,MAAM,GAAG;AACjC,MAAI,SAAS,SAAS,GAAG,KAAK,SAAS,SAAS,IAAI;AAClD,UAAM,IAAI,MAAM,wDAAwD,IAAI,EAAE;AAChF,QAAM,WAAW,UAAU,MAAM;AACjC,MAAI,aAAa,WAAW,CAAC,SAAS,WAAW,QAAQ;AACvD,UAAM,IAAI,MAAM,gDAAgD,IAAI,EAAE;AACxE,SAAO;AACT;AAEO,MAAM,UACX;",
6
+ "names": []
7
+ }
@@ -1,6 +1,7 @@
1
1
  import { type Context, type ReplicatedState } from '@earendil-works/chord';
2
2
  import type { ConversationView } from '@earendil-works/pi-durable';
3
3
  import { AgentControl, AgentSettings, type AgentsSummary, type Command, type Delivered, type SendMode } from './services.ts';
4
+ import type { Sprinkle } from './sprinkles/kind.ts';
4
5
  import { type PortEndpoint } from './wire.ts';
5
6
  export interface AgentConnection {
6
7
  readonly serverId: string;
@@ -10,6 +11,7 @@ export interface AgentConnection {
10
11
  readonly agents?: ReplicatedState<AgentsSummary>;
11
12
  readonly views?: ReplicatedState<Record<string, ConversationView>>;
12
13
  readonly commands?: ReplicatedState<Command[]>;
14
+ readonly sprinkles?: ReplicatedState<Sprinkle[]>;
13
15
  readonly settings: AgentSettings;
14
16
  prompt(text: string, whenBusy?: SendMode): Promise<string>;
15
17
  close(): Promise<void>;
@@ -5,6 +5,7 @@ import type { LickSources, Licks } from './licks/index.ts';
5
5
  import type { Scoops, ScoopsRuntime } from './scoops/index.ts';
6
6
  import type { AgentControl } from './services.ts';
7
7
  import type { SkillsRuntime } from './skills/index.ts';
8
+ import type { SprinklesRuntime } from './sprinkles/index.ts';
8
9
  export interface HostLicks {
9
10
  licks: Licks;
10
11
  sources: LickSources;
@@ -15,4 +16,4 @@ export interface HostScoops {
15
16
  }
16
17
  export declare function readable(out: string): string;
17
18
  export declare function submissionId(value: string): SubmissionId | undefined;
18
- export declare function createAgentControl(harness: Harness, target: Conversation | Cones, deliveries?: Deliveries, licks?: HostLicks, scoops?: HostScoops, skills?: Pick<SkillsRuntime, 'expand'>): AgentControl;
19
+ export declare function createAgentControl(harness: Harness, target: Conversation | Cones, deliveries?: Deliveries, licks?: HostLicks, scoops?: HostScoops, skills?: Pick<SkillsRuntime, 'expand'>, sprinkles?: Pick<SprinklesRuntime, 'send' | 'call'>): AgentControl;
@@ -3,6 +3,7 @@ import type { Agent } from './agent.ts';
3
3
  import { type HostLicks, type HostScoops } from './control.ts';
4
4
  import { AgentSettings } from './services.ts';
5
5
  import type { SkillsRuntime } from './skills/index.ts';
6
+ import type { SprinklesRuntime } from './sprinkles/index.ts';
6
7
  import type { PortEndpoint } from './wire.ts';
7
8
  export interface AgentHost {
8
9
  readonly serverId: string;
@@ -17,4 +18,5 @@ export declare function hostAgent(agent: Agent, options?: {
17
18
  licks?: HostLicks;
18
19
  scoops?: HostScoops;
19
20
  skills?: SkillsRuntime;
21
+ sprinkles?: SprinklesRuntime;
20
22
  }): Promise<AgentHost>;
@@ -19,6 +19,7 @@ export interface CliOptions {
19
19
  roles: () => Promise<Roles>;
20
20
  alive?: (pid: number) => Promise<boolean>;
21
21
  now?: () => number;
22
+ sprinkle?: (argv: readonly string[], caller: string | null, context: Context) => Promise<Answer>;
22
23
  }
23
24
  export type Deps = CliOptions & {
24
25
  now: () => number;
@@ -5,6 +5,7 @@ import { type Agents } from '../agents.ts';
5
5
  import type { ProcessGroups } from '../kernel/groups.ts';
6
6
  import type { Licks } from '../licks/licks.ts';
7
7
  import { type Roles } from '../roles/roles.ts';
8
+ import { type CliOptions } from './cli.ts';
8
9
  import { type ControlPlane } from './requests.ts';
9
10
  import { type Scoops } from './service.ts';
10
11
  export declare const BUILTIN_ROLES: string[];
@@ -23,6 +24,7 @@ export interface AttachOptions {
23
24
  sweepEvery?: number;
24
25
  alive?: (pid: number) => Promise<boolean>;
25
26
  reads: () => readonly string[];
27
+ sprinkle?: CliOptions['sprinkle'];
26
28
  }
27
29
  export interface ScoopsRuntime {
28
30
  roles(context: Context): Promise<Roles>;
@@ -6,7 +6,7 @@ export declare const PROTOCOL = "slicc-agent/1";
6
6
  export declare const STALE_MS: number;
7
7
  export interface Request {
8
8
  id: string;
9
- as: 'agent' | 'subagent';
9
+ as: 'agent' | 'subagent' | 'sprinkle';
10
10
  caller: string;
11
11
  cwd: string;
12
12
  pid: number | null;
@@ -1,5 +1,6 @@
1
- import { type Context, type ReplicatedState } from '@earendil-works/chord';
1
+ import { type Context, type JsonValue, type ReplicatedState } from '@earendil-works/chord';
2
2
  import type { ConversationView } from '@earendil-works/pi-durable';
3
+ import type { Sprinkle } from './sprinkles/kind.ts';
3
4
  export type SendMode = 'steer' | 'followUp' | 'reject';
4
5
  export interface SendRequest {
5
6
  text: string;
@@ -52,6 +53,14 @@ export interface AgentControl {
52
53
  unqueue(agentId: string | null, submissionId: string, context: Context): Promise<{
53
54
  outcome: 'withdrawn' | 'already_placed' | 'not_found';
54
55
  }>;
56
+ sprinkleSend(sprinkleId: string, payload: {
57
+ action: string;
58
+ data: JsonValue | null;
59
+ target: string | null;
60
+ }, context: Context): Promise<{
61
+ delivered: boolean;
62
+ }>;
63
+ sprinkleCall(sprinkleId: string, method: string, args: JsonValue[], context: Context): Promise<JsonValue>;
55
64
  }
56
65
  export type Created = {
57
66
  id: string;
@@ -110,6 +119,7 @@ export interface AgentTranscript {
110
119
  readonly agents: ReplicatedState<AgentsSummary>;
111
120
  readonly views: ReplicatedState<Record<string, ConversationView>>;
112
121
  readonly commands: ReplicatedState<Command[]>;
122
+ readonly sprinkles: ReplicatedState<Sprinkle[]>;
113
123
  }
114
124
  export interface Command {
115
125
  name: string;
@@ -1,10 +1,12 @@
1
1
  import type { AgentConnection } from '../client.ts';
2
2
  import { AgentAdapter } from './agent-port.ts';
3
3
  import { type Login, SettingsAdapter } from './settings-port.ts';
4
+ import { SprinkleAdapter } from './sprinkle-port.ts';
4
5
  import { TrayAdapter } from './tray-port.ts';
5
6
  export { AgentAdapter, CONE } from './agent-port.ts';
6
7
  export { assistant, errorAction, errorPart, isBusy, queued, textOf, toMessages, } from './messages.ts';
7
8
  export { type Login, SETTINGS_KEY, SettingsAdapter } from './settings-port.ts';
9
+ export { SprinkleAdapter } from './sprinkle-port.ts';
8
10
  export { TrayAdapter } from './tray-port.ts';
9
11
  export declare function createAgentModel(connection: AgentConnection, options?: {
10
12
  storage?: Pick<Storage, 'getItem' | 'setItem'> | null;
@@ -13,4 +15,5 @@ export declare function createAgentModel(connection: AgentConnection, options?:
13
15
  agent: AgentAdapter;
14
16
  settings: SettingsAdapter;
15
17
  tray: TrayAdapter;
18
+ sprinkles: SprinkleAdapter;
16
19
  };
@@ -0,0 +1,10 @@
1
+ import type { Sprinkle, SprinkleEvents, SprinklePort } from '@ai-ecoverse/slicc-spectrum/ui';
2
+ import type { AgentConnection } from '../client.ts';
3
+ import { Emitter } from './emitter.ts';
4
+ export declare class SprinkleAdapter extends Emitter<SprinkleEvents> implements SprinklePort {
5
+ #private;
6
+ constructor(connection: AgentConnection);
7
+ list(): readonly Sprinkle[];
8
+ send(id: string, payload: unknown): void;
9
+ call(id: string, method: string, args: readonly unknown[]): Promise<unknown>;
10
+ }
@@ -0,0 +1,38 @@
1
+ import { type Context, type JsonValue, type ReplicatedState } from '@earendil-works/chord';
2
+ import type { Harness } from '@earendil-works/pi-durable';
3
+ import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
4
+ import type { Agents } from '../agents.ts';
5
+ import type { Licks } from '../licks/licks.ts';
6
+ import { type Sprinkle } from './sprinkles.ts';
7
+ export type Answer = {
8
+ code: number;
9
+ out: string;
10
+ };
11
+ export interface SprinklePayload {
12
+ action: string;
13
+ data: JsonValue | null;
14
+ target: string | null;
15
+ }
16
+ export interface SprinklesRuntime {
17
+ readonly sprinkles: ReplicatedState<Sprinkle[]>;
18
+ send(id: string, payload: SprinklePayload, context: Context): Promise<{
19
+ delivered: boolean;
20
+ }>;
21
+ call(id: string, method: string, args: readonly unknown[], context: Context): Promise<JsonValue>;
22
+ command(argv: readonly string[], caller: string | null, context: Context): Promise<Answer>;
23
+ reload(context: Context): Promise<void>;
24
+ close(context: Context): Promise<void>;
25
+ }
26
+ export interface SprinklesAttach {
27
+ harness: Harness;
28
+ agents: Agents;
29
+ licks: Licks;
30
+ env: ExecutionEnv;
31
+ assets?: (path: string) => Promise<string>;
32
+ reloadMs: number;
33
+ dir?: string;
34
+ }
35
+ export declare const USAGE = "usage:\n sprinkle list every sprinkle in /home/sprinkles, with its owner\n sprinkle show <name> show a sprinkle in your chat\n sprinkle own <name> [<agent>] send its messages to <agent> (cone, cone-<n> or a scoop's handle)\n\nA sprinkle is an .shtml file in /home/sprinkles, as <name>.shtml or <name>/<name>.shtml.\nEvery one of them has a button in the rail; edit the file and the open panel reloads.\n";
36
+ export declare function setupSprinkles(): {
37
+ attach(options: SprinklesAttach, context: Context): Promise<SprinklesRuntime>;
38
+ };
@@ -0,0 +1,10 @@
1
+ export declare const SPRINKLE_KIND = "slicc.sprinkle";
2
+ export interface Sprinkle {
3
+ id: string;
4
+ name: string;
5
+ title: string;
6
+ icon: string;
7
+ agentId: string;
8
+ html: string;
9
+ inline?: boolean;
10
+ }
@@ -0,0 +1,24 @@
1
+ import type { Context, JsonValue } from '@earendil-works/chord';
2
+ import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
3
+ export { SPRINKLE_KIND, type Sprinkle } from './kind.ts';
4
+ export declare const SPRINKLES_DIR = "/home/sprinkles";
5
+ export declare const WELCOME = "welcome";
6
+ export declare const WELCOMED = "/home/.welcomed";
7
+ export declare const DEFAULT_ICON = "sparkles";
8
+ export type SprinkleMethod = 'readFile' | 'exists' | 'getState' | 'setState';
9
+ export declare const SprinklesDoc: import("@earendil-works/pi-durable").SessionDocToken<{
10
+ owners: Record<string, string>;
11
+ state: Record<string, JsonValue>;
12
+ welcomed: boolean;
13
+ }>;
14
+ export declare function describe(name: string, html: string): {
15
+ title: string;
16
+ icon: string;
17
+ };
18
+ export declare const NAME: RegExp;
19
+ export declare function sprinkleFiles(env: Pick<ExecutionEnv, 'listDir'>, dir: string, context: Context): Promise<{
20
+ name: string;
21
+ path: string;
22
+ }[]>;
23
+ export declare function homePath(path: unknown): string;
24
+ export declare const NO_EXEC = "sprinkles can't run commands in SLICC; send the agent a message with slicc.lick() and let it run them";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "3.10.0",
3
+ "version": "3.11.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",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@ai-ecoverse/slicc-shared-web": "1.8.0",
65
- "@ai-ecoverse/slicc-spectrum": "1.23.3",
65
+ "@ai-ecoverse/slicc-spectrum": "1.24.0",
66
66
  "@ai-ecoverse/wasm-bash": "5.3.0-7",
67
67
  "@ai-ecoverse/wasm-coreutils": "9.7.0-3",
68
68
  "@biomejs/biome": "2.5.15",
@@ -76,7 +76,7 @@
76
76
  "typescript": "7.0.2"
77
77
  },
78
78
  "dependencies": {
79
- "@ai-ecoverse/slicc-kernel": "1.17.3",
79
+ "@ai-ecoverse/slicc-kernel": "1.17.4",
80
80
  "@earendil-works/chord": "1.0.4",
81
81
  "@earendil-works/pi-ai": "1.0.4",
82
82
  "@earendil-works/pi-client": "1.0.4",