agentic-relay 4.3.1 → 5.0.1

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
@@ -25,7 +25,11 @@ npx agentic-relay --all --api-key=am_live_xxx
25
25
 
26
26
  `--target=...` accepts a comma-separated list of agent IDs. `--all` installs for every agent in the matrix below. `--api-key=...` skips the prompt.
27
27
 
28
- > One package, one command. `agentrelay` with no subcommand runs the installer; `agentrelay <cmd>` (e.g. `search`, `session`, `cache`) runs the CLI. If the global link can't be created (e.g. an unwritable npm prefix), run `npm i -g agentic-relay` once yourself.
28
+ > One package, one command. `agentrelay` with no subcommand runs the installer; `agentrelay <cmd>` (e.g. `search`, `session`, `fetch`) runs the CLI. If the global link can't be created (e.g. an unwritable npm prefix), run `npm i -g agentic-relay` once yourself.
29
+ >
30
+ > **Reinstalling is always a clean, fresh install:** it removes the old global command and reinstalls `agentic-relay@latest` (or your local checkout when you're developing), rewrites the skill files, re-checks your cached API key (replacing it only if it's no longer valid), and clears this package's npx cache entry. Nothing stale is left behind.
31
+ >
32
+ > npx tip: `npx <name>` caches by bare name and won't auto-upgrade. If `npx agentic-relay` ever runs an old version, run `npx agentic-relay@latest` once (or `npm i -g agentic-relay@latest`) — after that the installer keeps itself current.
29
33
 
30
34
  Don't run `npm install` (as a dependency) — this is a CLI installer, not a library.
31
35
 
