agentradar-cli 0.1.7 → 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/README.md CHANGED
@@ -11,7 +11,7 @@ npx -y agentradar-cli runner start <runner-id>
11
11
  npx -y agentradar-cli skill install
12
12
  ```
13
13
 
14
- The setup command prints a short approval code. Approve that code in the AgentRadar vendor portal; the CLI then receives the AgentRadar bearer in the polling response body and writes the project MCP entry plus the supported runner's private config under `~/.agentradar`, with secret-bearing files mode `0600`. It prints the paths and the exact runner start command. It never prints the bearer or puts it in a URL, command argument, or repository file. The supervisor stages credentials into private runtime files, scrubs its child environment, and gives Claude Code only the strict AgentRadar MCP config.
14
+ The setup command prints a short approval code. Approve that code in the AgentRadar vendor portal; the CLI then receives the AgentRadar bearer in the polling response body and writes the project MCP entry plus the supported runner's private config under `~/.agentradar`, with secret-bearing files mode `0600`. It prints the paths and the exact runner start command. It never prints the bearer or puts it in a URL, command argument, or repository file. The supervisor stages only AgentRadar and vendor-tool credentials into private runtime files, scrubs its child environment, and gives Claude Code only the strict AgentRadar MCP config.
15
15
 
16
16
  Claude Code asks you to trust the folder on first launch. `setup` does not pre-accept that for you, and the onboarding lines above deliberately do not: the `hasTrustDialogAccepted` flag trusts everything already in the directory — existing hooks, `.claude/settings.json`, skills — not just the MCP entry setup added. Dismiss the prompt yourself.
17
17
 
@@ -19,4 +19,4 @@ There is a `--trust` flag for unattended runners, where a launch prompt would ha
19
19
 
20
20
  The skill command installs only the package-bundled `agentradar-vendor` markdown skill at `.claude/skills/agentradar-vendor/SKILL.md` in the current project. It does not fetch or require access to the private AgentRadar repository.
21
21
 
22
- Claude authentication stays separate. On macOS, the runner reuses the vendor's existing Claude Code OAuth login by extracting only `claudeAiOauth.accessToken` from the `Claude Code-credentials` Keychain item into a mode-`0600` runtime file; it never copies the credential profile into the isolated config. An explicit `CLAUDE_CODE_OAUTH_TOKEN` in the private runner env remains supported and takes precedence. `ANTHROPIC_API_KEY` is prohibited and the supervisor refuses to start when it is set.
22
+ Claude authentication stays separate. On macOS, the runner uses the vendor's existing Keychain-backed Claude Code login through the normal user config path; it never copies, extracts, or injects Claude credentials. `CLAUDE_CODE_OAUTH_TOKEN` is intentionally unset before launch because the environment-token contract does not authenticate the interactive TUI. `ANTHROPIC_API_KEY` is prohibited and the supervisor refuses to start when it is set. Connector isolation remains enforced by `--strict-mcp-config` with the private AgentRadar MCP file.
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");
@@ -205,7 +213,7 @@ export async function setupVendorAgent(deps = {}) {
205
213
  : `Claude Code will ask you to trust ${projectPath} on first launch — approve it there. Not pre-accepted, because that would trust everything already in the folder, not just this MCP. Re-run with --trust for unattended onboarding.`);
206
214
  log(`Runner installed → ${runnerEnv}`);
207
215
  log(`Start it with: agentradar runner start ${approved.runnerId}`);
208
- log("On macOS the runner uses your existing Claude Code Keychain OAuth credential; an explicit CLAUDE_CODE_OAUTH_TOKEN is also supported. ANTHROPIC_API_KEY is prohibited.");
216
+ log("On macOS the runner uses your existing Claude Code Keychain-backed login. CLAUDE_CODE_OAUTH_TOKEN is ignored; ANTHROPIC_API_KEY is prohibited.");
209
217
  return;
210
218
  }
211
219
  throw new Error("AgentRadar setup code expired; run setup again");
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
  /**
@@ -9,8 +9,8 @@
9
9
  #
10
10
  # 1. a tmux session ($TMUX_SESSION) running the agent:
11
11
  # claude --continue $CLAUDE_EXTRA_FLAGS --strict-mcp-config --mcp-config=$MCP_CONFIG
12
- # in $AR_BASE, with a SCRUBBED allowlist env + a FRESH $CLAUDE_CONFIG_DIR
13
- # (so the agent sees ONLY the AgentRadar MCP — no operator connectors);
12
+ # in $AR_BASE, with a SCRUBBED allowlist env + the macOS user's normal
13
+ # Keychain-backed Claude session. Strict MCP flags allow ONLY AgentRadar's config;
14
14
  # 2. the feeder ($FEEDER_JS) OUTSIDE that session, holding $AWP_BEARER, long-
15
15
  # polling next_event and tmux-injecting each event into the pane.
16
16
  #
@@ -51,14 +51,8 @@ set +a
51
51
  : "${AR_BASE:?AR_BASE required (the agent working dir)}"
52
52
  : "${AWP_MCP_URL:?AWP_MCP_URL required}"
53
53
  : "${AWP_BEARER:?AWP_BEARER required}"
54
- # CLAUDE_CODE_OAUTH_TOKEN is an OPTIONAL explicit override. When unset on macOS, section 2b extracts
55
- # claudeAiOauth.accessToken from Claude Code's existing Keychain credential into the same private-file
56
- # handoff. With neither source, Claude uses its normal interactive auth path. ANTHROPIC_API_KEY remains
57
- # hard-refused below.
58
- : "${CLAUDE_CODE_OAUTH_TOKEN:=}"
59
54
  : "${MCP_CONFIG:?MCP_CONFIG required (path to awp-only.json)}"
60
55
  TMUX_SESSION="${TMUX_SESSION:-${AGENT_NAME}}"
61
- CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-${HOME}/.agentradar/config/${AGENT_NAME}}"
62
56
  FEEDER_JS="${FEEDER_JS:-$(cd "$(dirname "$0")/.." && pwd)/feeder.mjs}"
63
57
  PANE_LIVE_SH="${PANE_LIVE_SH:-$(cd "$(dirname "$0")/.." && pwd)/pane-live.sh}"
64
58
  # #133 M4 live terminal — the OPTIONAL feeder bridge (streams the tmux pane to the portals). It's an
@@ -111,6 +105,11 @@ if [ -n "${AGENT_TOOL_ENV_FILE}" ]; then
111
105
  log "loaded ${#TOOL_ENV_ARGS[@]} tool-env var(s) from ${AGENT_TOOL_ENV_FILE}"
112
106
  fi
113
107
 
108
+ # Interactive Claude must use the user's persisted Keychain-backed login. An isolated config or an
109
+ # environment OAuth token selects a different auth contract and can put the TUI back into onboarding.
110
+ # env -i below still scrubs everything else; strict MCP flags, not CLAUDE_CONFIG_DIR, isolate connectors.
111
+ unset CLAUDE_CONFIG_DIR CLAUDE_CODE_OAUTH_TOKEN
112
+
114
113
  # HARD RULE — OAuth only, never a pay-as-you-go API key.
115
114
  [ -z "${ANTHROPIC_API_KEY:-}" ] || die "ANTHROPIC_API_KEY is set — refuse to start (claude.ai OAuth only)"
116
115
 
@@ -119,15 +118,12 @@ command -v "${CLAUDE_BIN}" >/dev/null 2>&1 || die "claude CLI not found (${CLAUD
119
118
  [ -f "${FEEDER_JS}" ] || die "feeder not found: ${FEEDER_JS}"
120
119
  [ -f "${PANE_LIVE_SH}" ] || die "pane liveness check not found: ${PANE_LIVE_SH}"
121
120
  [ -d "${AR_BASE}" ] || die "AR_BASE dir not found: ${AR_BASE}"
122
- mkdir -p "${CLAUDE_CONFIG_DIR}"
123
121
 
124
122
  # --- 2b. Credential hygiene (#152): secrets to 0600 files, OUT of argv+env ------
125
- # The AWP bearer (feeder) and — when used — the OAuth setup-token (claude) must not linger in any
126
- # long-lived process's ARGV (`ps aux`) or ENVIRONMENT (`ps eww`), where a co-resident process on the
127
- # box could read them. We stage them into per-agent 0600 files and hand each consumer the FILE PATH
128
- # (not a secret): the feeder reads AWP_BEARER_FILE; the scrubbed claude wrapper reads AR_OAUTH_TOKEN_FILE.
129
- # Then we UNSET the raw vars from this supervisor's env so neither the supervisor nor the feeder it
130
- # spawns inherits them. (The agent's own outbound bearer already lives only in awp-only.json @0600.)
123
+ # The AWP bearer must not linger in any long-lived process's ARGV (`ps aux`) or ENVIRONMENT
124
+ # (`ps eww`), where a co-resident process on the box could read it. Stage it into a per-agent 0600
125
+ # file and hand the feeder only the FILE PATH. Claude authenticates directly through the user's normal
126
+ # macOS Keychain-backed session; no Claude credential is copied, extracted, or injected.
131
127
  _SECRETS_DIR="${HOME}/.agentradar/run/${AGENT_NAME}"
132
128
  ( umask 077; mkdir -p "${_SECRETS_DIR}" )
133
129
  chmod 700 "${_SECRETS_DIR}" 2>/dev/null || true
@@ -135,34 +131,12 @@ _BEARER_FILE="${_SECRETS_DIR}/awp_bearer"
135
131
  ( umask 077; printf '%s' "${AWP_BEARER}" > "${_BEARER_FILE}" )
136
132
  AWP_BEARER_FILE="${_BEARER_FILE}"
137
133
  unset AWP_BEARER # drop from the supervisor's exported env → feeder no longer inherits it; ps eww clean
138
- # OAuth token: same treatment. An explicit CLAUDE_CODE_OAUTH_TOKEN still wins. Otherwise, on macOS,
139
- # Claude Code stores a JSON credential in the "Claude Code-credentials" Keychain item; the env contract
140
- # needs only its claudeAiOauth.accessToken scalar, not that whole JSON copied into .credentials.json.
141
- # Pipe Keychain → plutil → a 0600 file so the token never appears in logs, argv, or this supervisor's env.
142
- _OAUTH_TOKEN_FILE="${_SECRETS_DIR}/oauth_token"
143
- if [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
144
- ( umask 077; printf '%s' "${CLAUDE_CODE_OAUTH_TOKEN}" > "${_OAUTH_TOKEN_FILE}" )
145
- elif [ "$(uname -s)" = "Darwin" ] && [ -x /usr/bin/security ] && [ -x /usr/bin/plutil ]; then
146
- _OAUTH_TOKEN_TMP="${_OAUTH_TOKEN_FILE}.$$"
147
- if ( umask 077; /usr/bin/security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null \
148
- | /usr/bin/plutil -extract claudeAiOauth.accessToken raw -o "${_OAUTH_TOKEN_TMP}" - 2>/dev/null ) \
149
- && [ -s "${_OAUTH_TOKEN_TMP}" ]; then
150
- mv "${_OAUTH_TOKEN_TMP}" "${_OAUTH_TOKEN_FILE}"
151
- log "loaded Claude Code OAuth credential from macOS Keychain"
152
- else
153
- rm -f "${_OAUTH_TOKEN_TMP}"
154
- _OAUTH_TOKEN_FILE=""
155
- fi
156
- else
157
- _OAUTH_TOKEN_FILE=""
158
- fi
159
- unset CLAUDE_CODE_OAUTH_TOKEN # out of the supervisor+feeder env (ps eww); staged to the 0600 file above
160
134
  # Vendor MCP-tool creds (#166): the agent's OWN tool API keys (WEBFLOW_TOKEN, PERPLEXITY_API_KEY, …) were
161
135
  # being passed INLINE as KEY=VALUE on the env -i line → stored PLAINTEXT in tmux's pane_start_command
162
136
  # (`tmux list-panes` = any co-resident proc) and in claude's env (ps eww). Same inline-secret class as the
163
137
  # bearer/token. Stage the already-validated entries to a 0600 file and pass ONLY AR_TOOL_ENV_FILE (a PATH);
164
138
  # the wrapper reads+exports them just before exec, so the VALUES never appear on the pane command / argv
165
- # (still in claude's env by necessity, exactly like the OAuth token — acceptable, same-user only).
139
+ # (tool credentials remain in Claude's environment because their MCP subprocesses require them).
166
140
  _TOOL_ENV_FILE=""
167
141
  if [ "${#TOOL_ENV_ARGS[@]}" -gt 0 ]; then
168
142
  _TOOL_ENV_FILE="${_SECRETS_DIR}/tool_env"
@@ -231,29 +205,22 @@ ensure_agent() {
231
205
  fi
232
206
  # SCRUBBED launch: env -i with a strict allowlist so the agent inherits nothing from this daemon
233
207
  # (AWP_BEARER, AWP_MCP_URL, etc. never reach the pane). CONNECTOR ISOLATION is enforced by
234
- # --strict-mcp-config (the agent loads ONLY $MCP_CONFIG's servers) — NOT by the config dir, which is
235
- # deliberately the agent's EXISTING CLAUDE_CONFIG_DIR (often the shared ~/.claude) so `claude
236
- # --continue` resumes the real conversation. The agent's own tool creds (from AGENT_TOOL_ENV_FILE) are
237
- # NOT inlined here anymore (#166): they were staged to a 0600 file in 2b and are passed only as the PATH
208
+ # --strict-mcp-config (the agent loads ONLY $MCP_CONFIG's servers). Claude's config override stays unset,
209
+ # so the normal HOME/USER Keychain-backed interactive session and conversation history remain available.
210
+ # The agent's own tool creds (from AGENT_TOOL_ENV_FILE) are NOT inlined here anymore (#166): they were staged to a 0600 file in 2b and are passed only as the PATH
238
211
  # AR_TOOL_ENV_FILE, which the wrapper reads+exports before exec — so no tool VALUE appears on the env -i
