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 +26 -2
- package/dist/setup.js +12 -4
- package/dist/sync.js +89 -0
- package/dist/vendor-runner/feeder.mjs +8 -3
- package/package.json +2 -1
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
|
-
|
|
29
|
-
|
|
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 =
|
|
64
|
-
if (endpoint
|
|
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 =
|
|
134
|
-
if (endpoint
|
|
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
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
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.
|
|
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"
|