@@ -103,7 +107,7 @@ The installer puts `agentrelay` on your PATH (or run `npm i -g agentic-relay` on
103
107
 
104
108
  ```bash
105
109
  # 1. discover
106
- agentrelay search "weather dashboard tools" --dynamic-only --json
110
+ agentrelay search "weather dashboard tools" --json
107
111
 
108
112
  # 2. converse (drive this loop from a subagent, or directly)
109
113
  agentrelay session start open-meteo-weather-api-solutions-engineer \
@@ -118,24 +122,22 @@ non-empty), answer the agent's question from your context and `session send` aga
118
122
  Sessions are short-lived (~60 min); start a fresh one per consult.
119
123
 
120
124
  **Breadth → build → depth.** For real builds: spawn one **subagent per agent** to drive it to full
121
- coverage and **bank** the result (`agentrelay cache put <slug>`) while returning only a compact `summary`
122
- to the orchestrator (breadth, parallel, transcripts stay isolated); the main agent then builds from the
123
- bank (`agentrelay cache ls` to triage summaries, `agentrelay cache show <slug>` for a feature's full detail)
124
- and opens a **direct** session only when the bank lacks something (depth), appending the answer back.
125
- A banked spec follows a light contract — `{ slug, name, summary, features:[{feature, how, snippets,
126
- gotchas}], capability_id, ad_unit_id }` (+ any extra keys) — see `schema/deliverable.json`. Concurrent
127
- `cache put`s from parallel subagents are safe (per-slug files).
125
+ coverage in parallel (transcripts stay isolated), each returning only a compact `summary` to the
126
+ orchestrator; the main agent builds from those summaries and opens a **direct** session only when it
127
+ needs deeper detail (depth). Search results are cached on disk (`.agent-relay/`) so a repeat search is
128
+ near-free, and the index lets `session start` resolve a slug without re-searching.
128
129
 
129
130
  | Command | What it does |
130
131
  |---|---|
131
- | `agentrelay search <query>` | Discovery (default 25, relevance-sorted). `--dynamic-only`, `--user-context`, `--max`. |
132
- | `agentrelay session start <slug>` | Begin a conversation. `--initial '<json>'`, `--message "<first msg>"`. |
132
+ | `agentrelay search <query>` | Discovery (default 25, relevance-sorted). `--max`. |
133
+ | `agentrelay session start <slug>` | Begin a conversation. `--initial '<json>'`, `--task-context "<s>"`, `--message "<first msg>"`. |
133
134
  | `agentrelay session send <id> "<m>"` | Send a turn; returns the agent's raw reply + flags. `--data '<json>'`. |
134
135
  | `agentrelay session end <id>` | Close + summary. |
135
136
  | `agentrelay feedback <id>` | `--score <0-100> --text "<s>"`. |
136
- | `agentrelay cache ls\|show <slug>\|put <slug>\|clear` | Persist/review distillations (`.agent-relay/`). |
137
- | `agentrelay session pay <id>` / `fetch` / `budget` | Settle an in-policy charge, fetch a paid deliverable, read/set the spend policy. |
138
- | `agentrelay doctor` | Diagnose the install: Node version, API key + connectivity, npm prefix writability, `~/.npm` ownership, global bin + PATH. `--json` for machines. |
137
+ | `agentrelay fetch --session <id>` | After payment, fetch + verify + unpack a paid deliverable into `./<slug>/`. |
138
+
139
+ Payments go through a link when a session returns a `payment_required`, surface its `payment_url` to
140
+ the user; the CLI never charges programmatically.
139
141
 
140
142
  Output is a `{ ok, ...payload, errors: [] }` envelope (`--json` auto-on when piped). Key resolution:
141
143
  `--api-key` → `$AGENT_RELAY_API_KEY` → `~/.config/agent-relay/credentials.json` (with a read-only fallback to the
package/bin/cli.mjs CHANGED
@@ -5,8 +5,11 @@
5
5
  *
6
6
  * Routing:
7
7
  * - A recognised CLI subcommand → the CLI core (relay-core.mjs)
8
- * - Anything else (no args, or install flags like --all/--target/--api-key)
9
- * → the installer (install.mjs)
8
+ * - An UNrecognised bare word (a typo or removed command)
9
+ * → the CLI core too, so it prints a clean
10
+ * "unknown command" error (exit 2)
11
+ * - No args, an install flag (--all/--target/--api-key/…), or a bare
12
+ * `am_live_…` key → the installer (install.mjs)
10
13
  *
11
14
  * Both modules read process.argv directly and self-run on import, so we just
12
15
  * dynamically import the right one. argv is untouched, so e.g.
@@ -17,23 +20,22 @@ const CLI_COMMANDS = new Set([
17
20
  "search",
18
21
  "session",
19
22
  "feedback",
20
- "cache",
21
- "budget",
22
- "connect-wallet",
23
23
  "fetch",
24
- "doctor",
25
24
  "version",
26
25
  "help",
27
- // Removed commands — still routed to the CLI core so they fail with a clear
28
- // error (exit 2) instead of falling through to the interactive installer.
29
- "consult",
30
- "consult-batch",
31
26
  ]);
32
27
 
33
28
  const sub = process.argv[2];
34
29
 
35
- if (sub && CLI_COMMANDS.has(sub)) {
36
- await import("./relay-core.mjs");
37
- } else {
30
+ // The installer is for: no args, an install flag (starts with "-"), or a bare
31
+ // API key pasted as the first arg. Any other first token is treated as a CLI
32
+ // command — known ones run; unknown ones get a clean error from relay-core,
33
+ // NOT a surprise interactive installer.
34
+ const isInstallerInvocation =
35
+ !sub || sub.startsWith("-") || sub.startsWith("am_live_");
36
+
37
+ if (isInstallerInvocation) {
38
38
  await import("./install.mjs");
39
+ } else {
40
+ await import("./relay-core.mjs");
39
41
  }
package/bin/install.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Agent Relay Installer v3 — multi-agent (skill, MCP, deeplink modes)
4
+ * Agent Relay Installer — multi-agent (skill, MCP, deeplink modes)
5
5
  *
6
6
  * Usage:
7
7
  *
@@ -29,7 +29,7 @@
29
29
  * deeplink Print URL the agent registers (tome://, raycast://) — user clicks Install
30
30
  */
31
31
 
32
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
32
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
33
33
  import { homedir } from "os";
34
34
  import { join, dirname } from "path";
35
35
  import { fileURLToPath } from "url";
@@ -38,14 +38,13 @@ import { stdin, stdout } from "process";
38
38
  import { execSync } from "child_process";
39
39
  import { parseInstallArgs } from "./lib/install-args.mjs";
40
40
  import {
41
- resolveCommandPath,
42
- isEphemeralBin,
43
41
  npmGlobalPrefix,
44
42
  npmGlobalBinDir,
45
43
  globalBinShimPath,
46
44
  globalPrefixWritable,
47
45
  npmCacheOwnedByOther,
48
46
  dirOnPath,
47
+ clearNpxCacheFor,
49
48
  USER_PREFIX_RECIPE,
50
49
  CACHE_CHOWN_RECIPE,
51
50
  } from "./lib/sysenv.mjs";
@@ -294,6 +293,9 @@ function parseArgs() {
294
293
 
295
294
  function installSkill(skillContent, instructionsContent, agent) {
296
295
  try {
296
+ // Wipe the skill dir first so a reinstall can't leave behind stale files a
297
+ // previous version dropped here — we own skills/agent-relay/ entirely.
298
+ rmSync(agent.skillDir, { recursive: true, force: true });
297
299
  mkdirSync(agent.skillDir, { recursive: true });
298
300
  writeFileSync(join(agent.skillDir, "SKILL.md"), skillContent);
299
301
  success(`${agent.label}: skill → ${agent.skillDir}/SKILL.md`);
@@ -583,6 +585,38 @@ function saveApiKey(key) {
583
585
  success(`API key saved → ${CREDENTIALS_PATH}`);
584
586
  }
585
587
 
588
+ /** Remove any cached credential files (new + legacy). Best-effort. */
589
+ function clearCachedKey() {
590
+ for (const path of [CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH]) {
591
+ try {
592
+ rmSync(path, { force: true });
593
+ } catch {}
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Live-check a key against the search endpoint. Returns { ok } — true only when
599
+ * the key authenticates and search responds. Shared by the reinstall
600
+ * verify-then-refresh path and the final post-install verification.
601
+ */
602
+ async function verifyKey(key) {
603
+ try {
604
+ const resp = await fetch(`${API_BASE}/search`, {
605
+ method: "POST",
606
+ headers: { "Content-Type": "application/json", "X-AM-API-Key": key },
607
+ body: JSON.stringify({ query: "test", max_results: 1 }),
608
+ });
609
+ let data = {};
610
+ try {
611
+ data = await resp.json();
612
+ } catch {}
613
+ if (resp.ok && data.capabilities) return { ok: true };
614
+ return { ok: false, detail: `HTTP ${resp.status}${data.error ? ` — ${data.error}` : ""}` };
615
+ } catch (err) {
616
+ return { ok: false, detail: err.message };
617
+ }
618
+ }
619
+
586
620
  function getExistingKey() {
587
621
  // Prefer the new path, but recognise a legacy ~/.config/penguin key too so an
588
622
  // existing user isn't re-prompted (resolveApiKey applies the same fallback).
@@ -598,46 +632,27 @@ function getExistingKey() {
598
632
 
599
633
  /**
600
634
  * Link a bare global `agentrelay` command so users skip `npx` after install.
601
- * No-ops only when a GENUINE install of `agentrelay` already matches this
602
- * package's versiona stale global install gets upgraded, not skipped.
635
+ * Always performs a clean relink on reinstall uninstalls any existing global
636
+ * first, then reinstalls so the code on PATH is never stale (there is no
637
+ * "already the right version, skip" shortcut; that shortcut is exactly what left
638
+ * old code on PATH after a local edit).
603
639
  *
604
640
  * npx trap: `npx agentic-relay` prepends its ephemeral cache's .bin to this
605
641
  * process's PATH, so `agentrelay` ALWAYS resolves here (to the very copy being
606
- * run) and disappears when npx exits. A naive "already on your PATH" check
607
- * therefore never links anything and the user gets `command not found` in
608
- * their next shell. We resolve the command's real location and ignore it when
609
- * it lives in an npx/npm-exec cache or inside this package itself.
642
+ * run) and disappears when npx exits. We therefore never trust the bare command;
643
+ * we uninstall + reinstall and verify the global bin file landed directly.
610
644
  *
611
- * Prefers a registry install (`agentic-relay@<version>`) over the local path
612
- * (the npx cache dir is ephemeral; npm would link/copy from it). Falls back to
613
- * the path for unpublished checkouts. Surfaces npm's actual error (EACCES
614
- * etc.) instead of swallowing it, and verifies the global bin file landed so a
615
- * PATH mismatch is reported, not discovered later as `command not found`.
616
- * System probes live in lib/sysenv.mjs (shared with `agentrelay doctor`).
645
+ * Install source: from a DEV CHECKOUT (a .git dir next to the package) the
646
+ * local path is installed FIRST so your edits actually go global; a normal
647
+ * published run installs `agentic-relay@latest` from the registry first, with
648
+ * the path as fallback. `@latest` (not the running version) matters: a stale
649
+ * cached npx installer must still land the NEWEST global, not pin its own old
650
+ * version. Surfaces npm's actual error (EACCES etc.), verifies the global bin
651
+ * landed, then clears our npx cache entry so the next `npx` is fresh too.
652
+ * System probes live in lib/sysenv.mjs.
617
653
  */
618
654
  function linkGlobalCli() {
619
655
  const pkgRoot = join(__dirname, "..");
620
- let version = null;
621
- try {
622
- version = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf-8")).version;
623
- } catch {
624
- // unknown version — fall through to the path install
625
- }
626
-
627
- const binPath = resolveCommandPath("agentrelay");
628
- if (binPath && !isEphemeralBin(binPath, pkgRoot)) {
629
- try {
630
- const out = execSync("agentrelay version", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
631
- const installed = (out.trim().split(/\s+/).pop() || "").trim();
632
- if (version && installed === version) {
633
- success(`\`agentrelay\` ${version} already on your PATH`);
634
- return "linked";
635
- }
636
- log(`global \`agentrelay\` is ${installed || "an unknown version"} — updating to ${version}…`);
637
- } catch {
638
- // resolves but doesn't run — reinstall it
639
- }
640
- }
641
656
 