239
212
  # argv / tmux pane_start_command. Reserved keys were already rejected at tool-file parse time (2b).
240
213
  log "starting agent in tmux '${TMUX_SESSION}' (flags: ${CLAUDE_EXTRA_FLAGS:-none}; tool-env file: ${_TOOL_ENV_FILE:-none})"
241
214
  # SC2086: CLAUDE_EXTRA_FLAGS is intentionally UNQUOTED so "--chrome" (etc.) word-splits into flags.
242
- # _tool_env / _oauth_env use the ${arr[@]+"${arr[@]}"} form so an EMPTY array is safe under `set -u` (bash 3.2).
215
+ # _tool_env uses the ${arr[@]+"${arr[@]}"} form so an EMPTY array is safe under `set -u` (bash 3.2).
243
216
  # First-launch handling (#153/H4 durable-feeder): `claude --continue` EXITS 1 when the cwd has NO prior
244
- # conversation (a fresh engagement folder / brand-new CLAUDE_CONFIG_DIR) → the pane dies → the supervisor
217
+ # conversation (for example, a fresh engagement folder) → the pane dies → the supervisor
245
218
  # crash-loops. So try `--continue`, and FALL BACK to a fresh `claude` (which stays up AND creates the
246
219
  # session) when there's nothing to resume. Self-healing: once a conversation exists, --continue resumes it;
