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.
@@ -2,22 +2,17 @@
2
2
  * Command implementations for the agentrelay CLI. Each returns an envelope payload
3
3
  * (the entry adds `ok`/`errors` and chooses the exit code).
4
4
  *
5
- * `session start|send|pay|end` are thin api_v1 wrappers; the caller (a host
6
- * subagent, or the main agent in direct mode) drives the conversation and
7
- * reads the agent's RAW turns. Compact specs get banked in the on-disk cache
8
- * (`cache put`/`ls`/`show`) so transcripts stay out of the main context.
5
+ * `session start|send|end` are thin api_v1 wrappers; the caller (a host subagent,
6
+ * or the main agent in direct mode) drives the conversation and reads the agent's
7
+ * RAW turns. Search results are cached on disk so a repeat search is near-free.
9
8
  */
10
9
 
11
- import { readFileSync } from "fs";
12
10
  import {
13
11
  search as apiSearch,
14
12
  sessionStart,
15
13
  sessionMessage,
16
14
  sessionEnd,
17
- sessionPay,
18
15
  feedback as apiFeedback,
19
- getPaymentPolicy,
20
- patchPaymentPolicy,
21
16
  } from "./api.mjs";
22
17
  import { slugify } from "./config.mjs";
23
18
  import { Cache } from "./cache.mjs";
