agentic-relay 4.3.1 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -14
- package/bin/cli.mjs +15 -13
- package/bin/install.mjs +90 -58
- package/bin/lib/api.mjs +2 -75
- package/bin/lib/cache.mjs +7 -74
- package/bin/lib/commands.mjs +10 -183
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/sysenv.mjs +29 -44
- package/bin/relay-core.mjs +15 -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/doctor.mjs
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agentrelay doctor` — diagnose the install/runtime environment.
|
|
3
|
-
*
|
|
4
|
-
* Every check maps to a failure mode seen in the field: stale global versions,
|
|
5
|
-
* EACCES on the npm prefix, root-owned ~/.npm (kills npx AND npm i -g), the
|
|
6
|
-
* global bin dir missing from PATH, a dead/misconfigured API key. Output is
|
|
7
|
-
* one line per check (✓ ok / ! warn / ✗ fail) or a --json envelope; exit 1
|
|
8
|
-
* only when a required check fails.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync, existsSync } from "fs";
|
|
12
|
-
import { homedir } from "os";
|
|
13
|
-
import { join } from "path";
|
|
14
|
-
import { resolveApiKey, CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH } from "./config.mjs";
|
|
15
|
-
import { search as apiSearch } from "./api.mjs";
|
|
16
|
-
import { compareSemver } from "./update-check.mjs";
|
|
17
|
-
import {
|
|
18
|
-
npmGlobalPrefix,
|
|
19
|
-
npmGlobalBinDir,
|
|
20
|
-
globalBinShimPath,
|
|
21
|
-
globalPrefixWritable,
|
|
22
|
-
npmCacheOwnedByOther,
|
|
23
|
-
dirOnPath,
|
|
24
|
-
isDirWritable,
|
|
25
|
-
USER_PREFIX_RECIPE,
|
|
26
|
-
CACHE_CHOWN_RECIPE,
|
|
27
|
-
} from "./sysenv.mjs";
|
|
28
|
-
|
|
29
|
-
export const MIN_NODE_MAJOR = 18;
|
|
30
|
-
|
|
31
|
-
/** Pure: ok/fail verdict for a Node version string. */
|
|
32
|
-
export function checkNodeVersion(versionString) {
|
|
33
|
-
const major = Number(String(versionString).split(".")[0]);
|
|
34
|
-
return {
|
|
35
|
-
id: "node",
|
|
36
|
-
label: "Node.js version",
|
|
37
|
-
status: major >= MIN_NODE_MAJOR ? "ok" : "fail",
|
|
38
|
-
detail:
|
|
39
|
-
major >= MIN_NODE_MAJOR
|
|
40
|
-
? `v${versionString}`
|
|
41
|
-
: `v${versionString} — agentrelay needs Node >= ${MIN_NODE_MAJOR}`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Pure: aggregate checks → overall ok + exit code. */
|
|
46
|
-
export function summarize(checks) {
|
|
47
|
-
const failed = checks.filter((c) => c.status === "fail");
|
|
48
|
-
return { ok: failed.length === 0, exitCode: failed.length === 0 ? 0 : 1 };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function fetchLatestVersion(timeoutMs = 3000) {
|
|
52
|
-
const controller = new AbortController();
|
|
53
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54
|
-
try {
|
|
55
|
-
const res = await fetch("https://registry.npmjs.org/agentic-relay/latest", {
|
|
56
|
-
signal: controller.signal,
|
|
57
|
-
});
|
|
58
|
-
if (!res.ok) return null;
|
|
59
|
-
const data = await res.json();
|
|
60
|
-
return data.version || null;
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
} finally {
|
|
64
|
-
clearTimeout(timer);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function apiKeySource(flagKey) {
|
|
69
|
-
if (flagKey && flagKey.trim()) return "--api-key flag";
|
|
70
|
-
if (process.env.AGENT_RELAY_API_KEY?.trim()) return "$AGENT_RELAY_API_KEY";
|
|
71
|
-
try {
|
|
72
|
-
const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
73
|
-
if (creds.api_key) return CREDENTIALS_PATH;
|
|
74
|
-
} catch {
|
|
75
|
-
/* fall through */
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const creds = JSON.parse(readFileSync(LEGACY_CREDENTIALS_PATH, "utf-8"));
|
|
79
|
-
if (creds.api_key) return `${LEGACY_CREDENTIALS_PATH} (legacy)`;
|
|
80
|
-
} catch {
|
|
81
|
-
/* fall through */
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function cmdDoctor(opts) {
|
|
87
|
-
const checks = [];
|
|
88
|
-
|
|
89
|
-
// 1. Node version
|
|
90
|
-
checks.push(checkNodeVersion(process.versions.node));
|
|
91
|
-
|
|
92
|
-
// 2. CLI version vs registry
|
|
93
|
-
let currentVersion = "unknown";
|
|
94
|
-
try {
|
|
95
|
-
currentVersion = JSON.parse(
|
|
96
|
-
readFileSync(new URL("../../package.json", import.meta.url), "utf-8"),
|
|
97
|
-
).version;
|
|
98
|
-
} catch {
|
|
99
|
-
/* keep "unknown" */
|
|
100
|
-
}
|
|
101
|
-
const latest = await fetchLatestVersion();
|
|
102
|
-
const behind = latest !== null && compareSemver(latest, currentVersion) > 0;
|
|
103
|
-
checks.push({
|
|
104
|
-
id: "version",
|
|
105
|
-
label: "CLI version",
|
|
106
|
-
status: latest === null ? "warn" : behind ? "warn" : "ok",
|
|
107
|
-
detail:
|
|
108
|
-
latest === null
|
|
109
|
-
? `${currentVersion} (couldn't reach the npm registry to compare)`
|
|
110
|
-
: behind
|
|
111
|
-
? `${currentVersion} — ${latest} is available: npm i -g agentic-relay`
|
|
112
|
-
: `${currentVersion} (latest is ${latest})`,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// 3. API key presence
|
|
116
|
-
const key = resolveApiKey(opts.apiKey || null);
|
|
117
|
-
const source = key ? apiKeySource(opts.apiKey || null) : null;
|
|
118
|
-
checks.push({
|
|
119
|
-
id: "api_key",
|
|
120
|
-
label: "API key",
|
|
121
|
-
status: key ? "ok" : "warn",
|
|
122
|
-
detail: key
|
|
123
|
-
? `configured via ${source}`
|
|
124
|
-
: `not found — set --api-key, $AGENT_RELAY_API_KEY, or ${CREDENTIALS_PATH}`,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// 4. API key live verification (skipped without a key)
|
|
128
|
-
if (key) {
|
|
129
|
-
try {
|
|
130
|
-
await apiSearch("doctor connectivity check", {
|
|
131
|
-
apiKey: key,
|
|
132
|
-
maxResults: 1,
|
|
133
|
-
timeoutMs: Math.min(opts.timeoutMs || 10000, 10000),
|
|
134
|
-
});
|
|
135
|
-
checks.push({ id: "api", label: "API connectivity", status: "ok", detail: "search responds" });
|
|
136
|
-
} catch (err) {
|
|
137
|
-
checks.push({
|
|
138
|
-
id: "api",
|
|
139
|
-
label: "API connectivity",
|
|
140
|
-
status: "fail",
|
|
141
|
-
detail: `search failed: ${err.message}`,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
checks.push({ id: "api", label: "API connectivity", status: "warn", detail: "skipped (no key)" });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 5. npm global prefix + writability
|
|
149
|
-
const prefix = npmGlobalPrefix();
|
|
150
|
-
if (!prefix) {
|
|
151
|
-
checks.push({ id: "prefix", label: "npm global prefix", status: "warn", detail: "npm not found on PATH" });
|
|
152
|
-
} else if (globalPrefixWritable(prefix)) {
|
|
153
|
-
checks.push({ id: "prefix", label: "npm global prefix", status: "ok", detail: `${prefix} (writable)` });
|
|
154
|
-
} else {
|
|
155
|
-
checks.push({
|
|
156
|
-
id: "prefix",
|
|
157
|
-
label: "npm global prefix",
|
|
158
|
-
status: "warn",
|
|
159
|
-
detail: `${prefix} is not writable — npm i -g will EACCES. Fix: ${USER_PREFIX_RECIPE.join("; ")}`,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 6. ~/.npm cache ownership
|
|
164
|
-
checks.push(
|
|
165
|
-
npmCacheOwnedByOther()
|
|
166
|
-
? {
|
|
167
|
-
id: "npm_cache",
|
|
168
|
-
label: "npm cache (~/.npm)",
|
|
169
|
-
status: "warn",
|
|
170
|
-
detail: `owned by another user — breaks npx AND npm i -g. Fix: ${CACHE_CHOWN_RECIPE}`,
|
|
171
|
-
}
|
|
172
|
-
: { id: "npm_cache", label: "npm cache (~/.npm)", status: "ok", detail: "owned by you" },
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// 7 + 8. global bin shim exists + its dir is on PATH
|
|
176
|
-
const shim = globalBinShimPath(prefix, "agentrelay");
|
|
177
|
-
const binDir = npmGlobalBinDir(prefix);
|
|
178
|
-
const shimExists = Boolean(shim) && existsSync(shim);
|
|
179
|
-
checks.push({
|
|
180
|
-
id: "global_bin",
|
|
181
|
-
label: "global `agentrelay` bin",
|
|
182
|
-
status: shimExists ? "ok" : "warn",
|
|
183
|
-
detail: shimExists ? shim : `not found in ${binDir || "npm's global bin dir"} — run: npm i -g agentic-relay`,
|
|
184
|
-
});
|
|
185
|
-
checks.push({
|
|
186
|
-
id: "path",
|
|
187
|
-
label: "global bin dir on PATH",
|
|
188
|
-
status: dirOnPath(binDir) ? "ok" : "warn",
|
|
189
|
-
detail: dirOnPath(binDir)
|
|
190
|
-
? binDir
|
|
191
|
-
: `${binDir || "npm's global bin dir"} is not on PATH — add it to your shell profile`,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// 9. working directory writable (cache + deliverables land here)
|
|
195
|
-
checks.push({
|
|
196
|
-
id: "cwd",
|
|
197
|
-
label: "working dir writable",
|
|
198
|
-
status: isDirWritable(process.cwd()) ? "ok" : "warn",
|
|
199
|
-
detail: process.cwd(),
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// 10. zip capability (informational — pure-JS, no system unzip needed)
|
|
203
|
-
checks.push({
|
|
204
|
-
id: "zip",
|
|
205
|
-
label: "deliverable unzip",
|
|
206
|
-
status: "ok",
|
|
207
|
-
detail: "pure-JS extractor (no system `unzip` required)",
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const { ok, exitCode } = summarize(checks);
|
|
211
|
-
return { checks, doctor_ok: ok, exit_code: exitCode, config_dir: join(homedir(), ".config", "agent-relay") };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const ICONS = { ok: "✓", warn: "!", fail: "✗" };
|
|
215
|
-
|
|
216
|
-
export function fmtDoctor(envelope) {
|
|
217
|
-
const lines = (envelope.checks || []).map(
|
|
218
|
-
(c) => `${ICONS[c.status] || "?"} ${c.label.padEnd(26)} ${c.detail}`,
|
|
219
|
-
);
|
|
220
|
-
lines.push(envelope.doctor_ok ? "doctor: no blocking problems found" : "doctor: blocking problems found (✗)");
|
|
221
|
-
return lines.join("\n");
|
|
222
|
-
}
|
package/bin/test/budget.test.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the `budget` command's dollar→cents conversion + validation.
|
|
3
|
-
* Run: `node --test bin/test/`.
|
|
4
|
-
*/
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { dollarsToCents } from "../lib/commands.mjs";
|
|
8
|
-
|
|
9
|
-
test("dollarsToCents: whole dollars → cents", () => {
|
|
10
|
-
assert.equal(dollarsToCents("--daily-cap", 50), 5000);
|
|
11
|
-
assert.equal(dollarsToCents("--daily-cap", "50"), 5000);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("dollarsToCents: fractional dollars round to nearest cent", () => {
|
|
15
|
-
assert.equal(dollarsToCents("--auto-approve", 2.5), 250);
|
|
16
|
-
assert.equal(dollarsToCents("--auto-approve", "0.99"), 99);
|
|
17
|
-
assert.equal(dollarsToCents("--auto-approve", 1.01), 101);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("dollarsToCents: zero is allowed (e.g. auto-approve nothing)", () => {
|
|
21
|
-
assert.equal(dollarsToCents("--auto-approve", 0), 0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("dollarsToCents: negative is a usage error", () => {
|
|
25
|
-
assert.throws(() => dollarsToCents("--daily-cap", -5), (e) => e.usage === true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("dollarsToCents: non-numeric is a usage error", () => {
|
|
29
|
-
assert.throws(() => dollarsToCents("--daily-cap", "abc"), (e) => e.usage === true);
|
|
30
|
-
assert.throws(() => dollarsToCents("--daily-cap", "5dollars"), (e) => e.usage === true);
|
|
31
|
-
});
|
package/bin/test/cache.test.mjs
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the banked-cache contract: race-safe `listSpecHeadlines` (reads
|
|
3
|
-
* spec files, no index.json) and `resolveCapability`'s spec-file id fallback
|
|
4
|
-
* (so a depth session resolves from what a breadth subagent banked). No network.
|
|
5
|
-
* Run: `node --test bin/test/cache.test.mjs`.
|
|
6
|
-
*/
|
|
7
|
-
import { test } from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { Cache } from "../lib/cache.mjs";
|
|
13
|
-
import { resolveCapability, cmdCache } from "../lib/commands.mjs";
|
|
14
|
-
|
|
15
|
-
const ISO = "2026-05-24T00:00:00.000Z";
|
|
16
|
-
|
|
17
|
-
function withCache(fn) {
|
|
18
|
-
const dir = mkdtempSync(join(tmpdir(), "agent-relay-bank-"));
|
|
19
|
-
try {
|
|
20
|
-
return fn(dir, new Cache(dir));
|
|
21
|
-
} finally {
|
|
22
|
-
rmSync(dir, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test("listSpecHeadlines: returns slug+name+summary+feature-count, no index.json needed", () => {
|
|
27
|
-
withCache((dir, cache) => {
|
|
28
|
-
cache.writeSpec("a-tool", {
|
|
29
|
-
slug: "a-tool", name: "A", summary: "does A",
|
|
30
|
-
features: [{ feature: "x", how: "y" }], agent_id: "agent-a", ts: ISO,
|
|
31
|
-
});
|
|
32
|
-
cache.writeSpec("b-tool", { slug: "b-tool", name: "B", summary: "does B", features: [], ts: ISO });
|
|
33
|
-
|
|
34
|
-
const h = cache.listSpecHeadlines();
|
|
35
|
-
assert.equal(h.length, 2);
|
|
36
|
-
const a = h.find((x) => x.slug === "a-tool");
|
|
37
|
-
assert.equal(a.name, "A");
|
|
38
|
-
assert.equal(a.summary, "does A");
|
|
39
|
-
assert.equal(a.features, 1);
|
|
40
|
-
assert.equal(a.agent_id, "agent-a");
|
|
41
|
-
// Race-safe: the put path never wrote a shared index.json.
|
|
42
|
-
assert.equal(existsSync(join(dir, "index.json")), false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("listSpecHeadlines: tolerates a spec missing the optional fields", () => {
|
|
47
|
-
withCache((_dir, cache) => {
|
|
48
|
-
cache.writeSpec("bare", { slug: "bare", ts: ISO }); // no name/summary/features
|
|
49
|
-
const h = cache.listSpecHeadlines();
|
|
50
|
-
assert.equal(h.length, 1);
|
|
51
|
-
assert.equal(h[0].slug, "bare");
|
|
52
|
-
assert.equal(h[0].summary, null);
|
|
53
|
-
assert.equal(h[0].features, null);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("resolveCapability: falls back to the banked spec's agent_id (no network)", async () => {
|
|
58
|
-
await withCache(async (dir, cache) => {
|
|
59
|
-
cache.writeSpec("a-tool", {
|
|
60
|
-
slug: "a-tool", name: "A Tool", summary: "s", features: [],
|
|
61
|
-
agent_id: "agent-a", ts: ISO,
|
|
62
|
-
});
|
|
63
|
-
// No index.json, refresh falsy → must resolve from the spec file alone.
|
|
64
|
-
const r = await resolveCapability("a-tool", { cacheDir: dir, noCache: false });
|
|
65
|
-
assert.equal(r.agentId, "agent-a");
|
|
66
|
-
assert.equal(r.slug, "a-tool");
|
|
67
|
-
assert.equal(r.name, "A Tool");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("cache put: soft contract warnings (missing session_id/summary/features), still stores", () => {
|
|
72
|
-
withCache((dir, cache) => {
|
|
73
|
-
const f = join(dir, "min.json");
|
|
74
|
-
writeFileSync(f, JSON.stringify({ slug: "bare-tool", name: "Bare" }));
|
|
75
|
-
const r = cmdCache("put", "bare-tool", { cacheDir: dir, file: f });
|
|
76
|
-
assert.equal(r.stored, true);
|
|
77
|
-
assert.equal(r.warnings.length, 3);
|
|
78
|
-
assert.match(r.warnings.join(" "), /session_id/);
|
|
79
|
-
assert.match(r.warnings.join(" "), /summary/);
|
|
80
|
-
assert.match(r.warnings.join(" "), /features/);
|
|
81
|
-
assert.ok(cache.readSpec("bare-tool")); // warned, but banked anyway
|
|
82
|
-
|
|
83
|
-
// A contract-complete spec yields no warnings.
|
|
84
|
-
writeFileSync(
|
|
85
|
-
f,
|
|
86
|
-
JSON.stringify({
|
|
87
|
-
slug: "full-tool",
|
|
88
|
-
session_id: "s-1",
|
|
89
|
-
summary: "maps the surface",
|
|
90
|
-
features: [{ feature: "x", how: "y", status: "surface_only" }],
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
const r2 = cmdCache("put", "full-tool", { cacheDir: dir, file: f });
|
|
94
|
-
assert.deepEqual(r2.warnings, []);
|
|
95
|
-
assert.ok(cache.readSpec("full-tool"));
|
|
96
|
-
|
|
97
|
-
// features[] present but entries missing status → one targeted warning.
|
|
98
|
-
writeFileSync(
|
|
99
|
-
f,
|
|
100
|
-
JSON.stringify({
|
|
101
|
-
slug: "statusless-tool",
|
|
102
|
-
session_id: "s-2",
|
|
103
|
-
summary: "mapped",
|
|
104
|
-
features: [{ feature: "x", how: "y" }, { feature: "z", how: "w", status: "coded" }],
|
|
105
|
-
}),
|
|
106
|
-
);
|
|
107
|
-
const r3 = cmdCache("put", "statusless-tool", { cacheDir: dir, file: f });
|
|
108
|
-
assert.equal(r3.warnings.length, 1);
|
|
109
|
-
assert.match(r3.warnings[0], /1 feature\(s\) missing status/);
|
|
110
|
-
});
|
|
111
|
-
});
|
package/bin/test/doctor.test.mjs
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `agentrelay doctor` (pure pieces) and the sysenv probes that are
|
|
3
|
-
* testable without touching the real npm install.
|
|
4
|
-
* Run: `node --test bin/test/doctor.test.mjs`.
|
|
5
|
-
*/
|
|
6
|
-
import { test } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { sep } from "node:path";
|
|
9
|
-
import { checkNodeVersion, summarize, fmtDoctor } from "../lib/doctor.mjs";
|
|
10
|
-
import { dirOnPath, npmGlobalBinDir, globalBinShimPath, isEphemeralBin } from "../lib/sysenv.mjs";
|
|
11
|
-
|
|
12
|
-
test("checkNodeVersion: >=18 ok, <18 fail", () => {
|
|
13
|
-
assert.equal(checkNodeVersion("18.0.0").status, "ok");
|
|
14
|
-
assert.equal(checkNodeVersion("23.11.0").status, "ok");
|
|
15
|
-
assert.equal(checkNodeVersion("16.20.2").status, "fail");
|
|
16
|
-
assert.match(checkNodeVersion("16.20.2").detail, /needs Node >= 18/);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("summarize: warns don't fail the doctor, fails do", () => {
|
|
20
|
-
const okWarn = summarize([
|
|
21
|
-
{ status: "ok" },
|
|
22
|
-
{ status: "warn" },
|
|
23
|
-
]);
|
|
24
|
-
assert.equal(okWarn.ok, true);
|
|
25
|
-
assert.equal(okWarn.exitCode, 0);
|
|
26
|
-
|
|
27
|
-
const withFail = summarize([{ status: "ok" }, { status: "fail" }]);
|
|
28
|
-
assert.equal(withFail.ok, false);
|
|
29
|
-
assert.equal(withFail.exitCode, 1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("fmtDoctor renders one line per check + a verdict line", () => {
|
|
33
|
-
const out = fmtDoctor({
|
|
34
|
-
checks: [
|
|
35
|
-
{ status: "ok", label: "Node.js version", detail: "v20.0.0" },
|
|
36
|
-
{ status: "warn", label: "API key", detail: "not found" },
|
|
37
|
-
],
|
|
38
|
-
doctor_ok: true,
|
|
39
|
-
});
|
|
40
|
-
const lines = out.split("\n");
|
|
41
|
-
assert.equal(lines.length, 3);
|
|
42
|
-
assert.match(lines[0], /^✓ /);
|
|
43
|
-
assert.match(lines[1], /^! /);
|
|
44
|
-
assert.match(lines[2], /no blocking problems/);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("dirOnPath: exact component match, trailing-slash tolerant", () => {
|
|
48
|
-
const delim = process.platform === "win32" ? ";" : ":";
|
|
49
|
-
const pathVar = ["/usr/bin", "/opt/homebrew/bin/"].join(delim);
|
|
50
|
-
assert.equal(dirOnPath("/opt/homebrew/bin", pathVar), true);
|
|
51
|
-
assert.equal(dirOnPath("/usr/bin", pathVar), true);
|
|
52
|
-
assert.equal(dirOnPath("/usr/local/bin", pathVar), false);
|
|
53
|
-
assert.equal(dirOnPath(null, pathVar), false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("npmGlobalBinDir/globalBinShimPath shape per platform", () => {
|
|
57
|
-
const prefix = process.platform === "win32" ? "C:\\npm" : "/opt/homebrew";
|
|
58
|
-
const binDir = npmGlobalBinDir(prefix);
|
|
59
|
-
const shim = globalBinShimPath(prefix, "agentrelay");
|
|
60
|
-
if (process.platform === "win32") {
|
|
61
|
-
assert.equal(binDir, prefix);
|
|
62
|
-
assert.ok(shim.endsWith("agentrelay.cmd"));
|
|
63
|
-
} else {
|
|
64
|
-
assert.equal(binDir, `${prefix}/bin`);
|
|
65
|
-
assert.ok(shim.endsWith("/bin/agentrelay"));
|
|
66
|
-
}
|
|
67
|
-
assert.equal(npmGlobalBinDir(null), null);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("isEphemeralBin flags npx-cache paths and dangling symlinks", () => {
|
|
71
|
-
const npxBin = ["", "Users", "x", ".npm", "_npx", "ab12", "node_modules", ".bin", "agentrelay"].join(sep);
|
|
72
|
-
assert.equal(isEphemeralBin(npxBin, "/tmp/pkg"), true);
|
|
73
|
-
// A path that doesn't exist → realpath throws → treated as not-usable.
|
|
74
|
-
assert.equal(isEphemeralBin("/definitely/not/a/real/bin", "/tmp/pkg"), true);
|
|
75
|
-
assert.equal(isEphemeralBin(null, "/tmp/pkg"), false);
|
|
76
|
-
});
|
package/schema/deliverable.json
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://attentionmarket-auth.vercel.app/schema/deliverable.json",
|
|
4
|
-
"title": "BankedSpec",
|
|
5
|
-
"description": "The cache contract for a banked capability spec (`.agent-relay/specs/<slug>.json`). A breadth subagent drives an SE agent to full coverage and writes this; it passes only `summary` back to the main agent, which later reads the full file (`agentrelay cache show <slug>`) when implementing. Light required spine (slug, name, summary, features) the main agent can always rely on; any additional keys the subagent finds useful are allowed.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": ["slug", "name", "summary", "features"],
|
|
8
|
-
"additionalProperties": true,
|
|
9
|
-
"properties": {
|
|
10
|
-
"slug": { "type": "string", "description": "Stable kebab-case key (also the file name)." },
|
|
11
|
-
"name": { "type": "string" },
|
|
12
|
-
"summary": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "1-3 sentences: what this capability offers + bottom line. This is what the breadth subagent returns to the main agent and what `cache ls` shows for triage."
|
|
15
|
-
},
|
|
16
|
-
"features": {
|
|
17
|
-
"type": "array",
|
|
18
|
-
"description": "The breadth detail — one entry per capability/feature the agent covered.",
|
|
19
|
-
"items": {
|
|
20
|
-
"type": "object",
|
|
21
|
-
"required": ["feature", "how"],
|
|
22
|
-
"additionalProperties": true,
|
|
23
|
-
"properties": {
|
|
24
|
-
"feature": { "type": "string", "description": "What it does." },
|
|
25
|
-
"how": { "type": "string", "description": "How to use it (endpoint/params/pattern)." },
|
|
26
|
-
"snippets": {
|
|
27
|
-
"type": "array",
|
|
28
|
-
"items": {
|
|
29
|
-
"type": "object",
|
|
30
|
-
"properties": {
|
|
31
|
-
"lang": { "type": "string" },
|
|
32
|
-
"path": { "type": "string" },
|
|
33
|
-
"desc": { "type": "string" },
|
|
34
|
-
"code": { "type": "string" }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"gotchas": { "type": "array", "items": { "type": "string" } }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"agent_id": { "type": ["string", "null"], "description": "The only id needed to open a session (ad_unit + price resolved server-side)." },
|
|
43
|
-
"ts": { "type": "string", "format": "date-time" }
|
|
44
|
-
}
|
|
45
|
-
}
|