247
220
  # if it's ever lost, we cleanly start fresh instead of exit-looping. (--continue exits 1 on no-session,
248
221
  # verified — so the `||` fallback is reliable.) The `/bin/sh -c` wrapper keeps EXTRA_FLAGS word-splitting.
249
222
  # shellcheck disable=SC2086
250
223
  _launch_flags=( ${CLAUDE_EXTRA_FLAGS} --strict-mcp-config --mcp-config="${MCP_CONFIG}" )
251
- # OAuth (#152): section 2b staged either the explicit token or the macOS Keychain accessToken into a
252
- # 0600 file and kept the value out of the supervisor+feeder env. Here we pass only that file PATH through
253
- # env -i; the wrapper reads CLAUDE_CODE_OAUTH_TOKEN immediately before exec'ing Claude. If neither source
254
- # exists, _OAUTH_TOKEN_FILE is empty and Claude falls back to its normal interactive auth path.
255
- _oauth_env=()
256
- [ -n "${_OAUTH_TOKEN_FILE}" ] && _oauth_env=( AR_OAUTH_TOKEN_FILE="${_OAUTH_TOKEN_FILE}" )
257
224
  # #166: pass the tool creds as a FILE PATH (AR_TOOL_ENV_FILE), never inline KEY=VALUE on env -i.
