@ai-ecoverse/slicc-agent 3.25.2 → 3.27.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 +13 -6
- package/dist/client.js +9 -1
- package/dist/client.js.map +2 -2
- package/dist/gelatiere/catalog.js +192 -0
- package/dist/gelatiere/catalog.js.map +7 -0
- package/dist/gelatiere/index.js +121 -13
- package/dist/gelatiere/index.js.map +3 -3
- package/dist/gelatiere/store.js +17 -5
- package/dist/gelatiere/store.js.map +2 -2
- package/dist/main.js +4 -1
- package/dist/main.js.map +2 -2
- package/dist/owner.js +10 -6
- package/dist/owner.js.map +2 -2
- package/dist/sprinkles/index.js +10 -1
- package/dist/sprinkles/index.js.map +2 -2
- package/dist/sprinkles/sprinkles.js +2 -0
- package/dist/sprinkles/sprinkles.js.map +2 -2
- package/dist/types/client.d.ts +1 -0
- package/dist/types/gelatiere/catalog.d.ts +69 -0
- package/dist/types/gelatiere/index.d.ts +11 -2
- package/dist/types/gelatiere/store.d.ts +2 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/owner.d.ts +3 -1
- package/dist/types/sprinkles/sprinkles.d.ts +1 -0
- package/dist/types/wire.d.ts +1 -0
- package/dist/wire.js +7 -0
- package/dist/wire.js.map +2 -2
- package/package.json +3 -3
- package/packages/vfs-root/gelatiere/GELATIERE.md +2 -0
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ await agent.prompt('What is in /home?');
|
|
|
30
30
|
- `openOpfsSqliteStorage({ directory?, file? })` keeps the session in wasm SQLite ([`@sqlite.org/sqlite-wasm`](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm)) in an `opfs-sahpool` pool, by default `/.slicc/agent/` in OPFS. It needs a dedicated worker. Only one worker holds a pool at a time: the opener takes a Web Lock per directory and waits until every file of the pool can be opened before sqlite-wasm installs it, because a failed install deletes the pool's directory. `openMemorySqliteStorage()` keeps the same database in memory.
|
|
31
31
|
- `hostAgent(agent)` serves the agent with pi's protocol: a pi-server with one session, `agent`, whose Chord services are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run). `serveConnections(self, host)` hands it every `MessagePort` the page sends.
|
|
32
32
|
- `@ai-ecoverse/slicc-agent/page` has what the page needs (`startAgent`, `connectAgent`) without the agent's runtime: no pi-durable, pi-ai, providers or SQLite, which load only in the worker. `@ai-ecoverse/slicc-agent/spectrum` is just as light, and a test keeps both that way.
|
|
33
|
-
- `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over.
|
|
33
|
+
- `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over. `port()` returns a raw `MessagePort` into the worker instead, for another tab: hand it over (through a SharedWorker, say), and that tab calls `connectAgent(port)`. A caller that already holds the lock passes `held: true`. A connection's `closed` resolves when its port ends (with the error, if any), and from then on its states keep their last values.
|
|
34
34
|
|
|
35
35
|
### Bash and files on slicc-kernel
|
|
36
36
|
|
|
@@ -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`, `welcome` and `transcript-export` (now `freezer show`).
|
|
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),
|
|
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), reading v6's www.sliccy.com catalog; `wiki` needs its `wiki` CLI.
|
|
213
213
|
- **The tray hub, upskill or the Install/Update panel:** `upgrade` (`upgrade apply`, bios#70), `handoff`, `slicc`, `ssh`, `cherry`.
|
|
214
214
|
- **A browser and CDP:** `playwright-cli` is rewritten as the `browser` skill (below); `computer` and `v86` don't apply.
|
|
215
215
|
- **v6's JS realm, `.jsh` and `ipk` (slicc-kernel#68):** `jshd`, `mcp`, `workflows`, `package-execution`, `biome`, `ffmpeg`.
|
|
@@ -262,9 +262,16 @@ The `memory` section, read before every request, holds the first 200 lines (at m
|
|
|
262
262
|
- at most 50 open;
|
|
263
263
|
- dismissed and taken ids are never reopened.
|
|
264
264
|
- `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.
|
|
265
|
-
- `gelatiere install <id>` is what a card's Install leads to. It installs [upskill](https://github.com/ai-ecoverse/gh-upskill) if it's missing (`command -v upskill || curl -fsSL …/install.sh | bash`, into `$PNPM_HOME`), runs `upskill <repo> --skill <name> --dest ~/.pi/agent/skills`, checks that the skill is there for the skills loader, and marks the suggestion taken. It refuses a skill that's already installed. In seven, upskill reaches GitHub through the page's fetch, which `raw.githubusercontent.com` and `api.github.com` allow with CORS; `GITHUB_TOKEN` raises GitHub's rate limit. Before anyone clicks, the card shows where the skill comes from: "Installs github from github.com/example/skills".
|
|
265
|
+
- `gelatiere install <id>` is what a card's Install leads to. It installs [upskill](https://github.com/ai-ecoverse/gh-upskill) if it's missing (`command -v upskill || curl -fsSL …/install.sh | bash`, into `$PNPM_HOME`), runs `upskill <repo> [--path <folder>] (--skill <name> | --all) --dest-path ~/.pi/agent/skills`, checks that the skill (or, with `all`, at least one new skill) is there for the skills loader, and marks the suggestion taken. It refuses a single skill that's already installed. A `skill` suggestion may carry `path` (a folder in the repo) and `all: true` instead of `skill`. In seven, upskill reaches GitHub through the page's fetch, which `raw.githubusercontent.com` and `api.github.com` allow with CORS; `GITHUB_TOKEN` raises GitHub's rate limit. Before anyone clicks, the card shows where the skill comes from: "Installs github from github.com/example/skills".
|
|
266
266
|
- `gelatiere run`, `list [--all] [--json]`, `dismiss <id>` and `status` complete the command.
|
|
267
|
-
-
|
|
267
|
+
- `gelatiere catalog [--refresh] [--all] [--json]` reads v6's catalog: `https://www.sliccy.com/skills/catalog.json`, the company's `<slug>.json` and the optional `use-cases.json`. Each is the Edge Delivery sheet shape `{ data: [rows] }` with string fields.
|
|
268
|
+
- Skill rows have `name`, `displayName`, `description`, `repo`, `path`, `skill`, `installAll`, the affinity lists `apps`, `tasks`, `role` and `purpose`, and `boost`. Use-case rows have `name`, `displayName`, `description`, `prompt`, `skills`, the same affinity lists and `boost`.
|
|
269
|
+
- It ranks rows as v6 does (apps ×3, tasks ×2, role +1, purpose +1, all times `boost`; rows that score 0 are dropped unless `--all`) against the welcome profile, which the welcome card's `onboarding-complete` writes to `/home/.welcome.json`. Without a profile the ranking is `boost` alone.
|
|
270
|
+
- The company slug is v6's `slugifyCompany` of the profile's `company`; company rows replace global ones of the same `name`.
|
|
271
|
+
- Skills the agent has loaded (from any skills root) or that sit in `~/.pi/agent/skills`, and dismissed or taken ids (`catalog-<name>`, `catalog-use-<name>`) are left out. `--json` prints candidates the gelatiere can pass to `gelatiere suggest` as they are.
|
|
272
|
+
- Each file is cached in `/home/.gelatiere/catalog/` and fetched again after 12 hours or with `--refresh`, through the agent's fetch (the kernel transport). A 404 is cached as empty. Offline or on an error the cached copy is used, and with none the pass goes on without a catalog.
|
|
273
|
+
- www.sliccy.com must send `Access-Control-Allow-Origin` for `/skills/*.json` for the plain browser transport; the extension, slicc-node and the proxy routes don't need it. `runAgentWorker({ catalog })` changes the base URL.
|
|
274
|
+
- The gelatiere doesn't consolidate other agents' memory, which only cones and the user write.
|
|
268
275
|
|
|
269
276
|
**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.
|
|
270
277
|
|
|
@@ -360,14 +367,14 @@ A sprinkle is a small HTML panel next to the chat. pi has nothing for this; it's
|
|
|
360
367
|
v6's `open`, `reload`, `close`, `send` and `chat` answer with what to do instead.
|
|
361
368
|
|
|
362
369
|
**Shipped sprinkles.** Only `welcome` and `suggestions`, with slicc-spectrum's reviewed copy.
|
|
363
|
-
- **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.
|
|
370
|
+
- **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 profile to `/home/.welcome.json` for the gelatiere's catalog ranking, and the built-in `welcome` skill answers the lick.
|
|
364
371
|
- **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.
|
|
365
372
|
|
|
366
373
|
The built-in `sprinkles` skill teaches the files, the bridge and the command.
|
|
367
374
|
|
|
368
375
|
## The kernel
|
|
369
376
|
|
|
370
|
-
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.26.3 <2`; older kernels have an IPv6-to-loopback security bug): the agent only calls `attachKernel`, the client end of the page kernel's port, so it must match the kernel the page runs. Seven chooses that version: slicc-bios pins the certified kernel next to the agent in `src/packages/agent/package.json`, and pnpm fills the peer from that pin. The devDependency is the lowest certified kernel the range allows (1.26.3), and the unit and integration tests run on it. A second CI job reruns the integration tests on the newest certified kernel (`NEWEST_CERTIFIED_KERNEL` in `.github/workflows/ci.yml`). Today the floor is 1.26.3 and the newest certified kernel 1.
|
|
377
|
+
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.26.3 <2`; older kernels have an IPv6-to-loopback security bug): the agent only calls `attachKernel`, the client end of the page kernel's port, so it must match the kernel the page runs. Seven chooses that version: slicc-bios pins the certified kernel next to the agent in `src/packages/agent/package.json`, and pnpm fills the peer from that pin. The devDependency is the lowest certified kernel the range allows (1.26.3), and the unit and integration tests run on it. A second CI job reruns the integration tests on the newest certified kernel (`NEWEST_CERTIFIED_KERNEL` in `.github/workflows/ci.yml`). Today the floor is 1.26.3 and the newest certified kernel 1.40.1; the second job moves by hand to each newer certified kernel, so both ends of the range are tested. If the port protocol ever breaks within 1.x, a patch release narrows the upper bound. Renovate opens PRs for the kernel, `wasi-pnpm`, the `wasm-*` packages and `slicc-cdp` but never merges them, because they move only after the homescoop coordinator certifies them.
|
|
371
378
|
|
|
372
379
|
## Patched dependencies
|
|
373
380
|
|
package/dist/client.js
CHANGED
|
@@ -90,6 +90,13 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
90
90
|
held.push(kept);
|
|
91
91
|
return kept.state;
|
|
92
92
|
};
|
|
93
|
+
const freeze = () => {
|
|
94
|
+
for (const kept of held) kept.freeze();
|
|
95
|
+
};
|
|
96
|
+
const closed = channel.ended.then((reason) => {
|
|
97
|
+
freeze();
|
|
98
|
+
return reason;
|
|
99
|
+
});
|
|
93
100
|
return {
|
|
94
101
|
serverId,
|
|
95
102
|
control,
|
|
@@ -104,6 +111,7 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
104
111
|
frozen: hold(frozen),
|
|
105
112
|
changes: hold(changes),
|
|
106
113
|
settings: lastingSettings(settings, hold),
|
|
114
|
+
closed,
|
|
107
115
|
async prompt(text, whenBusy = "followUp") {
|
|
108
116
|
const sent = await control.send({ text, whenBusy, requestId: null }, context);
|
|
109
117
|
if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);
|
|
@@ -112,7 +120,7 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
112
120
|
return settled.text;
|
|
113
121
|
},
|
|
114
122
|
async close() {
|
|
115
|
-
|
|
123
|
+
freeze();
|
|
116
124
|
await server.use(AgentSessions).detach(context);
|
|
117
125
|
await session.dispose(context);
|
|
118
126
|
await server.dispose(context);
|
package/dist/client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type Context,\n createRemoteServiceBinding,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { Client, createClientServiceTransport } from '@earendil-works/pi-client';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n type Command,\n type Delivered,\n type SendMode,\n} from './services.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { PortChannel, type PortEndpoint } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly settings: AgentSettings;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, region, context) =>\n settings.connect(providerId, secret, region, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n };\n}\n\nfunction attached(client: Client): Promise<void> {\n return new Promise((resolve) => {\n const stop = client.onAttachmentChange(() => {\n stop();\n resolve();\n });\n });\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const channel = new PortChannel(endpoint);\n const serverId = await channel.hello;\n const client = await Client.connect({ serverId, transportFactory: channel.transport });\n const server = createRemoteServiceBinding({\n services: [AgentSessions],\n transport: createClientServiceTransport(client, () => ({ serverId })),\n bound: true,\n });\n const ready = attached(client);\n await server.use(AgentSessions).attach(AGENT_SESSION, context);\n await ready;\n const session = createRemoteServiceBinding({\n services: [AgentControl, AgentTranscript, AgentSettings],\n transport: createClientServiceTransport(client, () => client.attachment),\n bound: true,\n });\n const control = session.use(AgentControl);\n const {\n state: transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n changes,\n } = session.use(AgentTranscript);\n const settings = session.use(AgentSettings);\n await session.ready(context);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n return {\n serverId,\n control,\n transcript: hold(transcript),\n deliveries: hold(deliveries),\n agents: hold(agents),\n views: hold(views),\n commands: hold(commands),\n sprinkles: hold(sprinkles),\n memories: hold(memories),\n memoryScopes: hold(memoryScopes),\n frozen: hold(frozen),\n changes: hold(changes),\n settings: lastingSettings(settings, hold),\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n
|
|
5
|
-
"mappings": "AAAA;AAAA,EAEE;AAAA,OAEK;AACP,SAAS,0BAA0B;AACnC,SAAS,QAAQ,oCAAoC;AAKrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAIK;AAEP,SAAS,mBAAsC;
|
|
4
|
+
"sourcesContent": ["import {\n type Context,\n createRemoteServiceBinding,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { Client, createClientServiceTransport } from '@earendil-works/pi-client';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n type Command,\n type Delivered,\n type SendMode,\n} from './services.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { PortChannel, type PortEndpoint } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly settings: AgentSettings;\n readonly closed: Promise<Error | undefined>;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, region, context) =>\n settings.connect(providerId, secret, region, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n };\n}\n\nfunction attached(client: Client): Promise<void> {\n return new Promise((resolve) => {\n const stop = client.onAttachmentChange(() => {\n stop();\n resolve();\n });\n });\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const channel = new PortChannel(endpoint);\n const serverId = await channel.hello;\n const client = await Client.connect({ serverId, transportFactory: channel.transport });\n const server = createRemoteServiceBinding({\n services: [AgentSessions],\n transport: createClientServiceTransport(client, () => ({ serverId })),\n bound: true,\n });\n const ready = attached(client);\n await server.use(AgentSessions).attach(AGENT_SESSION, context);\n await ready;\n const session = createRemoteServiceBinding({\n services: [AgentControl, AgentTranscript, AgentSettings],\n transport: createClientServiceTransport(client, () => client.attachment),\n bound: true,\n });\n const control = session.use(AgentControl);\n const {\n state: transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n changes,\n } = session.use(AgentTranscript);\n const settings = session.use(AgentSettings);\n await session.ready(context);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n const freeze = () => {\n for (const kept of held) kept.freeze();\n };\n const closed = channel.ended.then((reason) => {\n freeze();\n return reason;\n });\n return {\n serverId,\n control,\n transcript: hold(transcript),\n deliveries: hold(deliveries),\n agents: hold(agents),\n views: hold(views),\n commands: hold(commands),\n sprinkles: hold(sprinkles),\n memories: hold(memories),\n memoryScopes: hold(memoryScopes),\n frozen: hold(frozen),\n changes: hold(changes),\n settings: lastingSettings(settings, hold),\n closed,\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n freeze();\n await server.use(AgentSessions).detach(context);\n await session.dispose(context);\n await server.dispose(context);\n await client.dispose();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { redactSecrets } from "../memory/format.js";
|
|
2
|
+
import { STORE_DIR } from "./store.js";
|
|
3
|
+
const CATALOG_BASE = "https://www.sliccy.com/skills/";
|
|
4
|
+
const CATALOG_DIR = `${STORE_DIR}/catalog`;
|
|
5
|
+
const PROFILE = ".welcome.json";
|
|
6
|
+
const MAX_AGE = 12 * 60 * 60 * 1e3;
|
|
7
|
+
const MAX_BYTES = 1024 * 1024;
|
|
8
|
+
const WEIGHTS = { apps: 3, tasks: 2, role: 1, purpose: 1 };
|
|
9
|
+
const REPO = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/;
|
|
10
|
+
const SKILL = /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,79}$/;
|
|
11
|
+
const PATH = /^(?!.*\.\.)[A-Za-z0-9._/-]{1,200}$/;
|
|
12
|
+
const NAME = /^[a-z0-9][a-z0-9-]{0,55}$/;
|
|
13
|
+
function words(value) {
|
|
14
|
+
if (typeof value !== "string") return [];
|
|
15
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
function clean(value, limit) {
|
|
18
|
+
if (typeof value !== "string") return "";
|
|
19
|
+
return redactSecrets(value.trim()).text.slice(0, limit);
|
|
20
|
+
}
|
|
21
|
+
function boostOf(value) {
|
|
22
|
+
const parsed = typeof value === "string" && value.trim() ? Number.parseFloat(value) : Number.NaN;
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
24
|
+
}
|
|
25
|
+
function affinityOf(row) {
|
|
26
|
+
return {
|
|
27
|
+
apps: words(row.apps),
|
|
28
|
+
tasks: words(row.tasks),
|
|
29
|
+
role: words(row.role),
|
|
30
|
+
purpose: words(row.purpose)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function nameOf(value) {
|
|
34
|
+
const name = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
35
|
+
return NAME.test(name) ? name : void 0;
|
|
36
|
+
}
|
|
37
|
+
function truthy(value) {
|
|
38
|
+
return typeof value === "string" && ["true", "1", "yes"].includes(value.trim().toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
function parseSkills(rows) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
const name = nameOf(row.name);
|
|
44
|
+
const repo = typeof row.repo === "string" ? row.repo.trim() : "";
|
|
45
|
+
const skill = typeof row.skill === "string" ? row.skill.trim() : "";
|
|
46
|
+
const path = typeof row.path === "string" ? row.path.trim() : "";
|
|
47
|
+
const all = truthy(row.installAll);
|
|
48
|
+
if (!name || !REPO.test(repo)) continue;
|
|
49
|
+
if (!all && !SKILL.test(skill)) continue;
|
|
50
|
+
if (path && !PATH.test(path)) continue;
|
|
51
|
+
const boost = boostOf(row.boost);
|
|
52
|
+
out.push({
|
|
53
|
+
name,
|
|
54
|
+
title: clean(row.displayName, 80) || name,
|
|
55
|
+
description: clean(row.description, 500),
|
|
56
|
+
repo,
|
|
57
|
+
...all ? {} : { skill },
|
|
58
|
+
...path ? { path } : {},
|
|
59
|
+
all,
|
|
60
|
+
affinity: affinityOf(row),
|
|
61
|
+
...boost === void 0 ? {} : { boost }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function parseUseCases(rows) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const name = nameOf(row.name);
|
|
70
|
+
const prompt = clean(row.prompt, 1e3);
|
|
71
|
+
if (!name || !prompt) continue;
|
|
72
|
+
const boost = boostOf(row.boost);
|
|
73
|
+
out.push({
|
|
74
|
+
name,
|
|
75
|
+
title: clean(row.displayName, 80) || name,
|
|
76
|
+
description: clean(row.description, 500),
|
|
77
|
+
prompt,
|
|
78
|
+
skills: words(row.skills),
|
|
79
|
+
affinity: affinityOf(row),
|
|
80
|
+
...boost === void 0 ? {} : { boost }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function merge(base, company) {
|
|
86
|
+
const names = new Set(company.map((entry) => entry.name));
|
|
87
|
+
return [...base.filter((entry) => !names.has(entry.name)), ...company];
|
|
88
|
+
}
|
|
89
|
+
function slugifyCompany(company) {
|
|
90
|
+
if (typeof company !== "string" || !company) return null;
|
|
91
|
+
const slug = company.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-+|-+$)/g, "");
|
|
92
|
+
return slug || null;
|
|
93
|
+
}
|
|
94
|
+
function profileOf(value) {
|
|
95
|
+
if (!value || typeof value !== "object") return void 0;
|
|
96
|
+
const raw = value;
|
|
97
|
+
const list = (field) => Array.isArray(field) ? field.filter((item) => typeof item === "string") : [];
|
|
98
|
+
const text = (field) => typeof field === "string" ? field : "";
|
|
99
|
+
return {
|
|
100
|
+
purpose: text(raw.purpose),
|
|
101
|
+
role: text(raw.role),
|
|
102
|
+
tasks: list(raw.tasks),
|
|
103
|
+
apps: list(raw.apps),
|
|
104
|
+
...typeof raw.company === "string" && raw.company ? { company: raw.company } : {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function score(entry, profile) {
|
|
108
|
+
if (!profile) return { score: entry.boost ?? 1, reasons: [] };
|
|
109
|
+
let total = 0;
|
|
110
|
+
const reasons = [];
|
|
111
|
+
const apps = entry.affinity.apps.filter((app) => profile.apps.includes(app));
|
|
112
|
+
if (apps.length) {
|
|
113
|
+
total += apps.length * WEIGHTS.apps;
|
|
114
|
+
reasons.push(`apps(${apps.join(", ")})`);
|
|
115
|
+
}
|
|
116
|
+
const tasks = entry.affinity.tasks.filter((task) => profile.tasks.includes(task));
|
|
117
|
+
if (tasks.length) {
|
|
118
|
+
total += tasks.length * WEIGHTS.tasks;
|
|
119
|
+
reasons.push(`tasks(${tasks.join(", ")})`);
|
|
120
|
+
}
|
|
121
|
+
if (profile.role && entry.affinity.role.includes(profile.role)) {
|
|
122
|
+
total += WEIGHTS.role;
|
|
123
|
+
reasons.push(`role(${profile.role})`);
|
|
124
|
+
}
|
|
125
|
+
if (profile.purpose && entry.affinity.purpose.includes(profile.purpose)) {
|
|
126
|
+
total += WEIGHTS.purpose;
|
|
127
|
+
reasons.push(`purpose(${profile.purpose})`);
|
|
128
|
+
}
|
|
129
|
+
return { score: total * (entry.boost ?? 1), reasons };
|
|
130
|
+
}
|
|
131
|
+
function rowsOf(value) {
|
|
132
|
+
const data = value?.data;
|
|
133
|
+
if (!Array.isArray(data)) return void 0;
|
|
134
|
+
return data.filter((row) => Boolean(row) && typeof row === "object");
|
|
135
|
+
}
|
|
136
|
+
async function download(fetcher, url) {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetcher(url, { headers: { accept: "application/json" } });
|
|
139
|
+
if (response.status === 404) return "missing";
|
|
140
|
+
if (response.status !== 200) return void 0;
|
|
141
|
+
const text = await response.text();
|
|
142
|
+
if (text.length > MAX_BYTES) return void 0;
|
|
143
|
+
const rows = rowsOf(JSON.parse(text));
|
|
144
|
+
return rows ? { rows } : void 0;
|
|
145
|
+
} catch {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function cached(env, file, url, context) {
|
|
150
|
+
const read = await env.readTextFile(file, context);
|
|
151
|
+
if (!read.ok) return void 0;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(read.value);
|
|
154
|
+
return parsed.url === url && typeof parsed.fetchedAt === "number" && Array.isArray(parsed.rows) ? parsed : void 0;
|
|
155
|
+
} catch {
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function sheet(options, url, file, context) {
|
|
160
|
+
const { env, fetcher, now, refresh } = options;
|
|
161
|
+
const kept = await cached(env, file, url, context);
|
|
162
|
+
if (kept && !refresh && now - kept.fetchedAt < MAX_AGE)
|
|
163
|
+
return {
|
|
164
|
+
url,
|
|
165
|
+
rows: kept.rows,
|
|
166
|
+
state: kept.rows.length ? "fresh" : "missing",
|
|
167
|
+
fetchedAt: kept.fetchedAt
|
|
168
|
+
};
|
|
169
|
+
const got = await download(fetcher, url);
|
|
170
|
+
if (got === void 0)
|
|
171
|
+
return kept ? { url, rows: kept.rows, state: "cached", fetchedAt: kept.fetchedAt } : { url, rows: [], state: "unavailable" };
|
|
172
|
+
const rows = got === "missing" ? [] : got.rows;
|
|
173
|
+
await env.createDir(CATALOG_DIR, { recursive: true }, context);
|
|
174
|
+
await env.writeFile(file, `${JSON.stringify({ url, fetchedAt: now, rows })}
|
|
175
|
+
`, context);
|
|
176
|
+
return { url, rows, state: got === "missing" ? "missing" : "fresh", fetchedAt: now };
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
CATALOG_BASE,
|
|
180
|
+
CATALOG_DIR,
|
|
181
|
+
MAX_AGE,
|
|
182
|
+
PROFILE,
|
|
183
|
+
merge,
|
|
184
|
+
parseSkills,
|
|
185
|
+
parseUseCases,
|
|
186
|
+
profileOf,
|
|
187
|
+
score,
|
|
188
|
+
sheet,
|
|
189
|
+
slugifyCompany,
|
|
190
|
+
truthy
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/gelatiere/catalog.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 { STORE_DIR } from './store.ts';\n\nexport const CATALOG_BASE = 'https://www.sliccy.com/skills/';\nexport const CATALOG_DIR = `${STORE_DIR}/catalog`;\nexport const PROFILE = '.welcome.json';\nexport const MAX_AGE = 12 * 60 * 60 * 1000;\nconst MAX_BYTES = 1024 * 1024;\nconst WEIGHTS = { apps: 3, tasks: 2, role: 1, purpose: 1 };\nconst REPO = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\\/[A-Za-z0-9._-]{1,100}$/;\nconst SKILL = /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,79}$/;\nconst PATH = /^(?!.*\\.\\.)[A-Za-z0-9._/-]{1,200}$/;\nconst NAME = /^[a-z0-9][a-z0-9-]{0,55}$/;\n\nexport type Row = Record<string, string>;\n\nexport interface Profile {\n purpose: string;\n role: string;\n tasks: string[];\n apps: string[];\n company?: string;\n}\n\nexport interface Affinity {\n apps: string[];\n tasks: string[];\n role: string[];\n purpose: string[];\n}\n\nexport interface CatalogSkill {\n name: string;\n title: string;\n description: string;\n repo: string;\n skill?: string;\n path?: string;\n all: boolean;\n affinity: Affinity;\n boost?: number;\n}\n\nexport interface CatalogUseCase {\n name: string;\n title: string;\n description: string;\n prompt: string;\n skills: string[];\n affinity: Affinity;\n boost?: number;\n}\n\nexport type Fetcher = (url: string, init?: RequestInit) => Promise<Response>;\n\nexport type Freshness = 'fresh' | 'cached' | 'missing' | 'unavailable';\n\nexport interface Sheet {\n url: string;\n rows: Row[];\n state: Freshness;\n fetchedAt?: number;\n}\n\nfunction words(value: unknown): string[] {\n if (typeof value !== 'string') return [];\n return value\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nfunction clean(value: unknown, limit: number): string {\n if (typeof value !== 'string') return '';\n return redactSecrets(value.trim()).text.slice(0, limit);\n}\n\nfunction boostOf(value: unknown): number | undefined {\n const parsed = typeof value === 'string' && value.trim() ? Number.parseFloat(value) : Number.NaN;\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction affinityOf(row: Row): Affinity {\n return {\n apps: words(row.apps),\n tasks: words(row.tasks),\n role: words(row.role),\n purpose: words(row.purpose),\n };\n}\n\nfunction nameOf(value: unknown): string | undefined {\n const name = typeof value === 'string' ? value.trim().toLowerCase() : '';\n return NAME.test(name) ? name : undefined;\n}\n\nexport function truthy(value: unknown): boolean {\n return typeof value === 'string' && ['true', '1', 'yes'].includes(value.trim().toLowerCase());\n}\n\nexport function parseSkills(rows: readonly Row[]): CatalogSkill[] {\n const out: CatalogSkill[] = [];\n for (const row of rows) {\n const name = nameOf(row.name);\n const repo = typeof row.repo === 'string' ? row.repo.trim() : '';\n const skill = typeof row.skill === 'string' ? row.skill.trim() : '';\n const path = typeof row.path === 'string' ? row.path.trim() : '';\n const all = truthy(row.installAll);\n if (!name || !REPO.test(repo)) continue;\n if (!all && !SKILL.test(skill)) continue;\n if (path && !PATH.test(path)) continue;\n const boost = boostOf(row.boost);\n out.push({\n name,\n title: clean(row.displayName, 80) || name,\n description: clean(row.description, 500),\n repo,\n ...(all ? {} : { skill }),\n ...(path ? { path } : {}),\n all,\n affinity: affinityOf(row),\n ...(boost === undefined ? {} : { boost }),\n });\n }\n return out;\n}\n\nexport function parseUseCases(rows: readonly Row[]): CatalogUseCase[] {\n const out: CatalogUseCase[] = [];\n for (const row of rows) {\n const name = nameOf(row.name);\n const prompt = clean(row.prompt, 1000);\n if (!name || !prompt) continue;\n const boost = boostOf(row.boost);\n out.push({\n name,\n title: clean(row.displayName, 80) || name,\n description: clean(row.description, 500),\n prompt,\n skills: words(row.skills),\n affinity: affinityOf(row),\n ...(boost === undefined ? {} : { boost }),\n });\n }\n return out;\n}\n\nexport function merge(base: CatalogSkill[], company: CatalogSkill[]): CatalogSkill[] {\n const names = new Set(company.map((entry) => entry.name));\n return [...base.filter((entry) => !names.has(entry.name)), ...company];\n}\n\nexport function slugifyCompany(company: unknown): string | null {\n if (typeof company !== 'string' || !company) return null;\n const slug = company\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-+|-+$)/g, '');\n return slug || null;\n}\n\nexport function profileOf(value: unknown): Profile | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const raw = value as Record<string, unknown>;\n const list = (field: unknown) =>\n Array.isArray(field) ? field.filter((item): item is string => typeof item === 'string') : [];\n const text = (field: unknown) => (typeof field === 'string' ? field : '');\n return {\n purpose: text(raw.purpose),\n role: text(raw.role),\n tasks: list(raw.tasks),\n apps: list(raw.apps),\n ...(typeof raw.company === 'string' && raw.company ? { company: raw.company } : {}),\n };\n}\n\nexport interface Scored {\n score: number;\n reasons: string[];\n}\n\nexport function score(\n entry: { affinity: Affinity; boost?: number },\n profile: Profile | undefined\n): Scored {\n if (!profile) return { score: entry.boost ?? 1, reasons: [] };\n let total = 0;\n const reasons: string[] = [];\n const apps = entry.affinity.apps.filter((app) => profile.apps.includes(app));\n if (apps.length) {\n total += apps.length * WEIGHTS.apps;\n reasons.push(`apps(${apps.join(', ')})`);\n }\n const tasks = entry.affinity.tasks.filter((task) => profile.tasks.includes(task));\n if (tasks.length) {\n total += tasks.length * WEIGHTS.tasks;\n reasons.push(`tasks(${tasks.join(', ')})`);\n }\n if (profile.role && entry.affinity.role.includes(profile.role)) {\n total += WEIGHTS.role;\n reasons.push(`role(${profile.role})`);\n }\n if (profile.purpose && entry.affinity.purpose.includes(profile.purpose)) {\n total += WEIGHTS.purpose;\n reasons.push(`purpose(${profile.purpose})`);\n }\n return { score: total * (entry.boost ?? 1), reasons };\n}\n\nfunction rowsOf(value: unknown): Row[] | undefined {\n const data = (value as { data?: unknown } | null)?.data;\n if (!Array.isArray(data)) return undefined;\n return data.filter((row): row is Row => Boolean(row) && typeof row === 'object');\n}\n\nasync function download(\n fetcher: Fetcher,\n url: string\n): Promise<{ rows: Row[] } | 'missing' | undefined> {\n try {\n const response = await fetcher(url, { headers: { accept: 'application/json' } });\n if (response.status === 404) return 'missing';\n if (response.status !== 200) return undefined;\n const text = await response.text();\n if (text.length > MAX_BYTES) return undefined;\n const rows = rowsOf(JSON.parse(text));\n return rows ? { rows } : undefined;\n } catch {\n return undefined;\n }\n}\n\ninterface Cached {\n url: string;\n fetchedAt: number;\n rows: Row[];\n}\n\nasync function cached(\n env: Pick<ExecutionEnv, 'readTextFile'>,\n file: string,\n url: string,\n context: Context\n): Promise<Cached | undefined> {\n const read = await env.readTextFile(file, context);\n if (!read.ok) return undefined;\n try {\n const parsed = JSON.parse(read.value) as Cached;\n return parsed.url === url && typeof parsed.fetchedAt === 'number' && Array.isArray(parsed.rows)\n ? parsed\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface SheetOptions {\n env: Pick<ExecutionEnv, 'readTextFile' | 'writeFile' | 'createDir'>;\n fetcher: Fetcher;\n now: number;\n refresh: boolean;\n}\n\nexport async function sheet(\n options: SheetOptions,\n url: string,\n file: string,\n context: Context\n): Promise<Sheet> {\n const { env, fetcher, now, refresh } = options;\n const kept = await cached(env, file, url, context);\n if (kept && !refresh && now - kept.fetchedAt < MAX_AGE)\n return {\n url,\n rows: kept.rows,\n state: kept.rows.length ? 'fresh' : 'missing',\n fetchedAt: kept.fetchedAt,\n };\n const got = await download(fetcher, url);\n if (got === undefined)\n return kept\n ? { url, rows: kept.rows, state: 'cached', fetchedAt: kept.fetchedAt }\n : { url, rows: [], state: 'unavailable' };\n const rows = got === 'missing' ? [] : got.rows;\n await env.createDir(CATALOG_DIR, { recursive: true }, context);\n await env.writeFile(file, `${JSON.stringify({ url, fetchedAt: now, rows })}\\n`, context);\n return { url, rows, state: got === 'missing' ? 'missing' : 'fresh', fetchedAt: now };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/gelatiere/index.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { live } from "../agents.js";
|
|
2
2
|
import { skillFromText } from "../skills/skills.js";
|
|
3
|
+
import {
|
|
4
|
+
CATALOG_BASE,
|
|
5
|
+
CATALOG_DIR,
|
|
6
|
+
merge,
|
|
7
|
+
PROFILE,
|
|
8
|
+
parseSkills,
|
|
9
|
+
parseUseCases,
|
|
10
|
+
profileOf,
|
|
11
|
+
score,
|
|
12
|
+
sheet,
|
|
13
|
+
slugifyCompany
|
|
14
|
+
} from "./catalog.js";
|
|
3
15
|
import { fold, open, readStore, STORE, validate, writeStore } from "./store.js";
|
|
4
16
|
const ROLE = "gelatiere";
|
|
5
17
|
const SPRINKLE = "suggestions";
|
|
@@ -10,6 +22,8 @@ const USAGE = `usage:
|
|
|
10
22
|
gelatiere run ask the gelatiere for a pass now
|
|
11
23
|
gelatiere suggest <file> fold a JSON array of suggestions into the store
|
|
12
24
|
gelatiere deliver lick each cone with the suggestions that are new for it
|
|
25
|
+
gelatiere catalog [--refresh] [--all] [--json]
|
|
26
|
+
rank the www.sliccy.com skill and use-case catalog for this user
|
|
13
27
|
gelatiere install <id> install a suggested skill with upskill into ~/.pi/agent/skills
|
|
14
28
|
gelatiere list [--all] [--json]
|
|
15
29
|
gelatiere dismiss <id> mark a suggestion as not wanted
|
|
@@ -163,48 +177,141 @@ async function deliver(g, context) {
|
|
|
163
177
|
};
|
|
164
178
|
}
|
|
165
179
|
const INSTALLER = "https://raw.githubusercontent.com/ai-ecoverse/gh-upskill/main/install.sh";
|
|
166
|
-
function upskillScript(repo, skill) {
|
|
180
|
+
function upskillScript(repo, skill, where = {}) {
|
|
181
|
+
const from = where.path ? ` --path '${where.path}'` : "";
|
|
182
|
+
const which = where.all ? " --all" : ` --skill '${skill}'`;
|
|
167
183
|
return [
|
|
168
184
|
'bin="${PNPM_HOME:-$HOME/.local/share/pnpm}/bin"',
|
|
169
185
|
`{ command -v upskill >/dev/null 2>&1 || curl -fsSL ${INSTALLER} | bash -s -- --bin-dir "$bin"; }`,
|
|
170
|
-
`upskill '${repo}'
|
|
186
|
+
`upskill '${repo}'${from}${which} --dest-path "$HOME/.pi/agent/skills"`
|
|
171
187
|
].join(" && ");
|
|
172
188
|
}
|
|
173
|
-
async function
|
|
189
|
+
async function skillsIn(env, home, context) {
|
|
174
190
|
const root = `${home}/.pi/agent/skills`;
|
|
191
|
+
const found = /* @__PURE__ */ new Map();
|
|
175
192
|
const listed = await env.listDir(root, context);
|
|
176
193
|
for (const info of listed.ok ? listed.value : []) {
|
|
177
194
|
const path = `${root}/${info.name}/SKILL.md`;
|
|
178
195
|
const read = await env.readTextFile(path, context);
|
|
179
|
-
|
|
196
|
+
const name = read.ok ? skillFromText(read.value, path, "user").skills[0]?.name : void 0;
|
|
197
|
+
if (name) found.set(name, path);
|
|
180
198
|
}
|
|
181
|
-
return
|
|
199
|
+
return found;
|
|
182
200
|
}
|
|
183
201
|
async function install(g, id, context) {
|
|
184
202
|
const { env, home } = g.options;
|
|
185
203
|
const item = (await g.load(context)).find((entry) => entry.id === id && entry.kind === "skill");
|
|
186
204
|
const checked = item ? validate(item) : `there is no skill suggestion ${id}`;
|
|
187
205
|
if (typeof checked === "string") return fail(checked);
|
|
188
|
-
const { skill, repo } = checked;
|
|
189
|
-
const
|
|
190
|
-
|
|
206
|
+
const { skill, repo, path, all } = checked;
|
|
207
|
+
const what = all ? `the skills${path ? ` in ${path}` : ""}` : skill;
|
|
208
|
+
const before = await skillsIn(env, home, context);
|
|
209
|
+
const existing = skill ? before.get(skill) : void 0;
|
|
210
|
+
if (!all && existing) return fail(`${skill} is already installed in ${existing}`);
|
|
191
211
|
const lines = [];
|
|
192
212
|
const ran = await env.exec(
|
|
193
|
-
upskillScript(repo, skill),
|
|
213
|
+
upskillScript(repo, skill, { ...path ? { path } : {}, ...all ? { all } : {} }),
|
|
194
214
|
{ cwd: home, onOutput: (text) => void lines.push(text) },
|
|
195
215
|
context
|
|
196
216
|
);
|
|
197
217
|
const output = lines.join("").trim().split("\n").slice(-8).join("\n");
|
|
198
|
-
const
|
|
199
|
-
|
|
218
|
+
const after = await skillsIn(env, home, context);
|
|
219
|
+
const added = all ? [...after].filter(([name, at]) => before.get(name) !== at) : [...after].filter(([name]) => name === skill);
|
|
220
|
+
if (!ran.ok || ran.value.exitCode !== 0 || !added.length)
|
|
200
221
|
return fail(
|
|
201
|
-
`upskill couldn't install ${
|
|
222
|
+
`upskill couldn't install ${what} from github.com/${repo}${output ? `:
|
|
202
223
|
${output}` : ""}`
|
|
203
224
|
);
|
|
204
225
|
await g.mark(id, "takenAt", context);
|
|
226
|
+
const names = added.map(([name]) => name).sort();
|
|
205
227
|
return {
|
|
206
228
|
code: 0,
|
|
207
|
-
out: `installed ${
|
|
229
|
+
out: all ? `installed ${names.join(", ")} in ${home}/.pi/agent/skills; ${names.map((name) => `/skill:${name}`).join(", ")} once the skills reload
|
|
230
|
+
` : `installed ${skill} in ${added[0]?.[1]}; it is /skill:${skill} once the skills reload
|
|
231
|
+
`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function profileFor(g, context) {
|
|
235
|
+
const read = await g.options.env.readTextFile(`${g.options.home}/${PROFILE}`, context);
|
|
236
|
+
if (!read.ok) return void 0;
|
|
237
|
+
try {
|
|
238
|
+
return profileOf(JSON.parse(read.value));
|
|
239
|
+
} catch {
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function sourceLine(label, found) {
|
|
244
|
+
const at = found.fetchedAt ? `, fetched ${new Date(found.fetchedAt).toISOString()}` : "";
|
|
245
|
+
return `${label}: ${found.state} (${found.url}${at})`;
|
|
246
|
+
}
|
|
247
|
+
async function catalog(g, argv, context) {
|
|
248
|
+
const { env, home } = g.options;
|
|
249
|
+
const base = g.options.catalog ?? CATALOG_BASE;
|
|
250
|
+
const fetcher = g.options.fetch ?? ((url, init2) => globalThis.fetch(url, init2));
|
|
251
|
+
const options = { env, fetcher, now: g.now(), refresh: argv.includes("--refresh") };
|
|
252
|
+
const profile = await profileFor(g, context);
|
|
253
|
+
const slug = slugifyCompany(profile?.company);
|
|
254
|
+
const company = slug && !["catalog", "use-cases"].includes(slug) ? slug : null;
|
|
255
|
+
const [global, own, cases] = await Promise.all([
|
|
256
|
+
sheet(options, `${base}catalog.json`, `${CATALOG_DIR}/catalog.json`, context),
|
|
257
|
+
company ? sheet(options, `${base}${company}.json`, `${CATALOG_DIR}/company-${company}.json`, context) : void 0,
|
|
258
|
+
sheet(options, `${base}use-cases.json`, `${CATALOG_DIR}/use-cases.json`, context)
|
|
259
|
+
]);
|
|
260
|
+
const store = await g.load(context);
|
|
261
|
+
const closed = new Set(store.filter((item) => item.dismissedAt || item.takenAt).map((i) => i.id));
|
|
262
|
+
const have = /* @__PURE__ */ new Set([
|
|
263
|
+
...(await skillsIn(env, home, context)).keys(),
|
|
264
|
+
...(g.options.skills?.() ?? []).map((entry) => entry.name)
|
|
265
|
+
]);
|
|
266
|
+
const evidence = (reasons) => `www.sliccy.com catalog: ${reasons.length ? reasons.join(", ") : "featured"}`;
|
|
267
|
+
const skills = merge(parseSkills(global.rows), parseSkills(own?.rows ?? [])).filter((entry) => !entry.skill || !have.has(entry.skill)).map((entry) => {
|
|
268
|
+
const ranked2 = score(entry, profile);
|
|
269
|
+
return {
|
|
270
|
+
id: `catalog-${entry.name}`,
|
|
271
|
+
kind: "skill",
|
|
272
|
+
title: entry.title,
|
|
273
|
+
body: entry.description || entry.title,
|
|
274
|
+
evidence: evidence(ranked2.reasons),
|
|
275
|
+
score: ranked2.score,
|
|
276
|
+
...entry.skill ? { skill: entry.skill } : {},
|
|
277
|
+
repo: entry.repo,
|
|
278
|
+
...entry.path ? { path: entry.path } : {},
|
|
279
|
+
...entry.all ? { all: true } : {}
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
const uses = parseUseCases(cases.rows).map((entry) => {
|
|
283
|
+
const ranked2 = score(entry, profile);
|
|
284
|
+
return {
|
|
285
|
+
id: `catalog-use-${entry.name}`,
|
|
286
|
+
kind: "use-case",
|
|
287
|
+
title: entry.title,
|
|
288
|
+
body: entry.description || entry.title,
|
|
289
|
+
evidence: evidence(ranked2.reasons),
|
|
290
|
+
score: ranked2.score,
|
|
291
|
+
prompt: entry.prompt,
|
|
292
|
+
...entry.skills.length ? { skills: entry.skills } : {}
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
const all = argv.includes("--all");
|
|
296
|
+
const ranked = [...skills, ...uses].filter((item) => !closed.has(item.id) && (all || item.score > 0)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
297
|
+
const sources = [
|
|
298
|
+
sourceLine("skills", global),
|
|
299
|
+
...own ? [sourceLine(`company ${company}`, own)] : [],
|
|
300
|
+
sourceLine("use cases", cases)
|
|
301
|
+
];
|
|
302
|
+
const who = profile ? `profile: ${home}/${PROFILE} (${[profile.role, profile.purpose].filter(Boolean).join(", ") || "empty"})` : `profile: none (${home}/${PROFILE}); ranked by boost only`;
|
|
303
|
+
if (argv.includes("--json"))
|
|
304
|
+
return {
|
|
305
|
+
code: 0,
|
|
306
|
+
out: `${JSON.stringify({ sources, profile: Boolean(profile), candidates: ranked }, null, 2)}
|
|
307
|
+
`
|
|
308
|
+
};
|
|
309
|
+
const rows = ranked.map(
|
|
310
|
+
(item) => `${item.score} ${item.id} ${item.kind} ${item.title} ${item.evidence}`
|
|
311
|
+
);
|
|
312
|
+
return {
|
|
313
|
+
code: 0,
|
|
314
|
+
out: `${[...sources, who, ...rows.length ? rows : ["no catalog entries match"]].join("\n")}
|
|
208
315
|
`
|
|
209
316
|
};
|
|
210
317
|
}
|
|
@@ -241,6 +348,7 @@ const VERBS = {
|
|
|
241
348
|
suggest: (g, argv, context) => g.serial(() => suggest(g, argv[1], context)),
|
|
242
349
|
deliver: (g, _argv, context) => g.serial(() => deliver(g, context)),
|
|
243
350
|
install: (g, argv, context) => install(g, argv[1] ?? "", context),
|
|
351
|
+
catalog: (g, argv, context) => catalog(g, argv, context),
|
|
244
352
|
list: (g, argv, context) => list(g, argv, context),
|
|
245
353
|
status: (g, _argv, context) => status(g, context),
|
|
246
354
|
dismiss: (g, argv, context) => dismiss(g, argv[1] ?? "", context)
|