agentic-relay 4.3.0 → 5.0.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 +14 -14
- package/bin/cli.mjs +15 -13
- package/bin/install.mjs +81 -52
- package/bin/lib/api.mjs +5 -76
- package/bin/lib/cache.mjs +7 -74
- package/bin/lib/commands.mjs +14 -183
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/sysenv.mjs +3 -44
- package/bin/relay-core.mjs +17 -106
- package/bin/test/session.test.mjs +3 -28
- package/bin/test/sysenv.test.mjs +30 -0
- package/package.json +2 -3
- package/skill/SKILL.md +225 -165
- package/bin/lib/doctor.mjs +0 -222
- package/bin/test/budget.test.mjs +0 -31
- package/bin/test/cache.test.mjs +0 -111
- package/bin/test/doctor.test.mjs +0 -76
- package/schema/deliverable.json +0 -45
package/bin/lib/commands.mjs
CHANGED
|
@@ -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|
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
//
|
|
64
|
-
//
|
|
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
|
|
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
|
-
*
|
|
114
|
-
*
|
|
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 });
|
|
@@ -211,6 +186,10 @@ export async function cmdSession(sub, positionals, opts) {
|
|
|
211
186
|
const start = await sessionStart(
|
|
212
187
|
{
|
|
213
188
|
agentId: resolved.agentId,
|
|
189
|
+
// A2A current-task brief → forwarded to /session/start → /initiate, where it's
|
|
190
|
+
// stored on capability_sessions.task_context and rendered as the business agent's
|
|
191
|
+
// "## Current task" prompt block. Optional on the CLI path (server stores null when absent).
|
|
192
|
+
taskContext: opts.taskContext || null,
|
|
214
193
|
initialData: parseJsonFlag(opts.initial, "initial"),
|
|
215
194
|
},
|
|
216
195
|
net,
|
|
@@ -256,18 +235,7 @@ export async function cmdSession(sub, positionals, opts) {
|
|
|
256
235
|
return sessionEnd({ sessionId }, net);
|
|
257
236
|
}
|
|
258
237
|
|
|
259
|
-
|
|
260
|
-
const sessionId = positionals[0];
|
|
261
|
-
if (!sessionId) {
|
|
262
|
-
const e = new Error("usage: agentrelay session pay <session_id> [--credential <mpp_credential>]");
|
|
263
|
-
e.usage = true;
|
|
264
|
-
throw e;
|
|
265
|
-
}
|
|
266
|
-
// Server-side grant when no --credential; client-side MPP wallet when provided.
|
|
267
|
-
return sessionPay({ sessionId, paymentCredential: opts.credential || null }, net);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const e = new Error(`unknown session subcommand "${sub}" (start|send|end|pay)`);
|
|
238
|
+
const e = new Error(`unknown session subcommand "${sub}" (start|send|end)`);
|
|
271
239
|
e.usage = true;
|
|
272
240
|
throw e;
|
|
273
241
|
}
|
|
@@ -316,140 +284,3 @@ export async function cmdFeedback(sessionId, opts) {
|
|
|
316
284
|
{ apiKey: opts.apiKey, timeoutMs: opts.timeoutMs },
|
|
317
285
|
);
|
|
318
286
|
}
|
|
319
|
-
|
|
320
|
-
/** Convert a dollar string/number to integer cents; throws a usage error on garbage. */
|
|
321
|
-
export function dollarsToCents(label, value) {
|
|
322
|
-
const n = Number(value);
|
|
323
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
324
|
-
const e = new Error(`${label} must be a non-negative dollar amount (e.g. 5 or 2.50)`);
|
|
325
|
-
e.usage = true;
|
|
326
|
-
throw e;
|
|
327
|
-
}
|
|
328
|
-
return Math.round(n * 100);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Read or update the account's server-canonical autonomous-spend policy.
|
|
333
|
-
* No flags → GET (print current policy). Any flag → PATCH then return the new policy.
|
|
334
|
-
* Dollar flags are converted to cents; the server is canonical (nothing stored locally).
|
|
335
|
-
*/
|
|
336
|
-
export async function cmdBudget(opts) {
|
|
337
|
-
const patch = {};
|
|
338
|
-
if (opts.autoApprove != null) patch.auto_approve_under_cents = dollarsToCents("--auto-approve", opts.autoApprove);
|
|
339
|
-
if (opts.dailyCap != null) patch.daily_cap_cents = dollarsToCents("--daily-cap", opts.dailyCap);
|
|
340
|
-
if (opts.requireHumanOver != null) patch.require_human_over_cents = dollarsToCents("--require-human-over", opts.requireHumanOver);
|
|
341
|
-
if (opts.perSessionCap != null) patch.per_session_cap_cents = dollarsToCents("--per-session-cap", opts.perSessionCap);
|
|
342
|
-
if (opts.enable && opts.disable) {
|
|
343
|
-
const e = new Error("pass only one of --enable / --disable");
|
|
344
|
-
e.usage = true;
|
|
345
|
-
throw e;
|
|
346
|
-
}
|
|
347
|
-
if (opts.enable) patch.autonomous_enabled = true;
|
|
348
|
-
if (opts.disable) patch.autonomous_enabled = false;
|
|
349
|
-
|
|
350
|
-
const policy = Object.keys(patch).length > 0
|
|
351
|
-
? await patchPaymentPolicy(patch, { apiKey: opts.apiKey, timeoutMs: opts.timeoutMs })
|
|
352
|
-
: await getPaymentPolicy({ apiKey: opts.apiKey, timeoutMs: opts.timeoutMs });
|
|
353
|
-
return { policy };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Stub: real wallet connection is the (deferred) dashboard OAuth flow. The CLI can't
|
|
358
|
-
* provision a wallet, so this just points the user at the dashboard.
|
|
359
|
-
*/
|
|
360
|
-
export function cmdConnectWallet() {
|
|
361
|
-
return {
|
|
362
|
-
connected: false,
|
|
363
|
-
message:
|
|
364
|
-
"Wallet connection is set up from the AttentionMarket dashboard (OAuth flow). " +
|
|
365
|
-
"The CLI can't provision a wallet yet. Once a wallet is connected, run " +
|
|
366
|
-
"`agentrelay budget --enable --auto-approve=<$>` so the agent can auto-pay within your limits.",
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export function cmdCache(sub, slug, opts) {
|
|
371
|
-
const cache = new Cache(opts.cacheDir, { enabled: true });
|
|
372
|
-
if (sub === "ls" || !sub) {
|
|
373
|
-
// Triage view read from the spec files themselves (race-safe under the
|
|
374
|
-
// parallel breadth pass; includes each summary so the main agent can pick
|
|
375
|
-
// what to `cache show` without opening every file).
|
|
376
|
-
return {
|
|
377
|
-
cache_dir: opts.cacheDir,
|
|
378
|
-
specs: cache.listSpecHeadlines(),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
if (sub === "show") {
|
|
382
|
-
if (!slug) {
|
|
383
|
-
const e = new Error("usage: agentrelay cache show <slug>");
|
|
384
|
-
e.usage = true;
|
|
385
|
-
throw e;
|
|
386
|
-
}
|
|
387
|
-
const spec = cache.readSpec(slug, { ttlMs: Infinity });
|
|
388
|
-
if (!spec) throw new Error(`no cached spec for "${slug}"`);
|
|
389
|
-
return { deliverable: spec };
|
|
390
|
-
}
|
|
391
|
-
if (sub === "put") {
|
|
392
|
-
// Persist a distilled result a subagent/main agent produced, so it can be
|
|
393
|
-
// reviewed (`cache show`) or reused later. JSON from --file or stdin.
|
|
394
|
-
if (!slug) {
|
|
395
|
-
const e = new Error("usage: agentrelay cache put <slug> (--file f.json | JSON on stdin)");
|
|
396
|
-
e.usage = true;
|
|
397
|
-
throw e;
|
|
398
|
-
}
|
|
399
|
-
let raw;
|
|
400
|
-
try {
|
|
401
|
-
raw = opts.file ? readFileSync(opts.file, "utf-8") : readFileSync(0, "utf-8");
|
|
402
|
-
} catch (err) {
|
|
403
|
-
throw new Error(`could not read deliverable input: ${err.message}`);
|
|
404
|
-
}
|
|
405
|
-
let deliverable;
|
|
406
|
-
try {
|
|
407
|
-
deliverable = JSON.parse(raw);
|
|
408
|
-
} catch (err) {
|
|
409
|
-
const e = new Error(`deliverable must be valid JSON: ${err.message}`);
|
|
410
|
-
e.usage = true;
|
|
411
|
-
throw e;
|
|
412
|
-
}
|
|
413
|
-
if (!deliverable || typeof deliverable !== "object") {
|
|
414
|
-
const e = new Error("deliverable must be a JSON object");
|
|
415
|
-
e.usage = true;
|
|
416
|
-
throw e;
|
|
417
|
-
}
|
|
418
|
-
deliverable.slug = deliverable.slug || slug;
|
|
419
|
-
deliverable.ts = deliverable.ts || new Date().toISOString();
|
|
420
|
-
// Soft contract check (schema/deliverable.json spine). Warn-only: a spec
|
|
421
|
-
// with gaps is still worth banking, and concurrent breadth subagents must
|
|
422
|
-
// never be blocked by validation.
|
|
423
|
-
const warnings = [];
|
|
424
|
-
if (!deliverable.session_id || !String(deliverable.session_id).trim()) {
|
|
425
|
-
warnings.push("no session_id — build-outcome feedback can't be filed against this consult later");
|
|
426
|
-
}
|
|
427
|
-
if (!deliverable.summary || !String(deliverable.summary).trim()) {
|
|
428
|
-
warnings.push("no summary — `cache ls` triage will show a blank line for this spec");
|
|
429
|
-
}
|
|
430
|
-
if (!Array.isArray(deliverable.features) || deliverable.features.length === 0) {
|
|
431
|
-
warnings.push("no features[] — build-time has no surface map; depth sessions will have to rediscover it");
|
|
432
|
-
} else {
|
|
433
|
-
const missingStatus = deliverable.features.filter(
|
|
434
|
-
(f) => !f || (f.status !== "coded" && f.status !== "surface_only"),
|
|
435
|
-
).length;
|
|
436
|
-
if (missingStatus > 0) {
|
|
437
|
-
warnings.push(
|
|
438
|
-
`${missingStatus} feature(s) missing status ("coded"|"surface_only") — build-time can't tell banked code from surface-only`,
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Write ONLY the per-slug spec file — no shared index.json mutation — so N
|
|
443
|
-
// breadth subagents can `cache put` concurrently without clobbering each
|
|
444
|
-
// other. `cache ls` reads the spec files directly.
|
|
445
|
-
cache.writeSpec(slug, deliverable);
|
|
446
|
-
return { stored: true, slug, cache_dir: opts.cacheDir, warnings };
|
|
447
|
-
}
|
|
448
|
-
if (sub === "clear") {
|
|
449
|
-
cache.clear();
|
|
450
|
-
return { cleared: true, cache_dir: opts.cacheDir };
|
|
451
|
-
}
|
|
452
|
-
const e = new Error(`unknown cache subcommand "${sub}" (ls|show|put|clear)`);
|
|
453
|
-
e.usage = true;
|
|
454
|
-
throw e;
|
|
455
|
-
}
|
package/bin/lib/config.mjs
CHANGED
|
@@ -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
|
|
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()
|
package/bin/lib/sysenv.mjs
CHANGED
|
@@ -1,60 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* System-environment probes
|
|
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,
|
|
12
|
+
import { existsSync, accessSync, statSync, constants } from "fs";
|
|
15
13
|
import { homedir } from "os";
|
|
16
|
-
import { join,
|
|
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 {
|
package/bin/relay-core.mjs
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* agentrelay (CLI core) — live agent sessions
|
|
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
|
|
9
|
-
*
|
|
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|
|
|
15
|
-
* agentrelay
|
|
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,18 +49,13 @@ const FLAGS_WITH_VALUE = new Set([
|
|
|
54
49
|
"--cache-dir",
|
|
55
50
|
"--timeout",
|
|
56
51
|
"--max",
|
|
57
|
-
"--user-context",
|
|
58
52
|
"--initial",
|
|
53
|
+
"--task-context",
|
|
59
54
|
"--message",
|
|
60
55
|
"--data",
|
|
61
56
|
"--score",
|
|
62
57
|
"--text",
|
|
63
58
|
"--file",
|
|
64
|
-
"--auto-approve",
|
|
65
|
-
"--daily-cap",
|
|
66
|
-
"--require-human-over",
|
|
67
|
-
"--per-session-cap",
|
|
68
|
-
"--credential",
|
|
69
59
|
"--checksum",
|
|
70
60
|
"--filename",
|
|
71
61
|
"--session",
|
|
@@ -154,54 +144,19 @@ const fmtSearch = (e) => {
|
|
|
154
144
|
lines.push(` offers paid deliverable${dp}`);
|
|
155
145
|
}
|
|
156
146
|
}
|
|
157
|
-
// Surface the spend policy in human output too — agents reading --json get
|
|
158
|
-
// the full payment_context object; humans shouldn't be blind to it.
|
|
159
|
-
const p = e.payment_context;
|
|
160
|
-
if (p) {
|
|
161
|
-
const dol = (c) => (c == null ? "—" : `$${(c / 100).toFixed(2)}`);
|
|
162
|
-
lines.push(
|
|
163
|
-
`pay policy: autonomous ${p.autonomous_enabled ? "ON" : "off"}, auto-approve under ${dol(p.auto_approve_under_cents)},` +
|
|
164
|
-
` daily cap left ${dol(p.daily_cap_remaining_cents)}, wallet: ${p.wallet?.methods?.length ? p.wallet.methods.join(",") : "none"}` +
|
|
165
|
-
`${e.from_cache ? " (cached snapshot — `agentrelay budget` for live)" : ""}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
147
|
return lines.join("\n");
|
|
169
148
|
};
|
|
170
149
|
|
|
171
|
-
const fmtBudget = (e) => {
|
|
172
|
-
const p = e.policy || {};
|
|
173
|
-
const dol = (c) => (c == null ? "—" : `$${(c / 100).toFixed(2)}`);
|
|
174
|
-
return [
|
|
175
|
-
`autonomous pay: ${p.autonomous_enabled ? "ENABLED" : "disabled (ask every time)"}`,
|
|
176
|
-
` auto-approve under: ${dol(p.auto_approve_under_cents)}`,
|
|
177
|
-
` require human over: ${dol(p.require_human_over_cents)}`,
|
|
178
|
-
` daily cap: ${dol(p.daily_cap_cents)}`,
|
|
179
|
-
` per-session cap: ${dol(p.per_session_cap_cents)}`,
|
|
180
|
-
` wallet methods: ${(p.wallet_methods && p.wallet_methods.length) ? p.wallet_methods.join(", ") : "none connected"}`,
|
|
181
|
-
].join("\n");
|
|
182
|
-
};
|
|
183
|
-
|
|
184
150
|
const HELP = `agentrelay — converse with capability agents while you build.
|
|
185
151
|
|
|
186
152
|
Conversation (LLM-driven — drive these from a subagent, or directly when you're stuck):
|
|
187
|
-
agentrelay session start <slug> [--initial json] [--message "<
|
|
153
|
+
agentrelay session start <slug> [--initial json] [--task-context <s>] [--message "<m>"]
|
|
188
154
|
agentrelay session send <id> "<m>" [--data json]
|
|
189
|
-
agentrelay session pay <id> [--credential <mpp>] settle a payment_required
|
|
190
155
|
agentrelay session end <id>
|
|
191
156
|
agentrelay feedback <id> --score <0-100> --text "<s>"
|
|
192
157
|
|
|
193
|
-
Discovery
|
|
194
|
-
agentrelay search <query> [--max 25]
|
|
195
|
-
agentrelay cache ls | show <slug> | put <slug> [--file f.json] | clear
|
|
196
|
-
|
|
197
|
-
Payments (autonomous spend policy — server-canonical):
|
|
198
|
-
agentrelay budget show current policy
|
|
199
|
-
agentrelay budget --enable|--disable [--auto-approve=<$>] [--daily-cap=<$>]
|
|
200
|
-
[--require-human-over=<$>] [--per-session-cap=<$>]
|
|
201
|
-
agentrelay connect-wallet (wallet connect is set up in the dashboard)
|
|
202
|
-
|
|
203
|
-
Diagnostics:
|
|
204
|
-
agentrelay doctor check node/key/API/npm-prefix/PATH health (--json)
|
|
158
|
+
Discovery:
|
|
159
|
+
agentrelay search <query> [--max 25]
|
|
205
160
|
|
|
206
161
|
Deliverables (after a paid session returns a delivery object):
|
|
207
162
|
agentrelay fetch --session <id> [--message "<m>"] [dest] ONE-STEP after payment:
|
|
@@ -225,14 +180,6 @@ async function main() {
|
|
|
225
180
|
process.stdout.write(HELP + "\n");
|
|
226
181
|
process.exit(cmd ? EXIT.OK : EXIT.USAGE);
|
|
227
182
|
}
|
|
228
|
-
if (cmd === "consult" || cmd === "consult-batch") {
|
|
229
|
-
return fail(
|
|
230
|
-
`"${cmd}" was removed in v4 — drive a live session instead: ` +
|
|
231
|
-
"`agentrelay session start <slug>` → `session send` → `session end` (see SKILL.md)",
|
|
232
|
-
EXIT.USAGE,
|
|
233
|
-
Boolean(flags.json) || !process.stdout.isTTY,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
183
|
if (cmd === "version" || flags.version) {
|
|
237
184
|
process.stdout.write(`agentrelay ${packageVersion()}\n`);
|
|
238
185
|
process.exit(EXIT.OK);
|
|
@@ -253,9 +200,12 @@ async function main() {
|
|
|
253
200
|
}
|
|
254
201
|
const json = shared.json;
|
|
255
202
|
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
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);
|
|
259
209
|
if (!keylessCmd && !shared.apiKey) {
|
|
260
210
|
return fail(
|
|
261
211
|
"no API key — set --api-key, $AGENT_RELAY_API_KEY, or ~/.config/agent-relay/credentials.json",
|
|
@@ -272,8 +222,6 @@ async function main() {
|
|
|
272
222
|
const payload = await cmdSearch(query, {
|
|
273
223
|
...shared,
|
|
274
224
|
max: Number(flags.max) || 25,
|
|
275
|
-
userContext: flags["user-context"] || null,
|
|
276
|
-
dynamicOnly: Boolean(flags["dynamic-only"]),
|
|
277
225
|
});
|
|
278
226
|
return emit(payload, { json, human: fmtSearch });
|
|
279
227
|
}
|
|
@@ -282,9 +230,9 @@ async function main() {
|
|
|
282
230
|
const payload = await cmdSession(positionals[0], positionals.slice(1), {
|
|
283
231
|
...shared,
|
|
284
232
|
initial: flags.initial || null,
|
|
233
|
+
taskContext: flags["task-context"] || null,
|
|
285
234
|
message: flags.message || null,
|
|
286
235
|
data: flags.data || null,
|
|
287
|
-
credential: flags.credential || null,
|
|
288
236
|
});
|
|
289
237
|
return emit(payload, { json });
|
|
290
238
|
}
|
|
@@ -298,24 +246,6 @@ async function main() {
|
|
|
298
246
|
return emit(payload, { json });
|
|
299
247
|
}
|
|
300
248
|
|
|
301
|
-
case "budget": {
|
|
302
|
-
const payload = await cmdBudget({
|
|
303
|
-
...shared,
|
|
304
|
-
autoApprove: flags["auto-approve"] != null ? flags["auto-approve"] : null,
|
|
305
|
-
dailyCap: flags["daily-cap"] != null ? flags["daily-cap"] : null,
|
|
306
|
-
requireHumanOver: flags["require-human-over"] != null ? flags["require-human-over"] : null,
|
|
307
|
-
perSessionCap: flags["per-session-cap"] != null ? flags["per-session-cap"] : null,
|
|
308
|
-
enable: Boolean(flags.enable),
|
|
309
|
-
disable: Boolean(flags.disable),
|
|
310
|
-
});
|
|
311
|
-
return emit(payload, { json, human: fmtBudget });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
case "connect-wallet": {
|
|
315
|
-
const payload = cmdConnectWallet();
|
|
316
|
-
return emit(payload, { json, human: (e) => e.message });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
249
|
case "fetch": {
|
|
320
250
|
// --session: one-step deliverable retrieval for a PAID session — drive a turn
|
|
321
251
|
// to mint a fresh signed download_url, then download+unpack. Needs the key.
|
|
@@ -351,28 +281,9 @@ async function main() {
|
|
|
351
281
|
});
|
|
352
282
|
}
|
|
353
283
|
|
|
354
|
-
case "doctor": {
|
|
355
|
-
const payload = await cmdDoctor({ ...shared, apiKey: flags["api-key"] || null });
|
|
356
|
-
const exitCode = payload.exit_code;
|
|
357
|
-
return emit(payload, { json, ok: payload.doctor_ok, exitCode, human: fmtDoctor });
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
case "cache": {
|
|
361
|
-
const payload = cmdCache(positionals[0], positionals[1], { ...shared, file: flags.file || null });
|
|
362
|
-
const human = Array.isArray(payload.specs)
|
|
363
|
-
? (e) =>
|
|
364
|
-
[`${e.specs.length} banked specs (${e.cache_dir})`, ...e.specs.map((s) => {
|
|
365
|
-
const feat = s.features != null ? ` (${s.features} features)` : "";
|
|
366
|
-
const sum = s.summary ? ` — ${s.summary.length > 100 ? s.summary.slice(0, 100) + "…" : s.summary}` : "";
|
|
367
|
-
return ` ${s.slug}${feat}${sum}`;
|
|
368
|
-
})].join("\n")
|
|
369
|
-
: undefined;
|
|
370
|
-
return emit(payload, { json, human });
|
|
371
|
-
}
|
|
372
|
-
|
|
373
284
|
default:
|
|
374
285
|
return fail(
|
|
375
|
-
`unknown command "${cmd}" (session|
|
|
286
|
+
`unknown command "${cmd}" (search|session|feedback|fetch)`,
|
|
376
287
|
EXIT.USAGE,
|
|
377
288
|
json,
|
|
378
289
|
);
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the session-primitive output shaping
|
|
3
|
-
*
|
|
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("
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-relay",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Install Agent Relay AI search across 17+ agents — Claude Code, Codex, Cursor, Gemini CLI, Goose, Windsurf, Cline, BoltAI, Claude Desktop, VS Code, Amazon Q, Roo Code, Witsy, LibreChat, OpenClaw, Tome, Raycast — plus the `agentrelay` CLI for live agent sessions
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Install Agent Relay AI search across 17+ agents — Claude Code, Codex, Cursor, Gemini CLI, Goose, Windsurf, Cline, BoltAI, Claude Desktop, VS Code, Amazon Q, Roo Code, Witsy, LibreChat, OpenClaw, Tome, Raycast — plus the `agentrelay` CLI for live agent sessions and deliverable fetch",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentrelay": "bin/cli.mjs"
|
|
7
7
|
},
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"bin/",
|
|
45
|
-
"schema/",
|
|
46
45
|
"skill/",
|
|
47
46
|
"CLAUDE.md"
|
|
48
47
|
]
|