258
225
  _tool_env=()
259
226
  [ -n "${_TOOL_ENV_FILE}" ] && _tool_env=( AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" )
@@ -285,8 +252,8 @@ ensure_agent() {
285
252
  LANG="${LANG:-en_US.UTF-8}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${AR_AGENT_LC_CTYPE:-en_US.UTF-8}" \
286
253
  TMPDIR="${TMPDIR:-/tmp}" NODE_OPTIONS="${NODE_OPTIONS:-}" \
287
254
  DISABLE_AUTOUPDATER=1 \
288
- ${_tool_env[@]+"${_tool_env[@]}"} ${_oauth_env[@]+"${_oauth_env[@]}"} CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR}" \
289
- /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|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; if [ -n "${AR_OAUTH_TOKEN_FILE:-}" ]; then CLAUDE_CODE_OAUTH_TOKEN="$(cat "$AR_OAUTH_TOKEN_FILE")"; export CLAUDE_CODE_OAUTH_TOKEN; unset AR_OAUTH_TOKEN_FILE; fi; CB="$1"; shift; "$CB" --continue "$@" || exec "$CB" "$@"' sh "${CLAUDE_BIN}" "${_launch_flags[@]}" || {
255
+ ${_tool_env[@]+"${_tool_env[@]}"} \
256
+ /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|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; CB="$1"; shift; "$CB" --continue "$@" || exec "$CB" "$@"' sh "${CLAUDE_BIN}" "${_launch_flags[@]}" || {
290
257
  log "tmux could not start agent session '${TMUX_SESSION}'"
291
258
  return 1
292
259
  }