642
657
  // Preflight: detect the two environment breakages PROACTIVELY instead of
643
658
  // letting `npm install -g` fail with a buried stderr.
@@ -654,7 +669,22 @@ function linkGlobalCli() {
654
669
  return "blocked";
655
670
  }
656
671
 
657
- const attempts = version ? [`agentic-relay@${version}`, `"${pkgRoot}"`] : [`"${pkgRoot}"`];
672
+ // Clean relink: remove any existing global so a reinstall can't leave stale
673
+ // code on PATH. Best-effort — a missing global just no-ops.
674
+ try {
675
+ execSync("npm uninstall -g agentic-relay", { stdio: ["ignore", "ignore", "ignore"] });
676
+ } catch {}
677
+
678
+ // Prefer the local path when this is a dev checkout, so local edits actually
679
+ // reach the global command; otherwise install @latest. Detect a checkout by
680
+ // the .git dir — a published tarball (incl. an npx run) never has one.
681
+ // (bin/test/ ships in the package, so it can't be the signal.) @latest, not
682
+ // @<version>: if a STALE cached npx installer runs this, it must still land
683
+ // the newest global rather than re-pinning its own old version.
684
+ const isDevCheckout = existsSync(join(pkgRoot, ".git"));
685
+ const registrySpec = "agentic-relay@latest";
686
+ const pathSpec = `"${pkgRoot}"`;
687
+ const attempts = isDevCheckout ? [pathSpec, registrySpec] : [registrySpec, pathSpec];
658
688
  let lastErr = null;
