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