@ai-ecoverse/slicc-agent 3.15.0 → 3.16.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 +19 -3
- package/bin/agent +1 -1
- package/dist/gelatiere/index.js +307 -0
- package/dist/gelatiere/index.js.map +7 -0
- package/dist/gelatiere/store.js +122 -0
- package/dist/gelatiere/store.js.map +7 -0
- package/dist/main.js +14 -0
- package/dist/main.js.map +2 -2
- package/dist/memory/extract.js +252 -0
- package/dist/memory/extract.js.map +7 -0
- package/dist/memory/index.js +26 -2
- package/dist/memory/index.js.map +2 -2
- package/dist/memory/prompt.js.map +2 -2
- package/dist/memory/runtime.js +1 -1
- package/dist/memory/runtime.js.map +2 -2
- package/dist/scoops/cli.js +2 -0
- package/dist/scoops/cli.js.map +2 -2
- package/dist/scoops/index.js +4 -3
- package/dist/scoops/index.js.map +2 -2
- package/dist/scoops/requests.js +2 -1
- package/dist/scoops/requests.js.map +2 -2
- package/dist/skills/index.js +1 -0
- package/dist/skills/index.js.map +2 -2
- package/dist/sprinkles/index.js +37 -29
- package/dist/sprinkles/index.js.map +2 -2
- package/dist/sprinkles/sprinkles.js +6 -0
- package/dist/sprinkles/sprinkles.js.map +2 -2
- package/dist/types/gelatiere/index.d.ts +39 -0
- package/dist/types/gelatiere/store.d.ts +34 -0
- package/dist/types/memory/extract.d.ts +48 -0
- package/dist/types/memory/index.d.ts +1 -0
- package/dist/types/memory/prompt.d.ts +2 -0
- package/dist/types/memory/runtime.d.ts +3 -0
- package/dist/types/scoops/cli.d.ts +1 -0
- package/dist/types/scoops/index.d.ts +1 -0
- package/dist/types/scoops/requests.d.ts +1 -1
- package/dist/types/sprinkles/index.d.ts +1 -0
- package/dist/types/sprinkles/sprinkles.d.ts +3 -0
- package/package.json +1 -1
- package/packages/vfs-root/agents/gelatiere.md +15 -0
- package/packages/vfs-root/gelatiere/GELATIERE.md +19 -0
- package/packages/vfs-root/skills/gelatiere/SKILL.md +30 -0
- package/packages/vfs-root/skills/memory/SKILL.md +4 -0
- package/packages/vfs-root/sprinkles/suggestions.shtml +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ Each scoop is owned by a background durable task, its **anchor**, so Stop in its
|
|
|
165
165
|
- `agent --async …` (= `subagent spawn …`) starts a persistent scoop and prints its handle; only cones start them. `list [--agents]`, `status`, `rename`, `send [--follow-up]`, `wait [--timeout <s>] [--notify]` (30 s by default; `--notify` returns at once and brings the answers together in one `scoop-wait` lick) and `stop` manage them. From a scoop, `agent send parent "<note>"` posts a progress note to its cone as a lick, coalesced per scoop.
|
|
166
166
|
- Options: `--name`, `--agent <role>`, `--model <provider/model>`, `--thinking <level>` or `--effort low|medium|high|max`, `--tools <a,b>` (or `auto`/`full`), `--read-only <paths>`, `--system-prompt[-file]`. `--schema-b64`, `--tools output`, `--session`, `--resume`, `--image`, `--no-escalate`, `--minimal` and `--usage` answer "not supported in SLICC yet".
|
|
167
167
|
|
|
168
|
-
**Roles** are pi agent files: Markdown with a frontmatter subset (`name`, `description`, `tools`, `model`, `thinking`, `memory`, `inheritProjectContext`, `inheritGlobalContext`, …) and the role's prompt as the body, added to the scoop's instructions. The built-ins are `scout`, `worker`, `reviewer`, `oracle` and `
|
|
168
|
+
**Roles** are pi agent files: Markdown with a frontmatter subset (`name`, `description`, `tools`, `model`, `thinking`, `memory`, `inheritProjectContext`, `inheritGlobalContext`, …) and the role's prompt as the body, added to the scoop's instructions. The built-ins are `scout`, `worker`, `reviewer`, `oracle`, `delegate` and `gelatiere` (below). Later sources replace earlier ones by name: built-ins, then packages in the kernel's `node_modules` that name a folder in `pi-subagents.agents` or `pi.subagents.agents`, then `~/.pi/agent/agents/`. A project's `.pi/agents/` will count only in trusted folders, and folders can't be trusted yet. `~/.pi/agent/settings.json` can override roles (`subagents.agentOverrides`) and set the limits `subagents.maxLiveScoops` (16 live scoops per cone) and `subagents.maxPerTurn` (64 scoops started per cone turn), which the worker counts for both kinds.
|
|
169
169
|
|
|
170
170
|
**Isolation, for now.** The file tools of a scoop write only under its folder and `/tmp`, plus the cwd of a v6-form call, and read those, their cone's working folder (`/home` by default) and `--read-only` paths; paths are normalized, `..` included, before the check. bash isn't confined, and the scoop is told so: the guard is a guardrail, not a boundary. A scoop's commands share one process group (see Bash and files on slicc-kernel), and stopping the scoop signals it, together with any group recorded for the scoop on an older kernel.
|
|
171
171
|
|
|
@@ -209,7 +209,7 @@ The worker watches the roots and reloads on change. A `skills` prompt section, g
|
|
|
209
209
|
|
|
210
210
|
**v6 skills.** None of the 29 skills SLICC v6 ships runs here unchanged. They name v6 paths (`/shared`, `/workspace`) or commands this kernel doesn't have. By what they wait for:
|
|
211
211
|
- **Rewritten here:** `delegation` (now the `agent` skill), `automation` (now `licks`), `skill-authoring`, `sprinkles` and `welcome`.
|
|
212
|
-
- **Memory:** `memory` is rewritten here (below); its `status`, `log`, `curate` and `dream` and the curation ledger are not ported. `gelatiere`
|
|
212
|
+
- **Memory:** `memory` is rewritten here (below); its `status`, `log`, `curate` and `dream` and the curation ledger are not ported. `gelatiere` is rewritten here (below), without `use-cases` and the www.sliccy.com catalog; `wiki` needs its `wiki` CLI.
|
|
213
213
|
- **The freezer (PR 15):** `transcript-export`.
|
|
214
214
|
- **The tray hub, upskill or the Install/Update panel:** `upgrade` (`upgrade apply`, bios#70), `handoff`, `slicc`, `ssh`, `cherry`.
|
|
215
215
|
- **A browser and CDP:** `playwright-cli`, `computer`, `v86`.
|
|
@@ -252,6 +252,22 @@ The `memory` section, read before every request, holds the first 200 lines (at m
|
|
|
252
252
|
|
|
253
253
|
**`memory_write`** saves, replaces (same section and title) or removes one entry in the caller's own file, or in `global` for cones. Writes to a file are serialized and re-read the file, so edits by hand survive. A file holds 16 KB; a write over that must make it smaller. Anything that looks like a secret (API keys, tokens, `Authorization` values, private keys, `password=`, launch URLs with a `#key`) is redacted before it's written. A role with `memory` and write tools gets `memory_write` in its tool list.
|
|
254
254
|
|
|
255
|
+
**Extraction.** Durable compacts a long conversation into a summary; before it does, a `beforeCompact` hook (which never declines or replaces the summary) schedules a `slicc.memory-extract` task for the cone. A cone that has been idle for `memory.idleMinutes` (30 by default, `0` turns the idle trigger off) gets the same task. The task runs only when there is something new since the last extraction (a cursor per conversation in the session document `slicc.memory-extract`), makes one model call, with the cone's model unless `memory.extractModel` names a cheaper one (`provider/model`), and writes the proposed entries through `memory_write`'s writer into the cone's memory file, within its 16 KB. The transcript it sends is redacted first, its prompt forbids secrets, and the writer redacts again. `"memory": { "extract": false }` in `~/.pi/agent/settings.json` turns extraction off. v6's curator scoop, three-way merge, curation ledger and dreamer aren't ported.
|
|
256
|
+
|
|
257
|
+
**The gelatiere.** SLICC's resident advisor, as in v6, is a scoop with the built-in `gelatiere` role. That role has its own memory (`memory: { scope: user, path: gelatiere }`), so it keeps its notes there.
|
|
258
|
+
- `gelatiere init` starts the scoop in the first cone, writes `~/.pi/agent/GELATIERE.md` (its procedure, which the user can edit) unless it exists, and puts one crontab line `0 3 * * * gelatiere scoop:<handle> …` in `~/.slicc/crontab`, replacing an older one.
|
|
259
|
+
- On each pass it reads memory, the installed skills, the roles and the lick files, and writes at most three suggestions as JSON. `gelatiere suggest <file>` validates them and folds them into `/home/.gelatiere/suggestions.json` by id:
|
|
260
|
+
- kinds `skill`, `use-case`, `tip`, `skill-idea` and `issue`;
|
|
261
|
+
- a `skill` with its name and its `source`, the https URL of a single-file `SKILL.md`;
|
|
262
|
+
- https URLs only;
|
|
263
|
+
- secrets redacted;
|
|
264
|
+
- at most 50 open;
|
|
265
|
+
- dismissed and taken ids are never reopened.
|
|
266
|
+
- `gelatiere deliver` sends each cone named in a suggestion's `cones` (or the first cone) one `sprinkle` lick with the action `gelatiere-suggestions`. The built-in `gelatiere` skill answers it with `sprinkle show suggestions`, and looks ids up in the store before installing or trying anything.
|
|
267
|
+
- `gelatiere install <id>` is what a card's Install leads to. It fetches the suggestion's `source` with curl (seven has no `upskill` and no `git`), checks that the file is a valid `SKILL.md` for that name, writes `~/.pi/agent/skills/<name>/SKILL.md`, which the skills loader picks up, and marks the suggestion taken. It refuses a skill that already exists and any source that isn't https. Before anyone clicks, the card shows where the skill comes from: "Installs github from raw.githubusercontent.com/…".
|
|
268
|
+
- `gelatiere run`, `list [--all] [--json]`, `dismiss <id>` and `status` complete the command.
|
|
269
|
+
- The gelatiere doesn't read the www.sliccy.com skill catalog or use-case pages, and doesn't consolidate other agents' memory, which only cones and the user write.
|
|
270
|
+
|
|
255
271
|
**The panel.** The transcript service adds `memories` and `memoryScopes`, and `AgentControl` `memorySave` and `memoryRemove`; `createAgentModel()` returns a `MemoryAdapter` for spectrum's `MemoryPort`, with `scopes()` (global, cones, roles) for its picker. `memory show [<scope>]` and `memory scopes` in bash print the same files, and the built-in `memory` skill teaches all of it.
|
|
256
272
|
|
|
257
273
|
### Sprinkles
|
|
@@ -278,7 +294,7 @@ v6's `open`, `reload`, `close`, `send` and `chat` answer with what to do instead
|
|
|
278
294
|
|
|
279
295
|
**Shipped sprinkles.** Only `welcome` and `suggestions`, with slicc-spectrum's reviewed copy.
|
|
280
296
|
- **Welcome.** The worker posts it inline in the first cone's chat once per session. Its `onboarding-complete` send writes `/home/.welcomed`, which the card checks, and the built-in `welcome` skill answers the lick.
|
|
281
|
-
- **Suggestions.** It
|
|
297
|
+
- **Suggestions.** It's listed once the gelatiere's store `/home/.gelatiere/suggestions.json` exists, and owned by the active cone. Its `gelatiere-dismiss` send marks the suggestion in the store and never reaches an agent; `gelatiere-install` and `gelatiere-try` mark it taken and go to the owner as licks. Its `exec` for use cases is refused, so it shows its static empty state.
|
|
282
298
|
|
|
283
299
|
The built-in `sprinkles` skill teaches the files, the bridge and the command.
|
|
284
300
|
|
package/bin/agent
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
dir=/var/lib/slicc/agent/requests
|
|
3
3
|
as=$(basename "$0")
|
|
4
|
-
case "$as" in subagent|sprinkle|memory) ;; *) as=agent ;; esac
|
|
4
|
+
case "$as" in subagent|sprinkle|memory|gelatiere) ;; *) as=agent ;; esac
|
|
5
5
|
mkdir -p "$dir/in" "$dir/out" 2>/dev/null
|
|
6
6
|
|
|
7
7
|
submit() {
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { live } from "../agents.js";
|
|
2
|
+
import { skillFromText } from "../skills/skills.js";
|
|
3
|
+
import { fold, open, readStore, STORE, validate, writeStore } from "./store.js";
|
|
4
|
+
const ROLE = "gelatiere";
|
|
5
|
+
const SPRINKLE = "suggestions";
|
|
6
|
+
const PROCEDURE = "packages/vfs-root/gelatiere/GELATIERE.md";
|
|
7
|
+
const SCHEDULE = "0 3 * * *";
|
|
8
|
+
const USAGE = `usage:
|
|
9
|
+
gelatiere init start the gelatiere scoop and its nightly crontab line
|
|
10
|
+
gelatiere run ask the gelatiere for a pass now
|
|
11
|
+
gelatiere suggest <file> fold a JSON array of suggestions into the store
|
|
12
|
+
gelatiere deliver lick each cone with the suggestions that are new for it
|
|
13
|
+
gelatiere install <id> install a suggested skill from its source into ~/.pi/agent/skills
|
|
14
|
+
gelatiere list [--all] [--json]
|
|
15
|
+
gelatiere dismiss <id> mark a suggestion as not wanted
|
|
16
|
+
gelatiere status
|
|
17
|
+
|
|
18
|
+
The store is ${STORE}; the suggestions sprinkle shows it.
|
|
19
|
+
`;
|
|
20
|
+
function line(handle) {
|
|
21
|
+
return `${SCHEDULE} ${ROLE} scoop:${handle} Nightly pass: read ~/.pi/agent/GELATIERE.md and follow it.`;
|
|
22
|
+
}
|
|
23
|
+
const JOB = new RegExp(`^((?:\\S+\\s+){4}\\S+|@\\w+)\\s+${ROLE}(?:\\s|$)`);
|
|
24
|
+
function scheduleOf(entry) {
|
|
25
|
+
return JOB.exec(entry.trim())?.[1]?.replace(/\s+/g, " ");
|
|
26
|
+
}
|
|
27
|
+
function handleOf(agents) {
|
|
28
|
+
const found = Object.entries(agents.state().scoops).find(
|
|
29
|
+
([, record]) => record.role === ROLE && live(record)
|
|
30
|
+
);
|
|
31
|
+
return found?.[1].folder;
|
|
32
|
+
}
|
|
33
|
+
function row(item) {
|
|
34
|
+
const state = item.takenAt ? "taken" : item.dismissedAt ? "dismissed" : "open";
|
|
35
|
+
return `${item.id} ${item.kind} ${state} ${item.title}`;
|
|
36
|
+
}
|
|
37
|
+
const fail = (out) => ({ code: 1, out: `gelatiere: ${out}
|
|
38
|
+
` });
|
|
39
|
+
async function schedule(g, handle, context) {
|
|
40
|
+
const { env, home } = g.options;
|
|
41
|
+
const read = await env.readTextFile(g.crontab, context);
|
|
42
|
+
const kept = (read.ok ? read.value : "").split("\n").filter((entry) => entry.trim() && !scheduleOf(entry));
|
|
43
|
+
await env.createDir(`${home}/.slicc`, { recursive: true }, context);
|
|
44
|
+
await env.writeFile(g.crontab, `${[...kept, line(handle)].join("\n")}
|
|
45
|
+
`, context);
|
|
46
|
+
}
|
|
47
|
+
async function seed(g, context) {
|
|
48
|
+
const { env, assets } = g.options;
|
|
49
|
+
const existing = await env.exists(g.procedure, context);
|
|
50
|
+
if (existing.ok && existing.value || !assets) return;
|
|
51
|
+
const text = await assets(PROCEDURE).catch(() => void 0);
|
|
52
|
+
if (text !== void 0) await env.writeFile(g.procedure, text, context);
|
|
53
|
+
}
|
|
54
|
+
async function init(g, context) {
|
|
55
|
+
const { options } = g;
|
|
56
|
+
await seed(g, context);
|
|
57
|
+
let handle = handleOf(options.agents);
|
|
58
|
+
if (!handle) {
|
|
59
|
+
const roles = await options.roles(context);
|
|
60
|
+
const role = roles.roles.find((entry) => entry.name === ROLE);
|
|
61
|
+
if (!role) return fail("the gelatiere role is missing");
|
|
62
|
+
const spawned = await options.scoops.spawn(
|
|
63
|
+
{
|
|
64
|
+
cone: "cone",
|
|
65
|
+
kind: "async",
|
|
66
|
+
name: ROLE,
|
|
67
|
+
role,
|
|
68
|
+
prompts: [],
|
|
69
|
+
target: null,
|
|
70
|
+
fromAgent: false,
|
|
71
|
+
request: `gelatiere-init-${g.now()}`,
|
|
72
|
+
limits: roles.limits
|
|
73
|
+
},
|
|
74
|
+
context
|
|
75
|
+
);
|
|
76
|
+
if (spawned.code !== 0) return { code: 1, out: `gelatiere: ${spawned.out}` };
|
|
77
|
+
handle = spawned.out.trim();
|
|
78
|
+
}
|
|
79
|
+
await schedule(g, handle, context);
|
|
80
|
+
return {
|
|
81
|
+
code: 0,
|
|
82
|
+
out: `the gelatiere is scoop ${handle}; it runs at ${SCHEDULE} (crontab) and with gelatiere run, following ${g.procedure}
|
|
83
|
+
`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function run(g, context) {
|
|
87
|
+
const handle = handleOf(g.options.agents);
|
|
88
|
+
if (!handle) return fail("there is no gelatiere yet; run gelatiere init");
|
|
89
|
+
await g.options.licks.deliver(
|
|
90
|
+
{
|
|
91
|
+
channel: "sprinkle",
|
|
92
|
+
source: "sprinkle:gelatiere",
|
|
93
|
+
title: "Gelatiere",
|
|
94
|
+
text: "run",
|
|
95
|
+
body: `Run a pass now: read ${g.procedure} and follow it.`,
|
|
96
|
+
target: `scoop:${handle}`
|
|
97
|
+
},
|
|
98
|
+
context
|
|
99
|
+
);
|
|
100
|
+
return { code: 0, out: `asked scoop ${handle} for a pass
|
|
101
|
+
` };
|
|
102
|
+
}
|
|
103
|
+
async function suggest(g, path, context) {
|
|
104
|
+
if (!path) return { code: 2, out: USAGE };
|
|
105
|
+
const read = await g.options.env.readTextFile(path, context);
|
|
106
|
+
if (!read.ok) return fail(`can't read ${path}`);
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(read.value);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return fail(`${path} is not JSON (${error.message})`);
|
|
112
|
+
}
|
|
113
|
+
const folded = fold(await g.load(context), parsed, g.now());
|
|
114
|
+
await writeStore(g.options.env, folded.store, context);
|
|
115
|
+
const rejected = folded.rejected.map((reason) => `rejected ${reason}
|
|
116
|
+
`).join("");
|
|
117
|
+
return {
|
|
118
|
+
code: 0,
|
|
119
|
+
out: `added ${folded.added}, updated ${folded.updated}, rejected ${folded.rejected.length}
|
|
120
|
+
${rejected}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function deliver(g, context) {
|
|
124
|
+
const store = await g.load(context);
|
|
125
|
+
const fresh = open(store).filter((item) => !item.deliveredAt);
|
|
126
|
+
const cones = Object.keys(g.options.agents.state().cones);
|
|
127
|
+
const byCone = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const item of fresh) {
|
|
129
|
+
const named = item.cones.filter((cone) => cones.includes(cone));
|
|
130
|
+
for (const cone of named.length ? named : ["cone"])
|
|
131
|
+
byCone.set(cone, [...byCone.get(cone) ?? [], item]);
|
|
132
|
+
}
|
|
133
|
+
for (const [cone, items] of byCone) {
|
|
134
|
+
const body = {
|
|
135
|
+
added: items.length,
|
|
136
|
+
open: open(store).length,
|
|
137
|
+
suggestions: items.map(({ id, kind, title, body: text }) => ({
|
|
138
|
+
id,
|
|
139
|
+
kind,
|
|
140
|
+
title,
|
|
141
|
+
body: text
|
|
142
|
+
})),
|
|
143
|
+
path: STORE
|
|
144
|
+
};
|
|
145
|
+
await g.options.licks.deliver(
|
|
146
|
+
{
|
|
147
|
+
channel: "sprinkle",
|
|
148
|
+
source: `sprinkle:${SPRINKLE}`,
|
|
149
|
+
title: "Suggestions",
|
|
150
|
+
text: "gelatiere-suggestions",
|
|
151
|
+
body: JSON.stringify(body, null, 2),
|
|
152
|
+
target: `cone:${cone}`
|
|
153
|
+
},
|
|
154
|
+
context
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
for (const item of fresh) item.deliveredAt = g.now();
|
|
158
|
+
if (fresh.length) await writeStore(g.options.env, store, context);
|
|
159
|
+
return {
|
|
160
|
+
code: 0,
|
|
161
|
+
out: `delivered ${fresh.length} to ${[...byCone.keys()].join(", ") || "nobody"}
|
|
162
|
+
`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function fetchText(env, url, temp, context) {
|
|
166
|
+
const fetched = await env.exec(
|
|
167
|
+
["curl", "-fsSL", "--max-time", "30", "-o", temp, url],
|
|
168
|
+
void 0,
|
|
169
|
+
context
|
|
170
|
+
);
|
|
171
|
+
const read = await env.readTextFile(temp, context);
|
|
172
|
+
await env.remove(temp, { force: true }, context);
|
|
173
|
+
return fetched.ok && fetched.value.exitCode === 0 && read.ok ? read.value : void 0;
|
|
174
|
+
}
|
|
175
|
+
async function install(g, id, context) {
|
|
176
|
+
const { env, home } = g.options;
|
|
177
|
+
const item = (await g.load(context)).find((entry) => entry.id === id && entry.kind === "skill");
|
|
178
|
+
const checked = item ? validate(item) : `there is no skill suggestion ${id}`;
|
|
179
|
+
if (typeof checked === "string") return fail(checked);
|
|
180
|
+
const { skill, source } = checked;
|
|
181
|
+
const dir = `${home}/.pi/agent/skills/${skill}`;
|
|
182
|
+
const present = await env.exists(`${dir}/SKILL.md`, context);
|
|
183
|
+
if (present.ok && present.value) return fail(`${skill} is already installed in ${dir}`);
|
|
184
|
+
const text = await fetchText(env, source, `${home}/.gelatiere-${id}.md`, context);
|
|
185
|
+
if (text === void 0) return fail(`couldn't fetch ${source}`);
|
|
186
|
+
const parsed = skillFromText(text, `${dir}/SKILL.md`, "user");
|
|
187
|
+
if (parsed.skills[0]?.name !== skill) {
|
|
188
|
+
const why = parsed.diagnostics[0] ? ` (${parsed.diagnostics[0].message})` : "";
|
|
189
|
+
return fail(`${source} is not a SKILL.md for ${skill}${why}`);
|
|
190
|
+
}
|
|
191
|
+
await env.createDir(dir, { recursive: true }, context);
|
|
192
|
+
await env.writeFile(`${dir}/SKILL.md`, text, context);
|
|
193
|
+
await g.mark(id, "takenAt", context);
|
|
194
|
+
return {
|
|
195
|
+
code: 0,
|
|
196
|
+
out: `installed ${skill} in ${dir}; it is /skill:${skill} once the skills reload
|
|
197
|
+
`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function list(g, argv, context) {
|
|
201
|
+
const store = await g.load(context);
|
|
202
|
+
const shown = argv.includes("--all") ? store : open(store);
|
|
203
|
+
if (argv.includes("--json")) return { code: 0, out: `${JSON.stringify(shown, null, 2)}
|
|
204
|
+
` };
|
|
205
|
+
return { code: 0, out: shown.length ? `${shown.map(row).join("\n")}
|
|
206
|
+
` : "no suggestions\n" };
|
|
207
|
+
}
|
|
208
|
+
async function status(g, context) {
|
|
209
|
+
const { env, agents } = g.options;
|
|
210
|
+
const store = await readStore(env, context);
|
|
211
|
+
const handle = handleOf(agents);
|
|
212
|
+
const read = await env.readTextFile(g.crontab, context);
|
|
213
|
+
const scheduled = (read.ok ? read.value : "").split("\n").map(scheduleOf).find(Boolean);
|
|
214
|
+
const counts = store ? `${open(store).length} open, ${store.filter((item) => item.takenAt).length} taken, ${store.filter((item) => item.dismissedAt).length} dismissed` : "none yet";
|
|
215
|
+
const lines = [
|
|
216
|
+
`scoop: ${handle ?? "none (gelatiere init)"}`,
|
|
217
|
+
`schedule: ${scheduled ?? "none"}`,
|
|
218
|
+
`store: ${counts}`
|
|
219
|
+
];
|
|
220
|
+
return { code: 0, out: `${lines.join("\n")}
|
|
221
|
+
` };
|
|
222
|
+
}
|
|
223
|
+
async function dismiss(g, id, context) {
|
|
224
|
+
return await g.mark(id, "dismissedAt", context) ? { code: 0, out: `dismissed ${id}
|
|
225
|
+
` } : fail(`there is no suggestion ${id}`);
|
|
226
|
+
}
|
|
227
|
+
const VERBS = {
|
|
228
|
+
init: (g, _argv, context) => init(g, context),
|
|
229
|
+
run: (g, _argv, context) => run(g, context),
|
|
230
|
+
suggest: (g, argv, context) => g.serial(() => suggest(g, argv[1], context)),
|
|
231
|
+
deliver: (g, _argv, context) => g.serial(() => deliver(g, context)),
|
|
232
|
+
install: (g, argv, context) => install(g, argv[1] ?? "", context),
|
|
233
|
+
list: (g, argv, context) => list(g, argv, context),
|
|
234
|
+
status: (g, _argv, context) => status(g, context),
|
|
235
|
+
dismiss: (g, argv, context) => dismiss(g, argv[1] ?? "", context)
|
|
236
|
+
};
|
|
237
|
+
function attachGelatiere(options) {
|
|
238
|
+
const { env, home } = options;
|
|
239
|
+
const now = options.now ?? Date.now;
|
|
240
|
+
const load = async (context) => await readStore(env, context) ?? [];
|
|
241
|
+
let chain = Promise.resolve();
|
|
242
|
+
const serial = (work) => {
|
|
243
|
+
const next = chain.catch(() => void 0).then(work);
|
|
244
|
+
chain = next;
|
|
245
|
+
return next;
|
|
246
|
+
};
|
|
247
|
+
const g = {
|
|
248
|
+
serial,
|
|
249
|
+
options,
|
|
250
|
+
now,
|
|
251
|
+
crontab: `${home}/.slicc/crontab`,
|
|
252
|
+
procedure: `${home}/.pi/agent/GELATIERE.md`,
|
|
253
|
+
load,
|
|
254
|
+
mark: (id, field, context) => serial(async () => {
|
|
255
|
+
const store = await load(context);
|
|
256
|
+
const item = store.find((entry) => entry.id === id);
|
|
257
|
+
if (!item) return false;
|
|
258
|
+
item[field] ??= now();
|
|
259
|
+
await writeStore(env, store, context);
|
|
260
|
+
return true;
|
|
261
|
+
})
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
command(argv, _caller, context) {
|
|
265
|
+
const verb = argv[0] ?? "help";
|
|
266
|
+
const handler = VERBS[verb];
|
|
267
|
+
if (handler) return handler(g, argv, context);
|
|
268
|
+
return Promise.resolve({ code: verb === "help" || verb === "--help" ? 0 : 2, out: USAGE });
|
|
269
|
+
},
|
|
270
|
+
async intercept(action, data, context) {
|
|
271
|
+
const id = String(data?.id ?? "");
|
|
272
|
+
if (action === "gelatiere-dismiss") {
|
|
273
|
+
await g.mark(id, "dismissedAt", context);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (action === "gelatiere-install" || action === "gelatiere-try")
|
|
277
|
+
await g.mark(id, "takenAt", context);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function lateGelatiere() {
|
|
283
|
+
let runtime;
|
|
284
|
+
return {
|
|
285
|
+
async intercept(id, payload, context) {
|
|
286
|
+
return id === SPRINKLE && runtime !== void 0 ? runtime.intercept(payload.action, payload.data, context) : false;
|
|
287
|
+
},
|
|
288
|
+
command(argv, caller, context) {
|
|
289
|
+
return runtime ? runtime.command(argv, caller, context) : Promise.resolve({ code: 1, out: "gelatiere: not ready yet\n" });
|
|
290
|
+
},
|
|
291
|
+
attach(options) {
|
|
292
|
+
runtime = attachGelatiere(options);
|
|
293
|
+
return runtime;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export {
|
|
298
|
+
PROCEDURE,
|
|
299
|
+
ROLE,
|
|
300
|
+
SCHEDULE,
|
|
301
|
+
SPRINKLE,
|
|
302
|
+
USAGE,
|
|
303
|
+
attachGelatiere,
|
|
304
|
+
lateGelatiere,
|
|
305
|
+
scheduleOf
|
|
306
|
+
};
|
|
307
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/gelatiere/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { type Agents, live } from '../agents.ts';\nimport type { Licks } from '../licks/licks.ts';\nimport type { Roles } from '../roles/roles.ts';\nimport type { Scoops } from '../scoops/service.ts';\nimport { skillFromText } from '../skills/skills.ts';\nimport { fold, open, readStore, STORE, type Suggestion, validate, writeStore } from './store.ts';\n\nexport type Answer = { code: number; out: string };\n\nexport const ROLE = 'gelatiere';\nexport const SPRINKLE = 'suggestions';\nexport const PROCEDURE = 'packages/vfs-root/gelatiere/GELATIERE.md';\nexport const SCHEDULE = '0 3 * * *';\n\nexport interface GelatiereAttach {\n agents: Agents;\n licks: Licks;\n env: ExecutionEnv;\n home: string;\n scoops: Scoops;\n roles: (context: Context) => Promise<Roles>;\n assets?: (path: string) => Promise<string>;\n now?: () => number;\n}\n\nexport interface GelatiereRuntime {\n command(argv: readonly string[], caller: string | null, context: Context): Promise<Answer>;\n intercept(action: string, data: unknown, context: Context): Promise<boolean>;\n}\n\nexport const USAGE = `usage:\n gelatiere init start the gelatiere scoop and its nightly crontab line\n gelatiere run ask the gelatiere for a pass now\n gelatiere suggest <file> fold a JSON array of suggestions into the store\n gelatiere deliver lick each cone with the suggestions that are new for it\n gelatiere install <id> install a suggested skill from its source into ~/.pi/agent/skills\n gelatiere list [--all] [--json]\n gelatiere dismiss <id> mark a suggestion as not wanted\n gelatiere status\n\nThe store is ${STORE}; the suggestions sprinkle shows it.\n`;\n\nfunction line(handle: string): string {\n return `${SCHEDULE} ${ROLE} scoop:${handle} Nightly pass: read ~/.pi/agent/GELATIERE.md and follow it.`;\n}\n\nconst JOB = new RegExp(`^((?:\\\\S+\\\\s+){4}\\\\S+|@\\\\w+)\\\\s+${ROLE}(?:\\\\s|$)`);\n\nexport function scheduleOf(entry: string): string | undefined {\n return JOB.exec(entry.trim())?.[1]?.replace(/\\s+/g, ' ');\n}\n\nfunction handleOf(agents: Agents): string | undefined {\n const found = Object.entries(agents.state().scoops).find(\n ([, record]) => record.role === ROLE && live(record)\n );\n return found?.[1].folder;\n}\n\nfunction row(item: Suggestion): string {\n const state = item.takenAt ? 'taken' : item.dismissedAt ? 'dismissed' : 'open';\n return `${item.id}\\t${item.kind}\\t${state}\\t${item.title}`;\n}\n\ntype G = {\n serial: <T>(work: () => Promise<T>) => Promise<T>;\n options: GelatiereAttach;\n now: () => number;\n crontab: string;\n procedure: string;\n load: (context: Context) => Promise<Suggestion[]>;\n mark: (id: string, field: 'dismissedAt' | 'takenAt', context: Context) => Promise<boolean>;\n};\n\nconst fail = (out: string): Answer => ({ code: 1, out: `gelatiere: ${out}\\n` });\n\nasync function schedule(g: G, handle: string, context: Context): Promise<void> {\n const { env, home } = g.options;\n const read = await env.readTextFile(g.crontab, context);\n const kept = (read.ok ? read.value : '')\n .split('\\n')\n .filter((entry) => entry.trim() && !scheduleOf(entry));\n await env.createDir(`${home}/.slicc`, { recursive: true }, context);\n await env.writeFile(g.crontab, `${[...kept, line(handle)].join('\\n')}\\n`, context);\n}\n\nasync function seed(g: G, context: Context): Promise<void> {\n const { env, assets } = g.options;\n const existing = await env.exists(g.procedure, context);\n if ((existing.ok && existing.value) || !assets) return;\n const text = await assets(PROCEDURE).catch(() => undefined);\n if (text !== undefined) await env.writeFile(g.procedure, text, context);\n}\n\nasync function init(g: G, context: Context): Promise<Answer> {\n const { options } = g;\n await seed(g, context);\n let handle = handleOf(options.agents);\n if (!handle) {\n const roles = await options.roles(context);\n const role = roles.roles.find((entry) => entry.name === ROLE);\n if (!role) return fail('the gelatiere role is missing');\n const spawned = await options.scoops.spawn(\n {\n cone: 'cone',\n kind: 'async',\n name: ROLE,\n role,\n prompts: [],\n target: null,\n fromAgent: false,\n request: `gelatiere-init-${g.now()}`,\n limits: roles.limits,\n },\n context\n );\n if (spawned.code !== 0) return { code: 1, out: `gelatiere: ${spawned.out}` };\n handle = spawned.out.trim();\n }\n await schedule(g, handle, context);\n return {\n code: 0,\n out: `the gelatiere is scoop ${handle}; it runs at ${SCHEDULE} (crontab) and with gelatiere run, following ${g.procedure}\\n`,\n };\n}\n\nasync function run(g: G, context: Context): Promise<Answer> {\n const handle = handleOf(g.options.agents);\n if (!handle) return fail('there is no gelatiere yet; run gelatiere init');\n await g.options.licks.deliver(\n {\n channel: 'sprinkle',\n source: 'sprinkle:gelatiere',\n title: 'Gelatiere',\n text: 'run',\n body: `Run a pass now: read ${g.procedure} and follow it.`,\n target: `scoop:${handle}`,\n },\n context\n );\n return { code: 0, out: `asked scoop ${handle} for a pass\\n` };\n}\n\nasync function suggest(g: G, path: string | undefined, context: Context): Promise<Answer> {\n if (!path) return { code: 2, out: USAGE };\n const read = await g.options.env.readTextFile(path, context);\n if (!read.ok) return fail(`can't read ${path}`);\n let parsed: unknown;\n try {\n parsed = JSON.parse(read.value);\n } catch (error) {\n return fail(`${path} is not JSON (${(error as Error).message})`);\n }\n const folded = fold(await g.load(context), parsed, g.now());\n await writeStore(g.options.env, folded.store, context);\n const rejected = folded.rejected.map((reason) => `rejected ${reason}\\n`).join('');\n return {\n code: 0,\n out: `added ${folded.added}, updated ${folded.updated}, rejected ${folded.rejected.length}\\n${rejected}`,\n };\n}\n\nasync function deliver(g: G, context: Context): Promise<Answer> {\n const store = await g.load(context);\n const fresh = open(store).filter((item) => !item.deliveredAt);\n const cones = Object.keys(g.options.agents.state().cones);\n const byCone = new Map<string, Suggestion[]>();\n for (const item of fresh) {\n const named = item.cones.filter((cone) => cones.includes(cone));\n for (const cone of named.length ? named : ['cone'])\n byCone.set(cone, [...(byCone.get(cone) ?? []), item]);\n }\n for (const [cone, items] of byCone) {\n const body = {\n added: items.length,\n open: open(store).length,\n suggestions: items.map(({ id, kind, title, body: text }) => ({\n id,\n kind,\n title,\n body: text,\n })),\n path: STORE,\n };\n await g.options.licks.deliver(\n {\n channel: 'sprinkle',\n source: `sprinkle:${SPRINKLE}`,\n title: 'Suggestions',\n text: 'gelatiere-suggestions',\n body: JSON.stringify(body, null, 2),\n target: `cone:${cone}`,\n },\n context\n );\n }\n for (const item of fresh) item.deliveredAt = g.now();\n if (fresh.length) await writeStore(g.options.env, store, context);\n return {\n code: 0,\n out: `delivered ${fresh.length} to ${[...byCone.keys()].join(', ') || 'nobody'}\\n`,\n };\n}\n\nasync function fetchText(env: ExecutionEnv, url: string, temp: string, context: Context) {\n const fetched = await env.exec(\n ['curl', '-fsSL', '--max-time', '30', '-o', temp, url],\n undefined,\n context\n );\n const read = await env.readTextFile(temp, context);\n await env.remove(temp, { force: true }, context);\n return fetched.ok && fetched.value.exitCode === 0 && read.ok ? read.value : undefined;\n}\n\nasync function install(g: G, id: string, context: Context): Promise<Answer> {\n const { env, home } = g.options;\n const item = (await g.load(context)).find((entry) => entry.id === id && entry.kind === 'skill');\n const checked = item ? validate(item) : `there is no skill suggestion ${id}`;\n if (typeof checked === 'string') return fail(checked);\n const { skill, source } = checked as Required<Pick<Suggestion, 'skill' | 'source'>>;\n const dir = `${home}/.pi/agent/skills/${skill}`;\n const present = await env.exists(`${dir}/SKILL.md`, context);\n if (present.ok && present.value) return fail(`${skill} is already installed in ${dir}`);\n const text = await fetchText(env, source, `${home}/.gelatiere-${id}.md`, context);\n if (text === undefined) return fail(`couldn't fetch ${source}`);\n const parsed = skillFromText(text, `${dir}/SKILL.md`, 'user');\n if (parsed.skills[0]?.name !== skill) {\n const why = parsed.diagnostics[0] ? ` (${parsed.diagnostics[0].message})` : '';\n return fail(`${source} is not a SKILL.md for ${skill}${why}`);\n }\n await env.createDir(dir, { recursive: true }, context);\n await env.writeFile(`${dir}/SKILL.md`, text, context);\n await g.mark(id, 'takenAt', context);\n return {\n code: 0,\n out: `installed ${skill} in ${dir}; it is /skill:${skill} once the skills reload\\n`,\n };\n}\n\nasync function list(g: G, argv: readonly string[], context: Context): Promise<Answer> {\n const store = await g.load(context);\n const shown = argv.includes('--all') ? store : open(store);\n if (argv.includes('--json')) return { code: 0, out: `${JSON.stringify(shown, null, 2)}\\n` };\n return { code: 0, out: shown.length ? `${shown.map(row).join('\\n')}\\n` : 'no suggestions\\n' };\n}\n\nasync function status(g: G, context: Context): Promise<Answer> {\n const { env, agents } = g.options;\n const store = await readStore(env, context);\n const handle = handleOf(agents);\n const read = await env.readTextFile(g.crontab, context);\n const scheduled = (read.ok ? read.value : '').split('\\n').map(scheduleOf).find(Boolean);\n const counts = store\n ? `${open(store).length} open, ${store.filter((item) => item.takenAt).length} taken, ${store.filter((item) => item.dismissedAt).length} dismissed`\n : 'none yet';\n const lines = [\n `scoop: ${handle ?? 'none (gelatiere init)'}`,\n `schedule: ${scheduled ?? 'none'}`,\n `store: ${counts}`,\n ];\n return { code: 0, out: `${lines.join('\\n')}\\n` };\n}\n\nasync function dismiss(g: G, id: string, context: Context): Promise<Answer> {\n return (await g.mark(id, 'dismissedAt', context))\n ? { code: 0, out: `dismissed ${id}\\n` }\n : fail(`there is no suggestion ${id}`);\n}\n\nconst VERBS: Record<string, (g: G, argv: readonly string[], context: Context) => Promise<Answer>> =\n {\n init: (g, _argv, context) => init(g, context),\n run: (g, _argv, context) => run(g, context),\n suggest: (g, argv, context) => g.serial(() => suggest(g, argv[1], context)),\n deliver: (g, _argv, context) => g.serial(() => deliver(g, context)),\n install: (g, argv, context) => install(g, argv[1] ?? '', context),\n list: (g, argv, context) => list(g, argv, context),\n status: (g, _argv, context) => status(g, context),\n dismiss: (g, argv, context) => dismiss(g, argv[1] ?? '', context),\n };\n\nexport function attachGelatiere(options: GelatiereAttach): GelatiereRuntime {\n const { env, home } = options;\n const now = options.now ?? Date.now;\n const load = async (context: Context) => (await readStore(env, context)) ?? [];\n let chain: Promise<unknown> = Promise.resolve();\n const serial = <T>(work: () => Promise<T>): Promise<T> => {\n const next = chain.catch(() => undefined).then(work);\n chain = next;\n return next;\n };\n const g: G = {\n serial,\n options,\n now,\n crontab: `${home}/.slicc/crontab`,\n procedure: `${home}/.pi/agent/GELATIERE.md`,\n load,\n mark: (id, field, context) =>\n serial(async () => {\n const store = await load(context);\n const item = store.find((entry) => entry.id === id);\n if (!item) return false;\n item[field] ??= now();\n await writeStore(env, store, context);\n return true;\n }),\n };\n return {\n command(argv, _caller, context) {\n const verb = argv[0] ?? 'help';\n const handler = VERBS[verb];\n if (handler) return handler(g, argv, context);\n return Promise.resolve({ code: verb === 'help' || verb === '--help' ? 0 : 2, out: USAGE });\n },\n async intercept(action, data, context) {\n const id = String((data as { id?: unknown } | null)?.id ?? '');\n if (action === 'gelatiere-dismiss') {\n await g.mark(id, 'dismissedAt', context);\n return true;\n }\n if (action === 'gelatiere-install' || action === 'gelatiere-try')\n await g.mark(id, 'takenAt', context);\n return false;\n },\n };\n}\n\nexport function lateGelatiere() {\n let runtime: GelatiereRuntime | undefined;\n return {\n async intercept(\n id: string,\n payload: { action: string; data: unknown },\n context: Context\n ): Promise<boolean> {\n return id === SPRINKLE && runtime !== undefined\n ? runtime.intercept(payload.action, payload.data, context)\n : false;\n },\n command(argv: readonly string[], caller: string | null, context: Context): Promise<Answer> {\n return runtime\n ? runtime.command(argv, caller, context)\n : Promise.resolve({ code: 1, out: 'gelatiere: not ready yet\\n' });\n },\n attach(options: GelatiereAttach): GelatiereRuntime {\n runtime = attachGelatiere(options);\n return runtime;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { redactSecrets } from "../memory/format.js";
|
|
2
|
+
import { SUGGESTIONS_DIR, SUGGESTIONS_STORE } from "../sprinkles/sprinkles.js";
|
|
3
|
+
const STORE_DIR = SUGGESTIONS_DIR;
|
|
4
|
+
const STORE = SUGGESTIONS_STORE;
|
|
5
|
+
const KINDS = ["skill", "use-case", "tip", "skill-idea", "issue"];
|
|
6
|
+
const MAX_OPEN = 50;
|
|
7
|
+
const PROMPTED = /* @__PURE__ */ new Set(["use-case", "skill-idea", "issue"]);
|
|
8
|
+
const ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
9
|
+
const CONE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
10
|
+
const SOURCE = /^https:\/\/[^\s"'<>]+\/SKILL\.md$/;
|
|
11
|
+
function text(value, limit) {
|
|
12
|
+
if (typeof value !== "string") return void 0;
|
|
13
|
+
const clean = redactSecrets(value.trim()).text;
|
|
14
|
+
return clean ? clean.slice(0, limit) : void 0;
|
|
15
|
+
}
|
|
16
|
+
function origin(skill, source) {
|
|
17
|
+
const url = new URL(source);
|
|
18
|
+
return `${skill} from ${url.host}${url.pathname}`;
|
|
19
|
+
}
|
|
20
|
+
function kindProblem(kind, skill, source, prompt) {
|
|
21
|
+
if (kind === "skill" && (!skill || !ID.test(skill)))
|
|
22
|
+
return "a skill needs its name (a-z, 0-9, -)";
|
|
23
|
+
if (kind === "skill" && (!source || !SOURCE.test(source)))
|
|
24
|
+
return "a skill needs its source, an https URL of its SKILL.md";
|
|
25
|
+
if (PROMPTED.has(kind) && !prompt) return `a ${kind} needs its prompt`;
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
function shapeProblem(url, cones) {
|
|
29
|
+
if (url !== void 0 && !/^https:\/\/[^\s"'<>]+$/.test(url)) return "url must be https";
|
|
30
|
+
if (cones.some((cone) => !CONE.test(cone))) return "cones must be cone ids";
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
function validate(candidate) {
|
|
34
|
+
if (!candidate || typeof candidate !== "object") return "not an object";
|
|
35
|
+
const item = candidate;
|
|
36
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
37
|
+
if (!ID.test(id)) return `id "${String(item.id)}" must be 1\u201364 of a-z, 0-9 and -`;
|
|
38
|
+
if (!KINDS.includes(item.kind)) return `${id}: kind must be one of ${KINDS.join(", ")}`;
|
|
39
|
+
const title = text(item.title, 120);
|
|
40
|
+
const body = text(item.body, 600);
|
|
41
|
+
if (!title || !body) return `${id}: needs a title and a body`;
|
|
42
|
+
const skill = typeof item.skill === "string" ? item.skill.trim() : void 0;
|
|
43
|
+
const source = typeof item.source === "string" ? item.source.trim() : void 0;
|
|
44
|
+
const url = typeof item.url === "string" ? item.url.trim() : void 0;
|
|
45
|
+
const cones = Array.isArray(item.cones) ? item.cones.filter((cone) => typeof cone === "string") : [];
|
|
46
|
+
const evidence = text(item.evidence, 300);
|
|
47
|
+
const prompt = text(item.prompt, 1e3);
|
|
48
|
+
const problem = kindProblem(item.kind, skill, source, prompt) ?? shapeProblem(url, cones);
|
|
49
|
+
if (problem) return `${id}: ${problem}`;
|
|
50
|
+
const installs = item.kind === "skill" ? { skill, source, install: origin(skill, source) } : {};
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
kind: item.kind,
|
|
54
|
+
title,
|
|
55
|
+
body,
|
|
56
|
+
...evidence ? { evidence } : {},
|
|
57
|
+
...installs,
|
|
58
|
+
...prompt ? { prompt } : {},
|
|
59
|
+
...url ? { url } : {},
|
|
60
|
+
cones,
|
|
61
|
+
createdAt: 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function fold(store, candidates, now) {
|
|
65
|
+
const next = store.map((item) => ({ ...item }));
|
|
66
|
+
const rejected = [];
|
|
67
|
+
let added = 0;
|
|
68
|
+
let updated = 0;
|
|
69
|
+
if (!Array.isArray(candidates))
|
|
70
|
+
return { store: next, added, updated, rejected: ["not a JSON array"] };
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
const checked = validate(candidate);
|
|
73
|
+
if (typeof checked === "string") {
|
|
74
|
+
rejected.push(checked);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const at = next.findIndex((item) => item.id === checked.id);
|
|
78
|
+
const old = next[at];
|
|
79
|
+
if (old) {
|
|
80
|
+
if (old.dismissedAt || old.takenAt) continue;
|
|
81
|
+
next[at] = { ...checked, createdAt: old.createdAt };
|
|
82
|
+
updated++;
|
|
83
|
+
} else if (open(next).length >= MAX_OPEN)
|
|
84
|
+
rejected.push(`${checked.id}: ${MAX_OPEN} suggestions are already open`);
|
|
85
|
+
else {
|
|
86
|
+
next.push({ ...checked, createdAt: now });
|
|
87
|
+
added++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { store: next, added, updated, rejected };
|
|
91
|
+
}
|
|
92
|
+
function open(store) {
|
|
93
|
+
return store.filter((item) => !item.dismissedAt && !item.takenAt);
|
|
94
|
+
}
|
|
95
|
+
async function readStore(env, context) {
|
|
96
|
+
const read = await env.readTextFile(STORE, context);
|
|
97
|
+
if (!read.ok) return null;
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(read.value);
|
|
100
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof validate(item) !== "string") : [];
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function writeStore(env, store, context) {
|
|
106
|
+
await env.createDir(STORE_DIR, { recursive: true }, context);
|
|
107
|
+
const written = await env.writeFile(STORE, `${JSON.stringify(store, null, 2)}
|
|
108
|
+
`, context);
|
|
109
|
+
if (!written.ok) throw new Error(written.error.message);
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
KINDS,
|
|
113
|
+
MAX_OPEN,
|
|
114
|
+
STORE,
|
|
115
|
+
STORE_DIR,
|
|
116
|
+
fold,
|
|
117
|
+
open,
|
|
118
|
+
readStore,
|
|
119
|
+
validate,
|
|
120
|
+
writeStore
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/gelatiere/store.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { redactSecrets } from '../memory/format.ts';\nimport { SUGGESTIONS_DIR, SUGGESTIONS_STORE } from '../sprinkles/sprinkles.ts';\n\nexport const STORE_DIR = SUGGESTIONS_DIR;\nexport const STORE = SUGGESTIONS_STORE;\nexport const KINDS = ['skill', 'use-case', 'tip', 'skill-idea', 'issue'] as const;\nexport const MAX_OPEN = 50;\nconst PROMPTED = new Set<Kind>(['use-case', 'skill-idea', 'issue']);\n\nexport type Kind = (typeof KINDS)[number];\n\nexport interface Suggestion {\n id: string;\n kind: Kind;\n title: string;\n body: string;\n evidence?: string;\n skill?: string;\n source?: string;\n install?: string;\n prompt?: string;\n url?: string;\n cones: string[];\n createdAt: number;\n deliveredAt?: number;\n dismissedAt?: number;\n takenAt?: number;\n}\n\nconst ID = /^[a-z0-9][a-z0-9-]{0,63}$/;\nconst CONE = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nconst SOURCE = /^https:\\/\\/[^\\s\"'<>]+\\/SKILL\\.md$/;\n\nfunction text(value: unknown, limit: number): string | undefined {\n if (typeof value !== 'string') return undefined;\n const clean = redactSecrets(value.trim()).text;\n return clean ? clean.slice(0, limit) : undefined;\n}\n\nfunction origin(skill: string | undefined, source: string | undefined): string {\n const url = new URL(source as string);\n return `${skill} from ${url.host}${url.pathname}`;\n}\n\nfunction kindProblem(\n kind: Kind,\n skill: string | undefined,\n source: string | undefined,\n prompt: string | undefined\n): string | undefined {\n if (kind === 'skill' && (!skill || !ID.test(skill)))\n return 'a skill needs its name (a-z, 0-9, -)';\n if (kind === 'skill' && (!source || !SOURCE.test(source)))\n return 'a skill needs its source, an https URL of its SKILL.md';\n if (PROMPTED.has(kind) && !prompt) return `a ${kind} needs its prompt`;\n return undefined;\n}\n\nfunction shapeProblem(url: string | undefined, cones: string[]): string | undefined {\n if (url !== undefined && !/^https:\\/\\/[^\\s\"'<>]+$/.test(url)) return 'url must be https';\n if (cones.some((cone) => !CONE.test(cone))) return 'cones must be cone ids';\n return undefined;\n}\n\nexport function validate(candidate: unknown): Suggestion | string {\n if (!candidate || typeof candidate !== 'object') return 'not an object';\n const item = candidate as Record<string, unknown>;\n const id = typeof item.id === 'string' ? item.id : '';\n if (!ID.test(id)) return `id \"${String(item.id)}\" must be 1\u201364 of a-z, 0-9 and -`;\n if (!KINDS.includes(item.kind as Kind)) return `${id}: kind must be one of ${KINDS.join(', ')}`;\n const title = text(item.title, 120);\n const body = text(item.body, 600);\n if (!title || !body) return `${id}: needs a title and a body`;\n const skill = typeof item.skill === 'string' ? item.skill.trim() : undefined;\n const source = typeof item.source === 'string' ? item.source.trim() : undefined;\n const url = typeof item.url === 'string' ? item.url.trim() : undefined;\n const cones = Array.isArray(item.cones)\n ? item.cones.filter((cone): cone is string => typeof cone === 'string')\n : [];\n const evidence = text(item.evidence, 300);\n const prompt = text(item.prompt, 1000);\n const problem = kindProblem(item.kind as Kind, skill, source, prompt) ?? shapeProblem(url, cones);\n if (problem) return `${id}: ${problem}`;\n const installs = item.kind === 'skill' ? { skill, source, install: origin(skill, source) } : {};\n return {\n id,\n kind: item.kind as Kind,\n title,\n body,\n ...(evidence ? { evidence } : {}),\n ...installs,\n ...(prompt ? { prompt } : {}),\n ...(url ? { url } : {}),\n cones,\n createdAt: 0,\n };\n}\n\nexport function fold(\n store: readonly Suggestion[],\n candidates: unknown,\n now: number\n): { store: Suggestion[]; added: number; updated: number; rejected: string[] } {\n const next = store.map((item) => ({ ...item }));\n const rejected: string[] = [];\n let added = 0;\n let updated = 0;\n if (!Array.isArray(candidates))\n return { store: next, added, updated, rejected: ['not a JSON array'] };\n for (const candidate of candidates) {\n const checked = validate(candidate);\n if (typeof checked === 'string') {\n rejected.push(checked);\n continue;\n }\n const at = next.findIndex((item) => item.id === checked.id);\n const old = next[at];\n if (old) {\n if (old.dismissedAt || old.takenAt) continue;\n next[at] = { ...checked, createdAt: old.createdAt };\n updated++;\n } else if (open(next).length >= MAX_OPEN)\n rejected.push(`${checked.id}: ${MAX_OPEN} suggestions are already open`);\n else {\n next.push({ ...checked, createdAt: now });\n added++;\n }\n }\n return { store: next, added, updated, rejected };\n}\n\nexport function open(store: readonly Suggestion[]): Suggestion[] {\n return store.filter((item) => !item.dismissedAt && !item.takenAt);\n}\n\nexport async function readStore(\n env: Pick<ExecutionEnv, 'readTextFile'>,\n context: Context\n): Promise<Suggestion[] | null> {\n const read = await env.readTextFile(STORE, context);\n if (!read.ok) return null;\n try {\n const parsed = JSON.parse(read.value) as unknown;\n return Array.isArray(parsed)\n ? parsed.filter((item): item is Suggestion => typeof validate(item) !== 'string')\n : [];\n } catch {\n return [];\n }\n}\n\nexport async function writeStore(\n env: Pick<ExecutionEnv, 'writeFile' | 'createDir'>,\n store: readonly Suggestion[],\n context: Context\n): Promise<void> {\n await env.createDir(STORE_DIR, { recursive: true }, context);\n const written = await env.writeFile(STORE, `${JSON.stringify(store, null, 2)}\\n`, context);\n if (!written.ok) throw new Error(written.error.message);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
sandboxFactory
|
|
14
14
|
} from "./codemode/index.js";
|
|
15
15
|
import { EncryptedCredentialStore } from "./credentials.js";
|
|
16
|
+
import { lateGelatiere } from "./gelatiere/index.js";
|
|
16
17
|
import { hostAgent } from "./host.js";
|
|
17
18
|
import { createActivity } from "./kernel/activity.js";
|
|
18
19
|
import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
|
|
@@ -96,8 +97,10 @@ async function start(scope, port, options) {
|
|
|
96
97
|
{ env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },
|
|
97
98
|
BACKGROUND_CONTEXT
|
|
98
99
|
);
|
|
100
|
+
const gelatiere = lateGelatiere();
|
|
99
101
|
const sprinkles = await setupSprinkles().attach(
|
|
100
102
|
{
|
|
103
|
+
intercept: gelatiere.intercept,
|
|
101
104
|
harness: agent.harness,
|
|
102
105
|
agents: agent.agents,
|
|
103
106
|
licks: licks.licks,
|
|
@@ -110,6 +113,7 @@ async function start(scope, port, options) {
|
|
|
110
113
|
let roles;
|
|
111
114
|
const memoryRuntime = await memory.attach(
|
|
112
115
|
{
|
|
116
|
+
harness: agent.harness,
|
|
113
117
|
agents: agent.agents,
|
|
114
118
|
env: home,
|
|
115
119
|
home: HOME,
|
|
@@ -129,11 +133,21 @@ async function start(scope, port, options) {
|
|
|
129
133
|
reads: skillsRuntime.dirs,
|
|
130
134
|
sprinkle: sprinkles.command,
|
|
131
135
|
memory: memoryRuntime.command,
|
|
136
|
+
gelatiere: gelatiere.command,
|
|
132
137
|
...options.assets ? { assets: options.assets } : {}
|
|
133
138
|
},
|
|
134
139
|
BACKGROUND_CONTEXT
|
|
135
140
|
);
|
|
136
141
|
roles = runtime;
|
|
142
|
+
gelatiere.attach({
|
|
143
|
+
agents: agent.agents,
|
|
144
|
+
licks: licks.licks,
|
|
145
|
+
env: home,
|
|
146
|
+
home: HOME,
|
|
147
|
+
scoops: scoops.scoops,
|
|
148
|
+
roles: runtime.roles,
|
|
149
|
+
assets: options.assets ?? packageAssets()
|
|
150
|
+
});
|
|
137
151
|
await memoryRuntime.reload(BACKGROUND_CONTEXT).catch(() => void 0);
|
|
138
152
|
return hostAgent(agent, {
|
|
139
153
|
settings: await createAgentSettings(models, credentials, providers),
|