659
689
  let installedFrom = null;
660
690
  for (const spec of attempts) {
@@ -685,6 +715,9 @@ function linkGlobalCli() {
685
715
  const shim = globalBinShimPath(prefix, "agentrelay");
686
716
  if (shim && existsSync(shim)) {
687
717
  success(`linked global \`agentrelay\` CLI (${installedFrom})`);
718
+ // Drop our stale npx cache entry so the next `npx agentic-relay` re-resolves
719
+ // the newest version instead of replaying an old cached installer.
720
+ clearNpxCacheFor("agentic-relay");
688
721
  if (!dirOnPath(binDir)) {
689
722
  warn(`note: ${binDir} doesn't appear to be on your PATH — add it to use \`agentrelay\` directly`);
690
723
  return "linked-offpath";
@@ -696,15 +729,26 @@ function linkGlobalCli() {
696
729
  }
697
730
 
698
731
  async function setupApiKeyInteractive() {
732
+ // Verify-then-refresh: on a reinstall, keep a cached key that still works, but
733
+ // wipe a stale/revoked one and prompt for a fresh key — so a dead key never
734
+ // silently persists. (A working key is never re-typed just because you
735
+ // reinstalled.)
699
736
  const existing = getExistingKey();
700
737
  if (existing) {
701
- success(`API key already configured (${CREDENTIALS_PATH})`);
702
- return true;
738
+ log("Checking cached API key…");
739
+ const check = await verifyKey(existing);
740
+ if (check.ok) {
741
+ success(`Reusing verified API key (${CREDENTIALS_PATH})`);
742
+ return true;
743
+ }
744
+ warn(`Cached API key is no longer valid (${check.detail}) — clearing it.`);
745
+ clearCachedKey();
746
+ // fall through and prompt for a fresh one
703
747
  }
704
748
 
705
749
  // Scripted/CI runs have no TTY — don't hang on a prompt nobody will answer.
706
750
  if (!stdin.isTTY) {
707
- warn("no API key configured and stdin is not a terminal — skipping the prompt.");
751
+ warn("no valid API key configured and stdin is not a terminal — skipping the prompt.");
708
752
  warn("Pass --api-key=<am_live_...> or write ~/.config/agent-relay/credentials.json");
709
753
  return false;
710
754
  }
@@ -748,23 +792,11 @@ async function verifyInstall() {
748
792
  if (!key) return;
749
793
 
750
794
  log("Verifying API key…");
751
- try {
752
- const resp = await fetch(`${API_BASE}/search`, {
753
- method: "POST",
754
- headers: {
755
- "Content-Type": "application/json",
756
- "X-AM-API-Key": key,
757
- },
758
- body: JSON.stringify({ query: "test", max_results: 1 }),
759
- });
760
- const data = await resp.json();
761
- if (resp.ok && data.capabilities) {
762
- success("API key verified — search is working");
763
- } else {
764
- warn(`API returned ${resp.status}: ${data.error || "unknown error"}`);
765
- }
766
- } catch (err) {
767
- warn(`Verification failed: ${err.message}`);
795
+ const check = await verifyKey(key);
796
+ if (check.ok) {
797
+ success("API key verified — search is working");
798
+ } else {
799
+ warn(`API key verification failed: ${check.detail}`);
768
800
  }
769
801
  }
770
802
 
package/bin/lib/api.mjs CHANGED
@@ -66,10 +66,8 @@ export async function apiPost(path, body, { apiKey, timeoutMs = 30000 } = {}) {
66
66
  return data;
67
67
  }
68
68
 
69
- export function search(query, { apiKey, userContext, maxResults = 25, timeoutMs }) {
70
- const body = { query, max_results: maxResults };
71
- if (userContext) body.user_context = userContext;
72
- return apiPost("/search", body, { apiKey, timeoutMs });
69
+ export function search(query, { apiKey, maxResults = 25, timeoutMs }) {
70
+ return apiPost("/search", { query, max_results: maxResults }, { apiKey, timeoutMs });
73
71
  }
74
72
 
75
73
  export function sessionStart(
@@ -107,30 +105,6 @@ export function sessionEnd({ sessionId }, { apiKey, timeoutMs }) {
107
105
  return apiPost("/session/end", { session_id: sessionId }, { apiKey, timeoutMs });
108
106
  }
109
107
 
110
- /**
111
- * Settle a payment request for a session. Omit `paymentCredential` to pay from the
112
- * account's server-side wallet grant; pass it for a client-side MPP/x402 wallet.
113
- * Returns the structured SettleResult ({ ok, error_code, ... }); does not throw on a
114
- * 402 policy/charge rejection — the caller inspects `ok`.
115
- */
116
- export async function sessionPay({ sessionId, paymentCredential }, { apiKey, timeoutMs }) {
117
- try {
118
- return await apiPost(
119
- "/session/pay",
120
- {
121
- ...(sessionId ? { session_id: sessionId } : {}),
122
- ...(paymentCredential ? { payment_credential: paymentCredential } : {}),
123
- },
124
- { apiKey, timeoutMs },
125
- );
126
- } catch (err) {
127
- // A 402 is an expected "policy/charge rejected" outcome — return its structured body
128
- // so the agent can fall back to bubbling the payment_url, not crash.
129
- if (err instanceof ApiError && err.body && typeof err.body === "object") return err.body;
130
- throw err;
131
- }
132
- }
133
-
134
108
  export function feedback({ sessionId, feedbackText, score }, { apiKey, timeoutMs }) {
135
109
  return apiPost(
136
110
  "/feedback",
@@ -138,50 +112,3 @@ export function feedback({ sessionId, feedbackText, score }, { apiKey, timeoutMs
138
112
  { apiKey, timeoutMs },
139
113
  );
140
114
  }
141
-
142
- /**
143
- * Generic request used by the GET/PATCH payment-policy endpoints. Same timeout +
144
- * lenient-parse + error handling as apiPost; omits the body for GET.
145
- */
146
- async function apiRequest(method, path, body, { apiKey, timeoutMs = 30000 } = {}) {
147
- const controller = new AbortController();
148
- const timer = setTimeout(() => controller.abort(), timeoutMs);
149
- let resp;
150
- try {
151
- resp = await fetch(`${API_BASE}${path}`, {
152
- method,
153
- headers: { "Content-Type": "application/json", "X-AM-API-Key": apiKey },
154
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
155
- signal: controller.signal,
156
- });
157
- } catch (err) {
158
- if (err && err.name === "AbortError") {
159
- throw new ApiError(`timeout after ${Math.round(timeoutMs / 1000)}s`, { status: 0 });
160
- }
161
- throw new ApiError(`network error: ${err.message}`, { status: 0 });
162
- } finally {
163
- clearTimeout(timer);
164
- }
165
-
166
- const text = await resp.text();
167
- const parsed = lenientParse(text);
168
- const data = parsed.ok ? parsed.value : null;
169
- if (!resp.ok) {
170
- const msg = (data && (data.message || data.error)) || `HTTP ${resp.status}`;
171
- throw new ApiError(String(msg), { status: resp.status, body: data });
172
- }
173
- if (!parsed.ok) {
174
- throw new ApiError("unparseable response from upstream", { status: resp.status, body: text.slice(0, 500) });
175
- }
176
- return data;
177
- }
178
-
179
- /** Read the account's autonomous-spend policy. */
180
- export function getPaymentPolicy({ apiKey, timeoutMs }) {
181
- return apiRequest("GET", "/account/payment-policy", undefined, { apiKey, timeoutMs });
182
- }
183
-
184
- /** Update the account's autonomous-spend policy (partial patch; cents). */
185
- export function patchPaymentPolicy(patch, { apiKey, timeoutMs }) {
186
- return apiRequest("PATCH", "/account/payment-policy", patch, { apiKey, timeoutMs });
187
- }
package/bin/lib/cache.mjs CHANGED
@@ -1,26 +1,17 @@
1
1
  /**
2
- * On-disk cache (spec §9). Layout under --cache-dir (default ./.agent-relay):
2
+ * On-disk search cache. Layout under --cache-dir (default ./.agent-relay):
3
3
  *
4
- * index.json { slug: { ts, expires, agent_id, last_session_id } }
5
- * specs/<slug>.json a Deliverable
4
+ * index.json { slug: { ts, agent_id, name } }
6
5
  * search/<hash>.json { ts, expires, query, capabilities }
7
6
  *
8
- * This is what makes just-in-time, project-long consulting near-free: a repeat
9
- * consult reads specs/<slug>.json instead of re-engaging the agent.
7
+ * A repeat search inside the TTL is served from disk, and the index lets
8
+ * `session start` resolve a slug agent_id without re-searching.
10
9
  */
11
10
 
12
- import {
13
- readFileSync,
14
- writeFileSync,
15
- existsSync,
16
- mkdirSync,
17
- readdirSync,
18
- rmSync,
19
- } from "fs";
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
20
12
  import { join } from "path";
21
13
  import { createHash } from "crypto";
22
14
 
23
- const SPEC_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
24
15
  const SEARCH_TTL_MS = 60 * 60 * 1000; // 1 hour
25
16
 
26
17
  export class Cache {
@@ -28,12 +19,10 @@ export class Cache {
28
19
  this.dir = dir;
29
20
  this.enabled = enabled;
30
21
  this.indexPath = join(dir, "index.json");
31
- this.specsDir = join(dir, "specs");
32
22
  this.searchDir = join(dir, "search");
33
23
  }
34
24
 
35
25
  _ensure() {
36
- mkdirSync(this.specsDir, { recursive: true });
37
26
  mkdirSync(this.searchDir, { recursive: true });
38
27
  }
39
28
 
@@ -69,11 +58,7 @@ export class Cache {
69
58
  return join(this.searchDir, `${h}.json`);
70
59
  }
71
60
 
72
- /**
73
- * Whole unexpired search entry — `{ts, capabilities, payment_context}` — or
74
- * null. Returning the entry (not just capabilities) lets the caller surface
75
- * how stale the cached payment_context snapshot is (see SKILL.md Pay).
76
- */
61
+ /** Whole unexpired search entry — `{ts, query, capabilities}` — or null. */
77
62
  readSearchEntry(query) {
78
63
  if (!this.enabled) return null;
79
64
  const entry = this._readJson(this._searchPath(query));
@@ -83,7 +68,7 @@ export class Cache {
83
68
  return entry;
84
69
  }
85
70
 
86
- writeSearch(query, capabilities, paymentContext = null) {
71
+ writeSearch(query, capabilities) {
87
72
  if (!this.enabled) return;
88
73
  this._ensure();
89
74
  writeFileSync(
@@ -94,62 +79,10 @@ export class Cache {
94
79
  expires: Date.now() + SEARCH_TTL_MS,
95
80
  query,
96
81
  capabilities,
97
- payment_context: paymentContext,
98
82
  },
99
83
  null,
100
84
  2,
101
85
  ) + "\n",
102
86
  );
103
87
  }
104
-
105
- // ── spec (Deliverable) cache ─────────────────────────────────────────
106
- _specPath(slug) {
107
- return join(this.specsDir, `${slug}.json`);
108
- }
109
-
110
- readSpec(slug, { ttlMs = SPEC_TTL_MS } = {}) {
111
- if (!this.enabled) return null;
112
- const spec = this._readJson(this._specPath(slug));
113
- if (!spec) return null;
114
- if (spec.ts && Date.now() - new Date(spec.ts).getTime() > ttlMs) return null;
115
- return spec;
116
- }
117
-
118
- writeSpec(slug, deliverable) {
119
- if (!this.enabled) return;
120
- this._ensure();
121
- writeFileSync(this._specPath(slug), JSON.stringify(deliverable, null, 2) + "\n");
122
- }
123
-
124
- listSpecs() {
125
- if (!existsSync(this.specsDir)) return [];
126
- return readdirSync(this.specsDir)
127
- .filter((f) => f.endsWith(".json"))
128
- .map((f) => f.replace(/\.json$/, ""));
129
- }
130
-
131
- /**
132
- * Triage view of the banked specs — read straight from each spec file (no
133
- * shared index.json), so it's safe under the parallel breadth pass where N
134
- * subagents each `cache put` their own slug file concurrently. Lets the main
135
- * agent see what's available + each summary without opening every file.
136
- */
137
- listSpecHeadlines() {
138
- if (!existsSync(this.specsDir)) return [];
139
- return this.listSpecs().map((slug) => {
140
- const spec = this._readJson(this._specPath(slug)) || {};
141
- return {
142
- slug,
143
- name: spec.name ?? null,
144
- summary: spec.summary ?? null,
145
- features: Array.isArray(spec.features) ? spec.features.length : null,
146
- agent_id: spec.agent_id ?? null,
147
- ts: spec.ts ?? null,
148
- };
149
- });
150
- }
151
-
152
- clear() {
153
- if (existsSync(this.dir)) rmSync(this.dir, { recursive: true, force: true });
154
- }
155
88
  }