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. Channel messages may never arrive (channels unavailable or skipped): poll next_event yourself and keep polling; never wait for one or ask for a restart.";
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. Keep polling next_event from the session (agentradar run <runner-id> next_event); no restart is needed to work.";
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: it is not polling next_event and no channel push reached it. Have it poll (agentradar run ${runnerId} next_event), then run this again.`;
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
- return !inMcp && !/^\s*mcp_servers\s*[.=]/.test(line) && (inTable || !/^\s*check_for_update_on_startup\s*=/.test(line));
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
- /** `install-agent.sh <id> --load` with the CLI that is running now, so the service does not depend on PATH. */
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 = dirname(fileURLToPath(import.meta.url));
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 tools installed may export CODEX_HOME too.
401
- const harness = deps.harness ?? (env.CLAUDECODE || !env.CODEX_HOME ? "claude" : "codex");
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 ${harness === "claude" ? "Claude Code" : "Codex"} 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}`);
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), self-poll.
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). self-poll: the agent polls for itself. Neither
88
- # of the last two starts a feeder. ACP is named so a vendor asking for it is told plainly it is not built.
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|self-poll) : ;;
92
- acp) die "AR_FEED=acp is not built yet; use feeder, push or self-poll" ;;
93
- *) die "AR_FEED must be feeder, push or self-poll (got '${AR_FEED}')" ;;
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:-}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Secure AgentRadar vendor setup and skill installer",
5
5
  "type": "module",
6
6
  "bin": {