@@ -30,7 +25,6 @@ function normalizeCapability(c) {
30
25
  return {
31
26
  slug: slugify(name),
32
27
  name,
33
- type: "dynamic_capability", // single type now; kept for the --dynamic-only filter
34
28
  agent_id: c.agent_id || null,
35
29
  relevance: typeof c.relevance_score === "number" ? c.relevance_score : null,
36
30
  reputation: typeof c.reputation_score === "number" ? c.reputation_score : null,
@@ -60,13 +54,9 @@ export async function cmdSearch(query, opts) {
60
54
  const cache = new Cache(opts.cacheDir, { enabled: !opts.noCache });
61
55
  const maxResults = opts.max || 25;
62
56
 
63
- // The buyer-side spend policy snapshot the search API returns alongside the
64
- // menu the agent decides auto-pay-vs-bubble against it (SKILL.md Pay). Pass
65
- // it through here so it survives to the CLI's JSON output and gets cached.
66
- // On a cache hit the snapshot can be up to the search TTL (1h) stale, so the
67
- // payload carries from_cache/cached_at for the caller's pay decision.
57
+ // A repeat search inside the cache TTL (1h) is served from disk; the payload
58
+ // carries from_cache/cached_at so the caller knows how fresh the menu is.
68
59
  const cachedEntry = opts.refresh ? null : cache.readSearchEntry(query);
69
- let paymentContext = cachedEntry ? (cachedEntry.payment_context ?? null) : null;
70
60
  let capabilities = cachedEntry ? cachedEntry.capabilities : null;
71
61
  const fromCache = Boolean(cachedEntry);
72
62
  const cachedAt = cachedEntry ? (cachedEntry.ts ?? null) : null;
@@ -74,13 +64,11 @@ export async function cmdSearch(query, opts) {
74
64
  if (!capabilities) {
75
65
  const data = await apiSearch(query, {
76
66
  apiKey: opts.apiKey,
77
- userContext: opts.userContext,
78
67
  maxResults,
79
68
  timeoutMs: opts.timeoutMs,
80
69
  });
81
- paymentContext = data.payment_context ?? null;
82
70
  capabilities = dedupeSlugs((data.capabilities || []).map(normalizeCapability));
83
- cache.writeSearch(query, capabilities, paymentContext);
71
+ cache.writeSearch(query, capabilities);
84
72
  // Seed the index so session start can resolve agent_id without re-searching.
85
73
  for (const c of capabilities) {
86
74
  if (c.agent_id) {
@@ -93,9 +81,6 @@ export async function cmdSearch(query, opts) {
93
81
  }
94
82
  }
95
83
 
96
- if (opts.dynamicOnly) {
97
- capabilities = capabilities.filter((c) => c.type === "dynamic_capability");
98
- }
99
84
  capabilities = capabilities.slice(0, maxResults);
100
85
 
101
86
  return {
@@ -104,15 +89,13 @@ export async function cmdSearch(query, opts) {
104
89
  from_cache: fromCache,
105
90
  cached_at: cachedAt,
106
91
  capabilities,
107
- payment_context: paymentContext,
108
92
  };
109
93
  }
110
94
 
111
95
  /**
112
- * Resolve a slug (or agent_id) to { agentId, slug, name }. Order: cache index
113
- * banked spec file → fresh search keyed on the de-kebabed slug. The spec-file
114
- * fallback lets a depth session resolve even when the cache was filled only by
115
- * `cache put` during the breadth pass (which doesn't write the index).
96
+ * Resolve a slug (or agent_id) to { agentId, slug, name }. Order: cache index (seeded
97
+ * by a prior `search`) → fresh live search keyed on the de-kebabed slug. A cold
98
+ * `session start` with no prior search still resolves via the live search below.
116
99
  * agent_id is the only id needed — the ad_unit + price are resolved server-side.
117
100
  */
118
101
  export async function resolveCapability(slugOrId, opts) {
@@ -124,14 +107,6 @@ export async function resolveCapability(slugOrId, opts) {
124
107
  return { agentId: entry.agent_id, slug, name: entry.name || slug };
125
108
  }
126
109
 
127
- // Banked spec fallback — a breadth subagent stored the agent_id here via `cache put`.
128
- if (!opts.refresh) {
129
- const spec = cache.readSpec(slug, { ttlMs: Infinity });
130
- if (spec && spec.agent_id) {
131
- return { agentId: spec.agent_id, slug, name: spec.name || slug };
132
- }
133
- }
134
-
135
110
  // Fresh search keyed on the human form of the slug.
136
111
  const queryGuess = String(slugOrId).replace(/-/g, " ");
137
112
  const found = await cmdSearch(queryGuess, { ...opts, refresh: true, max: 25 });
@@ -260,18 +235,7 @@ export async function cmdSession(sub, positionals, opts) {
260
235
  return sessionEnd({ sessionId }, net);
261
236
  }
262
237
 
263
- if (sub === "pay") {
264
- const sessionId = positionals[0];
265
- if (!sessionId) {
266
- const e = new Error("usage: agentrelay session pay <session_id> [--credential <mpp_credential>]");
267
- e.usage = true;
268
- throw e;
269
- }
270
- // Server-side grant when no --credential; client-side MPP wallet when provided.
271
- return sessionPay({ sessionId, paymentCredential: opts.credential || null }, net);
272
- }
273
-
274
- const e = new Error(`unknown session subcommand "${sub}" (start|send|end|pay)`);
238
+ const e = new Error(`unknown session subcommand "${sub}" (start|send|end)`);
275
239
  e.usage = true;
276
240
  throw e;
277
241
  }
@@ -320,140 +284,3 @@ export async function cmdFeedback(sessionId, opts) {
320
284
  { apiKey: opts.apiKey, timeoutMs: opts.timeoutMs },
321
285
  );
322
286
  }
323
-
324
- /** Convert a dollar string/number to integer cents; throws a usage error on garbage. */
325
- export function dollarsToCents(label, value) {
326
- const n = Number(value);
327
- if (!Number.isFinite(n) || n < 0) {
328
- const e = new Error(`${label} must be a non-negative dollar amount (e.g. 5 or 2.50)`);
329
- e.usage = true;
330
- throw e;
331
- }
332
- return Math.round(n * 100);
333
- }
334
-
335
- /**
336
- * Read or update the account's server-canonical autonomous-spend policy.
337
- * No flags → GET (print current policy). Any flag → PATCH then return the new policy.
338
- * Dollar flags are converted to cents; the server is canonical (nothing stored locally).
339
- */
340
- export async function cmdBudget(opts) {
341
- const patch = {};
342
- if (opts.autoApprove != null) patch.auto_approve_under_cents = dollarsToCents("--auto-approve", opts.autoApprove);
343
- if (opts.dailyCap != null) patch.daily_cap_cents = dollarsToCents("--daily-cap", opts.dailyCap);
344
- if (opts.requireHumanOver != null) patch.require_human_over_cents = dollarsToCents("--require-human-over", opts.requireHumanOver);
345
- if (opts.perSessionCap != null) patch.per_session_cap_cents = dollarsToCents("--per-session-cap", opts.perSessionCap);
346
- if (opts.enable && opts.disable) {
347
- const e = new Error("pass only one of --enable / --disable");
348
- e.usage = true;
349
- throw e;
350
- }
351
- if (opts.enable) patch.autonomous_enabled = true;
352
- if (opts.disable) patch.autonomous_enabled = false;
353
-
354
- const policy = Object.keys(patch).length > 0
355
- ? await patchPaymentPolicy(patch, { apiKey: opts.apiKey, timeoutMs: opts.timeoutMs })
356
- : await getPaymentPolicy({ apiKey: opts.apiKey, timeoutMs: opts.timeoutMs });
357
- return { policy };
358
- }
359
-
360
- /**
361
- * Stub: real wallet connection is the (deferred) dashboard OAuth flow. The CLI can't
362
- * provision a wallet, so this just points the user at the dashboard.
363
- */
364
- export function cmdConnectWallet() {
365
- return {
366
- connected: false,
367
- message:
368
- "Wallet connection is set up from the AttentionMarket dashboard (OAuth flow). " +
369
- "The CLI can't provision a wallet yet. Once a wallet is connected, run " +
370
- "`agentrelay budget --enable --auto-approve=<$>` so the agent can auto-pay within your limits.",
371
- };
372
- }
373
-
374
- export function cmdCache(sub, slug, opts) {
375
- const cache = new Cache(opts.cacheDir, { enabled: true });
376
- if (sub === "ls" || !sub) {
377
- // Triage view read from the spec files themselves (race-safe under the
378
- // parallel breadth pass; includes each summary so the main agent can pick
379
- // what to `cache show` without opening every file).
380
- return {
381
- cache_dir: opts.cacheDir,
382
- specs: cache.listSpecHeadlines(),
383
- };
384
- }
385
- if (sub === "show") {
386
- if (!slug) {
387
- const e = new Error("usage: agentrelay cache show <slug>");
388
- e.usage = true;
389
- throw e;
390
- }
391
- const spec = cache.readSpec(slug, { ttlMs: Infinity });
392
- if (!spec) throw new Error(`no cached spec for "${slug}"`);
393
- return { deliverable: spec };
394
- }
395
- if (sub === "put") {
396
- // Persist a distilled result a subagent/main agent produced, so it can be
397
- // reviewed (`cache show`) or reused later. JSON from --file or stdin.
398
- if (!slug) {
399
- const e = new Error("usage: agentrelay cache put <slug> (--file f.json | JSON on stdin)");
400
- e.usage = true;
401
- throw e;
402
- }
403
- let raw;
404
- try {
405
- raw = opts.file ? readFileSync(opts.file, "utf-8") : readFileSync(0, "utf-8");
406
- } catch (err) {
407
- throw new Error(`could not read deliverable input: ${err.message}`);
408
- }
409
- let deliverable;
410
- try {
411
- deliverable = JSON.parse(raw);
412
- } catch (err) {
413
- const e = new Error(`deliverable must be valid JSON: ${err.message}`);
414
- e.usage = true;
415
- throw e;
416
- }
417
- if (!deliverable || typeof deliverable !== "object") {
418
- const e = new Error("deliverable must be a JSON object");
419
- e.usage = true;
420
- throw e;
421
- }
422
- deliverable.slug = deliverable.slug || slug;
423
- deliverable.ts = deliverable.ts || new Date().toISOString();
424
- // Soft contract check (schema/deliverable.json spine). Warn-only: a spec
425
- // with gaps is still worth banking, and concurrent breadth subagents must
426
- // never be blocked by validation.
427
- const warnings = [];
428
- if (!deliverable.session_id || !String(deliverable.session_id).trim()) {
429
- warnings.push("no session_id — build-outcome feedback can't be filed against this consult later");
430
- }
431
- if (!deliverable.summary || !String(deliverable.summary).trim()) {
432
- warnings.push("no summary — `cache ls` triage will show a blank line for this spec");
433
- }
434
- if (!Array.isArray(deliverable.features) || deliverable.features.length === 0) {
435
- warnings.push("no features[] — build-time has no surface map; depth sessions will have to rediscover it");
436
- } else {
437
- const missingStatus = deliverable.features.filter(
438
- (f) => !f || (f.status !== "coded" && f.status !== "surface_only"),
439
- ).length;
440
- if (missingStatus > 0) {
441
- warnings.push(
442
- `${missingStatus} feature(s) missing status ("coded"|"surface_only") — build-time can't tell banked code from surface-only`,
443
- );
444
- }
445
- }
446
- // Write ONLY the per-slug spec file — no shared index.json mutation — so N
447
- // breadth subagents can `cache put` concurrently without clobbering each
448
- // other. `cache ls` reads the spec files directly.
449
- cache.writeSpec(slug, deliverable);
450
- return { stored: true, slug, cache_dir: opts.cacheDir, warnings };
451
- }
452
- if (sub === "clear") {
453
- cache.clear();
454
- return { cleared: true, cache_dir: opts.cacheDir };
455
- }
456
- const e = new Error(`unknown cache subcommand "${sub}" (ls|show|put|clear)`);
457
- e.usage = true;
458
- throw e;
459
- }
@@ -58,7 +58,7 @@ export function resolveApiKey(flagKey) {
58
58
  return readKeyFile(CREDENTIALS_PATH) || readKeyFile(LEGACY_CREDENTIALS_PATH);
59
59
  }
60
60
 
61
- /** kebab-case a capability name into a stable slug used for cache + consult. */
61
+ /** kebab-case a capability name into a stable slug (search cache key + session resolution). */
62
62
  export function slugify(name) {
63
63
  return String(name || "")
64
64
  .toLowerCase()
@@ -1,60 +1,19 @@
1
1
  /**
2
- * System-environment probes shared by the installer and `agentrelay doctor`.
2
+ * System-environment probes used by the installer's global-CLI linking.
3
3
  *
4
4
  * These exist because the failure modes they detect are exactly the ones that
5
5
  * made "successful" installs produce `command not found` in the field:
6
- * - npx prepends its ephemeral cache .bin to PATH, shadowing (or faking) a
7
- * real global install for the lifetime of the npx process
8
6
  * - npm's global prefix may not be writable (system Node → EACCES)
9
7
  * - ~/.npm may contain root-owned files (old sudo npm) which kills BOTH
10
8
  * `npm install -g` and npx itself
11
9
  * - npm's global bin dir may not be on the user's PATH
12
10
  */
13
11
 
14
- import { existsSync, realpathSync, accessSync, statSync, constants } from "fs";
12
+ import { existsSync, accessSync, statSync, constants, readdirSync, rmSync } from "fs";
15
13
  import { homedir } from "os";
16
- import { join, sep, dirname } from "path";
14
+ import { join, dirname } from "path";
17
15
  import { execSync } from "child_process";
18
16
 
19
- /** Where a command actually resolves on this PATH, or null. */
20
- export function resolveCommandPath(cmd) {
21
- try {
22
- const probe = process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`;
23
- const out = execSync(probe, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
24
- return out.split("\n")[0].trim() || null;
25
- } catch {
26
- return null;
27
- }
28
- }
29
-
30
- export function realpathSafe(p) {
31
- try {
32
- return realpathSync(p);
33
- } catch {
34
- return p;
35
- }
36
- }
37
-
38
- /**
39
- * True when a resolved bin path is an ephemeral npx/npm-exec shim (or a copy
40
- * inside the running package itself), not a real install. A dangling symlink
41
- * also counts as not-a-usable-install.
42
- */
43
- export function isEphemeralBin(binPath, pkgRoot) {
44
- if (!binPath) return false;
45
- let real = binPath;
46
- try {
47
- real = realpathSync(binPath);
48
- } catch {
49
- return true;
50
- }
51
- return (
52
- real.includes(`${sep}_npx${sep}`) ||
53
- binPath.includes(`${sep}_npx${sep}`) ||
54
- (pkgRoot ? real.startsWith(realpathSafe(pkgRoot)) : false)
55
- );
56
- }
57
-
58
17
  /** npm's global prefix (e.g. /opt/homebrew, %AppData%\npm), or null. */
59
18
  export function npmGlobalPrefix() {
60
19
  try {
@@ -140,6 +99,32 @@ export function dirOnPath(dir, pathVar = process.env.PATH || "") {
140
99
  return pathVar.split(delim).map(norm).includes(norm(dir));
141
100
  }
142
101
 
102
+ /**
103
+ * Remove ONLY this package's entries from npm's npx cache, so the next
104
+ * `npx <pkg>` re-resolves from the registry instead of replaying a stale cached
105
+ * copy (npx reuses its cache for a bare name and won't auto-upgrade). Scoped to
106
+ * `<npm cache>/_npx/<hash>` dirs whose node_modules contains pkgName — other
107
+ * tools' npx caches are left alone. Best-effort; never throws.
108
+ */
109
+ export function clearNpxCacheFor(pkgName) {
110
+ try {
111
+ const cacheDir = execSync("npm config get cache", {
112
+ encoding: "utf-8",
113
+ stdio: ["ignore", "pipe", "ignore"],
114
+ }).trim();
115
+ if (!cacheDir) return;
116
+ const npxDir = join(cacheDir, "_npx");
117
+ if (!existsSync(npxDir)) return;
118
+ for (const entry of readdirSync(npxDir)) {
119
+ if (existsSync(join(npxDir, entry, "node_modules", pkgName))) {
120
+ try {
121
+ rmSync(join(npxDir, entry), { recursive: true, force: true });
122
+ } catch {}
123
+ }
124
+ }
125
+ } catch {}
126
+ }
127
+
143
128
  /** The official npm user-level-prefix recipe for EACCES on the global prefix. */
144
129
  export const USER_PREFIX_RECIPE = [
145
130
  "mkdir -p ~/.npm-global",
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * agentrelay (CLI core) — live agent sessions, payments, and deliverable fetch.
4
+ * agentrelay (CLI core) — live agent sessions and deliverable fetch.
5
5
  *
6
6
  * Wraps the AttentionMarket REST API so a coding agent can converse with expert
7
7
  * "solutions-engineer" capability agents. The caller (a host subagent, or the
8
- * main agent directly) drives the conversation via the session primitives and
9
- * banks compact specs in the on-disk cache; raw transcripts stay out of the
10
- * main context.
8
+ * main agent directly) drives the conversation via the session primitives; raw
9
+ * transcripts stay out of the main context.
11
10
  *
12
11
  * Commands:
13
12
  * agentrelay search <query> broad discovery (cheap; safe in main context)
14
- * agentrelay session start|send|pay|end drive a live consult
15
- * agentrelay cache ls|show|put|clear inspect/manage the on-disk spec cache
13
+ * agentrelay session start|send|end drive a live consult
14
+ * agentrelay feedback <id> rate a session
16
15
  * agentrelay fetch download + verify + unpack a paid deliverable
17
16
  *
18
17
  * Output: a `{ ok, ...payload, errors: [] }` envelope. --json forces machine
@@ -29,13 +28,9 @@ import {
29
28
  cmdSearch,
30
29
  cmdSession,
31
30
  cmdFeedback,
32
- cmdCache,
33
- cmdBudget,
34
- cmdConnectWallet,
35
31
  cmdFetchSession,
36
32
  } from "./lib/commands.mjs";
37
33
  import { cmdFetch } from "./lib/fetch.mjs";
38
- import { cmdDoctor, fmtDoctor } from "./lib/doctor.mjs";
39
34
  import { maybeNotifyUpdate } from "./lib/update-check.mjs";
40
35
 
41
36
  const EXIT = { OK: 0, PARTIAL: 1, USAGE: 2, AUTH: 3 };
@@ -54,7 +49,6 @@ const FLAGS_WITH_VALUE = new Set([
54
49
  "--cache-dir",
55
50
  "--timeout",
56
51
  "--max",
57
- "--user-context",
58
52
  "--initial",
59
53
  "--task-context",
60
54
  "--message",
@@ -62,11 +56,6 @@ const FLAGS_WITH_VALUE = new Set([
62
56
  "--score",
63
57
  "--text",
64
58
  "--file",
65
- "--auto-approve",
66
- "--daily-cap",
67
- "--require-human-over",
68
- "--per-session-cap",
69
- "--credential",
70
59
  "--checksum",
71
60
  "--filename",
72
61
  "--session",
@@ -155,54 +144,19 @@ const fmtSearch = (e) => {
155
144
  lines.push(` offers paid deliverable${dp}`);
156
145
  }
157
146
  }
158
- // Surface the spend policy in human output too — agents reading --json get
159
- // the full payment_context object; humans shouldn't be blind to it.
160
- const p = e.payment_context;
161
- if (p) {
162
- const dol = (c) => (c == null ? "—" : `$${(c / 100).toFixed(2)}`);
163
- lines.push(
164
- `pay policy: autonomous ${p.autonomous_enabled ? "ON" : "off"}, auto-approve under ${dol(p.auto_approve_under_cents)},` +
165
- ` daily cap left ${dol(p.daily_cap_remaining_cents)}, wallet: ${p.wallet?.methods?.length ? p.wallet.methods.join(",") : "none"}` +
166
- `${e.from_cache ? " (cached snapshot — `agentrelay budget` for live)" : ""}`,
167
- );
168
- }
169
147
  return lines.join("\n");
170
148
  };
171
149
 
172
- const fmtBudget = (e) => {
173
- const p = e.policy || {};
174
- const dol = (c) => (c == null ? "—" : `$${(c / 100).toFixed(2)}`);
175
- return [
176
- `autonomous pay: ${p.autonomous_enabled ? "ENABLED" : "disabled (ask every time)"}`,
177
- ` auto-approve under: ${dol(p.auto_approve_under_cents)}`,
178
- ` require human over: ${dol(p.require_human_over_cents)}`,
179
- ` daily cap: ${dol(p.daily_cap_cents)}`,
180
- ` per-session cap: ${dol(p.per_session_cap_cents)}`,
181
- ` wallet methods: ${(p.wallet_methods && p.wallet_methods.length) ? p.wallet_methods.join(", ") : "none connected"}`,
182
- ].join("\n");
183
- };
184
-
185
150
  const HELP = `agentrelay — converse with capability agents while you build.
186
151
 
187
152
  Conversation (LLM-driven — drive these from a subagent, or directly when you're stuck):
188
- agentrelay session start <slug> [--initial json] [--message "<first msg>"]
153
+ agentrelay session start <slug> [--initial json] [--task-context <s>] [--message "<m>"]
189
154
  agentrelay session send <id> "<m>" [--data json]
190
- agentrelay session pay <id> [--credential <mpp>] settle a payment_required
191
155
  agentrelay session end <id>
192
156
  agentrelay feedback <id> --score <0-100> --text "<s>"
193
157
 
194
- Discovery + cache:
195
- agentrelay search <query> [--max 25] [--user-context <s>] [--dynamic-only]
196
- agentrelay cache ls | show <slug> | put <slug> [--file f.json] | clear
197
-
198
- Payments (autonomous spend policy — server-canonical):
199
- agentrelay budget show current policy
200
- agentrelay budget --enable|--disable [--auto-approve=<$>] [--daily-cap=<$>]
201
- [--require-human-over=<$>] [--per-session-cap=<$>]
202
- agentrelay connect-wallet (wallet connect is set up in the dashboard)
203
-
204
- Diagnostics:
205
- agentrelay doctor check node/key/API/npm-prefix/PATH health (--json)
158
+ Discovery:
159
+ agentrelay search <query> [--max 25]
206
160
 
207
161
  Deliverables (after a paid session returns a delivery object):
208
162
  agentrelay fetch --session <id> [--message "<m>"] [dest] ONE-STEP after payment:
@@ -226,14 +180,6 @@ async function main() {
226
180
  process.stdout.write(HELP + "\n");
227
181
  process.exit(cmd ? EXIT.OK : EXIT.USAGE);
228
182
  }
229
- if (cmd === "consult" || cmd === "consult-batch") {
230
- return fail(
231
- `"${cmd}" was removed in v4 — drive a live session instead: ` +
232
- "`agentrelay session start <slug>` → `session send` → `session end` (see SKILL.md)",
233
- EXIT.USAGE,
234
- Boolean(flags.json) || !process.stdout.isTTY,
235
- );
236
- }
237
183
  if (cmd === "version" || flags.version) {
238
184
  process.stdout.write(`agentrelay ${packageVersion()}\n`);
239
185
  process.exit(EXIT.OK);
@@ -254,9 +200,12 @@ async function main() {
254
200
  }
255
201
  const json = shared.json;
256
202
 
257
- // cache, raw-URL fetch, and doctor don't need a key (doctor resolves and
258
- // reports on the key itself). `fetch --session` calls the session API.
259
- const keylessCmd = cmd === "cache" || cmd === "doctor" || (cmd === "fetch" && !flags.session);
203
+ // Raw-URL fetch downloads a signed URL directly and needs no key. Everything
204
+ // else (including `fetch --session`, which calls the session API) does. An
205
+ // UNKNOWN command needs no key either it should get the clean "unknown
206
+ // command" error below, not a misleading "no API key" one.
207
+ const KNOWN_COMMANDS = new Set(["search", "session", "feedback", "fetch"]);
208
+ const keylessCmd = (cmd === "fetch" && !flags.session) || !KNOWN_COMMANDS.has(cmd);
260
209
  if (!keylessCmd && !shared.apiKey) {
261
210
  return fail(
262
211
  "no API key — set --api-key, $AGENT_RELAY_API_KEY, or ~/.config/agent-relay/credentials.json",
@@ -273,8 +222,6 @@ async function main() {
273
222
  const payload = await cmdSearch(query, {
274
223
  ...shared,
275
224
  max: Number(flags.max) || 25,
276
- userContext: flags["user-context"] || null,
277
- dynamicOnly: Boolean(flags["dynamic-only"]),
278
225
  });
279
226
  return emit(payload, { json, human: fmtSearch });
280
227
  }
@@ -286,7 +233,6 @@ async function main() {
286
233
  taskContext: flags["task-context"] || null,
287
234
  message: flags.message || null,
288
235
  data: flags.data || null,
289
- credential: flags.credential || null,
290
236
  });
291
237
  return emit(payload, { json });
292
238
  }
@@ -300,24 +246,6 @@ async function main() {
300
246
  return emit(payload, { json });
301
247
  }
302
248
 
303
- case "budget": {
304
- const payload = await cmdBudget({
305
- ...shared,
306
- autoApprove: flags["auto-approve"] != null ? flags["auto-approve"] : null,
307
- dailyCap: flags["daily-cap"] != null ? flags["daily-cap"] : null,
308
- requireHumanOver: flags["require-human-over"] != null ? flags["require-human-over"] : null,
309
- perSessionCap: flags["per-session-cap"] != null ? flags["per-session-cap"] : null,
310
- enable: Boolean(flags.enable),
311
- disable: Boolean(flags.disable),
312
- });
313
- return emit(payload, { json, human: fmtBudget });
314
- }
315
-
316
- case "connect-wallet": {
317
- const payload = cmdConnectWallet();
318
- return emit(payload, { json, human: (e) => e.message });
319
- }
320
-
321
249
  case "fetch": {
322
250
  // --session: one-step deliverable retrieval for a PAID session — drive a turn
323
251
  // to mint a fresh signed download_url, then download+unpack. Needs the key.
@@ -353,28 +281,9 @@ async function main() {
353
281
  });
354
282
  }
355
283
 
356
- case "doctor": {
357
- const payload = await cmdDoctor({ ...shared, apiKey: flags["api-key"] || null });
358
- const exitCode = payload.exit_code;
359
- return emit(payload, { json, ok: payload.doctor_ok, exitCode, human: fmtDoctor });
360
- }
361
-
362
- case "cache": {
363
- const payload = cmdCache(positionals[0], positionals[1], { ...shared, file: flags.file || null });
364
- const human = Array.isArray(payload.specs)
365
- ? (e) =>
366
- [`${e.specs.length} banked specs (${e.cache_dir})`, ...e.specs.map((s) => {
367
- const feat = s.features != null ? ` (${s.features} features)` : "";
368
- const sum = s.summary ? ` — ${s.summary.length > 100 ? s.summary.slice(0, 100) + "…" : s.summary}` : "";
369
- return ` ${s.slug}${feat}${sum}`;
370
- })].join("\n")
371
- : undefined;
372
- return emit(payload, { json, human });
373
- }
374
-
375
284
  default:
376
285
  return fail(
377
- `unknown command "${cmd}" (session|search|feedback|cache|budget|connect-wallet|fetch|doctor)`,
286
+ `unknown command "${cmd}" (search|session|feedback|fetch)`,
378
287
  EXIT.USAGE,
379
288
  json,
380
289
  );
@@ -1,15 +1,10 @@
1
1
  /**
2
- * Tests for the session-primitive output shaping and the cache put/show
3
- * round-trip (the LLM-driven path's transport). No network. Run:
4
- * `node --test bin/test/session.test.mjs`.
2
+ * Tests for the session-primitive output shaping (the LLM-driven path's
3
+ * transport). No network. Run: `node --test bin/test/session.test.mjs`.
5
4
  */
6
5
  import { test } from "node:test";
7
6
  import assert from "node:assert/strict";
8
- import { mkdtempSync, rmSync } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
7
  import { shapeSessionTurn, deliveryToFetchArgs } from "../lib/commands.mjs";
12
- import { Cache } from "../lib/cache.mjs";
13
8
  import { slugify } from "../lib/config.mjs";
14
9
 
15
10
  test("shapeSessionTurn: passes the agent message through verbatim + surfaces flags", () => {
@@ -81,27 +76,7 @@ test("deliveryToFetchArgs: maps a delivery object to cmdFetch args (tolerant of
81
76
  assert.equal(deliveryToFetchArgs(null), null);
82
77
  });
83
78
 
84
- test("cache: put/show round-trip preserves an arbitrary LLM-chosen structure", () => {
85
- const dir = mkdtempSync(join(tmpdir(), "agent-relay-cache-"));
86
- try {
87
- const cache = new Cache(dir);
88
- // A non-dev shape an agent's distillation might use — must survive verbatim.
89
- const distilled = {
90
- summary: "Quote obtained",
91
- quote_usd: 1240,
92
- coverages: ["liability", "collision"],
93
- next_steps: ["sign", "pay deposit"],
94
- };
95
- cache.writeSpec("acme-insurance", distilled);
96
- const got = cache.readSpec("acme-insurance", { ttlMs: Infinity });
97
- assert.deepEqual(got, distilled);
98
- assert.deepEqual(cache.listSpecs(), ["acme-insurance"]);
99
- } finally {
100
- rmSync(dir, { recursive: true, force: true });
101
- }
102
- });
103
-
104
- test("slugify: stable kebab-case keys for cache/consult", () => {
79
+ test("slugify: stable kebab-case keys for slugs", () => {
105
80
  assert.equal(slugify("Open-Meteo Weather API Solutions Engineer"), "open-meteo-weather-api-solutions-engineer");
106
81
  assert.equal(slugify("Acme Insurance ✨ (Auto)"), "acme-insurance-auto");
107
82
  assert.equal(slugify(""), "capability");
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tests for the pure sysenv probes used by the installer's global-CLI linking.
3
+ * No network, no real npm install. Run: `node --test bin/test/sysenv.test.mjs`.
4
+ */
5
+ import { test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { dirOnPath, npmGlobalBinDir, globalBinShimPath } from "../lib/sysenv.mjs";
8
+
9
+ test("dirOnPath: exact component match, trailing-slash tolerant", () => {
10
+ const delim = process.platform === "win32" ? ";" : ":";
11
+ const pathVar = ["/usr/bin", "/opt/homebrew/bin/"].join(delim);
12
+ assert.equal(dirOnPath("/opt/homebrew/bin", pathVar), true);
13
+ assert.equal(dirOnPath("/usr/bin", pathVar), true);
14
+ assert.equal(dirOnPath("/usr/local/bin", pathVar), false);
15
+ assert.equal(dirOnPath(null, pathVar), false);
16
+ });
17
+
18
+ test("npmGlobalBinDir/globalBinShimPath shape per platform", () => {
19
+ const prefix = process.platform === "win32" ? "C:\\npm" : "/opt/homebrew";
20
+ const binDir = npmGlobalBinDir(prefix);
21
+ const shim = globalBinShimPath(prefix, "agentrelay");
22
+ if (process.platform === "win32") {
23
+ assert.equal(binDir, prefix);
24
+ assert.ok(shim.endsWith("agentrelay.cmd"));
25
+ } else {
26
+ assert.equal(binDir, `${prefix}/bin`);
27
+ assert.ok(shim.endsWith("/bin/agentrelay"));
28
+ }
29
+ assert.equal(npmGlobalBinDir(null), null);
30
+ });