@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 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), without `use-cases` and the www.sliccy.com catalog; `wiki` needs its `wiki` CLI.
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
- - 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.
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.30.0; 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.
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
- for (const kept of held) kept.freeze();
123
+ freeze();
116
124
  await server.use(AgentSessions).detach(context);
117
125
  await session.dispose(context);
118
126
  await server.dispose(context);
@@ -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 for (const kept of held) kept.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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",
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
+ }
@@ -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}' --skill '${skill}' --dest-path "$HOME/.pi/agent/skills"`
186
+ `upskill '${repo}'${from}${which} --dest-path "$HOME/.pi/agent/skills"`
171
187
  ].join(" && ");
172
188
  }
173
- async function installed(env, home, skill, context) {
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
- if (read.ok && skillFromText(read.value, path, "user").skills[0]?.name === skill) return path;
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 void 0;
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 before = await installed(env, home, skill, context);
190
- if (before) return fail(`${skill} is already installed in ${before}`);
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 path = await installed(env, home, skill, context);
199
- if (!ran.ok || ran.value.exitCode !== 0 || !path)
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 ${skill} from github.com/${repo}${output ? `:
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 ${skill} in ${path}; it is /skill:${skill} once the skills reload
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)