agentic-relay 4.0.0 → 4.1.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/CLAUDE.md +7 -7
- package/README.md +10 -0
- package/bin/cli.mjs +1 -0
- package/bin/install.mjs +78 -99
- package/bin/lib/cache.mjs +8 -14
- package/bin/lib/commands.mjs +43 -6
- 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/sysenv.mjs +152 -0
- package/bin/lib/update-check.mjs +121 -0
- package/bin/lib/zip.mjs +139 -0
- package/bin/relay-core.mjs +43 -10
- package/bin/test/cache.test.mjs +44 -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/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 +1 -1
- package/skill/SKILL.md +32 -19
package/CLAUDE.md
CHANGED
|
@@ -6,16 +6,16 @@ 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 session (
|
|
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` (start → send → end) is fine — but it's still a `session`, not a shortcut, and you still settle payments by the rules below.
|
|
10
10
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
agentrelay search "<capability>" --dynamic-only --json # find agents (returns payment_context)
|
|
15
|
-
agentrelay cache ls # list banked specs (summaries) — dedup before
|
|
15
|
+
agentrelay cache ls # list banked specs (summaries) — dedup before starting a session
|
|
16
16
|
agentrelay cache show <slug> # full banked spec for one tool
|
|
17
|
-
agentrelay cache put <slug> # bank a spec (
|
|
18
|
-
agentrelay session start <slug> --initial '<ctx json>' --json # open a live
|
|
17
|
+
agentrelay cache put <slug> # bank a spec (one slug file; warns if session_id/status missing)
|
|
18
|
+
agentrelay session start <slug> --initial '<ctx json>' --json # open a live session (the ONLY way to talk to an agent)
|
|
19
19
|
agentrelay session send <session_id> "<reply>" --json # next turn (carries awaiting_input, payment_required, delivery)
|
|
20
20
|
agentrelay session end <session_id> --json
|
|
21
21
|
agentrelay session pay <session_id> # settle an in-policy charge
|
|
@@ -28,11 +28,11 @@ CLI or MCP tools only — **never call the raw HTTP API.** API key lives at `~/.
|
|
|
28
28
|
|
|
29
29
|
## Workflow (one paragraph — the skill is authoritative)
|
|
30
30
|
|
|
31
|
-
**Breadth → build → depth.** As the *first* step of a real build, `search` per need, dedup against `cache ls`, then map
|
|
31
|
+
**Breadth → build → depth.** As the *first* step of a real build, `search` per need, dedup against `cache ls`, then map each chosen tool's surface and bank a spec. **The only way to talk to an agent is a `session` (`start` → `send` → `end`).** Parallelism depends on your host: if it can spawn subagents, fan out one per tool, transcripts stay with them, only summaries return; if it can't (or you're not spawning them here), map agents inline one `session` at a time — same primitive, you drive it. **Build** from the bank: `cache show <slug>`, build from the snippet if the feature is `coded`, open a depth session if it's `surface_only`. **Depth** is a direct just-in-time session for code that isn't banked or when you're stuck; merge the result back into the spec. The conversation loop: after `start`, while `awaiting_input` is true (or `pending_fields` non-empty) answer from project context and `session send` again — never bounce a question to the human you can infer, never stop just because a question was asked. Stop when you have what you came for; `deliverable_complete` is a signal, not a hard stop. Use as few turns as the goal needs. See the skill for the mapping brief, depth triggers, and the spec contract.
|
|
32
32
|
|
|
33
33
|
## Payments — three tiers
|
|
34
34
|
|
|
35
|
-
Read live policy first (`agentrelay budget`, or the `payment_context` returned by `search`). Never state pay posture from memory.
|
|
35
|
+
Read live policy first (`agentrelay budget`, or the `payment_context` returned by `search`). Never state pay posture from memory. Note `search`'s `payment_context` can be up to 1h stale (search cache) — `budget` always hits the server and is authoritative.
|
|
36
36
|
|
|
37
37
|
- **Always (autonomous, no user check):** when autonomous pay is on, settle charges the build genuinely needs that fall within the policy ceiling. `session pay <session_id>`. The server independently rejects anything over budget/cap (`not_authorized` / `over_daily_cap` / `over_session_cap`), so you can't overspend — a rejection just means surface it.
|
|
38
38
|
- **Ask first (surface, don't block):** autonomous off, over a threshold/cap, card-only, or server-rejected. Tag the session payment-pending-human, keep the `payment_url`, keep working other sessions, and consolidate all pending payments into one human moment at the end.
|
|
@@ -42,7 +42,7 @@ Full matrix, wallet models, the consolidation pattern, and the fetch/deliver ste
|
|
|
42
42
|
|
|
43
43
|
## Feedback — file it, more than once
|
|
44
44
|
|
|
45
|
-
After using a capability, `agentrelay feedback <session_id> --score <0-100> --text "<...>"`.
|
|
45
|
+
After using a capability, `agentrelay feedback <session_id> --score <0-100> --text "<...>"`. The backend accepts multiple feedback rows per session (only the first triggers developer payout), so there are two signals against the same `session_id`: **mapping usefulness**, filed by whoever ran the session (a subagent, or you inline) before ending it — how useful the agent was at contributing to the problem; and **build-outcome**, filed by the main coding agent once the code ships or fails — how the spec fared in production. Front-load the verdict (a background summarizer only reads the first ~2000 chars), and put concrete tool problems in the text — a stale/404 link or broken endpoint it served (quote the URL), an implementation hurdle, a knowledge gap. Mark live-verified claims with a date. This trains the capability's learned context; don't skip it because the build "worked."
|
|
46
46
|
|
|
47
47
|
## Guardrails
|
|
48
48
|
|
package/README.md
CHANGED
|
@@ -135,11 +135,21 @@ gotchas}], capability_id, ad_unit_id }` (+ any extra keys) — see `schema/deliv
|
|
|
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
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
package/bin/install.mjs
CHANGED
|
@@ -29,18 +29,35 @@
|
|
|
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();
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
// Matches the CLAUDE.md/AGENTS.md section header. The current header is
|
|
57
|
+
// "# Agent Relay"; older installs wrote "# Agent Relay MCP Server" — the
|
|
58
|
+
// replace regex tolerates the legacy suffix so re-installs REPLACE the old
|
|
59
|
+
// section instead of appending a duplicate.
|
|
60
|
+
const SECTION_START = "# Agent Relay";
|
|
44
61
|
const CREDENTIALS_PATH = join(HOME, ".config", "agent-relay", "credentials.json");
|
|
45
62
|
const LEGACY_CREDENTIALS_PATH = join(HOME, ".config", "penguin", "credentials.json");
|
|
46
63
|
const SIGNUP_URL = "https://attentionmarket-auth.vercel.app";
|
|
@@ -266,39 +283,8 @@ function escapeRegex(str) {
|
|
|
266
283
|
// ═══════════════════════════════════════════════════════════════════════
|
|
267
284
|
|
|
268
285
|
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
|
-
|
|
286
|
+
const { flags, warnings } = parseInstallArgs(process.argv.slice(2));
|
|
287
|
+
for (const w of warnings) warn(w);
|
|
302
288
|
return flags;
|
|
303
289
|
}
|
|
304
290
|
|
|
@@ -329,9 +315,11 @@ function installInstructions(instructions, path, label) {
|
|
|
329
315
|
|
|
330
316
|
if (existing.includes(SECTION_START)) {
|
|
331
317
|
const sectionRegex = new RegExp(
|
|
332
|
-
`${escapeRegex(SECTION_START)}[\\s\\S]*?(?=\\n# (?!Agent Relay)|\\s*$)`,
|
|
318
|
+
`${escapeRegex(SECTION_START)}( MCP Server)?[\\s\\S]*?(?=\\n# (?!Agent Relay)|\\s*$)`,
|
|
333
319
|
);
|
|
334
|
-
|
|
320
|
+
// Replacer fn: the instructions contain `$` (payment examples) which
|
|
321
|
+
// String.replace would otherwise treat as capture-group references.
|
|
322
|
+
writeFileSync(path, existing.replace(sectionRegex, () => instructions));
|
|
335
323
|
success(`${label}: instructions updated → ${path}`);
|
|
336
324
|
} else {
|
|
337
325
|
const sep = existing.length > 0 ? "\n\n" : "";
|
|
@@ -623,45 +611,10 @@ function getExistingKey() {
|
|
|
623
611
|
* Prefers a registry install (`agentic-relay@<version>`) over the local path
|
|
624
612
|
* (the npx cache dir is ephemeral; npm would link/copy from it). Falls back to
|
|
625
613
|
* the path for unpublished checkouts. Surfaces npm's actual error (EACCES
|
|
626
|
-
* etc.) instead of swallowing it, and verifies the
|
|
627
|
-
*
|
|
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`).
|
|
628
617
|
*/
|
|
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
618
|
function linkGlobalCli() {
|
|
666
619
|
const pkgRoot = join(__dirname, "..");
|
|
667
620
|
let version = null;
|
|
@@ -678,7 +631,7 @@ function linkGlobalCli() {
|
|
|
678
631
|
const installed = (out.trim().split(/\s+/).pop() || "").trim();
|
|
679
632
|
if (version && installed === version) {
|
|
680
633
|
success(`\`agentrelay\` ${version} already on your PATH`);
|
|
681
|
-
return;
|
|
634
|
+
return "linked";
|
|
682
635
|
}
|
|
683
636
|
log(`global \`agentrelay\` is ${installed || "an unknown version"} — updating to ${version}…`);
|
|
684
637
|
} catch {
|
|
@@ -686,6 +639,21 @@ function linkGlobalCli() {
|
|
|
686
639
|
}
|
|
687
640
|
}
|
|
688
641
|
|
|
642
|
+
// Preflight: detect the two environment breakages PROACTIVELY instead of
|
|
643
|
+
// letting `npm install -g` fail with a buried stderr.
|
|
644
|
+
const prefix = npmGlobalPrefix();
|
|
645
|
+
if (npmCacheOwnedByOther()) {
|
|
646
|
+
warn("your ~/.npm cache contains files owned by another user (an old `sudo npm`).");
|
|
647
|
+
warn(`This breaks npx AND npm i -g. Fix it once, then re-run: ${CACHE_CHOWN_RECIPE}`);
|
|
648
|
+
return "blocked";
|
|
649
|
+
}
|
|
650
|
+
if (prefix && !globalPrefixWritable(prefix)) {
|
|
651
|
+
warn(`npm's global prefix (${prefix}) isn't writable — npm i -g would fail with EACCES.`);
|
|
652
|
+
warn("Use a user-level prefix (the official npm fix):");
|
|
653
|
+
for (const line of USER_PREFIX_RECIPE) warn(` ${line}`);
|
|
654
|
+
return "blocked";
|
|
655
|
+
}
|
|
656
|
+
|
|
689
657
|
const attempts = version ? [`agentic-relay@${version}`, `"${pkgRoot}"`] : [`"${pkgRoot}"`];
|
|
690
658
|
let lastErr = null;
|
|
691
659
|
let installedFrom = null;
|
|
@@ -702,34 +670,29 @@ function linkGlobalCli() {
|
|
|
702
670
|
const stderr = String((lastErr && lastErr.stderr) || "");
|
|
703
671
|
const detail =
|
|
704
672
|
stderr.split("\n").find((l) => /EACCES|EEXIST|E404|ERR!|error/i.test(l)) || "";
|
|
705
|
-
warn(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return;
|
|
673
|
+
warn(`could not auto-link the global CLI${detail ? ` — ${detail.trim()}` : ""}. Run once: npm i -g agentic-relay`);
|
|
674
|
+
if (/EACCES/.test(stderr)) {
|
|
675
|
+
warn("EACCES — use a user-level prefix (the official npm fix):");
|
|
676
|
+
for (const line of USER_PREFIX_RECIPE) warn(` ${line}`);
|
|
677
|
+
}
|
|
678
|
+
return "failed";
|
|
711
679
|
}
|
|
712
680
|
|
|
713
681
|
// Verify the global bin actually landed. Don't probe the bare command here:
|
|
714
682
|
// in this process it resolves to the ephemeral npx shim (which shadows the
|
|
715
683
|
// 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) {
|
|
684
|
+
const binDir = npmGlobalBinDir(prefix);
|
|
685
|
+
const shim = globalBinShimPath(prefix, "agentrelay");
|
|
686
|
+
if (shim && existsSync(shim)) {
|
|
727
687
|
success(`linked global \`agentrelay\` CLI (${installedFrom})`);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
688
|
+
if (!dirOnPath(binDir)) {
|
|
689
|
+
warn(`note: ${binDir} doesn't appear to be on your PATH — add it to use \`agentrelay\` directly`);
|
|
690
|
+
return "linked-offpath";
|
|
691
|
+
}
|
|
692
|
+
return "linked";
|
|
732
693
|
}
|
|
694
|
+
warn(`installed, but no global \`agentrelay\` found in ${binDir || "npm's global bin directory"} — run once: npm i -g agentic-relay`);
|
|
695
|
+
return "failed";
|
|
733
696
|
}
|
|
734
697
|
|
|
735
698
|
async function setupApiKeyInteractive() {
|
|
@@ -739,6 +702,13 @@ async function setupApiKeyInteractive() {
|
|
|
739
702
|
return true;
|
|
740
703
|
}
|
|
741
704
|
|
|
705
|
+
// Scripted/CI runs have no TTY — don't hang on a prompt nobody will answer.
|
|
706
|
+
if (!stdin.isTTY) {
|
|
707
|
+
warn("no API key configured and stdin is not a terminal — skipping the prompt.");
|
|
708
|
+
warn("Pass --api-key=<am_live_...> or write ~/.config/agent-relay/credentials.json");
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
742
712
|
console.log("");
|
|
743
713
|
log("API key setup");
|
|
744
714
|
console.log("");
|
|
@@ -964,8 +934,12 @@ async function main() {
|
|
|
964
934
|
|
|
965
935
|
// Put a bare `agentrelay` command on PATH so users don't need npx every time.
|
|
966
936
|
console.log("");
|
|
967
|
-
linkGlobalCli();
|
|
937
|
+
const linkStatus = linkGlobalCli();
|
|
968
938
|
|
|
939
|
+
// The closing banner states what ACTUALLY works on this machine — a
|
|
940
|
+
// "successful" install whose global link failed must not advertise a
|
|
941
|
+
// command that will be `command not found`.
|
|
942
|
+
const linked = linkStatus === "linked";
|
|
969
943
|
console.log("");
|
|
970
944
|
success("Done! Agent Relay is installed.");
|
|
971
945
|
console.log("");
|
|
@@ -973,7 +947,12 @@ async function main() {
|
|
|
973
947
|
" Your agents will now search Agent Relay before web search or training knowledge.",
|
|
974
948
|
);
|
|
975
949
|
console.log("");
|
|
976
|
-
|
|
950
|
+
if (linked) {
|
|
951
|
+
console.log(` Try it: agentrelay search "weather api"`);
|
|
952
|
+
} else {
|
|
953
|
+
console.log(` Try it: npx agentic-relay search "weather api"`);
|
|
954
|
+
console.log(` (the global \`agentrelay\` command isn't set up — see the warning above)`);
|
|
955
|
+
}
|
|
977
956
|
console.log(` API key: ${CREDENTIALS_PATH}`);
|
|
978
957
|
console.log("");
|
|
979
958
|
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
|
@@ -58,9 +58,14 @@ export async function cmdSearch(query, opts) {
|
|
|
58
58
|
// The buyer-side spend policy snapshot the search API returns alongside the
|
|
59
59
|
// menu — the agent decides auto-pay-vs-bubble against it (SKILL.md Pay). Pass
|
|
60
60
|
// it through here so it survives to the CLI's JSON output and gets cached.
|
|
61
|
-
|
|
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;
|
|
62
68
|
|
|
63
|
-
let capabilities = opts.refresh ? null : cache.readSearch(query);
|
|
64
69
|
if (!capabilities) {
|
|
65
70
|
const data = await apiSearch(query, {
|
|
66
71
|
apiKey: opts.apiKey,
|
|
@@ -90,7 +95,14 @@ export async function cmdSearch(query, opts) {
|
|
|
90
95
|
}
|
|
91
96
|
capabilities = capabilities.slice(0, maxResults);
|
|
92
97
|
|
|
93
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
query,
|
|
100
|
+
count: capabilities.length,
|
|
101
|
+
from_cache: fromCache,
|
|
102
|
+
cached_at: cachedAt,
|
|
103
|
+
capabilities,
|
|
104
|
+
payment_context: paymentContext,
|
|
105
|
+
};
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/**
|
|
@@ -183,17 +195,20 @@ export function shapeSessionTurn(r) {
|
|
|
183
195
|
|
|
184
196
|
/**
|
|
185
197
|
* Map a `delivery` object (from a paid session turn) to cmdFetch args. Tolerant of
|
|
186
|
-
* 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.
|
|
187
201
|
*/
|
|
188
202
|
export function deliveryToFetchArgs(d, { dest = null } = {}) {
|
|
189
203
|
if (!d || typeof d !== "object") return null;
|
|
190
204
|
const url = d.download_url || d.url || null;
|
|
191
205
|
if (!url) return null;
|
|
206
|
+
const slug = d.slug ? slugify(String(d.slug)) : null;
|
|
192
207
|
return {
|
|
193
208
|
url,
|
|
194
209
|
checksum: d.checksum_sha256 || d.checksum || null,
|
|
195
210
|
filename: d.filename || null,
|
|
196
|
-
dest,
|
|
211
|
+
dest: dest || (slug ? `./${slug}` : null),
|
|
197
212
|
};
|
|
198
213
|
}
|
|
199
214
|
|
|
@@ -419,11 +434,33 @@ export function cmdCache(sub, slug, opts) {
|
|
|
419
434
|
}
|
|
420
435
|
deliverable.slug = deliverable.slug || slug;
|
|
421
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
|
+
} else {
|
|
450
|
+
const missingStatus = deliverable.features.filter(
|
|
451
|
+
(f) => !f || (f.status !== "coded" && f.status !== "surface_only"),
|
|
452
|
+
).length;
|
|
453
|
+
if (missingStatus > 0) {
|
|
454
|
+
warnings.push(
|
|
455
|
+
`${missingStatus} feature(s) missing status ("coded"|"surface_only") — build-time can't tell banked code from surface-only`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
422
459
|
// Write ONLY the per-slug spec file — no shared index.json mutation — so N
|
|
423
460
|
// breadth subagents can `cache put` concurrently without clobbering each
|
|
424
461
|
// other. `cache ls` reads the spec files directly.
|
|
425
462
|
cache.writeSpec(slug, deliverable);
|
|
426
|
-
return { stored: true, slug, cache_dir: opts.cacheDir };
|
|
463
|
+
return { stored: true, slug, cache_dir: opts.cacheDir, warnings };
|
|
427
464
|
}
|
|
428
465
|
if (sub === "clear") {
|
|
429
466
|
cache.clear();
|