agentradar-cli 0.3.12 → 0.3.13
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/dist/channel.js +3 -3
- package/dist/runner.js +2 -2
- package/dist/secrets.js +2 -2
- package/dist/setup.js +4 -3
- package/dist/tool-run.js +2 -2
- package/dist/update.js +15 -0
- package/dist/vendor-runner/session-frame.mjs +32 -3
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { awpEndpoint, envValue } from "./tool-run.js";
|
|
10
|
-
import { noteCliUpdate } from "./update.js";
|
|
10
|
+
import { noteCliUpdate, refusedLine } from "./update.js";
|
|
11
11
|
import { trustedMcpEndpoint } from "./setup.js";
|
|
12
12
|
import { syncJoinedSkills } from "./skill-install.js";
|
|
13
13
|
export const CHANNEL_GUIDANCE = "AgentRadar channel messages carry your new events in full, in order, one per line, each with its own next step: act on each line as it says. Run `agentradar run $AR_CLI_RUNNER_ID inbox next` only when a channel message tells you to. Answer a liveness check with `agentradar run $AR_CLI_RUNNER_ID liveness_ack --nonce <nonce>`. Never acknowledge an event you have not read. Never poll on your own or keep a loop running: when a channel message arrives, drain it, then end your turn. Only you reply or act as this agent: a sub-agent or teammate returns its result to you, and you send it.";
|
|
@@ -143,12 +143,12 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
143
143
|
await noteCliUpdate(response.headers, logError);
|
|
144
144
|
if (!response.ok) {
|
|
145
145
|
const said = await response.json().then((body) => body.error?.message).catch(() => null);
|
|
146
|
-
throw new Error(said ??
|
|
146
|
+
throw new Error(said ?? refusedLine("the channel call", response.status));
|
|
147
147
|
}
|
|
148
148
|
const text = await response.text();
|
|
149
149
|
const json = text.trim().startsWith("{") ? text : text.split("\n").find((line) => line.startsWith("data:"))?.slice(5);
|
|
150
150
|
if (!json)
|
|
151
|
-
throw new Error("
|
|
151
|
+
throw new Error("AgentRadar answered the channel call with no JSON-RPC body. Nothing was done. Now: run `agentradar upgrade`, then restart the channel.");
|
|
152
152
|
return JSON.parse(json);
|
|
153
153
|
};
|
|
154
154
|
const next = (args = {}) => remote("tools/call", { name: "next_event", arguments: { ...args, sessionId } });
|
package/dist/runner.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { claimPendingJoin, removeAgentService, shellValue, trustedMcpEndpoint, writePrivateFile } from "./setup.js";
|
|
8
8
|
import { callAwpTool, envValue } from "./tool-run.js";
|
|
9
|
-
import { cliHeader, installedVersion, noteCliUpdate, revertCli, runningVersion, selfUpdate } from "./update.js";
|
|
9
|
+
import { cliHeader, installedVersion, noteCliUpdate, refusedLine, revertCli, runningVersion, selfUpdate } from "./update.js";
|
|
10
10
|
const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
11
11
|
const ENGAGEMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
12
|
// Per-hire bearers are ordinary agent keys (`pcp_…`); the runner's own is an AWP key (`ar_…`).
|
|
@@ -332,7 +332,7 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
|
|
|
332
332
|
const claim = async () => {
|
|
333
333
|
const response = await post("agent-hires/bearers/claim");
|
|
334
334
|
if (!response.ok)
|
|
335
|
-
throw new Error(
|
|
335
|
+
throw new Error(await response.json().then((b) => typeof b.error === "string" ? b.error : null).catch(() => null) ?? refusedLine("the hire claim", response.status));
|
|
336
336
|
const { hires: claimed } = await response.json();
|
|
337
337
|
const written = [];
|
|
338
338
|
for (const hire of claimed) {
|
package/dist/secrets.js
CHANGED
|
@@ -15,7 +15,7 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { awpRequest, envValue } from "./tool-run.js";
|
|
18
|
-
import { cliHeader, noteCliUpdate } from "./update.js";
|
|
18
|
+
import { cliHeader, noteCliUpdate, refusedLine } from "./update.js";
|
|
19
19
|
export const SECRETS_FILE = join(".agentradar", "secrets.enc");
|
|
20
20
|
export const SECRETS_USAGE = "Usage: agentradar secrets pull|renew|env <runner-id> | agentradar secrets exec <runner-id> -- <command> [args...]"
|
|
21
21
|
+ " | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--only <NAME,...>] [--replace --approval-details '<json>'] [--token <token>]"
|
|
@@ -40,7 +40,7 @@ async function post(runnerId, path, body, deps) {
|
|
|
40
40
|
await noteCliUpdate(res.headers, deps.logError);
|
|
41
41
|
const json = await res.json().catch(() => ({}));
|
|
42
42
|
if (!res.ok)
|
|
43
|
-
throw new Error(json.error ??
|
|
43
|
+
throw new Error(json.error ?? refusedLine("the secrets request", res.status));
|
|
44
44
|
return json;
|
|
45
45
|
}
|
|
46
46
|
/**
|
package/dist/setup.js
CHANGED
|
@@ -924,9 +924,10 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
924
924
|
};
|
|
925
925
|
const restartInPlace = (runnerId) => {
|
|
926
926
|
const config = join(home, ".agentradar", "config", runnerId);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
927
|
+
const sh = (v) => /^[\w@%+=:,./-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
|
|
928
|
+
return `restart your agent ONCE with this one command, which already keeps your conversation (--continue); if you normally launch claude with other flags, add them:
|
|
929
|
+
env AR_CLI_RUNNER_ID=${sh(runnerId)} claude --continue --mcp-config ${sh(join(config, "awp-only.json"))} --dangerously-load-development-channels server:agentradar --settings ${sh(join(config, "agentradar-settings.json"))}
|
|
930
|
+
Accept the one-time development-channel prompt when it appears. To stop: agentradar leave ${sh(runnerId)}, then launch without those flags.`;
|
|
930
931
|
};
|
|
931
932
|
const installService = deps.installService ?? ((id) => installAgentService(id, home, env));
|
|
932
933
|
const connected = (runnerId, projectPath, approvedBypass, bypass) => {
|
package/dist/tool-run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, realpath, stat, utimes, writeFile } from "node:fs/prom
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { hostIsTeammate } from "./channel.js";
|
|
5
|
-
import { cliHeader, noteCliUpdate, withCliUpdate } from "./update.js";
|
|
5
|
+
import { cliHeader, noteCliUpdate, refusedLine, withCliUpdate } from "./update.js";
|
|
6
6
|
const UNAUTHORIZED = "AgentRadar refused the tool call (401): this agent's key is no longer valid on this server (it was revoked, or the hire it was issued for has ended). Nothing was done. Stop calling tools with this key and tell the human who runs you.";
|
|
7
7
|
/**
|
|
8
8
|
* One `KEY='value'` line as `setup.ts` wrote it, single-quoted with `'` escaped as `'\''`.
|
|
@@ -657,7 +657,7 @@ async function postAwp(url, bearer, method, params, deps) {
|
|
|
657
657
|
if (!response.ok) {
|
|
658
658
|
// 426 says how to upgrade; 401 says why the key was refused and how to re-join.
|
|
659
659
|
const said = await response.json().then((body) => body.error?.message).catch(() => null);
|
|
660
|
-
logError(said ?? (response.status === 401 ? UNAUTHORIZED :
|
|
660
|
+
logError(said ?? (response.status === 401 ? UNAUTHORIZED : refusedLine("the tool call", response.status)));
|
|
661
661
|
return null;
|
|
662
662
|
}
|
|
663
663
|
const body = await response.json();
|
package/dist/update.js
CHANGED
|
@@ -21,6 +21,21 @@ export const runningVersion = async () => JSON.parse(await readFile(join(PACKAGE
|
|
|
21
21
|
* and as `x-agentradar-surface` so it can count which client a call came from (MCPR-1).
|
|
22
22
|
*/
|
|
23
23
|
export const cliHeader = async (surface = "cli") => ({ "x-agentradar-cli": await runningVersion().catch(() => ""), "x-agentradar-surface": surface });
|
|
24
|
+
/**
|
|
25
|
+
* CLI-VERSION-GATE (Vadym 12835): the server names its newest CLI on every reply (`x-agentradar-cli-latest`). When this one
|
|
26
|
+
* is behind, the command prints ONE warning line on stderr and its JSON output carries `cliUpdate` (`withCliUpdate`).
|
|
27
|
+
* A CLI below the server's minimum is refused with 426 and the same command, so an agent can always fix itself.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Agent's-eye (Vadym 13105): a bare status says nothing. A refusal the server gave no sentence for names what was refused,
|
|
31
|
+
* says nothing was done, and the one step to take. A 401 means the key is no good here; a 429 or 5xx means the server is busy or
|
|
32
|
+
* down (an upgrade would not help: wait and retry); any other status may be an old CLI.
|
|
33
|
+
*/
|
|
34
|
+
export const refusedLine = (what, status) => status === 401
|
|
35
|
+
? `AgentRadar refused ${what} (401): this agent's key is not valid on this server. Nothing was done. Now: tell the human who runs you, then run \`npx agentradar run <join-link>\` with the new join link they give you.`
|
|
36
|
+
: status === 429 || status >= 500
|
|
37
|
+
? `AgentRadar is unavailable for ${what} (${status}). Nothing was done. Now: wait a minute and run the same command again; if it still fails, tell the human who runs you.`
|
|
38
|
+
: `AgentRadar refused ${what} (${status}) and gave no reason. Nothing was done. Now: run \`agentradar upgrade\`, then run the same command again; if it is refused again, tell the human who runs you.`;
|
|
24
39
|
let behind = null;
|
|
25
40
|
export const forgetCliUpdate = () => { behind = null; };
|
|
26
41
|
export async function noteCliUpdate(headers, logError = console.error) {
|
|
@@ -57,12 +57,15 @@ const NO_TEXT = (run, source, turn, evId) => {
|
|
|
57
57
|
/**
|
|
58
58
|
* FRAME-CATCHUP-STRUCTURED: the catch-up step, built only from next_event's structured `catchUp` (catch-up.ts CatchUp) and never from
|
|
59
59
|
* guidance text, where a name or title could forge a "Now: catch up" line (qa-sec MED #2679). Only counts, a normalized time, this
|
|
60
|
-
* wake's own conversation id
|
|
60
|
+
* wake's own conversation id, a base64url cursor and, per file, its plain-character name and type and a plain id reach the text,
|
|
61
|
+
* so nothing in it can carry a command. Mirrors catchUpLine.
|
|
61
62
|
*/
|
|
62
63
|
function catchUpStep(c, conversationId, run) {
|
|
63
64
|
const count = (v) => Number.isSafeInteger(v) && v >= 0 ? v : null;
|
|
64
65
|
const total = count(c?.newMessages), nonMembers = count(c?.nonMembers) ?? 0, edited = count(c?.edited) ?? 0;
|
|
65
|
-
|
|
66
|
+
const slack = slackThreadStep(c?.slackThread, total ?? 0, c?.more === true, run);
|
|
67
|
+
if ((!total && !slack) || /["`$\\]/.test(conversationId)) return null;
|
|
68
|
+
if (!total) return `Now: read this thread first, then act on it.${slack}`;
|
|
66
69
|
const seen = c.lastAckAt && !Number.isNaN(Date.parse(c.lastAckAt)) ? new Date(c.lastAckAt).toISOString().slice(0, 16).replace("T", " ") : null;
|
|
67
70
|
const after = typeof c.readHistoryAfter === "string" && /^[A-Za-z0-9_-]+$/.test(c.readHistoryAfter) ? c.readHistoryAfter : null;
|
|
68
71
|
const n = `${total}${c.more === true ? "+" : ""}`, one = total === 1 && c.more !== true;
|
|
@@ -73,7 +76,33 @@ function catchUpStep(c, conversationId, run) {
|
|
|
73
76
|
? ` ${edited === 1 ? "One of them is a message" : `${edited} of them are messages`} you had already seen that the sender EDITED or deleted since: `
|
|
74
77
|
+ `an edited line is marked "(EDITED <time> ...)" and a deleted one is no longer listed. Answer from the new text, never from the version you read before.`
|
|
75
78
|
: "";
|
|
76
|
-
return `Now: catch up with the thread first (${what}${strangers}) with ${how}, then act on it.${edits}`;
|
|
79
|
+
return `Now: catch up with the thread first (${what}${strangers}) with ${how}, then act on it.${edits}${catchUpFiles(c.attachments, run)}${slack}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** SLACK-THREAD-BACKFILL: catch-up.ts slackThreadText, byte for byte; the ids are re-checked here so only Slack-shaped ids reach the text. */
|
|
83
|
+
function slackThreadStep(t, held, more, run) {
|
|
84
|
+
if (!t || !/^[CG][A-Z0-9]{2,20}$/.test(String(t.channel)) || !/^\d{10}\.\d{6}$/.test(String(t.threadTs))) return "";
|
|
85
|
+
const has = held ? `${held}${more ? "+" : ""} earlier message${held === 1 && !more ? "" : "s"} of it so far` : "none of its earlier messages yet";
|
|
86
|
+
return ` This Slack thread began before you were tagged in it, and your Inbox holds ${has}.`
|
|
87
|
+
+ ` Read the whole thread from Slack now: \`${run} slack_history --args '{"channel":"${t.channel}","threadTs":"${t.threadTs}"}'\`;`
|
|
88
|
+
+ ` for more context, read the channel around it with the same command without "threadTs".`
|
|
89
|
+
+ ` Slack gives 15 messages and about one read a minute: pass the "nextCursor" it returns as "cursor" for the next 15, and if it answers "ratelimited", wait a minute and run it again.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** ATTACHMENT-MARKER: the files in the catch-up, as catch-up.ts catchUpFilesText writes them (kept byte for byte by catch-up-cta-realpg). */
|
|
93
|
+
function catchUpFiles(files, run) {
|
|
94
|
+
if (!Array.isArray(files) || !files.length) return "";
|
|
95
|
+
const plainId = (v) => typeof v === "string" && /^[\w-]{1,256}$/.test(v);
|
|
96
|
+
const sizeText = (n) => typeof n !== "number" || !Number.isFinite(n) || n <= 0 ? "" : n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`;
|
|
97
|
+
const list = files.slice(0, 5).map((f, i) => {
|
|
98
|
+
const name = String(f?.name ?? "").replace(/[^\p{L}\p{N} ._'-]/gu, "").slice(0, 60).trim();
|
|
99
|
+
const kind = String(f?.kind ?? "").split(";")[0].replace(/[^\w/+.-]/g, "").slice(0, 60) || "file";
|
|
100
|
+
const cmd = plainId(f?.eventId) && plainId(f?.id) ? ` - download: \`${run} download_attachment --args '{"eventId":"${f.eventId}","attachmentId":"${f.id}"}'\`` : "";
|
|
101
|
+
return `[Attachment ${i + 1}: ${kind}${name ? ` "${name}"` : ""}${sizeText(f?.size) ? ` ${sizeText(f.size)}` : ""}${plainId(f?.id) ? `, id ${f.id}` : ""}]${cmd}`;
|
|
102
|
+
}).join(" ");
|
|
103
|
+
const more = files.length > 5 ? ` and ${files.length - 5} more (\`${run} read_history\` lists each with its command)` : "";
|
|
104
|
+
return ` ${files.length === 1 ? "One of them carries a file" : `They carry ${files.length} files`}: ${list}${more}. `
|
|
105
|
+
+ `MUST download a file before you answer anything about it (a photo or document someone "posted above" is one of these); a message with no caption is not an empty message.`;
|
|
77
106
|
}
|
|
78
107
|
|
|
79
108
|
const NO_REPLY_SOURCES = new Set(["moderator", "approval", "agentradar", undefined, null, ""]);
|