agentic-relay 3.8.5 → 4.1.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/CLAUDE.md +1 -1
- package/README.md +11 -1
- package/bin/cli.mjs +5 -2
- package/bin/install.mjs +52 -90
- package/bin/lib/cache.mjs +8 -14
- package/bin/lib/commands.mjs +41 -126
- package/bin/lib/doctor.mjs +222 -0
- package/bin/lib/fetch.mjs +14 -79
- package/bin/lib/install-args.mjs +67 -0
- package/bin/lib/parse.mjs +0 -65
- package/bin/lib/sysenv.mjs +152 -0
- package/bin/lib/update-check.mjs +121 -0
- package/bin/lib/zip.mjs +139 -0
- package/bin/relay-core.mjs +50 -85
- package/bin/test/cache.test.mjs +30 -2
- package/bin/test/doctor.test.mjs +76 -0
- package/bin/test/fetch.test.mjs +0 -15
- package/bin/test/install-args.test.mjs +68 -0
- package/bin/test/parse.test.mjs +1 -27
- package/bin/test/session.test.mjs +5 -1
- package/bin/test/update-check.test.mjs +56 -0
- package/bin/test/zip.test.mjs +212 -0
- package/package.json +3 -3
- package/skill/SKILL.md +3 -2
- package/bin/lib/deliverable.mjs +0 -70
- package/bin/lib/driver.mjs +0 -165
- package/bin/lib/pool.mjs +0 -26
- package/bin/test/driver.test.mjs +0 -49
package/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@ Expert "solutions-engineer" agents for real third-party tools (APIs, SDKs, libra
|
|
|
6
6
|
|
|
7
7
|
On any non-trivial build that touches a third-party tool, **load the `agent-relay` skill before doing Agent Relay work.** This file is a map, not the manual — the skill holds the real procedure. Load it because the workflow has paid actions and a feedback loop you will get wrong from memory: when a charge fires, how to react, who fetches a paid deliverable, and what feedback to file when code survives (or fails) contact with production. Don't reconstruct that from this file.
|
|
8
8
|
|
|
9
|
-
Lightweight exception: a one-off lookup ("find me X", "how do I install X") doesn't need the full breadth→build→depth ceremony. A single inline
|
|
9
|
+
Lightweight exception: a one-off lookup ("find me X", "how do I install X") doesn't need the full breadth→build→depth ceremony. A single inline session (`session start` → a send or two → `session end`) is fine — but still settle payments by the rules below.
|
|
10
10
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
package/README.md
CHANGED
|
@@ -134,12 +134,22 @@ gotchas}], capability_id, ad_unit_id }` (+ any extra keys) — see `schema/deliv
|
|
|
134
134
|
| `agentrelay session end <id>` | Close + summary. |
|
|
135
135
|
| `agentrelay feedback <id>` | `--score <0-100> --text "<s>"`. |
|
|
136
136
|
| `agentrelay cache ls\|show <slug>\|put <slug>\|clear` | Persist/review distillations (`.agent-relay/`). |
|
|
137
|
-
| `agentrelay
|
|
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. |
|
|
138
139
|
|
|
139
140
|
Output is a `{ ok, ...payload, errors: [] }` envelope (`--json` auto-on when piped). Key resolution:
|
|
140
141
|
`--api-key` → `$AGENT_RELAY_API_KEY` → `~/.config/agent-relay/credentials.json` (with a read-only fallback to the
|
|
141
142
|
legacy `~/.config/penguin/credentials.json`). The key is never printed.
|
|
142
143
|
|
|
144
|
+
**Staying current:** interactive runs print a one-line stderr notice when a newer version is on npm
|
|
145
|
+
(checked in the background at most once per 24h; never blocks a command, never fires when piped, in CI,
|
|
146
|
+
or under `npx`). Opt out with `AGENT_RELAY_NO_UPDATE_CHECK=1`. Deliverable zips are extracted by a
|
|
147
|
+
built-in pure-JS extractor — no system `unzip` needed, so `fetch` works on Windows too.
|
|
148
|
+
|
|
149
|
+
**Future work (not in this release):** standalone binaries, a Homebrew tap, winget/scoop manifests, and
|
|
150
|
+
code signing. The package uses no npm lifecycle scripts (no postinstall), so npm v12's
|
|
151
|
+
scripts-blocked-by-default change does not affect installs.
|
|
152
|
+
|
|
143
153
|
## What is Agent Relay?
|
|
144
154
|
|
|
145
155
|
AI-native search for software, tools, services, and business solutions. Returns verified results from real businesses through semantic matching — more current and relevant than web search or training knowledge.
|
package/bin/cli.mjs
CHANGED
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
|
|
16
16
|
const CLI_COMMANDS = new Set([
|
|
17
17
|
"search",
|
|
18
|
-
"consult",
|
|
19
|
-
"consult-batch",
|
|
20
18
|
"session",
|
|
21
19
|
"feedback",
|
|
22
20
|
"cache",
|
|
23
21
|
"budget",
|
|
24
22
|
"connect-wallet",
|
|
25
23
|
"fetch",
|
|
24
|
+
"doctor",
|
|
26
25
|
"version",
|
|
27
26
|
"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",
|
|
28
31
|
]);
|
|
29
32
|
|
|
30
33
|
const sub = process.argv[2];
|
package/bin/install.mjs
CHANGED
|
@@ -29,13 +29,26 @@
|
|
|
29
29
|
* deeplink Print URL the agent registers (tome://, raycast://) — user clicks Install
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync
|
|
32
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
33
33
|
import { homedir } from "os";
|
|
34
34
|
import { join, dirname } from "path";
|
|
35
35
|
import { fileURLToPath } from "url";
|
|
36
36
|
import { createInterface } from "readline/promises";
|
|
37
37
|
import { stdin, stdout } from "process";
|
|
38
38
|
import { execSync } from "child_process";
|
|
39
|
+
import { parseInstallArgs } from "./lib/install-args.mjs";
|
|
40
|
+
import {
|
|
41
|
+
resolveCommandPath,
|
|
42
|
+
isEphemeralBin,
|
|
43
|
+
npmGlobalPrefix,
|
|
44
|
+
npmGlobalBinDir,
|
|
45
|
+
globalBinShimPath,
|
|
46
|
+
globalPrefixWritable,
|
|
47
|
+
npmCacheOwnedByOther,
|
|
48
|
+
dirOnPath,
|
|
49
|
+
USER_PREFIX_RECIPE,
|
|
50
|
+
CACHE_CHOWN_RECIPE,
|
|
51
|
+
} from "./lib/sysenv.mjs";
|
|
39
52
|
|
|
40
53
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
54
|
const HOME = homedir();
|
|
@@ -266,39 +279,8 @@ function escapeRegex(str) {
|
|
|
266
279
|
// ═══════════════════════════════════════════════════════════════════════
|
|
267
280
|
|
|
268
281
|
function parseArgs() {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
targets: null,
|
|
272
|
-
all: args.includes("--all"),
|
|
273
|
-
project: args.includes("--project"),
|
|
274
|
-
apiKey: null,
|
|
275
|
-
librechat: null,
|
|
276
|
-
// Legacy single-flag shortcuts (kept for backward compat)
|
|
277
|
-
legacyClaude: args.includes("--claude"),
|
|
278
|
-
legacyCodex: args.includes("--codex"),
|
|
279
|
-
legacyCursor: args.includes("--cursor"),
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
for (const arg of args) {
|
|
283
|
-
if (arg.startsWith("--target=")) {
|
|
284
|
-
flags.targets = arg
|
|
285
|
-
.slice("--target=".length)
|
|
286
|
-
.split(",")
|
|
287
|
-
.map((s) => s.trim())
|
|
288
|
-
.filter(Boolean);
|
|
289
|
-
} else if (arg.startsWith("--api-key=")) {
|
|
290
|
-
flags.apiKey = arg.slice("--api-key=".length);
|
|
291
|
-
} else if (arg.startsWith("--librechat=")) {
|
|
292
|
-
flags.librechat = arg.slice("--librechat=".length);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Support `--api-key VALUE` (space-separated) too
|
|
297
|
-
const keyIdx = args.indexOf("--api-key");
|
|
298
|
-
if (keyIdx !== -1 && args[keyIdx + 1] && !args[keyIdx + 1].startsWith("--")) {
|
|
299
|
-
flags.apiKey = args[keyIdx + 1];
|
|
300
|
-
}
|
|
301
|
-
|
|
282
|
+
const { flags, warnings } = parseInstallArgs(process.argv.slice(2));
|
|
283
|
+
for (const w of warnings) warn(w);
|
|
302
284
|
return flags;
|
|
303
285
|
}
|
|
304
286
|
|
|
@@ -623,45 +605,10 @@ function getExistingKey() {
|
|
|
623
605
|
* Prefers a registry install (`agentic-relay@<version>`) over the local path
|
|
624
606
|
* (the npx cache dir is ephemeral; npm would link/copy from it). Falls back to
|
|
625
607
|
* the path for unpublished checkouts. Surfaces npm's actual error (EACCES
|
|
626
|
-
* etc.) instead of swallowing it, and verifies the
|
|
627
|
-
*
|
|
608
|
+
* etc.) instead of swallowing it, and verifies the global bin file landed so a
|
|
609
|
+
* PATH mismatch is reported, not discovered later as `command not found`.
|
|
610
|
+
* System probes live in lib/sysenv.mjs (shared with `agentrelay doctor`).
|
|
628
611
|
*/
|
|
629
|
-
function resolveCommandPath(cmd) {
|
|
630
|
-
try {
|
|
631
|
-
const probe = process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`;
|
|
632
|
-
const out = execSync(probe, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
633
|
-
return out.split("\n")[0].trim() || null;
|
|
634
|
-
} catch {
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/** True when a resolved bin path is an ephemeral npx/npm-exec shim (or this package's own copy), not a real install. */
|
|
640
|
-
function isEphemeralBin(binPath, pkgRoot) {
|
|
641
|
-
if (!binPath) return false;
|
|
642
|
-
let real = binPath;
|
|
643
|
-
try {
|
|
644
|
-
real = realpathSync(binPath);
|
|
645
|
-
} catch {
|
|
646
|
-
// dangling symlink — definitely not a usable install
|
|
647
|
-
return true;
|
|
648
|
-
}
|
|
649
|
-
const sep = process.platform === "win32" ? "\\" : "/";
|
|
650
|
-
return (
|
|
651
|
-
real.includes(`${sep}_npx${sep}`) ||
|
|
652
|
-
binPath.includes(`${sep}_npx${sep}`) ||
|
|
653
|
-
real.startsWith(realpathSafe(pkgRoot))
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function realpathSafe(p) {
|
|
658
|
-
try {
|
|
659
|
-
return realpathSync(p);
|
|
660
|
-
} catch {
|
|
661
|
-
return p;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
612
|
function linkGlobalCli() {
|
|
666
613
|
const pkgRoot = join(__dirname, "..");
|
|
667
614
|
let version = null;
|
|
@@ -686,6 +633,21 @@ function linkGlobalCli() {
|
|
|
686
633
|
}
|
|
687
634
|
}
|
|
688
635
|
|
|
636
|
+
// Preflight: detect the two environment breakages PROACTIVELY instead of
|
|
637
|
+
// letting `npm install -g` fail with a buried stderr.
|
|
638
|
+
const prefix = npmGlobalPrefix();
|
|
639
|
+
if (npmCacheOwnedByOther()) {
|
|
640
|
+
warn("your ~/.npm cache contains files owned by another user (an old `sudo npm`).");
|
|
641
|
+
warn(`This breaks npx AND npm i -g. Fix it once, then re-run: ${CACHE_CHOWN_RECIPE}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (prefix && !globalPrefixWritable(prefix)) {
|
|
645
|
+
warn(`npm's global prefix (${prefix}) isn't writable — npm i -g would fail with EACCES.`);
|
|
646
|
+
warn("Use a user-level prefix (the official npm fix):");
|
|
647
|
+
for (const line of USER_PREFIX_RECIPE) warn(` ${line}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
689
651
|
const attempts = version ? [`agentic-relay@${version}`, `"${pkgRoot}"`] : [`"${pkgRoot}"`];
|
|
690
652
|
let lastErr = null;
|
|
691
653
|
let installedFrom = null;
|
|
@@ -702,31 +664,24 @@ function linkGlobalCli() {
|
|
|
702
664
|
const stderr = String((lastErr && lastErr.stderr) || "");
|
|
703
665
|
const detail =
|
|
704
666
|
stderr.split("\n").find((l) => /EACCES|EEXIST|E404|ERR!|error/i.test(l)) || "";
|
|
705
|
-
warn(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
667
|
+
warn(`could not auto-link the global CLI${detail ? ` — ${detail.trim()}` : ""}. Run once: npm i -g agentic-relay`);
|
|
668
|
+
if (/EACCES/.test(stderr)) {
|
|
669
|
+
warn("EACCES — use a user-level prefix (the official npm fix):");
|
|
670
|
+
for (const line of USER_PREFIX_RECIPE) warn(` ${line}`);
|
|
671
|
+
}
|
|
710
672
|
return;
|
|
711
673
|
}
|
|
712
674
|
|
|
713
675
|
// Verify the global bin actually landed. Don't probe the bare command here:
|
|
714
676
|
// in this process it resolves to the ephemeral npx shim (which shadows the
|
|
715
677
|
// global install), so check the file in npm's global prefix directly.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const prefix = execSync("npm prefix -g", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
720
|
-
binDir = process.platform === "win32" ? prefix : join(prefix, "bin");
|
|
721
|
-
const cand = join(binDir, process.platform === "win32" ? "agentrelay.cmd" : "agentrelay");
|
|
722
|
-
if (existsSync(cand)) globalBin = cand;
|
|
723
|
-
} catch {
|
|
724
|
-
// can't resolve the prefix — fall through to the warn
|
|
725
|
-
}
|
|
726
|
-
if (globalBin) {
|
|
678
|
+
const binDir = npmGlobalBinDir(prefix);
|
|
679
|
+
const shim = globalBinShimPath(prefix, "agentrelay");
|
|
680
|
+
if (shim && existsSync(shim)) {
|
|
727
681
|
success(`linked global \`agentrelay\` CLI (${installedFrom})`);
|
|
728
|
-
|
|
729
|
-
|
|
682
|
+
if (!dirOnPath(binDir)) {
|
|
683
|
+
warn(`note: ${binDir} doesn't appear to be on your PATH — add it to use \`agentrelay\` directly`);
|
|
684
|
+
}
|
|
730
685
|
} else {
|
|
731
686
|
warn(`installed, but no global \`agentrelay\` found in ${binDir || "npm's global bin directory"} — run once: npm i -g agentic-relay`);
|
|
732
687
|
}
|
|
@@ -739,6 +694,13 @@ async function setupApiKeyInteractive() {
|
|
|
739
694
|
return true;
|
|
740
695
|
}
|
|
741
696
|
|
|
697
|
+
// Scripted/CI runs have no TTY — don't hang on a prompt nobody will answer.
|
|
698
|
+
if (!stdin.isTTY) {
|
|
699
|
+
warn("no API key configured and stdin is not a terminal — skipping the prompt.");
|
|
700
|
+
warn("Pass --api-key=<am_live_...> or write ~/.config/agent-relay/credentials.json");
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
|
|
742
704
|
console.log("");
|
|
743
705
|
log("API key setup");
|
|
744
706
|
console.log("");
|
package/bin/lib/cache.mjs
CHANGED
|
@@ -69,24 +69,18 @@ export class Cache {
|
|
|
69
69
|
return join(this.searchDir, `${h}.json`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// The buyer-side spend policy snapshot the search API returns alongside the
|
|
81
|
-
// menu. Cached with the search so a cache hit still hands the agent a policy
|
|
82
|
-
// to decide auto-pay-vs-bubble against (see SKILL.md Pay). Null on older
|
|
83
|
-
// cache files written before this field existed.
|
|
84
|
-
readSearchPaymentContext(query) {
|
|
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
|
+
*/
|
|
77
|
+
readSearchEntry(query) {
|
|
85
78
|
if (!this.enabled) return null;
|
|
86
79
|
const entry = this._readJson(this._searchPath(query));
|
|
87
80
|
if (!entry) return null;
|
|
88
81
|
if (Date.now() > entry.expires) return null;
|
|
89
|
-
|
|
82
|
+
if (!entry.capabilities) return null;
|
|
83
|
+
return entry;
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
writeSearch(query, capabilities, paymentContext = null) {
|
package/bin/lib/commands.mjs
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - `consult` / `consult-batch` — the code-driven TEMPLATE fallback for
|
|
10
|
-
* no-LLM/scripted use; returns a distilled result, transcript stays out.
|
|
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.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
import { readFileSync } from "fs";
|
|
@@ -23,7 +21,6 @@ import {
|
|
|
23
21
|
} from "./api.mjs";
|
|
24
22
|
import { slugify } from "./config.mjs";
|
|
25
23
|
import { Cache } from "./cache.mjs";
|
|
26
|
-
import { consultOne } from "./driver.mjs";
|
|
27
24
|
import { cmdFetch } from "./fetch.mjs";
|
|
28
25
|
|
|
29
26
|
/** Normalize one upstream capability into the spec §4 menu shape. */
|
|
@@ -61,9 +58,14 @@ export async function cmdSearch(query, opts) {
|
|
|
61
58
|
// The buyer-side spend policy snapshot the search API returns alongside the
|
|
62
59
|
// menu — the agent decides auto-pay-vs-bubble against it (SKILL.md Pay). Pass
|
|
63
60
|
// it through here so it survives to the CLI's JSON output and gets cached.
|
|
64
|
-
|
|
61
|
+
// On a cache hit the snapshot can be up to the search TTL (1h) stale, so the
|
|
62
|
+
// payload carries from_cache/cached_at for the caller's pay decision.
|
|
63
|
+
const cachedEntry = opts.refresh ? null : cache.readSearchEntry(query);
|
|
64
|
+
let paymentContext = cachedEntry ? (cachedEntry.payment_context ?? null) : null;
|
|
65
|
+
let capabilities = cachedEntry ? cachedEntry.capabilities : null;
|
|
66
|
+
const fromCache = Boolean(cachedEntry);
|
|
67
|
+
const cachedAt = cachedEntry ? (cachedEntry.ts ?? null) : null;
|
|
65
68
|
|
|
66
|
-
let capabilities = opts.refresh ? null : cache.readSearch(query);
|
|
67
69
|
if (!capabilities) {
|
|
68
70
|
const data = await apiSearch(query, {
|
|
69
71
|
apiKey: opts.apiKey,
|
|
@@ -74,7 +76,7 @@ export async function cmdSearch(query, opts) {
|
|
|
74
76
|
paymentContext = data.payment_context ?? null;
|
|
75
77
|
capabilities = dedupeSlugs((data.capabilities || []).map(normalizeCapability));
|
|
76
78
|
cache.writeSearch(query, capabilities, paymentContext);
|
|
77
|
-
// Seed the index so
|
|
79
|
+
// Seed the index so session start can resolve ids without re-searching.
|
|
78
80
|
for (const c of capabilities) {
|
|
79
81
|
if (c.capability_id && c.ad_unit_id) {
|
|
80
82
|
cache.updateIndex(c.slug, {
|
|
@@ -93,7 +95,14 @@ export async function cmdSearch(query, opts) {
|
|
|
93
95
|
}
|
|
94
96
|
capabilities = capabilities.slice(0, maxResults);
|
|
95
97
|
|
|
96
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
query,
|
|
100
|
+
count: capabilities.length,
|
|
101
|
+
from_cache: fromCache,
|
|
102
|
+
cached_at: cachedAt,
|
|
103
|
+
capabilities,
|
|
104
|
+
payment_context: paymentContext,
|
|
105
|
+
};
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/**
|
|
@@ -153,116 +162,6 @@ export async function resolveCapability(slugOrId, opts) {
|
|
|
153
162
|
};
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
async function consultResolved(resolved, want, opts) {
|
|
157
|
-
const cache = new Cache(opts.cacheDir, { enabled: !opts.noCache });
|
|
158
|
-
|
|
159
|
-
// Cache hit: return the stored spec unless --refresh.
|
|
160
|
-
if (!opts.refresh) {
|
|
161
|
-
const cached = cache.readSpec(resolved.slug);
|
|
162
|
-
if (cached) return { ...cached, cached: true };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const { deliverable, sessionId, turns } = await consultOne(resolved, {
|
|
166
|
-
want,
|
|
167
|
-
context: opts.context,
|
|
168
|
-
deliverNow: opts.deliverNow,
|
|
169
|
-
raw: opts.raw,
|
|
170
|
-
maxTurns: opts.maxTurns,
|
|
171
|
-
timeoutMs: opts.timeoutMs,
|
|
172
|
-
apiKey: opts.apiKey,
|
|
173
|
-
});
|
|
174
|
-
deliverable.turns = turns;
|
|
175
|
-
cache.writeSpec(resolved.slug, deliverable);
|
|
176
|
-
cache.updateIndex(resolved.slug, {
|
|
177
|
-
last_session_id: sessionId,
|
|
178
|
-
capability_id: resolved.capabilityId,
|
|
179
|
-
ad_unit_id: resolved.adUnitId,
|
|
180
|
-
clearing_price_cents: resolved.clearingPriceCents,
|
|
181
|
-
name: resolved.name,
|
|
182
|
-
});
|
|
183
|
-
return deliverable;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* `consult`/`consult-batch` only run the code-driven TEMPLATE driver. The
|
|
188
|
-
* LLM-driven path is a host subagent driving the `session` primitives — there's
|
|
189
|
-
* no CLI-internal LLM. Reject `--driver llm` with a pointer rather than
|
|
190
|
-
* silently degrading.
|
|
191
|
-
*/
|
|
192
|
-
function assertTemplateDriver(opts) {
|
|
193
|
-
const d = opts.driver || "template";
|
|
194
|
-
if (d !== "template") {
|
|
195
|
-
const e = new Error(
|
|
196
|
-
`--driver "${d}" is not available. consult runs the template driver only; ` +
|
|
197
|
-
"for LLM-driven conversation, drive `agentrelay session start|send|end` from a subagent (see SKILL.md).",
|
|
198
|
-
);
|
|
199
|
-
e.usage = true;
|
|
200
|
-
throw e;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function cmdConsult(slugOrId, opts) {
|
|
205
|
-
assertTemplateDriver(opts);
|
|
206
|
-
if (!opts.want) {
|
|
207
|
-
const e = new Error("--want is required");
|
|
208
|
-
e.usage = true;
|
|
209
|
-
throw e;
|
|
210
|
-
}
|
|
211
|
-
const resolved = await resolveCapability(slugOrId, opts);
|
|
212
|
-
const deliverable = await consultResolved(resolved, opts.want, opts);
|
|
213
|
-
return { deliverable };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function loadWantFile(path) {
|
|
217
|
-
try {
|
|
218
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
219
|
-
} catch (err) {
|
|
220
|
-
throw new Error(`could not read --want-file ${path}: ${err.message}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export async function cmdConsultBatch(opts) {
|
|
225
|
-
assertTemplateDriver(opts);
|
|
226
|
-
if (!opts.agents || opts.agents.length === 0) {
|
|
227
|
-
const e = new Error("--agents <a,b,c> is required");
|
|
228
|
-
e.usage = true;
|
|
229
|
-
throw e;
|
|
230
|
-
}
|
|
231
|
-
const wantMap = opts.wantFile ? loadWantFile(opts.wantFile) : null;
|
|
232
|
-
if (!wantMap && !opts.want) {
|
|
233
|
-
const e = new Error("provide --want <str> or --want-file <map.json>");
|
|
234
|
-
e.usage = true;
|
|
235
|
-
throw e;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const { pool } = await import("./pool.mjs");
|
|
239
|
-
const t0 = Date.now();
|
|
240
|
-
|
|
241
|
-
const results = await pool(opts.agents, opts.concurrency || 5, async (slug) => {
|
|
242
|
-
const want = (wantMap && wantMap[slug]) || opts.want;
|
|
243
|
-
if (!want) throw new Error(`no want for "${slug}" (missing from --want-file)`);
|
|
244
|
-
const resolved = await resolveCapability(slug, opts);
|
|
245
|
-
return consultResolved(resolved, want, opts);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const deliverables = [];
|
|
249
|
-
const failures = [];
|
|
250
|
-
results.forEach((r, i) => {
|
|
251
|
-
if (r.ok) deliverables.push(r.value);
|
|
252
|
-
else failures.push({ slug: opts.agents[i], reason: String(r.error.message || r.error) });
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const totals = {
|
|
256
|
-
consulted: opts.agents.length,
|
|
257
|
-
ok: deliverables.length,
|
|
258
|
-
failed: failures.length,
|
|
259
|
-
wall_seconds: Math.round((Date.now() - t0) / 1000),
|
|
260
|
-
tokens_estimate: deliverables.reduce((s, d) => s + (d.tokens_estimate || 0), 0),
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
return { deliverables, failures, totals };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
165
|
// ── session primitives (thin api_v1 wrappers — the LLM-driven path) ─────────
|
|
267
166
|
function parseJsonFlag(s, name) {
|
|
268
167
|
if (!s) return undefined;
|
|
@@ -296,17 +195,20 @@ export function shapeSessionTurn(r) {
|
|
|
296
195
|
|
|
297
196
|
/**
|
|
298
197
|
* Map a `delivery` object (from a paid session turn) to cmdFetch args. Tolerant of
|
|
299
|
-
* minor key variants so it survives small upstream shape differences.
|
|
198
|
+
* minor key variants so it survives small upstream shape differences. The dest
|
|
199
|
+
* defaults to ./<delivery.slug>/ (slugified — a hostile slug can't traverse);
|
|
200
|
+
* an explicit dest always wins.
|
|
300
201
|
*/
|
|
301
202
|
export function deliveryToFetchArgs(d, { dest = null } = {}) {
|
|
302
203
|
if (!d || typeof d !== "object") return null;
|
|
303
204
|
const url = d.download_url || d.url || null;
|
|
304
205
|
if (!url) return null;
|
|
206
|
+
const slug = d.slug ? slugify(String(d.slug)) : null;
|
|
305
207
|
return {
|
|
306
208
|
url,
|
|
307
209
|
checksum: d.checksum_sha256 || d.checksum || null,
|
|
308
210
|
filename: d.filename || null,
|
|
309
|
-
dest,
|
|
211
|
+
dest: dest || (slug ? `./${slug}` : null),
|
|
310
212
|
};
|
|
311
213
|
}
|
|
312
214
|
|
|
@@ -421,13 +323,13 @@ export async function cmdFetchSession(sessionId, opts) {
|
|
|
421
323
|
}
|
|
422
324
|
|
|
423
325
|
export async function cmdFeedback(sessionId, opts) {
|
|
424
|
-
if (!sessionId || opts.score == null) {
|
|
326
|
+
if (!sessionId || opts.score == null || !opts.text || !String(opts.text).trim()) {
|
|
425
327
|
const e = new Error("usage: agentrelay feedback <session_id> --score <0-100> --text <str>");
|
|
426
328
|
e.usage = true;
|
|
427
329
|
throw e;
|
|
428
330
|
}
|
|
429
331
|
return apiFeedback(
|
|
430
|
-
{ sessionId, feedbackText: opts.text
|
|
332
|
+
{ sessionId, feedbackText: String(opts.text), score: Number(opts.score) },
|
|
431
333
|
{ apiKey: opts.apiKey, timeoutMs: opts.timeoutMs },
|
|
432
334
|
);
|
|
433
335
|
}
|
|
@@ -532,11 +434,24 @@ export function cmdCache(sub, slug, opts) {
|
|
|
532
434
|
}
|
|
533
435
|
deliverable.slug = deliverable.slug || slug;
|
|
534
436
|
deliverable.ts = deliverable.ts || new Date().toISOString();
|
|
437
|
+
// Soft contract check (schema/deliverable.json spine). Warn-only: a spec
|
|
438
|
+
// with gaps is still worth banking, and concurrent breadth subagents must
|
|
439
|
+
// never be blocked by validation.
|
|
440
|
+
const warnings = [];
|
|
441
|
+
if (!deliverable.session_id || !String(deliverable.session_id).trim()) {
|
|
442
|
+
warnings.push("no session_id — build-outcome feedback can't be filed against this consult later");
|
|
443
|
+
}
|
|
444
|
+
if (!deliverable.summary || !String(deliverable.summary).trim()) {
|
|
445
|
+
warnings.push("no summary — `cache ls` triage will show a blank line for this spec");
|
|
446
|
+
}
|
|
447
|
+
if (!Array.isArray(deliverable.features) || deliverable.features.length === 0) {
|
|
448
|
+
warnings.push("no features[] — build-time has no surface map; depth sessions will have to rediscover it");
|
|
449
|
+
}
|
|
535
450
|
// Write ONLY the per-slug spec file — no shared index.json mutation — so N
|
|
536
451
|
// breadth subagents can `cache put` concurrently without clobbering each
|
|
537
452
|
// other. `cache ls` reads the spec files directly.
|
|
538
453
|
cache.writeSpec(slug, deliverable);
|
|
539
|
-
return { stored: true, slug, cache_dir: opts.cacheDir };
|
|
454
|
+
return { stored: true, slug, cache_dir: opts.cacheDir, warnings };
|
|
540
455
|
}
|
|
541
456
|
if (sub === "clear") {
|
|
542
457
|
cache.clear();
|