agentradar-cli 0.1.8 → 0.1.9

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/index.js CHANGED
@@ -2,8 +2,13 @@
2
2
  import { installVendorSkill } from "./skill-install.js";
3
3
  import { startVendorRunner } from "./runner.js";
4
4
  import { setupVendorAgent } from "./setup.js";
5
+ import { installSyncTimer, syncAgentDir } from "./sync.js";
5
6
  async function main() {
6
7
  const args = process.argv.slice(2);
8
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
9
+ console.log("Usage: agentradar setup [--trust] | agentradar runner start <runner-id> | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
10
+ return;
11
+ }
7
12
  if (args.length === 1 && args[0] === "setup") {
8
13
  await setupVendorAgent();
9
14
  return;
@@ -25,8 +30,27 @@ async function main() {
25
30
  await installVendorSkill();
26
31
  return;
27
32
  }
28
- console.error("Usage: agentradar setup [--trust] | agentradar runner start <runner-id> | agentradar skill install");
29
- process.exitCode = 1;
33
+ if (args[0] === "sync" && args[1] === "install") {
34
+ const rest = args.slice(2);
35
+ const iIdx = rest.indexOf("--interval");
36
+ const interval = iIdx >= 0 ? Number(rest[iIdx + 1]) : 600;
37
+ if (!Number.isFinite(interval) || interval < 60) {
38
+ console.error("--interval must be a number of seconds >= 60");
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ const dir = rest.filter((_, i) => i !== iIdx && i !== iIdx + 1)[0];
43
+ process.exitCode = await installSyncTimer(dir, interval);
44
+ return;
45
+ }
46
+ if (args[0] === "sync" && args.length <= 2) {
47
+ process.exitCode = await syncAgentDir(args[1]);
48
+ return;
49
+ }
50
+ const help = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
51
+ (help ? console.log : console.error)("Usage: agentradar setup [--trust] | agentradar runner start <runner-id> | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
52
+ if (!help)
53
+ process.exitCode = 1;
30
54
  }
31
55
  main().catch((error) => {
32
56
  console.error(error instanceof Error ? error.message : "AgentRadar command failed");
package/dist/setup.js CHANGED
@@ -59,9 +59,17 @@ export function claudeConfigPath(home, env = process.env) {
59
59
  *
60
60
  * Returns the resolved project path so the caller can name it in the trust guidance.
61
61
  */
62
+ function trustedMcpEndpoint(value) {
63
+ const endpoint = new URL(value);
64
+ const loopbackHttp = endpoint.protocol === "http:" && (endpoint.hostname === "localhost" ||
65
+ endpoint.hostname === "127.0.0.1" ||
66
+ endpoint.hostname === "[::1]" ||
67
+ endpoint.hostname.endsWith(".localhost"));
68
+ return endpoint.protocol === "https:" || loopbackHttp ? endpoint : null;
69
+ }
62
70
  async function installProjectMcp(configPath, cwd, installation, trust) {
63
- const endpoint = new URL(installation.mcpEndpoint);
64
- if (endpoint.protocol !== "https:" || !/^ar_[A-Za-z0-9_-]+$/.test(installation.bearer)) {
71
+ const endpoint = trustedMcpEndpoint(installation.mcpEndpoint);
72
+ if (!endpoint || !/^ar_[A-Za-z0-9_-]+$/.test(installation.bearer)) {
65
73
  throw new Error("AgentRadar returned an invalid setup payload");
66
74
  }
67
75
  const projectPath = await realpath(cwd);
@@ -130,8 +138,8 @@ async function installRunner(home, projectPath, approved) {
130
138
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(approved.runnerId)) {
131
139
  throw new Error("AgentRadar returned an invalid runner ID");
132
140
  }
133
- const endpoint = new URL(approved.installation.mcpEndpoint);
134
- if (endpoint.protocol !== "https:" || !/^ar_[A-Za-z0-9_-]+$/.test(approved.installation.bearer)) {
141
+ const endpoint = trustedMcpEndpoint(approved.installation.mcpEndpoint);
142
+ if (!endpoint || !/^ar_[A-Za-z0-9_-]+$/.test(approved.installation.bearer)) {
135
143
  throw new Error("AgentRadar returned an invalid setup payload");
136
144
  }
137
145
  const agentDir = join(home, ".agentradar", "agents");
package/dist/sync.js ADDED
@@ -0,0 +1,89 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { homedir, platform } from "node:os";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ const run = promisify(execFile);
7
+ /**
8
+ * `agentradar sync [dir]` — fast-forward a git-scoped agent project dir to its remote branch.
9
+ *
10
+ * Safe by construction:
11
+ * - refuses when tracked files are locally MODIFIED (vendor edits are never clobbered);
12
+ * - `checkout -f -B <branch> <remote>/<branch>` rewrites tracked paths only, so untracked runtime
13
+ * state (sessions, .env, caches) survives;
14
+ * - uses the vendor's own git credentials — the CLI never carries any.
15
+ *
16
+ * Config (optional, via `git config`): `agentradar.syncRemote` (default `origin`),
17
+ * `agentradar.syncBranch` (default `main`).
18
+ */
19
+ export async function syncAgentDir(dir = process.cwd()) {
20
+ const git = (...args) => run("git", ["-C", dir, ...args]);
21
+ try {
22
+ await git("rev-parse", "--git-dir");
23
+ }
24
+ catch {
25
+ console.error(`Not a git repository: ${dir}`);
26
+ return 1;
27
+ }
28
+ const remote = (await git("config", "--get", "agentradar.syncRemote").catch(() => ({ stdout: "origin" }))).stdout.trim() || "origin";
29
+ const branch = (await git("config", "--get", "agentradar.syncBranch").catch(() => ({ stdout: "main" }))).stdout.trim() || "main";
30
+ const dirty = (await git("status", "--porcelain")).stdout
31
+ .split("\n")
32
+ .filter((l) => l && !l.startsWith("??"));
33
+ if (dirty.length > 0) {
34
+ console.error(`Refusing to sync: ${dirty.length} tracked file(s) locally modified in ${dir}. Commit, stash, or revert them first.`);
35
+ return 1;
36
+ }
37
+ await git("fetch", remote, branch);
38
+ const target = (await git("rev-parse", `${remote}/${branch}`)).stdout.trim();
39
+ const head = (await git("rev-parse", "HEAD").catch(() => ({ stdout: "" }))).stdout.trim();
40
+ if (head === target) {
41
+ console.log(`Up to date at ${target.slice(0, 8)}.`);
42
+ return 0;
43
+ }
44
+ await git("checkout", "-f", "-B", branch, `${remote}/${branch}`);
45
+ console.log(`Synced ${dir} to ${remote}/${branch} @ ${target.slice(0, 8)}.`);
46
+ return 0;
47
+ }
48
+ /**
49
+ * `agentradar sync install [dir] [--interval <seconds>]` — install a background timer that runs
50
+ * `agentradar sync <dir>` periodically. launchd LaunchAgent on macOS, systemd user timer on Linux.
51
+ */
52
+ export async function installSyncTimer(dir = process.cwd(), intervalSeconds = 600) {
53
+ const abs = dir;
54
+ const cli = process.argv[1] ?? "agentradar";
55
+ const node = process.execPath;
56
+ if (platform() === "darwin") {
57
+ const label = `com.agentradar.sync.${abs.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-40)}`;
58
+ const plistDir = join(homedir(), "Library", "LaunchAgents");
59
+ await mkdir(plistDir, { recursive: true });
60
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
61
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
62
+ <plist version="1.0"><dict>
63
+ <key>Label</key><string>${label}</string>
64
+ <key>ProgramArguments</key><array><string>${node}</string><string>${cli}</string><string>sync</string><string>${abs}</string></array>
65
+ <key>StartInterval</key><integer>${intervalSeconds}</integer>
66
+ <key>RunAtLoad</key><true/>
67
+ <key>StandardOutPath</key><string>${join(homedir(), "Library", "Logs", `${label}.log`)}</string>
68
+ <key>StandardErrorPath</key><string>${join(homedir(), "Library", "Logs", `${label}.log`)}</string>
69
+ </dict></plist>
70
+ `;
71
+ const plistPath = join(plistDir, `${label}.plist`);
72
+ await writeFile(plistPath, plist);
73
+ await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label}`]).catch(() => undefined);
74
+ await run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plistPath]);
75
+ console.log(`Installed launchd timer ${label} (every ${intervalSeconds}s) for ${abs}.`);
76
+ return 0;
77
+ }
78
+ // Linux: systemd user service + timer.
79
+ const unit = `agentradar-sync-${abs.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-40)}`;
80
+ const unitDir = join(homedir(), ".config", "systemd", "user");
81
+ await mkdir(unitDir, { recursive: true });
82
+ await writeFile(join(unitDir, `${unit}.service`), `[Unit]\nDescription=AgentRadar sync ${abs}\n\n[Service]\nType=oneshot\nExecStart=${node} ${cli} sync ${abs}\n`);
83
+ await writeFile(join(unitDir, `${unit}.timer`), `[Unit]\nDescription=AgentRadar sync timer ${abs}\n\n[Timer]\nOnBootSec=60\nOnUnitActiveSec=${intervalSeconds}\n\n[Install]\nWantedBy=timers.target\n`);
84
+ await run("systemctl", ["--user", "daemon-reload"]);
85
+ await run("systemctl", ["--user", "enable", "--now", `${unit}.timer`]);
86
+ console.log(`Installed systemd user timer ${unit} (every ${intervalSeconds}s) for ${abs}.`);
87
+ return 0;
88
+ }
89
+ //# sourceMappingURL=sync.js.map
@@ -421,6 +421,10 @@ export function stripVolatilePaneLines(text) {
421
421
  .join("\n");
422
422
  }
423
423
 
424
+ export function paneInputReady(text) {
425
+ return /bypass permissions on\b|^\s*❯/im.test(String(text ?? "").split("\n").slice(-12).join("\n"));
426
+ }
427
+
424
428
  function capturePane() {
425
429
  const r = spawnSync("tmux", ["capture-pane", "-p", "-t", TMUX_SESSION], { encoding: "utf8" });
426
430
  if (r.status !== 0 || typeof r.stdout !== "string") {
@@ -439,9 +443,10 @@ function capturePane() {
439
443
  // claude's TUI shows a working line ("… esc to interrupt") while mid-turn. This is only a HINT that
440
444
  // triggers a probe on stagnation — it never decides liveness, so a missed/extra hint is harmless.
441
445
  const inProgress = /esc to interrupt|\besc to cancel\b/i.test(text);
442
- // This runner always starts Claude with --dangerously-skip-permissions. Its input footer is therefore a
443
- // positive signal that startup/trust/auth screens are gone and the interactive TUI can accept a work turn.
444
- const inputReady = /bypass permissions on\b/i.test(text.split("\n").slice(-12).join("\n"));
446
+ // Claude's idle input prompt is the durable readiness signal. The optional bypass-permissions footer
447
+ // disappeared from a live Claude build after several turns, which falsely demoted a healthy pane to
448
+ // `starting` and stopped next_event from draining until its heartbeat aged stale.
449
+ const inputReady = paneInputReady(text);
445
450
  return { hash, inProgress, inputReady };
446
451
  }
447
452
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Secure AgentRadar vendor setup and skill installer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "dist/setup.js",
12
12
  "dist/runner.js",
13
13
  "dist/skill-install.js",
14
+ "dist/sync.js",
14
15
  "dist/vendor-runner/**",
15
16
  "dist/agentradar-vendor/SKILL.md",
16
17
  "README.md"