agentradar-cli 0.2.10 → 0.3.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/dist/agent-guard/hooks/hooks.json +2 -1
- package/dist/agent-guard/scripts/guard.mjs +5 -1
- package/dist/agentradar-vendor/SKILL.md +3 -1
- package/dist/channel.js +4 -1
- package/dist/commands.js +74 -0
- package/dist/index.js +81 -6
- package/dist/skill-install.js +21 -1
- package/dist/tool-run.js +135 -11
- package/dist/update.js +1 -1
- package/dist/vault-clone.js +59 -0
- package/dist/vault-import.js +135 -0
- package/dist/vendor-runner/feeder.mjs +35 -2
- package/dist/vendor-runner/session-frame.mjs +1 -0
- package/dist/vendor-runner/supervisor/supervise-agent.sh +34 -5
- package/package.json +4 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "
|
|
2
|
+
"description": "Marks the session as guarded (the runner reports ready only then); blocks going idle while AgentRadar work is owed; reminds of the house rules every AR_DISCIPLINE_EVERY turns; guards AgentRadar compact-then-switch model changes (a crashed PreModelSwitch hook exits 2 and refuses).",
|
|
3
3
|
"hooks": {
|
|
4
|
+
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" start", "timeout": 10 }] }],
|
|
4
5
|
"Stop": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" stop", "timeout": 30 }] }],
|
|
5
6
|
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" prompt", "timeout": 10 }] }],
|
|
6
7
|
"PreCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreCompact", "timeout": 30 }] }],
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// blocks the stop while a message or task is unacknowledged, at most MAX_BLOCKS times in a row.
|
|
7
7
|
// Tasks still in progress get one reminder. Anything unexpected (no runner, server down, bad
|
|
8
8
|
// output) lets the agent stop: a guard that wedges the session is worse than no guard.
|
|
9
|
+
// start: writes AR_GUARD_MARKER, the runner's proof that this session loaded the guard: its feeder and
|
|
10
|
+
// channel report "starting", never "ready", until the file exists (AGENT-GUARD-ACTIVE, Vadym 10699).
|
|
9
11
|
// prompt: every AR_DISCIPLINE_EVERY turns (default 20, 0 = off) re-adds the house rules, the must-nots
|
|
10
12
|
// and the installed skills to the context.
|
|
11
13
|
import { spawnSync } from "node:child_process";
|
|
@@ -58,7 +60,9 @@ function skills() {
|
|
|
58
60
|
return [...names].sort();
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
if (process.argv[2] === "
|
|
63
|
+
if (process.argv[2] === "start") {
|
|
64
|
+
if (process.env.AR_GUARD_MARKER) writeFileSync(process.env.AR_GUARD_MARKER, `${input.session_id ?? ""}\n`);
|
|
65
|
+
} else if (process.argv[2] === "prompt") {
|
|
62
66
|
const every = Number.parseInt(process.env.AR_DISCIPLINE_EVERY ?? "20", 10);
|
|
63
67
|
// No reset of `blocks` here: Codex may run this hook for a Stop continuation, which would lift the cap.
|
|
64
68
|
state.turns += 1;
|
|
@@ -20,9 +20,11 @@ You work on the vendor's computer and connect to AgentRadar through the `agentra
|
|
|
20
20
|
|
|
21
21
|
Call `get_my_context` on the AgentRadar MCP FIRST, before any other tool. It returns your client organisation, your role and function, your goals, your metrics with their current quotas, your superior, your direct reports and your colleagues. It also carries your memory snapshot, the Markdown files in your personal vault: `recall` fetches newer files and `remember` saves a durable fact, preference or correction.
|
|
22
22
|
|
|
23
|
+
Keep each person's page in company memory current, every fact cited, as the People section of the `agentradar-memory-and-skills` skill says. A page is knowledge, never permission.
|
|
24
|
+
|
|
23
25
|
In your first session, call `read_onboarding_obligations` once (reading them acknowledges the safety rules), then `read_readiness`.
|
|
24
26
|
|
|
25
|
-
Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own with `report_metric
|
|
27
|
+
Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own, and your subordinates' when needed, with `report_metric`: only true numbers, in good faith, each backed by `evidence` of what you counted it from, or by `fromEventId` when a person told you the number. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and anyone below you on the org chart — only for real trackable work, never for chat. You also manage your subordinates' tasks and repeated tasks (`update_task`, `update_recurring_task`, `stop_recurring_task`) and their metrics: edit a definition (`edit_metric`) or remove a wrong value with a reason (`void_metric_record`).
|
|
26
28
|
|
|
27
29
|
## Talking to people
|
|
28
30
|
|
package/dist/channel.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { homedir } from "node:os";
|
|
@@ -78,7 +79,9 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
78
79
|
if (unconfirmed === "reported" && agentPolled)
|
|
79
80
|
unconfirmed = null;
|
|
80
81
|
// While reported, no runnerState: the stored state stands, and the poll still drains for wake signals.
|
|
81
|
-
|
|
82
|
+
// AGENT-GUARD-ACTIVE: the supervisor's marker, written by agent-guard's SessionStart hook in this session.
|
|
83
|
+
const guarded = !process.env.AR_GUARD_MARKER || existsSync(process.env.AR_GUARD_MARKER);
|
|
84
|
+
const response = await next(unconfirmed ? {} : { runnerState: guarded ? "ready" : "starting" });
|
|
82
85
|
if (response.error || response.result?.isError)
|
|
83
86
|
throw new Error("AgentRadar refused next_event; check the runner key and its poll permission");
|
|
84
87
|
const content = response.result?.content;
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vadym 9970 (CLI-FIRST): every agent-facing MCP tool is also a CLI command, `agentradar run <runner-id> <noun> <verb>`,
|
|
3
|
+
* in AgentRadar terms. The tree is derived from the server's own tools/list at call time, so it cannot fall behind
|
|
4
|
+
* the MCP; this file only says how a tool name reads as a noun and a verb.
|
|
5
|
+
*/
|
|
6
|
+
/** Nouns a tool name is grouped under, most specific first. A tool takes the first one its name contains. */
|
|
7
|
+
const NOUNS = [
|
|
8
|
+
"inbox", "thread", "message", "task", "metric", "quota", "vault", "memory", "skill", "escalation", "incident",
|
|
9
|
+
"karma", "wallet", "spend", "budget", "usage", "browser", "email", "telegram", "slack", "discord", "whatsapp",
|
|
10
|
+
"github", "model", "outcome", "role", "org_chart", "org", "project", "integration", "channel", "peer",
|
|
11
|
+
"subordinate", "secret", "service", "onboarding", "intervention", "marketplace", "hire", "egress", "feedback",
|
|
12
|
+
"contact", "placement", "mcp", "tool", "join_link", "handoff", "selftest", "readiness", "pane", "brief",
|
|
13
|
+
"teammate", "approval", "agent",
|
|
14
|
+
];
|
|
15
|
+
const PLURAL = {
|
|
16
|
+
messages: "message", tasks: "task", metrics: "metric", vaults: "vault", skills: "skill", escalations: "escalation",
|
|
17
|
+
threads: "thread", channels: "channel", peers: "peer", subordinates: "subordinate", integrations: "integration",
|
|
18
|
+
secrets: "secret", agents: "agent", interventions: "intervention", events: "event", retrospectives: "retrospective",
|
|
19
|
+
};
|
|
20
|
+
/** Where the rule above would put a tool somewhere an agent would not look for it. */
|
|
21
|
+
const OVERRIDES = {
|
|
22
|
+
next_event: ["inbox", "next"],
|
|
23
|
+
mark_messages_read: ["inbox", "mark-read"],
|
|
24
|
+
search_history: ["thread", "history"],
|
|
25
|
+
reply: ["thread", "reply"],
|
|
26
|
+
read_history: ["thread", "read"],
|
|
27
|
+
mark_read: ["thread", "mark-read"],
|
|
28
|
+
expect_reply: ["thread", "expect-reply"],
|
|
29
|
+
clear_reply_allowance: ["thread", "clear-reply-allowance"],
|
|
30
|
+
request_reply_clearance: ["thread", "request-reply-clearance"],
|
|
31
|
+
github_thread: ["github", "thread"],
|
|
32
|
+
post_escalation_discussion_message: ["escalation", "post-message"],
|
|
33
|
+
read_escalation_discussion: ["escalation", "read-discussion"],
|
|
34
|
+
get_self: ["agent", "self"],
|
|
35
|
+
get_my_context: ["agent", "context"],
|
|
36
|
+
liveness_ack: ["agent", "liveness-ack"],
|
|
37
|
+
remember: ["memory", "remember"],
|
|
38
|
+
recall: ["memory", "recall"],
|
|
39
|
+
my_skills: ["skill", "mine"],
|
|
40
|
+
list_my_threads: ["thread", "list"],
|
|
41
|
+
list_my_metrics: ["metric", "list"],
|
|
42
|
+
read_my_usage: ["usage", "read"],
|
|
43
|
+
// #496 (APPROVAL-ASK): the comment thread on an approval, and the decision it acknowledges.
|
|
44
|
+
approval_comments: ["approval", "comments"],
|
|
45
|
+
approval_comment: ["approval", "comment"],
|
|
46
|
+
acknowledge_decision: ["approval", "acknowledge"],
|
|
47
|
+
};
|
|
48
|
+
/** One-release aliases: still on the MCP, never given a command of their own. */
|
|
49
|
+
export const DEPRECATED_TOOLS = new Set(["acknowledge_messages"]);
|
|
50
|
+
/** `<noun> <verb>` for a tool name. */
|
|
51
|
+
export function commandFor(tool) {
|
|
52
|
+
const fixed = OVERRIDES[tool];
|
|
53
|
+
if (fixed)
|
|
54
|
+
return fixed;
|
|
55
|
+
// Plurals fold only to FIND the noun (list_tasks is a task command); the verb keeps the tool's own words, so
|
|
56
|
+
// slack_list_channels reads "list-channels" as slack_list_users reads "list-users".
|
|
57
|
+
const raw = tool.split("_");
|
|
58
|
+
const words = raw.map((word) => PLURAL[word] ?? word);
|
|
59
|
+
for (const noun of NOUNS) {
|
|
60
|
+
const parts = noun.split("_");
|
|
61
|
+
for (let i = 0; i + parts.length <= words.length; i++) {
|
|
62
|
+
if (parts.every((part, k) => words[i + k] === part)) {
|
|
63
|
+
const rest = [...raw.slice(0, i), ...raw.slice(i + parts.length)];
|
|
64
|
+
return [noun.replace(/_/g, "-"), rest.join("-") || "get"];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return ["agent", raw.join("-")];
|
|
69
|
+
}
|
|
70
|
+
/** The tool a `<noun> <verb>` names among the server's tools, or undefined. */
|
|
71
|
+
export function toolFor(noun, verb, tools) {
|
|
72
|
+
return tools.find((tool) => !DEPRECATED_TOOLS.has(tool) && commandFor(tool).join(" ") === `${noun} ${verb}`);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=commands.js.map
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { installVendorSkill } from "./skill-install.js";
|
|
|
4
4
|
import { startChannel } from "./channel.js";
|
|
5
5
|
import { runRestartSelftest, startVendorRunner } from "./runner.js";
|
|
6
6
|
import { cancelJoin, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
7
|
-
import { callAwpTool, callToolOverCli, createTaskOverCli, listServiceToolsOverCli, parseCallArgs, runServiceToolOverCli } from "./tool-run.js";
|
|
7
|
+
import { callAwpTool, callToolOverCli, createTaskOverCli, INBOX_SEARCH_USAGE, inboxSearchOverCli, LISTING_DESCRIBE_USAGE, listServiceToolsOverCli, parseCallArgs, PERMISSION_VERB_TOOLS, runServiceToolOverCli, TASK_VERB_TOOLS } from "./tool-run.js";
|
|
8
8
|
import { installSyncTimer, syncAgentDir } from "./sync.js";
|
|
9
|
+
import { importVaultOverCli } from "./vault-import.js";
|
|
10
|
+
import { cloneVaultOverCli } from "./vault-clone.js";
|
|
9
11
|
// `run` is the one prefix every agent-facing command shares, so one harness "always allow agentradar run"
|
|
10
12
|
// covers a whole session: `run <link>` (or `run join <link>`) joins, `run <runner-id> <tool>` calls a tool.
|
|
11
13
|
export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
@@ -15,7 +17,28 @@ export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
|
15
17
|
async function main() {
|
|
16
18
|
const args = runArgs(process.argv.slice(2));
|
|
17
19
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
18
|
-
console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
20
|
+
console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request <runner-id> [--<argument> <value> ...] | agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write] | agentradar vault clone <runner-id> <vault-id> [dir] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
21
|
+
console.log([
|
|
22
|
+
"agentradar run <runner-id> <noun> <verb> [--<argument> <value> ...] [--dry-run] [--json]",
|
|
23
|
+
" Every agent tool, grouped in AgentRadar terms: inbox, thread, message, task, metric, vault, ...",
|
|
24
|
+
" agentradar run <runner-id> --help the command groups",
|
|
25
|
+
" agentradar run <runner-id> thread --help a group's verbs",
|
|
26
|
+
" agentradar run <runner-id> thread reply --help one command's arguments",
|
|
27
|
+
" Examples:",
|
|
28
|
+
" agentradar run r1 inbox next",
|
|
29
|
+
" agentradar run r1 thread reply --thread-id <id> --text \"On it\"",
|
|
30
|
+
" agentradar run r1 inbox ack --message-ids <id> --message-ids <id>",
|
|
31
|
+
" agentradar run r1 task create --title \"Draft the brief\" --dry-run",
|
|
32
|
+
" --dry-run prints the exact call and sends nothing. Output is JSON. Exit codes: 0 done; 1 refused or",
|
|
33
|
+
" unreachable (reason on stderr); 2 the command was wrong.",
|
|
34
|
+
"agentradar upgrade install the latest agentradar-cli",
|
|
35
|
+
].join("\n"));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (args.length === 1 && args[0] === "upgrade") {
|
|
39
|
+
const { selfUpdate, runningVersion } = await import("./update.js");
|
|
40
|
+
const installed = await selfUpdate();
|
|
41
|
+
console.log(installed ? `agentradar upgraded to ${installed}` : `agentradar ${await runningVersion()} is the latest`);
|
|
19
42
|
return;
|
|
20
43
|
}
|
|
21
44
|
if (args[0] === "leave" && args.length === 2) {
|
|
@@ -111,13 +134,65 @@ async function main() {
|
|
|
111
134
|
: await runServiceToolOverCli(parsed.runnerId, parsed.tool, parsed.json);
|
|
112
135
|
return;
|
|
113
136
|
}
|
|
114
|
-
if (args[0] === "
|
|
137
|
+
if (args[0] === "vault" && args[1] === "import") {
|
|
138
|
+
const [runnerId, dir, ...rest] = args.slice(2);
|
|
139
|
+
const flag = (name) => { const at = rest.indexOf(`--${name}`); return at >= 0 ? rest[at + 1] : undefined; };
|
|
140
|
+
if (!runnerId || !dir || rest.some((arg) => arg.startsWith("--") && !["--vault", "--source", "--write"].includes(arg))) {
|
|
141
|
+
console.error("Usage: agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write]");
|
|
142
|
+
process.exitCode = 2;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
process.exitCode = await importVaultOverCli(runnerId, dir, { vaultId: flag("vault"), source: flag("source"), write: rest.includes("--write") });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (args[0] === "vault" && args[1] === "clone") {
|
|
149
|
+
if (args.length < 4 || args.length > 5) {
|
|
150
|
+
console.error("Usage: agentradar vault clone <runner-id> <vault-id> [dir]");
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
process.exitCode = await cloneVaultOverCli(args[2], args[3], args[4]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (args[0] === "inbox" && args[1] === "search") {
|
|
158
|
+
const parsed = parseCallArgs([...args.slice(2).filter((arg) => arg !== "--json"), "inbox_search"]);
|
|
159
|
+
if (!parsed || parsed.tool !== "inbox_search") {
|
|
160
|
+
console.error(INBOX_SEARCH_USAGE);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
process.exitCode = await inboxSearchOverCli(parsed.runnerId, parsed.flags, args.includes("--json"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (args[0] === "listing" && args[1] === "describe") {
|
|
168
|
+
// UAT U1: the agent's public catalog description, the MCP `set_listing_description` over the same endpoint and key.
|
|
169
|
+
const parsed = parseCallArgs([...args.slice(2), "set_listing_description"]);
|
|
170
|
+
if (!parsed || !parsed.flags.some(([flag]) => flag === "capabilities")) {
|
|
171
|
+
console.error(LISTING_DESCRIBE_USAGE);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
process.exitCode = await callToolOverCli(parsed.runnerId, parsed.tool, undefined, {}, parsed.flags);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (args[0] === "permissions" && args[1] && Object.hasOwn(PERMISSION_VERB_TOOLS, args[1])) {
|
|
179
|
+
const tool = PERMISSION_VERB_TOOLS[args[1]];
|
|
180
|
+
const parsed = parseCallArgs([...args.slice(2), tool]);
|
|
181
|
+
if (!parsed || parsed.tool !== tool) {
|
|
182
|
+
console.error("Usage: agentradar permissions mine|roles|set|request <runner-id> [--<argument> <value> ...]");
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
process.exitCode = await callToolOverCli(parsed.runnerId, tool, parsed.json, {}, parsed.flags);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (args[0] === "task" && args[1] && Object.hasOwn(TASK_VERB_TOOLS, args[1])) {
|
|
115
190
|
if (args.length !== 5 || args[3] !== "--args") {
|
|
116
|
-
console.error("Usage: agentradar task create|repeat <runner-id> --args '<json>'");
|
|
191
|
+
console.error("Usage: agentradar task create|repeat|make-recurring <runner-id> --args '<json>'");
|
|
117
192
|
process.exitCode = 1;
|
|
118
193
|
return;
|
|
119
194
|
}
|
|
120
|
-
process.exitCode = await createTaskOverCli(args[2], args[1]
|
|
195
|
+
process.exitCode = await createTaskOverCli(args[2], args[1], args[4]);
|
|
121
196
|
return;
|
|
122
197
|
}
|
|
123
198
|
if (args.length === 2 && args[0] === "skill" && args[1] === "install") {
|
|
@@ -142,7 +217,7 @@ async function main() {
|
|
|
142
217
|
return;
|
|
143
218
|
}
|
|
144
219
|
const help = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
|
|
145
|
-
(help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
220
|
+
(help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request <runner-id> [--<argument> <value> ...] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
146
221
|
if (!help)
|
|
147
222
|
process.exitCode = 1;
|
|
148
223
|
}
|
package/dist/skill-install.js
CHANGED
|
@@ -121,12 +121,32 @@ export async function writeManagedSkills(skillsDir, skills) {
|
|
|
121
121
|
* Codex `$CODEX_HOME/skills`. Done here because a session that acts only through `agentradar run` has no
|
|
122
122
|
* feeder; where the service runs, its feeder keeps them current.
|
|
123
123
|
*/
|
|
124
|
+
const BRIEF_MARK = "<!-- Written by AgentRadar and replaced on every sync; edits here are lost. -->";
|
|
125
|
+
/** Replaces the file itself, never a symlink's target, and never a file it did not write (a vendor's AGENTS.md). */
|
|
126
|
+
async function writeBrief(file, text) {
|
|
127
|
+
const current = await lstat(file).catch(() => null);
|
|
128
|
+
if (current && (!current.isFile() || !(await readFile(file, "utf8")).startsWith(BRIEF_MARK)))
|
|
129
|
+
return;
|
|
130
|
+
await mkdir(dirname(file), { recursive: true });
|
|
131
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
132
|
+
await writeFile(temporary, `${BRIEF_MARK}\n${text.trim()}\n`);
|
|
133
|
+
await rename(temporary, file);
|
|
134
|
+
}
|
|
124
135
|
export async function syncJoinedSkills(runnerId, deps) {
|
|
125
136
|
const env = await readFile(join(deps.home, ".agentradar", "agents", `${runnerId}.env`), "utf8");
|
|
126
137
|
const root = envValue(env, "AR_AGENT_HARNESS") === "codex" ? envValue(env, "CODEX_HOME") : envValue(env, "AR_BASE");
|
|
127
138
|
if (!root)
|
|
128
139
|
return;
|
|
129
|
-
const
|
|
140
|
+
const codex = envValue(env, "AR_AGENT_HARNESS") === "codex";
|
|
141
|
+
const skillsDir = codex ? join(root, "skills") : join(root, ".claude", "skills");
|
|
142
|
+
// Vadym 9988: the server's start instructions with this agent's brief, before its first session starts.
|
|
143
|
+
// Same file and mark as the runner's feeder, which keeps it fresh from then on.
|
|
144
|
+
const init = await awpRequest(runnerId, "initialize", {
|
|
145
|
+
protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "agentradar-cli", version: "1.0.0" },
|
|
146
|
+
}, { home: deps.home, fetch: deps.fetch, logError: deps.log });
|
|
147
|
+
const briefFile = codex ? join(root, "AGENTS.md") : join(root, ".claude", "rules", "agentradar-brief.md");
|
|
148
|
+
if (init?.result?.instructions)
|
|
149
|
+
await writeBrief(briefFile, init.result.instructions);
|
|
130
150
|
const body = await awpRequest(runnerId, "tools/call", { name: "my_skills", arguments: {} }, { home: deps.home, fetch: deps.fetch, logError: deps.log });
|
|
131
151
|
const text = body?.result?.content?.find((c) => c.type === "text")?.text;
|
|
132
152
|
const answer = (text ? JSON.parse(text) : {});
|
package/dist/tool-run.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { commandFor, DEPRECATED_TOOLS, toolFor } from "./commands.js";
|
|
5
|
+
import { newer, runningVersion } from "./update.js";
|
|
4
6
|
/**
|
|
5
7
|
* One `KEY='value'` line as `setup.ts` wrote it, single-quoted with `'` escaped as `'\''`.
|
|
6
8
|
* Nothing here is printed: the file holds the runner's bearer.
|
|
@@ -21,17 +23,31 @@ export async function runServiceToolOverCli(runnerId, toolKey, argumentsJson, de
|
|
|
21
23
|
}
|
|
22
24
|
/** `agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>']`: any AWP tool from a shell, so a just-joined agent works before its harness reloads MCP tools. */
|
|
23
25
|
export async function callToolOverCli(runnerId, tool, argumentsJson, deps = {}, flags = []) {
|
|
24
|
-
|
|
26
|
+
const switches = new Set(flags.filter(([flag]) => ["help", "dry-run", "json"].includes(flag)).map(([flag]) => flag));
|
|
27
|
+
flags = flags.filter(([flag]) => !switches.has(flag));
|
|
28
|
+
// A noun has no underscore; a tool name has one unless it is a single word (reply, recall), which the resolver returns as is.
|
|
29
|
+
if (tool.includes(" ") || (switches.has("help") && tool !== "" && !tool.includes("_"))) {
|
|
30
|
+
const resolved = await resolveCommand(runnerId, tool, switches.has("help"), deps);
|
|
31
|
+
if (typeof resolved === "number")
|
|
32
|
+
return resolved;
|
|
33
|
+
tool = resolved;
|
|
34
|
+
}
|
|
35
|
+
if (switches.has("help"))
|
|
25
36
|
return await describeTools(runnerId, tool, deps);
|
|
26
37
|
const parameters = argumentsJson === undefined ? {} : parseArgumentsObject(argumentsJson, deps.logError ?? console.error);
|
|
27
38
|
if (!parameters)
|
|
28
|
-
return
|
|
39
|
+
return EXIT.usage;
|
|
29
40
|
if (flags.length) {
|
|
30
41
|
const typed = await flagArguments(runnerId, tool, flags, deps);
|
|
31
42
|
if (!typed)
|
|
32
|
-
return
|
|
43
|
+
return EXIT.usage;
|
|
33
44
|
Object.assign(parameters, typed);
|
|
34
45
|
}
|
|
46
|
+
// --dry-run: say exactly what would be sent, and send nothing.
|
|
47
|
+
if (switches.has("dry-run")) {
|
|
48
|
+
(deps.log ?? console.log)(JSON.stringify({ dryRun: true, tool, arguments: parameters }));
|
|
49
|
+
return EXIT.ok;
|
|
50
|
+
}
|
|
35
51
|
if (tool !== "next_event" || !RUNNER_ID.test(runnerId))
|
|
36
52
|
return await callAwpTool(runnerId, tool, parameters, deps);
|
|
37
53
|
// A bare poll acknowledges the batch the previous poll printed, so the command stays byte-identical and
|
|
@@ -56,6 +72,35 @@ export async function callToolOverCli(runnerId, tool, argumentsJson, deps = {},
|
|
|
56
72
|
await writeFile(delivered, JSON.stringify(ids), { mode: 0o600 }).catch(() => undefined);
|
|
57
73
|
return 0;
|
|
58
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Stable exit codes, the same for every command: 0 done; 1 the server refused or could not be reached
|
|
77
|
+
* (its sentence is on stderr); 2 the command itself was wrong (unknown command, missing or mistyped argument).
|
|
78
|
+
*/
|
|
79
|
+
export const EXIT = { ok: 0, refused: 1, usage: 2 };
|
|
80
|
+
/**
|
|
81
|
+
* `<noun> <verb>` → the tool it names on this server; or an exit code once `<noun> --help` has listed its
|
|
82
|
+
* verbs, or after saying what is wrong.
|
|
83
|
+
*/
|
|
84
|
+
async function resolveCommand(runnerId, command, help, deps) {
|
|
85
|
+
const tools = await listedTools(runnerId, deps);
|
|
86
|
+
if (!tools)
|
|
87
|
+
return EXIT.refused;
|
|
88
|
+
const names = tools.map((entry) => entry.name ?? "").filter((name) => name && !DEPRECATED_TOOLS.has(name));
|
|
89
|
+
const [noun, verb] = command.split(" ");
|
|
90
|
+
if (!verb && names.includes(noun))
|
|
91
|
+
return noun;
|
|
92
|
+
if (verb) {
|
|
93
|
+
const tool = toolFor(noun, verb, names);
|
|
94
|
+
if (tool)
|
|
95
|
+
return tool;
|
|
96
|
+
}
|
|
97
|
+
const verbs = names.filter((name) => commandFor(name)[0] === noun).map((name) => commandFor(name)[1]).sort();
|
|
98
|
+
const log = help && !verb ? deps.log ?? console.log : deps.logError ?? console.error;
|
|
99
|
+
log(verbs.length
|
|
100
|
+
? `${verb ? `${noun} ${verb} is not a command. ` : ""}agentradar run ${runnerId} ${noun} <verb>: ${verbs.join(" ")}. Add --help to a command for its arguments.`
|
|
101
|
+
: `${noun} is not a command group. Groups: ${[...new Set(names.map((name) => commandFor(name)[0]))].sort().join(" ")}.`);
|
|
102
|
+
return help && !verb && verbs.length ? EXIT.ok : EXIT.usage;
|
|
103
|
+
}
|
|
59
104
|
/** Split `<runner-id> <tool> [--args '<json>'] [--<argument> <value> ...]`; null when it is not that shape. */
|
|
60
105
|
export function parseCallArgs(rest) {
|
|
61
106
|
const positional = [];
|
|
@@ -67,8 +112,8 @@ export function parseCallArgs(rest) {
|
|
|
67
112
|
positional.push(arg);
|
|
68
113
|
continue;
|
|
69
114
|
}
|
|
70
|
-
if (arg === "--help") {
|
|
71
|
-
flags.push([
|
|
115
|
+
if (arg === "--help" || arg === "--dry-run" || arg === "--json") {
|
|
116
|
+
flags.push([arg.slice(2), ""]);
|
|
72
117
|
continue;
|
|
73
118
|
}
|
|
74
119
|
const value = rest[++i];
|
|
@@ -80,6 +125,9 @@ export function parseCallArgs(rest) {
|
|
|
80
125
|
flags.push([arg.slice(2), value]);
|
|
81
126
|
}
|
|
82
127
|
const help = flags.some(([flag]) => flag === "help");
|
|
128
|
+
// `<runner-id> <noun> <verb>` (Vadym 9970) names the same tool as `<runner-id> <tool>`; resolved against the server's list.
|
|
129
|
+
if (positional.length === 3)
|
|
130
|
+
return { runnerId: positional[0], tool: `${positional[1]} ${positional[2]}`, json, flags };
|
|
83
131
|
return positional.length === 2 || (help && positional.length === 1)
|
|
84
132
|
? { runnerId: positional[0], tool: positional[1] ?? "", json, flags } : null;
|
|
85
133
|
}
|
|
@@ -112,7 +160,7 @@ async function describeTools(runnerId, tool, deps) {
|
|
|
112
160
|
return 0;
|
|
113
161
|
}
|
|
114
162
|
/**
|
|
115
|
-
* `--text "hi" --
|
|
163
|
+
* `--text "hi" --thread-id X` as the tool's own arguments, typed by the inputSchema the server
|
|
116
164
|
* advertises, so an agent never types JSON (braces and quotes) into a shell command.
|
|
117
165
|
*/
|
|
118
166
|
async function flagArguments(runnerId, tool, flags, deps) {
|
|
@@ -190,16 +238,59 @@ function coerce(type, value) {
|
|
|
190
238
|
return type === undefined ? value : undefined;
|
|
191
239
|
}
|
|
192
240
|
}
|
|
241
|
+
// The parity test (server listing-description-realpg) fails when set_listing_description gains an argument this line does not name.
|
|
242
|
+
export const LISTING_DESCRIBE_USAGE = "Usage: agentradar listing describe <runner-id> --capabilities <text>";
|
|
243
|
+
// The parity test (server inbox-search-realpg) fails when inbox_search gains an argument this line does not name.
|
|
244
|
+
export const INBOX_SEARCH_USAGE = "Usage: agentradar inbox search <runner-id> [--query <text>] [--channel <c>] [--thread-id <id>] [--sender <s>] [--from <iso>] [--to <iso>] [--has-attachment true] [--unread true] [--unacked true] [--limit <n>] [--before <cursor>] [--after <cursor>] [--around <message-id>] [--json]";
|
|
193
245
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* server's decision exactly as it is over MCP.
|
|
246
|
+
* INBOX-SEARCH (Vadym 9974) — `agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json]`:
|
|
247
|
+
* the MCP `inbox_search` on the same endpoint with the same key. One block per message, or the tool's JSON with --json.
|
|
197
248
|
*/
|
|
198
|
-
export async function
|
|
249
|
+
export async function inboxSearchOverCli(runnerId, flags, json, deps = {}) {
|
|
250
|
+
const log = deps.log ?? console.log;
|
|
251
|
+
return await callToolOverCli(runnerId, "inbox_search", undefined, json ? deps : { ...deps, log: (text) => log(inboxLines(text)) }, flags);
|
|
252
|
+
}
|
|
253
|
+
function inboxLines(text) {
|
|
254
|
+
let body;
|
|
255
|
+
try {
|
|
256
|
+
body = JSON.parse(text);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return text;
|
|
260
|
+
}
|
|
261
|
+
const results = body.results;
|
|
262
|
+
if (!Array.isArray(results))
|
|
263
|
+
return text;
|
|
264
|
+
if (!results.length)
|
|
265
|
+
return "No messages found.";
|
|
266
|
+
const pages = [body.older && `Older: --before ${body.older}`, body.newer && `Newer: --after ${body.newer}`].filter(Boolean);
|
|
267
|
+
return [...results.map((r) => `${r.at} ${r.channel} ${r.sender?.name ?? r.from ?? ""}\n thread ${r.threadId} message ${r.messageId}\n ${r.snippet}`), ...pages].join("\n");
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Vadym 10719 — `agentradar permissions mine|roles|set|request <runner-id> [--<argument> <value> ...]`: the MCP permission
|
|
271
|
+
* tools on the same endpoint with the same key. The parity test (server permission-tools-realpg) fails when one drifts.
|
|
272
|
+
*/
|
|
273
|
+
export const PERMISSION_VERB_TOOLS = {
|
|
274
|
+
mine: "my_permissions",
|
|
275
|
+
roles: "read_role_permissions",
|
|
276
|
+
set: "set_role_permission",
|
|
277
|
+
request: "request_permission",
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Ruling 49 — `agentradar task create|repeat|make-recurring <runner-id> --args '<json>'`: the agent's own
|
|
281
|
+
* `create_task`, `create_recurring_task` or `make_task_recurring` (Vadym 10317), on the same AWP endpoint
|
|
282
|
+
* with the same key, so who it may assign to is the server's decision exactly as it is over MCP.
|
|
283
|
+
*/
|
|
284
|
+
export const TASK_VERB_TOOLS = {
|
|
285
|
+
create: "create_task",
|
|
286
|
+
repeat: "create_recurring_task",
|
|
287
|
+
"make-recurring": "make_task_recurring",
|
|
288
|
+
};
|
|
289
|
+
export async function createTaskOverCli(runnerId, verb, argumentsJson, deps = {}) {
|
|
199
290
|
const parameters = parseArgumentsObject(argumentsJson, deps.logError ?? console.error);
|
|
200
291
|
if (!parameters)
|
|
201
292
|
return 1;
|
|
202
|
-
return await callAwpTool(runnerId,
|
|
293
|
+
return await callAwpTool(runnerId, TASK_VERB_TOOLS[verb], parameters, deps);
|
|
203
294
|
}
|
|
204
295
|
function parseArgumentsObject(argumentsJson, logError) {
|
|
205
296
|
let parsed;
|
|
@@ -240,11 +331,40 @@ export async function callAwpTool(runnerId, name, toolArguments, deps) {
|
|
|
240
331
|
return 1;
|
|
241
332
|
}
|
|
242
333
|
log(said);
|
|
334
|
+
await cliVersionNotice(said, logError);
|
|
243
335
|
return 0;
|
|
244
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Vadym 9970: the server names the CLI it expects (`cli: { latest, min }`, on a successful get_my_context).
|
|
339
|
+
* When this one is behind, the agent reads it in its own output (stderr, so --json stdout stays parseable).
|
|
340
|
+
*/
|
|
341
|
+
async function cliVersionNotice(said, logError) {
|
|
342
|
+
let cli;
|
|
343
|
+
try {
|
|
344
|
+
cli = JSON.parse(said).cli;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!cli || typeof cli.latest !== "string")
|
|
350
|
+
return;
|
|
351
|
+
const mine = await runningVersion().catch(() => null);
|
|
352
|
+
if (!mine || !newer(cli.latest, mine))
|
|
353
|
+
return;
|
|
354
|
+
logError(typeof cli.min === "string" && newer(cli.min, mine)
|
|
355
|
+
? `agentradar ${mine} is below the minimum ${cli.min}: upgrade required. Run \`agentradar upgrade\`.`
|
|
356
|
+
: `agentradar ${cli.latest} is available (you run ${mine}). Run \`agentradar upgrade\`.`);
|
|
357
|
+
}
|
|
245
358
|
const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
246
359
|
/** One JSON-RPC request on the runner's AWP MCP endpoint with the runner's own key; null after logging why it failed. */
|
|
247
360
|
export async function awpRequest(runnerId, method, params, deps) {
|
|
361
|
+
const target = await awpEndpoint(runnerId, deps);
|
|
362
|
+
if (!target)
|
|
363
|
+
return null;
|
|
364
|
+
return postAwp(target.url, target.bearer, method, params, deps);
|
|
365
|
+
}
|
|
366
|
+
/** The runner's MCP endpoint and bearer: its runner file, else a hosted run's own environment. */
|
|
367
|
+
export async function awpEndpoint(runnerId, deps) {
|
|
248
368
|
const logError = deps.logError ?? console.error;
|
|
249
369
|
if (!RUNNER_ID.test(runnerId)) {
|
|
250
370
|
logError("Invalid runner ID");
|
|
@@ -275,6 +395,10 @@ export async function awpRequest(runnerId, method, params, deps) {
|
|
|
275
395
|
}
|
|
276
396
|
url = `${origin}/api/mcp`;
|
|
277
397
|
}
|
|
398
|
+
return { url, bearer };
|
|
399
|
+
}
|
|
400
|
+
async function postAwp(url, bearer, method, params, deps) {
|
|
401
|
+
const logError = deps.logError ?? console.error;
|
|
278
402
|
const response = await (deps.fetch ?? fetch)(url, {
|
|
279
403
|
method: "POST",
|
|
280
404
|
headers: {
|
package/dist/update.js
CHANGED
|
@@ -16,7 +16,7 @@ const serviceCopy = (root) => basename(dirname(root)) === "cli" && basename(dirn
|
|
|
16
16
|
export const runningVersion = async () => JSON.parse(await readFile(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
17
17
|
/** The version on disk: for a service's copy, the one `current` points at, which another runner may have moved. */
|
|
18
18
|
export const installedVersion = async (root = PACKAGE_ROOT) => JSON.parse(await readFile(join(serviceCopy(root) ? join(dirname(root), "current") : root, "package.json"), "utf8")).version;
|
|
19
|
-
const newer = (a, b) => {
|
|
19
|
+
export const newer = (a, b) => {
|
|
20
20
|
const [x, y] = [VERSION.exec(a), VERSION.exec(b)];
|
|
21
21
|
if (!x || !y)
|
|
22
22
|
return false;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { awpEndpoint, awpRequest } from "./tool-run.js";
|
|
7
|
+
/**
|
|
8
|
+
* VAULT-GIT (Vadym 10186): `agentradar vault clone <runner-id> <vault-id> [dir]` — the vault as a git repository.
|
|
9
|
+
* `export_vault` mints a 15-minute link to a git bundle of the documents this agent can read; this downloads it and
|
|
10
|
+
* clones from it, so each revision is a commit with its author and `git log` / `git blame` work locally.
|
|
11
|
+
*/
|
|
12
|
+
export async function cloneVaultOverCli(runnerId, vaultId, dir, deps = {}) {
|
|
13
|
+
const log = deps.log ?? console.log;
|
|
14
|
+
const logError = deps.logError ?? console.error;
|
|
15
|
+
const target = resolve(dir ?? `vault-${vaultId.slice(0, 8)}`);
|
|
16
|
+
// git clone refuses a non-empty target; say so before asking the server for anything.
|
|
17
|
+
if (await stat(target).then(() => true, () => false)) {
|
|
18
|
+
logError(`${target} already exists. Pick a new folder.`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const endpoint = await awpEndpoint(runnerId, deps);
|
|
22
|
+
const body = endpoint && await awpRequest(runnerId, "tools/call", { name: "export_vault", arguments: { vaultId } }, deps);
|
|
23
|
+
if (!endpoint || !body)
|
|
24
|
+
return 1;
|
|
25
|
+
const text = body.result?.content?.[0]?.text;
|
|
26
|
+
const answer = (() => { try {
|
|
27
|
+
return JSON.parse(String(text));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
} })();
|
|
32
|
+
if (body.result?.isError || !answer?.ok || !answer.url) {
|
|
33
|
+
logError(answer?.error ?? (typeof text === "string" ? text : "AgentRadar did not export the vault."));
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
// The link is relative when the server does not know its public address: it is on the MCP's own origin.
|
|
37
|
+
const bundleUrl = new URL(answer.url, endpoint.url);
|
|
38
|
+
const download = await (deps.fetch ?? fetch)(bundleUrl);
|
|
39
|
+
if (!download.ok) {
|
|
40
|
+
logError(`The export link was refused (${download.status}). It lasts 15 minutes; run the clone again.`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const scratch = await mkdtemp(join(tmpdir(), "agentradar-vault-"));
|
|
44
|
+
try {
|
|
45
|
+
const bundle = join(scratch, "vault.bundle");
|
|
46
|
+
await writeFile(bundle, Buffer.from(await download.arrayBuffer()));
|
|
47
|
+
await (deps.git ?? ((args) => promisify(execFile)("git", args).then(() => undefined)))(["clone", "--quiet", bundle, target]);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logError(`git could not clone the vault: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await rm(scratch, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
log(`Cloned vault ${vaultId} into ${target}: one commit per revision; git log and git blame work there.`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=vault-clone.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join, posix, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { awpRequest } from "./tool-run.js";
|
|
4
|
+
/**
|
|
5
|
+
* VAULT-IMPORT (Vadym 10190): `agentradar vault import <runner-id> <dir> [--vault <id>] [--source <name>] [--write]`.
|
|
6
|
+
* Reads a folder of Markdown and skills (an Obsidian vault, or a Notion, Confluence or GitBook export) and sends it
|
|
7
|
+
* to `import_vault` in batches. Every batch is dry-run first, so a secret or a refusal anywhere stops the import
|
|
8
|
+
* before any batch is written; `--write` then writes them. Without `--write` it only reports.
|
|
9
|
+
*/
|
|
10
|
+
const LIMITS = { files: 2_000, fileBytes: 1_000_000, totalBytes: 50_000_000, batchBytes: 8_000_000 };
|
|
11
|
+
const DOCUMENT = /\.(md|markdown|txt)$/i;
|
|
12
|
+
/** Images and PDFs travel as base64, up to what fits one request; anything else as its path only. */
|
|
13
|
+
const ATTACHMENT = /\.(png|jpe?g|gif|webp|pdf)$/i;
|
|
14
|
+
const ATTACHMENT_BYTES = 6_000_000;
|
|
15
|
+
const SKIPPED_DIRS = new Set([".git", ".obsidian", ".trash", "node_modules"]);
|
|
16
|
+
async function walk(root, dir = root, out = []) {
|
|
17
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (!SKIPPED_DIRS.has(entry.name))
|
|
20
|
+
await walk(root, join(dir, entry.name), out);
|
|
21
|
+
}
|
|
22
|
+
else if (entry.isFile())
|
|
23
|
+
out.push(relative(root, join(dir, entry.name)).split(sep).join("/"));
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export async function importVaultOverCli(runnerId, dir, options, deps = {}) {
|
|
28
|
+
const log = deps.log ?? console.log;
|
|
29
|
+
const logError = deps.logError ?? console.error;
|
|
30
|
+
const root = resolve(dir);
|
|
31
|
+
if (!(await stat(root).catch(() => null))?.isDirectory()) {
|
|
32
|
+
logError(`${dir} is not a folder.`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
const paths = (await walk(root)).sort();
|
|
36
|
+
if (paths.length > LIMITS.files) {
|
|
37
|
+
logError(`${paths.length} files: an import is at most ${LIMITS.files}. Nothing was sent.`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
const skillRoots = paths.filter((p) => posix.basename(p).toLowerCase() === "skill.md").map((p) => posix.dirname(p));
|
|
41
|
+
const skillOf = (p) => skillRoots.find((r) => r === "." || p.startsWith(`${r}/`));
|
|
42
|
+
// A skill folder travels whole in one batch; an attachment travels as its path only, so the server can report it.
|
|
43
|
+
const groups = new Map();
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const p of paths) {
|
|
46
|
+
const skill = skillOf(p);
|
|
47
|
+
let content = null;
|
|
48
|
+
let contentBase64;
|
|
49
|
+
if (skill === undefined && ATTACHMENT.test(p)) {
|
|
50
|
+
const bytes = await readFile(join(root, p));
|
|
51
|
+
total += bytes.length;
|
|
52
|
+
if (bytes.length <= ATTACHMENT_BYTES)
|
|
53
|
+
contentBase64 = bytes.toString("base64");
|
|
54
|
+
}
|
|
55
|
+
else if (skill !== undefined || DOCUMENT.test(p)) {
|
|
56
|
+
const bytes = await readFile(join(root, p));
|
|
57
|
+
if (bytes.length > LIMITS.fileBytes) {
|
|
58
|
+
logError(`${p} is over ${LIMITS.fileBytes} bytes. Nothing was sent.`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
total += bytes.length;
|
|
62
|
+
content = bytes.toString("utf8");
|
|
63
|
+
}
|
|
64
|
+
const key = skill ?? p;
|
|
65
|
+
groups.set(key, [...(groups.get(key) ?? []), { path: p, content, ...(contentBase64 ? { contentBase64 } : {}) }]);
|
|
66
|
+
}
|
|
67
|
+
if (total > LIMITS.totalBytes) {
|
|
68
|
+
logError(`${total} bytes: an import is at most ${LIMITS.totalBytes}. Nothing was sent.`);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const batches = [[]];
|
|
72
|
+
let size = 0;
|
|
73
|
+
for (const group of groups.values()) {
|
|
74
|
+
const bytes = group.reduce((sum, file) => sum + Buffer.byteLength(file.content ?? "") + (file.contentBase64?.length ?? 0) + file.path.length, 0);
|
|
75
|
+
if (size + bytes > LIMITS.batchBytes && batches.at(-1).length) {
|
|
76
|
+
batches.push([]);
|
|
77
|
+
size = 0;
|
|
78
|
+
}
|
|
79
|
+
batches.at(-1).push(...group);
|
|
80
|
+
size += bytes;
|
|
81
|
+
}
|
|
82
|
+
const source = options.source ?? `folder:${basename(root)}`;
|
|
83
|
+
const send = async (files, dryRun) => {
|
|
84
|
+
const body = await awpRequest(runnerId, "tools/call", { name: "import_vault", arguments: {
|
|
85
|
+
...(options.vaultId ? { vaultId: options.vaultId } : {}), files, source, dryRun, sourcePaths: paths, sourceBytes: total,
|
|
86
|
+
} }, deps);
|
|
87
|
+
const text = body?.result?.content?.[0]?.text;
|
|
88
|
+
return typeof text === "string" ? JSON.parse(text) : null;
|
|
89
|
+
};
|
|
90
|
+
const run = async (dryRun) => {
|
|
91
|
+
const counts = {};
|
|
92
|
+
const refused = [];
|
|
93
|
+
let notInSource = [];
|
|
94
|
+
for (const [n, batch] of batches.entries()) {
|
|
95
|
+
const answer = await send(batch, dryRun);
|
|
96
|
+
// Each call is all-or-nothing; earlier calls of a write stay written, and a rerun skips them as unchanged.
|
|
97
|
+
const done = !dryRun && n > 0 ? ` ${n} of ${batches.length} batches were imported before this one; run the import again to finish (unchanged pages are skipped).` : "";
|
|
98
|
+
if (!answer) {
|
|
99
|
+
if (done)
|
|
100
|
+
logError(done.trim());
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (!answer.ok) {
|
|
104
|
+
logError(`${answer.error ?? "AgentRadar refused the import."}${done}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
for (const [action, n] of Object.entries(answer.counts ?? {}))
|
|
108
|
+
counts[action] = (counts[action] ?? 0) + n;
|
|
109
|
+
refused.push(...(answer.files ?? []).filter((f) => f.action === "refuse").map((f) => `${f.source}: ${f.reason}`));
|
|
110
|
+
notInSource = answer.notInSource ?? notInSource;
|
|
111
|
+
}
|
|
112
|
+
return { counts, refused, notInSource };
|
|
113
|
+
};
|
|
114
|
+
const planned = await run(true);
|
|
115
|
+
if (!planned)
|
|
116
|
+
return 1;
|
|
117
|
+
const report = (r, verb) => {
|
|
118
|
+
log(`${verb}: ${r.counts.new ?? 0} new, ${r.counts.update ?? 0} updated, ${r.counts.skip ?? 0} unchanged, ${r.counts.refuse ?? 0} left out, ${r.counts.skill ?? 0} skill files (source ${source}).`);
|
|
119
|
+
for (const line of r.refused)
|
|
120
|
+
log(` left out ${line}`);
|
|
121
|
+
if (r.notInSource.length)
|
|
122
|
+
log(` imported before from ${source} and no longer in the folder (kept): ${r.notInSource.join(", ")}`);
|
|
123
|
+
};
|
|
124
|
+
if (!options.write) {
|
|
125
|
+
report(planned, "Dry run");
|
|
126
|
+
log("Nothing was written. Add --write to import.");
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const written = await run(false);
|
|
130
|
+
if (!written)
|
|
131
|
+
return 1;
|
|
132
|
+
report(written, "Imported");
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=vault-import.js.map
|
|
@@ -102,6 +102,26 @@ export function skillsDirFor(harness, env) {
|
|
|
102
102
|
return env.AR_BASE ? join(env.AR_BASE, ".claude", "skills") : null;
|
|
103
103
|
}
|
|
104
104
|
const SKILLS_DIR = skillsDirFor(AR_AGENT_HARNESS, process.env);
|
|
105
|
+
/**
|
|
106
|
+
* Vadym 9988: the server's start instructions with this agent's brief, where the harness reads standing rules at
|
|
107
|
+
* every session start: Claude's project rules, Codex's per-agent AGENTS.md. Refreshed on the skills cadence.
|
|
108
|
+
*/
|
|
109
|
+
export function briefFileFor(harness, env) {
|
|
110
|
+
if (harness === "codex") return env.CODEX_HOME ? join(env.CODEX_HOME, "AGENTS.md") : null;
|
|
111
|
+
return env.AR_BASE ? join(env.AR_BASE, ".claude", "rules", "agentradar-brief.md") : null;
|
|
112
|
+
}
|
|
113
|
+
const BRIEF_FILE = briefFileFor(AR_AGENT_HARNESS, process.env);
|
|
114
|
+
export const BRIEF_MARK = "<!-- Written by AgentRadar and replaced on every sync; edits here are lost. -->";
|
|
115
|
+
/** Replaces the file itself, never a symlink's target, and never a file it did not write (a vendor's AGENTS.md). */
|
|
116
|
+
export function writeBrief(file, text) {
|
|
117
|
+
const current = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
118
|
+
if (current && (!current.isFile() || !fs.readFileSync(file, "utf8").startsWith(BRIEF_MARK))) return false;
|
|
119
|
+
fs.mkdirSync(dirname(file), { recursive: true });
|
|
120
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
121
|
+
fs.writeFileSync(temporary, `${BRIEF_MARK}\n${text.trim()}\n`);
|
|
122
|
+
fs.renameSync(temporary, file);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
105
125
|
const SKILLS_SYNC_INTERVAL_MS = Number(process.env.SKILLS_SYNC_INTERVAL_MS ?? "600000");
|
|
106
126
|
const MANAGED_MARK = ".agentradar-managed";
|
|
107
127
|
|
|
@@ -312,10 +332,13 @@ export function agentAuthState() {
|
|
|
312
332
|
return AR_AGENT_HARNESS === "codex" ? codexAuthState() : claudeAuthState();
|
|
313
333
|
}
|
|
314
334
|
|
|
315
|
-
|
|
335
|
+
/** AGENT-GUARD-ACTIVE: the supervisor sets AR_GUARD_MARKER when the session must load agent-guard, whose SessionStart hook writes it. */
|
|
336
|
+
export const guardLoaded = (marker = process.env.AR_GUARD_MARKER) => !marker || fs.existsSync(marker);
|
|
337
|
+
|
|
338
|
+
export function runnerState(auth = agentAuthState, sample = capturePane, guard = guardLoaded) {
|
|
316
339
|
const authState = auth();
|
|
317
340
|
if (authState === "unauthenticated") return "auth_required";
|
|
318
|
-
if (authState !== "authenticated") return "starting";
|
|
341
|
+
if (authState !== "authenticated" || !guard()) return "starting";
|
|
319
342
|
const pane = sample();
|
|
320
343
|
if (pane?.rateLimited) return "rate_limited";
|
|
321
344
|
return pane?.inputReady ? "ready" : "starting";
|
|
@@ -723,6 +746,16 @@ export function writeManagedSkills(skillsDir, skills) {
|
|
|
723
746
|
}
|
|
724
747
|
|
|
725
748
|
async function syncSkills() {
|
|
749
|
+
try {
|
|
750
|
+
const init = await mcpPostIsolated("initialize", {
|
|
751
|
+
protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "agentradar-i2-feeder", version: "1.0.0" },
|
|
752
|
+
});
|
|
753
|
+
if (BRIEF_FILE && init?.instructions && !writeBrief(BRIEF_FILE, init.instructions)) {
|
|
754
|
+
console.warn(`[feeder] brief sync: ${BRIEF_FILE} is not AgentRadar's; left alone`);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
console.warn(`[feeder] brief sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
758
|
+
}
|
|
726
759
|
try {
|
|
727
760
|
const answer = toolJson(await mcpPostIsolated("tools/call", { name: "my_skills", arguments: {} }));
|
|
728
761
|
// A refusal or an older server keeps whatever this box already has.
|
|
@@ -33,6 +33,7 @@ export function formatInjectionFrame(event, cliRunnerId) {
|
|
|
33
33
|
const tags = [`source=${e.source ?? "unknown"}`];
|
|
34
34
|
if (e.channel) tags.push(`channel=${e.channel}`);
|
|
35
35
|
if (e.conversationId) tags.push(`conversationId=${e.conversationId}`);
|
|
36
|
+
if (e.taskId) tags.push(`taskId=${e.taskId}`); // assignment events: the agent works THIS task, not one dug out of its log
|
|
36
37
|
if (e.eventId) tags.push(`eventId=${e.eventId}`);
|
|
37
38
|
const header = `[AgentRadar inbound · ${tags.join(" · ")}]`;
|
|
38
39
|
const body = oneLine(e.text) || "(no text)";
|
|
@@ -70,6 +70,8 @@ BROWSER_APP="${BROWSER_APP:-Google Chrome}" # #150: app to ensure-up fo
|
|
|
70
70
|
BROWSER_READY_MAX_SEC="${BROWSER_READY_MAX_SEC:-20}" # #150: bounded wait for the browser proc on cold boot
|
|
71
71
|
BROWSER_READY_SETTLE_SEC="${BROWSER_READY_SETTLE_SEC:-3}" # #150: settle for extension/native-host after cold launch
|
|
72
72
|
HEALTH_POLL_SEC="${HEALTH_POLL_SEC:-15}"
|
|
73
|
+
AR_API400_LOOP_MAX="${AR_API400_LOOP_MAX:-3}" # same `API Error: 400` this many times in the pane = poisoned session
|
|
74
|
+
AR_API400_COOLDOWN_SEC="${AR_API400_COOLDOWN_SEC:-300}" # at most one fresh-session heal per this window
|
|
73
75
|
|
|
74
76
|
# --- 2a. R004 HARNESS SELECTION — one documented variable picks the CLI ---------
|
|
75
77
|
# The Bible names two agent harnesses: Claude Code and Codex. `claude` is the default, so every existing
|
|
@@ -175,7 +177,7 @@ if [ -n "${AGENT_TOOL_ENV_FILE}" ]; then
|
|
|
175
177
|
# identity). Reject at parse time (visible warning) AND — belt-and-suspenders — the launch lists
|
|
176
178
|
# the reserved allowlist AFTER TOOL_ENV_ARGS so `env` last-wins keeps reserved vars authoritative.
|
|
177
179
|
case "${_tline%%=*}" in
|
|
178
|
-
HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_CLI_RUNNER_ID|AR_AGENTRADAR_BIN|AR_DISCIPLINE_EVERY|CLAUDE_CODE_PLUGIN_CACHE_DIR|AR_RESUME_FLAGS|AR_FRESH_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN)
|
|
180
|
+
HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_CLI_RUNNER_ID|AR_AGENTRADAR_BIN|AR_DISCIPLINE_EVERY|CLAUDE_CODE_PLUGIN_CACHE_DIR|AR_GUARD_MARKER|AR_RESUME_FLAGS|AR_FRESH_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN)
|
|
179
181
|
log "WARN ignoring reserved key '${_tline%%=*}' in ${AGENT_TOOL_ENV_FILE} (cannot override supervisor/agent env)"; continue ;;
|
|
180
182
|
esac
|
|
181
183
|
TOOL_ENV_ARGS+=("${_tline}")
|
|
@@ -193,7 +195,13 @@ unset CLAUDE_CODE_OAUTH_TOKEN
|
|
|
193
195
|
_claude_cfg=()
|
|
194
196
|
[ -n "${CLAUDE_CONFIG_DIR:-}" ] && _claude_cfg=( CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR}" )
|
|
195
197
|
# agent-guard (templates/agent-guard): its hooks ask the server what this runner still owes through the CLI.
|
|
196
|
-
|
|
198
|
+
# AGENT-GUARD-ACTIVE (Vadym 10699): the guard's SessionStart hook writes AR_GUARD_MARKER, and the feeder and the
|
|
199
|
+
# channel report "starting", never "ready", until it exists. A session that did not load the guard gets no work.
|
|
200
|
+
# Outside the secrets dir, which goes on exit: a pane that outlives this supervisor keeps its proof.
|
|
201
|
+
# The caller's own pane (AR_ATTACH) was not started with --settings, so it is not gated here.
|
|
202
|
+
AR_GUARD_MARKER=""
|
|
203
|
+
[ "${AR_AGENT_HARNESS}" = "claude" ] && [ -n "${CLAUDE_SETTINGS:-}" ] && [ "${AR_ATTACH:-}" != "1" ] && AR_GUARD_MARKER="${HOME}/.agentradar/run/${AGENT_NAME}.guard-loaded"
|
|
204
|
+
for _k in AR_CLI_RUNNER_ID AR_AGENTRADAR_BIN AR_DISCIPLINE_EVERY AR_MODEL_SWITCH_DIR CLAUDE_CODE_PLUGIN_CACHE_DIR AR_GUARD_MARKER; do
|
|
197
205
|
[ -n "${!_k:-}" ] && _claude_cfg+=( "${_k}=${!_k}" )
|
|
198
206
|
done
|
|
199
207
|
|
|
@@ -282,6 +290,16 @@ ensure_browser() {
|
|
|
282
290
|
log "WARN browser '${BROWSER_APP}' not detected within ${BROWSER_READY_MAX_SEC}s — launching --chrome anyway (it will retry-connect)"
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
# A session whose history the API rejects (e.g. `messages.240.content.0.thinking: each thinking block must
|
|
294
|
+
# contain thinking`) answers every new turn with the same 400 forever; resuming it replays the same history.
|
|
295
|
+
# True when one identical `API Error: 400` line (digits normalised: the message index moves) fills the pane
|
|
296
|
+
# scrollback AR_API400_LOOP_MAX times. ponytail: only the last 200 lines are read, so a very slow loop could slip by.
|
|
297
|
+
api_error_loop() {
|
|
298
|
+
"${TMUX_BIN}" capture-pane -p -J -t "${TMUX_SESSION}" -S -200 2>/dev/null \
|
|
299
|
+
| grep -o 'API Error: 400 .*' | sed -E 's/[0-9]+/N/g' | sort | uniq -c | sort -rn \
|
|
300
|
+
| awk -v m="${AR_API400_LOOP_MAX}" 'NR==1{ok=($1>=m)} END{exit !ok}'
|
|
301
|
+
}
|
|
302
|
+
|
|
285
303
|
# --- 3. Ensure the agent's tmux+claude session (idempotent) -------------------
|
|
286
304
|
ensure_agent() {
|
|
287
305
|
if session_exists && pane_alive; then
|
|
@@ -298,6 +316,9 @@ ensure_agent() {
|
|
|
298
316
|
log "session '${TMUX_SESSION}' exists but claude pane is dead — recreating"
|
|
299
317
|
"${TMUX_BIN}" kill-session -t "${TMUX_SESSION}" 2>/dev/null || true
|
|
300
318
|
fi
|
|
319
|
+
[ -n "${AR_GUARD_MARKER}" ] && rm -f "${AR_GUARD_MARKER}" # this launch must prove the guard afresh
|
|
320
|
+
_resume_now="${AR_RESUME_FLAGS}"
|
|
321
|
+
if [ "${_FORCE_FRESH:-0}" = 1 ]; then _resume_now="${AR_FRESH_FLAGS}"; _FORCE_FRESH=0; fi # poisoned history: do not resume it
|
|
301
322
|
ensure_browser # #150: bring up the browser first for --chrome agents (idempotent, no-op otherwise)
|
|
302
323
|
if [ -n "${PRE_LAUNCH_HOOK}" ] && [ -x "${PRE_LAUNCH_HOOK}" ]; then
|
|
303
324
|
log "running pre-launch hook: ${PRE_LAUNCH_HOOK}"
|
|
@@ -358,10 +379,10 @@ ensure_agent() {
|
|
|
358
379
|
TERM="${AR_AGENT_TERM:-tmux-256color}" COLORTERM="${AR_AGENT_COLORTERM:-truecolor}" \
|
|
359
380
|
LANG="${LANG:-en_US.UTF-8}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${AR_AGENT_LC_CTYPE:-en_US.UTF-8}" \
|
|
360
381
|
TMPDIR="${TMPDIR:-/tmp}" NODE_OPTIONS="${NODE_OPTIONS:-}" \
|
|
361
|
-
CODEX_HOME="${CODEX_HOME:-}" AR_RESUME_FLAGS="${
|
|
382
|
+
CODEX_HOME="${CODEX_HOME:-}" AR_RESUME_FLAGS="${_resume_now}" AR_FRESH_FLAGS="${AR_FRESH_FLAGS}" \
|
|
362
383
|
DISABLE_AUTOUPDATER=1 CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF=1 \
|
|
363
384
|
${_claude_cfg[@]+"${_claude_cfg[@]}"} ${_tool_env[@]+"${_tool_env[@]}"} \
|
|
364
|
-
/bin/sh -c 'if [ -n "${AR_TOOL_ENV_FILE:-}" ]; then while IFS= read -r _l || [ -n "$_l" ]; do case "$_l" in ""|\#*) continue ;; esac; _k=${_l%%=*}; _v=${_l#*=}; case "$_k" in HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_CLI_RUNNER_ID|AR_AGENTRADAR_BIN|AR_DISCIPLINE_EVERY|CLAUDE_CODE_PLUGIN_CACHE_DIR|AR_RESUME_FLAGS|AR_FRESH_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN) continue ;; esac; export "$_k=$_v"; done < "$AR_TOOL_ENV_FILE"; unset AR_TOOL_ENV_FILE _l _k _v; fi; RF="${AR_RESUME_FLAGS:-}"; FF="${AR_FRESH_FLAGS:-}"; unset AR_RESUME_FLAGS AR_FRESH_FLAGS; CB="$1"; shift; S=$(date +%s); "$CB" $RF "$@" || [ $(( $(date +%s) - S )) -ge 30 ] || exec "$CB" $FF "$@"' sh "${AGENT_BIN}" "${_launch_flags[@]}" || {
|
|
385
|
+
/bin/sh -c 'if [ -n "${AR_TOOL_ENV_FILE:-}" ]; then while IFS= read -r _l || [ -n "$_l" ]; do case "$_l" in ""|\#*) continue ;; esac; _k=${_l%%=*}; _v=${_l#*=}; case "$_k" in HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_CLI_RUNNER_ID|AR_AGENTRADAR_BIN|AR_DISCIPLINE_EVERY|CLAUDE_CODE_PLUGIN_CACHE_DIR|AR_GUARD_MARKER|AR_RESUME_FLAGS|AR_FRESH_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN) continue ;; esac; export "$_k=$_v"; done < "$AR_TOOL_ENV_FILE"; unset AR_TOOL_ENV_FILE _l _k _v; fi; RF="${AR_RESUME_FLAGS:-}"; FF="${AR_FRESH_FLAGS:-}"; unset AR_RESUME_FLAGS AR_FRESH_FLAGS; CB="$1"; shift; S=$(date +%s); "$CB" $RF "$@" || [ $(( $(date +%s) - S )) -ge 30 ] || exec "$CB" $FF "$@"' sh "${AGENT_BIN}" "${_launch_flags[@]}" || {
|
|
365
386
|
log "tmux could not start agent session '${TMUX_SESSION}'"
|
|
366
387
|
return 1
|
|
367
388
|
}
|
|
@@ -412,7 +433,7 @@ if [ "${AR_FEED}" = "feeder" ]; then
|
|
|
412
433
|
# AR_TOOL_ENV_FILE: the login probe must see the same gateway env the pane runs with.
|
|
413
434
|
AWP_MCP_URL="${AWP_MCP_URL}" AWP_BEARER_FILE="${AWP_BEARER_FILE}" TMUX_SESSION="${TMUX_SESSION}" \
|
|
414
435
|
AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" AR_AGENT_HARNESS="${AR_AGENT_HARNESS}" AGENT_BIN="${AGENT_BIN}" \
|
|
415
|
-
AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" \
|
|
436
|
+
AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" AR_GUARD_MARKER="${AR_GUARD_MARKER}" \
|
|
416
437
|
node "${FEEDER_JS}" &
|
|
417
438
|
FEEDER_PID=$!
|
|
418
439
|
log "feeder pid ${FEEDER_PID}"
|
|
@@ -449,8 +470,16 @@ while true; do
|
|
|
449
470
|
log "feeder exited — exiting so the service manager restarts the whole unit"
|
|
450
471
|
exit 1
|
|
451
472
|
fi
|
|
473
|
+
_heal=0
|
|
452
474
|
if ! pane_alive; then
|
|
453
475
|
log "agent pane died — recovering it in place"
|
|
476
|
+
_heal=1
|
|
477
|
+
elif [ "${AR_ATTACH:-}" != "1" ] && [ $(( $(date +%s) - ${_LAST_HEAL:-0} )) -ge "${AR_API400_COOLDOWN_SEC}" ] && api_error_loop; then
|
|
478
|
+
log "agent pane is looping on the same API 400 — replacing it with a FRESH session"
|
|
479
|
+
_LAST_HEAL=$(date +%s); _FORCE_FRESH=1; _heal=1
|
|
480
|
+
"${TMUX_BIN}" kill-session -t "${TMUX_SESSION}" 2>/dev/null || true
|
|
481
|
+
fi
|
|
482
|
+
if [ "${_heal}" = 1 ]; then
|
|
454
483
|
if ensure_agent; then
|
|
455
484
|
if [ -n "${BRIDGE_PID}" ]; then
|
|
456
485
|
kill "${BRIDGE_PID}" 2>/dev/null || true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentradar-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20"
|
|
6
6
|
},
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"dist/skill-install.js",
|
|
18
18
|
"dist/tool-run.js",
|
|
19
19
|
"dist/sync.js",
|
|
20
|
+
"dist/vault-import.js",
|
|
20
21
|
"dist/update.js",
|
|
22
|
+
"dist/commands.js",
|
|
23
|
+
"dist/vault-clone.js",
|
|
21
24
|
"dist/vendor-runner/**",
|
|
22
25
|
"dist/agentradar-vendor/SKILL.md",
|
|
23
26
|
"dist/agent-guard",
|