agentradar-cli 0.2.1 → 0.2.2
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
CHANGED
|
@@ -6,8 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
import { envValue } from "./tool-run.js";
|
|
8
8
|
import { trustedMcpEndpoint } from "./setup.js";
|
|
9
|
-
export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only. Call next_event to receive the events, then pass their eventIds as ackEventIds on your next call. Answer livenessProbe with liveness_ack. Never acknowledge an event you have not read.
|
|
10
|
-
export const CHANNEL_BLOCKED = "AgentRadar has not observed a next_event call from this session. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy.
|
|
9
|
+
export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only. Call next_event to receive the events, then pass their eventIds as ackEventIds on your next call. Answer livenessProbe with liveness_ack. Never acknowledge an event you have not read. Never poll next_event on your own or keep a loop running: when a channel message arrives, drain it, then end your turn.";
|
|
10
|
+
export const CHANNEL_BLOCKED = "AgentRadar has not observed a next_event call from this session. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy. Until then, deliver with the feeder: set AR_FEED=feeder in ~/.agentradar/agents/<runner-id>.env and restart the runner.";
|
|
11
11
|
/** One local MCP, forwarding tools to AWP. Wake signals never consume the durable queue. */
|
|
12
12
|
export async function startChannel(runnerId, deps = {}) {
|
|
13
13
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(runnerId))
|
package/dist/runner.js
CHANGED
|
@@ -37,7 +37,7 @@ export async function runRestartSelftest(runnerId, deps = {}) {
|
|
|
37
37
|
} while (Date.now() < deadline);
|
|
38
38
|
throw new Error(timedOut);
|
|
39
39
|
};
|
|
40
|
-
const unanswered = `The agent did not act on the check:
|
|
40
|
+
const unanswered = `The agent did not act on the check: neither its feeder nor a channel push delivered it. Check the runner is running (agentradar runner start ${runnerId}), then run this again.`;
|
|
41
41
|
const restart = deps.restart ?? ((beforeRestart) => restartRunnerHarness(runnerId, deps.home, run, beforeRestart));
|
|
42
42
|
// Refuse before the drill starts: run every restart check, stopping exactly where the real restart would begin.
|
|
43
43
|
const checked = new Error("restart checks passed");
|
package/dist/setup.js
CHANGED
|
@@ -21,6 +21,7 @@ const SUPERVISOR_HARNESS = {
|
|
|
21
21
|
claude: "claude",
|
|
22
22
|
claude_code: "claude",
|
|
23
23
|
codex: "codex",
|
|
24
|
+
generic: "generic",
|
|
24
25
|
};
|
|
25
26
|
async function postJson(fetchImpl, url, body) {
|
|
26
27
|
return fetchImpl(url, {
|
|
@@ -200,14 +201,23 @@ async function codexServiceHome(home, runnerId, env) {
|
|
|
200
201
|
await chmod(codexHome, 0o700);
|
|
201
202
|
let inMcp = false;
|
|
202
203
|
let inTable = false;
|
|
204
|
+
let depth = 0;
|
|
205
|
+
let dropping = false;
|
|
203
206
|
// A restart would otherwise stop on Codex's "Update available" screen, with nobody there to press a key.
|
|
204
207
|
const config = ["check_for_update_on_startup = false", ...(await readFile(join(source, "config.toml"), "utf8").catch(() => "")).split("\n").filter((line) => {
|
|
208
|
+
// Brackets inside strings and comments don't count; a line inside a multi-line array goes with its key.
|
|
209
|
+
const bare = line.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, '""').replace(/#.*/, "");
|
|
210
|
+
const continued = depth > 0;
|
|
211
|
+
depth = Math.max(0, depth + (bare.match(/\[/g)?.length ?? 0) - (bare.match(/\]/g)?.length ?? 0));
|
|
212
|
+
if (continued)
|
|
213
|
+
return !dropping;
|
|
205
214
|
const header = /^\s*\[\[?\s*([^\]]+)\]/.exec(line);
|
|
206
215
|
if (header) {
|
|
207
|
-
inMcp = /^"?mcp_servers\b/.test(header[1]);
|
|
216
|
+
inMcp = /^["']?mcp_servers\b/.test(header[1]);
|
|
208
217
|
inTable = true;
|
|
209
218
|
}
|
|
210
|
-
|
|
219
|
+
dropping = inMcp || /^\s*["']?mcp_servers["']?\s*[.=]/.test(line) || (!inTable && /^\s*["']?check_for_update_on_startup["']?\s*=/.test(line));
|
|
220
|
+
return !dropping;
|
|
211
221
|
})].join("\n");
|
|
212
222
|
await writePrivateFile(join(codexHome, "config.toml"), config);
|
|
213
223
|
await cp(join(source, "rules"), join(codexHome, "rules"), { recursive: true, force: true }).catch((error) => {
|
|
@@ -251,6 +261,14 @@ async function installRunner(home, projectPath, approved, service) {
|
|
|
251
261
|
joined.push(`CODEX_HOME=${shellValue(codexHome)}`);
|
|
252
262
|
toolKeys = keys;
|
|
253
263
|
}
|
|
264
|
+
else if (harness === "generic") {
|
|
265
|
+
// Any other interactive CLI, named by the vendor. The pane-alive check walks only this pane's own
|
|
266
|
+
// process tree, so the binary's name is a unique enough pattern.
|
|
267
|
+
const bin = (service.env.AGENTRADAR_AGENT_BIN ?? "").trim();
|
|
268
|
+
if (!bin)
|
|
269
|
+
throw new Error("Set AGENTRADAR_AGENT_BIN to the command that starts your agent's CLI");
|
|
270
|
+
joined.push(`AR_AGENT_BIN=${shellValue(bin)}`, `AR_AGENT_ARGS=${shellValue(service.env.AGENTRADAR_AGENT_ARGS ?? "")}`, `AR_RESUME_FLAGS=${shellValue(service.env.AGENTRADAR_RESUME_ARGS ?? "")}`, `AGENT_PID_PATTERN=${shellValue(basename(bin).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))}`);
|
|
271
|
+
}
|
|
254
272
|
else {
|
|
255
273
|
joined.push(`AR_RESUME_FLAGS=${shellValue(`--resume ${service.sessionId}`)}`, `AR_FRESH_FLAGS=${shellValue(`--session-id ${service.sessionId}`)}`);
|
|
256
274
|
const configDirEnv = service.env.CLAUDE_CONFIG_DIR?.trim();
|
|
@@ -374,9 +392,37 @@ export async function setupVendorAgent(deps = {}) {
|
|
|
374
392
|
}
|
|
375
393
|
throw new Error("AgentRadar setup code expired; run setup again");
|
|
376
394
|
}
|
|
377
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* Copies the running CLI to ~/.agentradar/cli/<version> and points ~/.agentradar/cli/current at it. The service
|
|
397
|
+
* runs `current`, never the npx cache the CLI may be running from (npm can clean that) or a pinned version.
|
|
398
|
+
* Returns the `current` dist directory.
|
|
399
|
+
*/
|
|
400
|
+
export async function installCli(home, pkg = dirname(dirname(fileURLToPath(import.meta.url)))) {
|
|
401
|
+
const { version } = JSON.parse(await readFile(join(pkg, "package.json"), "utf8"));
|
|
402
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
403
|
+
throw new Error(`agentradar-cli has an invalid version: ${JSON.stringify(version)}`);
|
|
404
|
+
const cli = join(home, ".agentradar", "cli");
|
|
405
|
+
const target = join(cli, version);
|
|
406
|
+
if (!(await lstat(target).then(() => true, () => false))) {
|
|
407
|
+
// Staged then renamed, so a half-finished copy is never a version directory.
|
|
408
|
+
const staged = `${target}.${process.pid}.tmp`;
|
|
409
|
+
await cp(join(pkg, "dist"), join(staged, "dist"), { recursive: true });
|
|
410
|
+
await cp(join(pkg, "package.json"), join(staged, "package.json"));
|
|
411
|
+
await rename(staged, target).catch(async (error) => {
|
|
412
|
+
await rm(staged, { recursive: true, force: true });
|
|
413
|
+
if (error.code !== "ENOTEMPTY" && error.code !== "EEXIST")
|
|
414
|
+
throw error;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const link = join(cli, `current.${process.pid}.tmp`);
|
|
418
|
+
await rm(link, { force: true });
|
|
419
|
+
await symlink(version, link);
|
|
420
|
+
await rename(link, join(cli, "current"));
|
|
421
|
+
return join(cli, "current", "dist");
|
|
422
|
+
}
|
|
423
|
+
/** `install-agent.sh <id> --load` with the CLI at ~/.agentradar/cli/current, so the service does not depend on PATH. */
|
|
378
424
|
async function installAgentService(runnerId, home, env) {
|
|
379
|
-
const dist =
|
|
425
|
+
const dist = await installCli(home);
|
|
380
426
|
await promisify(execFile)("bash", [join(dist, "vendor-runner", "supervisor", "install-agent.sh"), runnerId, "--load"], {
|
|
381
427
|
env: { ...env, HOME: home, NODE_BIN: process.execPath, AGENTRADAR_JS: join(dist, "index.js") },
|
|
382
428
|
});
|
|
@@ -397,8 +443,10 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
397
443
|
const cwd = deps.cwd ?? process.cwd();
|
|
398
444
|
const home = deps.home ?? homedir();
|
|
399
445
|
const env = deps.env ?? process.env;
|
|
400
|
-
// Claude Code sets CLAUDECODE in its shell; a machine with both
|
|
401
|
-
|
|
446
|
+
// AGENTRADAR_AGENT_BIN names any other CLI. Claude Code sets CLAUDECODE in its shell; a machine with both
|
|
447
|
+
// tools installed may export CODEX_HOME too.
|
|
448
|
+
const harness = deps.harness ?? (env.AGENTRADAR_AGENT_BIN?.trim() ? "generic" : env.CLAUDECODE || !env.CODEX_HOME ? "claude" : "codex");
|
|
449
|
+
const label = harness === "generic" ? basename(env.AGENTRADAR_AGENT_BIN.trim()) : harness === "claude" ? "Claude Code" : "Codex";
|
|
402
450
|
// The agent is a persistent session the service owns, not this one: no MCP entry in this folder (it acts
|
|
403
451
|
// through `agentradar run`), a runner env pinned to its own session, and a LaunchAgent / systemd unit.
|
|
404
452
|
const startJoined = async (requestId, agentId, installation, saved) => {
|
|
@@ -419,7 +467,7 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
419
467
|
log(`Could not start the service: ${error.message.split("\n")[0]}. Start it by hand with: agentradar runner start ${runnerId}`);
|
|
420
468
|
return 1;
|
|
421
469
|
}
|
|
422
|
-
log(`Connected. Your agent now runs in its own ${
|
|
470
|
+
log(`Connected. Your agent now runs in its own ${label} session in ${projectPath} (tmux session ${runnerId}): your welcome and every client message are typed into it, and it answers there. There is nothing more to do in this session. To stop it: agentradar leave ${runnerId}`);
|
|
423
471
|
log("Running as a service; it restarts on crash and reboot.");
|
|
424
472
|
return 0;
|
|
425
473
|
};
|
|
@@ -77,20 +77,21 @@ HEALTH_POLL_SEC="${HEALTH_POLL_SEC:-15}"
|
|
|
77
77
|
# interactive CLI: the env file names its binary, arguments, resume/fresh flags, pid pattern and how work
|
|
78
78
|
# reaches it (AR_FEED). Anything else is REFUSED by name: a supervisor that quietly fell back to Claude for an
|
|
79
79
|
# unrecognized value would come up healthy, keep a pane alive, and answer nothing the vendor expected.
|
|
80
|
-
# Tried: claude and codex with AR_FEED=feeder. Untried: generic, AR_FEED=push (Claude channels)
|
|
80
|
+
# Tried: claude and codex with AR_FEED=feeder. Untried: generic, AR_FEED=push (Claude channels).
|
|
81
81
|
AR_AGENT_HARNESS="${AR_AGENT_HARNESS:-claude}"
|
|
82
82
|
case "${AR_AGENT_HARNESS}" in
|
|
83
83
|
claude|codex|generic) : ;;
|
|
84
84
|
*) die "AR_AGENT_HARNESS must be 'claude', 'codex' or 'generic' (got '${AR_AGENT_HARNESS}')" ;;
|
|
85
85
|
esac
|
|
86
86
|
# How work reaches the session. feeder: the feeder polls next_event and types each event into the pane.
|
|
87
|
-
# push: the session's own MCP pushes it (Claude channels).
|
|
88
|
-
#
|
|
87
|
+
# push: the session's own MCP pushes it (Claude channels) and no feeder starts. There is no mode where the
|
|
88
|
+
# agent polls for itself: an agent eventually forgets to. ACP is named so a vendor asking for it is told
|
|
89
|
+
# plainly it is not built.
|
|
89
90
|
AR_FEED="${AR_FEED:-feeder}"
|
|
90
91
|
case "${AR_FEED}" in
|
|
91
|
-
feeder|push
|
|
92
|
-
acp) die "AR_FEED=acp is not built yet; use feeder
|
|
93
|
-
*) die "AR_FEED must be feeder
|
|
92
|
+
feeder|push) : ;;
|
|
93
|
+
acp) die "AR_FEED=acp is not built yet; use feeder or push" ;;
|
|
94
|
+
*) die "AR_FEED must be feeder or push (got '${AR_FEED}')" ;;
|
|
94
95
|
esac
|
|
95
96
|
# Launched only when the resume form exits non-zero (nothing to resume). Empty = the same argv without it.
|
|
96
97
|
AR_FRESH_FLAGS="${AR_FRESH_FLAGS:-}"
|