@@ -301,7 +268,7 @@ FEEDER_PID=""
301
268
  cleanup() {
302
269
  [ -n "${FEEDER_PID}" ] && kill "${FEEDER_PID}" 2>/dev/null || true
303
270
  [ -n "${BRIDGE_PID:-}" ] && kill "${BRIDGE_PID}" 2>/dev/null || true # #133 terminal bridge
304
- # #152: remove the staged 0600 secret files (bearer + optional oauth token) on exit.
271
+ # #152: remove the staged 0600 secret files on exit.
305
272
  [ -n "${_SECRETS_DIR:-}" ] && rm -rf "${_SECRETS_DIR}" 2>/dev/null || true
306
273
  }
307
274
  trap cleanup EXIT INT TERM
@@ -315,8 +282,7 @@ log "starting feeder (${FEEDER_JS}) → session '${TMUX_SESSION}'"
315
282
  # #152: pass AWP_BEARER_FILE (a 0600 PATH), NEVER the raw AWP_BEARER — the feeder reads the file, so the
316
283
  # bearer is not in the feeder process's environment (`ps eww`) or argv. AWP_MCP_URL is not a secret.
317
284
  AWP_MCP_URL="${AWP_MCP_URL}" AWP_BEARER_FILE="${AWP_BEARER_FILE}" TMUX_SESSION="${TMUX_SESSION}" \
318
- AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" CLAUDE_BIN="${CLAUDE_BIN}" CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR}" \
319
- AR_OAUTH_TOKEN_FILE="${_OAUTH_TOKEN_FILE}" node "${FEEDER_JS}" &
285
+ AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" CLAUDE_BIN="${CLAUDE_BIN}" node "${FEEDER_JS}" &
320
286
  FEEDER_PID=$!
321
287
  log "feeder pid ${FEEDER_PID}"
322
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.1.7",
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"