@ai-ecoverse/slicc-agent 3.14.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 +22 -6
- 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 +6 -4
- 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/scoops/service.js +19 -4
- package/dist/scoops/service.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/scoops/service.d.ts +2 -0
- package/dist/types/sprinkles/index.d.ts +1 -0
- package/dist/types/sprinkles/sprinkles.d.ts +3 -0
- package/package.json +3 -3
- 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
|
@@ -156,20 +156,20 @@ gone /home/notes/old.md
|
|
|
156
156
|
|
|
157
157
|
### Cones and scoops
|
|
158
158
|
|
|
159
|
-
A **cone** is a top-level agent the user talks to; a **scoop** is a helper agent with its own conversation and its own folder, `/scoops/<handle>/`. The session document `slicc.agents` is the registry. The first cone's id is `cone`, later ones `cone-<n>`, counting up only. A scoop's id is `scoop:<handle>`, where the handle is its name made safe for a folder; a handle that is or was ever taken gets `-2`, `-3` and so on, so ids and folders are never reused. `AgentControl` adds `selectCone`, `createCone`, `createScoop`, `stopAgent`, `drop` (stop a scoop
|
|
159
|
+
A **cone** is a top-level agent the user talks to; a **scoop** is a helper agent with its own conversation and its own folder, `/scoops/<handle>/`. The session document `slicc.agents` is the registry. The first cone's id is `cone`, later ones `cone-<n>`, counting up only. A scoop's id is `scoop:<handle>`, where the handle is its name made safe for a folder; a handle that is or was ever taken gets `-2`, `-3` and so on, so ids and folders are never reused. `AgentControl` adds `selectCone`, `createCone`, `createScoop`, `stopAgent`, `drop` (stop a scoop, as `agent stop` does) and `unqueue(agentId, submissionId)`; `send` and `configure` take an `agentId`. Its errors are sentences the UI can show as they are, such as "A cone named harbor already exists." The transcript service adds `agents`, a summary of every cone and scoop, and `views`, one durable view per live agent, which the spectrum adapter shows in the agents rail (the header picker lists cones only). With spectrum ≥ 1.23.0 the rail drops scoops and the picker creates cones; the adapter needs spectrum ≥ 1.28.0, whose Drop confirmation says the working folder is deleted. A scoop created from the rail has a provisional id until the worker answers; the adapter then moves the selection, and sends, to the id the worker gave it.
|
|
160
160
|
|
|
161
|
-
Each scoop is owned by a background durable task, its **anchor**, so Stop in its cone doesn't end it; `agent stop`
|
|
161
|
+
Each scoop is owned by a background durable task, its **anchor**, so Stop in its cone doesn't end it; `agent stop` and Drop do. They keep its transcript in durable storage and its reports in `/scoops/<handle>/reports/`, which report licks point at, and remove its working folder `/scoops/<handle>/workspace`. Each request handed to a scoop gets a **reporter** task that waits for the run, writes the answer to `/scoops/<handle>/reports/<task id>.md` and reports it to whoever asked. Requests from a cone's or a scoop's bash report into that agent's `pi.inbox` as a lick, through the licks outbox, so they steer and coalesce like any lick; requests from a terminal report nowhere, and a user typing in a scoop's chat gets the answer there. In a scoop's chat, a request someone else sent shows who sent it (`UserMessage.from`): the asking cone's or scoop's name, with `origin: 'agent'` so spectrum (≥ 1.26.0) shows it as that agent's request with an "Agent" badge, or `terminal` without an origin. Once the request is placed, the reporter appends a `slicc.from` entry (`{ from, entry }`) without model messages, and the adapter sets `from` on that entry's message. A request withdrawn before it was placed leaves no entry. The report lick's text is `[scoop <name> (<role>) <status>]`, and its body has the report's path and line count and a 1,000-character preview. A scoop's prompt has a `scoop` section with its name, its cone, its folders and its limits. The `agent` command is taught by the built-in `agent` skill (below), whose description says that roles exist and that `agent list --agents` lists them.
|
|
162
162
|
|
|
163
163
|
**The `agent` command.** One bash script is installed as `agent` and `subagent` in `$PNPM_HOME/bin`, so the user, a cone or a scoop runs it in any bash. It writes a request (NUL-separated fields: `slicc-agent/1`, the name it was called by, `$SLICC_AGENT`, the working directory, its pid, argc, the arguments and stdin) to `/var/lib/slicc/agent/requests/in/` and polls `out/` for the answer and exit code; the worker writes `<id>.ack` when it picks a request up, and the script gives up after 20 s without one ("the SLICC agent isn't running", exit 1). The kernel environment exports `SLICC_AGENT`, `PI_PROVIDER`, `PI_MODEL` and `PI_REASONING_LEVEL` into every agent command, so the worker knows the caller. `/var/lib/slicc/agent/scoops/<handle>/metadata.json` mirrors each scoop's state for scripts.
|
|
164
164
|
- `agent [options] --prompt <text>` (or `--file <path>`, or `-` for stdin, or v6's `agent <cwd> <allowed-commands> <prompt>`) is **synchronous**: an ephemeral scoop runs, and the command prints its final answer and exits 0, or prints the reason on stderr and exits 1. Afterwards the scoop is stopped, its folder removed, its conversation marked dropped and its transcript archived as Markdown in `/tmp/agent-sessions/<handle>.md` (`/home/sessions/` with `--persist-session`, nowhere with `--no-persist-session`). The script traps TERM, INT and HUP and cancels the scoop, and the worker checks the script's pid in the kernel's process table every second for a SIGKILL. If the caller's run is still going when the bash call dies (a bash timeout), the scoop continues and its answer arrives as a `bash` lick; `--background-after <seconds>` does the same on purpose. Sync calls nest up to three deep. Allowed commands in v6's form are told to the scoop, not enforced.
|
|
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
|
|
|
172
|
-
**Rewind.** Dropping a turn never waits for scoops. Scoops the dropped turn created are stopped and marked gone; requests it handed to older scoops are aborted and their reports withdrawn; scoops it stopped come back idle. The `slicc.rewound` entry lists them, and its notice says "Stopped scoop X. Restored scoop Y."
|
|
172
|
+
**Rewind.** Dropping a turn never waits for scoops. Scoops the dropped turn created are stopped and marked gone; requests it handed to older scoops are aborted and their reports withdrawn; scoops it stopped come back idle, with a fresh, empty working folder. The `slicc.rewound` entry lists them, and its notice says "Stopped scoop X. Restored scoop Y."
|
|
173
173
|
|
|
174
174
|
### Skills and prompt templates
|
|
175
175
|
|
|
@@ -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
|
+
}
|