agentradar-cli 0.1.3 → 0.1.5

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.
@@ -47,6 +47,8 @@ const AWP_BEARER = (() => {
47
47
  const TMUX_SESSION = process.env.TMUX_SESSION ?? "agent";
48
48
  // The claude pid marker (same predicate entrypoint.sh uses) — proves the pane is live.
49
49
  const AGENT_PID_PATTERN = process.env.AGENT_PID_PATTERN ?? "mcp-config.*awp-only";
50
+ const CLAUDE_BIN = process.env.CLAUDE_BIN ?? "claude";
51
+ const AR_OAUTH_TOKEN_FILE = process.env.AR_OAUTH_TOKEN_FILE ?? "";
50
52
 
51
53
  const MCP_CALL_TIMEOUT_MS = Number(process.env.AWP_MCP_CALL_TIMEOUT_MS ?? "30000");
52
54
  // #171: mirror the SERVER long-poll hold (NEXT_EVENT_HOLD_MS, default 200s), held UNDER the ~240s ALB
@@ -193,12 +195,47 @@ function toolJson(result) {
193
195
  const text = result?.content?.find((c) => c.type === "text")?.text;
194
196
  return text ? JSON.parse(text) : {};
195
197
  }
196
- async function nextEvent(ackEventIds) {
197
- const args = ackEventIds && ackEventIds.length ? { ackEventIds } : {};
198
+ async function nextEvent(ackEventIds, runnerState) {
199
+ const args = {
200
+ ...(ackEventIds && ackEventIds.length ? { ackEventIds } : {}),
201
+ ...(runnerState ? { runnerState } : {}),
202
+ };
198
203
  const result = await mcpPost("tools/call", { name: "next_event", arguments: args }, { timeoutMs: NEXT_EVENT_TIMEOUT_MS });
199
204
  return toolJson(result); // { events: [...] }
200
205
  }
201
206
 
207
+ // --------------------------------------------------------------------------
208
+ // Work readiness — process liveness is not enough. A live Claude process can
209
+ // be blocked on subscription OAuth, where injected work is consumed as the OAuth code.
210
+ // --------------------------------------------------------------------------
211
+ export function claudeAuthState(run = spawnSync, readFile = fs.readFileSync) {
212
+ const env = {};
213
+ for (const name of ["HOME", "PATH", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "CLAUDE_CONFIG_DIR"]) {
214
+ if (typeof process.env[name] === "string") env[name] = process.env[name];
215
+ }
216
+ if (AR_OAUTH_TOKEN_FILE) {
217
+ try {
218
+ env.CLAUDE_CODE_OAUTH_TOKEN = readFile(AR_OAUTH_TOKEN_FILE, "utf8").trim();
219
+ } catch {
220
+ return "unknown";
221
+ }
222
+ }
223
+ const r = run(CLAUDE_BIN, ["auth", "status", "--json"], { encoding: "utf8", env, timeout: 10000 });
224
+ if (typeof r.stdout !== "string") return "unknown";
225
+ try {
226
+ return JSON.parse(r.stdout).loggedIn === true ? "authenticated" : "unauthenticated";
227
+ } catch {
228
+ return "unknown";
229
+ }
230
+ }
231
+
232
+ export function runnerState(auth = claudeAuthState, sample = capturePane) {
233
+ const authState = auth();
234
+ if (authState === "unauthenticated") return "auth_required";
235
+ if (authState !== "authenticated") return "starting";
236
+ return sample()?.inputReady ? "ready" : "starting";
237
+ }
238
+
202
239
  // --------------------------------------------------------------------------
203
240
  // tmux injection — push one event into the live pane as the agent's next turn.
204
241
  // --------------------------------------------------------------------------
@@ -402,7 +439,10 @@ function capturePane() {
402
439
  // claude's TUI shows a working line ("… esc to interrupt") while mid-turn. This is only a HINT that
403
440
  // triggers a probe on stagnation — it never decides liveness, so a missed/extra hint is harmless.
404
441
  const inProgress = /esc to interrupt|\besc to cancel\b/i.test(text);
405
- return { hash, inProgress };
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"));
445
+ return { hash, inProgress, inputReady };
406
446
  }
407
447
  /**
408
448
  * #173b (disc obs) — format-drift detector. If the STRIPPED fingerprint keeps CHANGING while the pane
@@ -459,10 +499,11 @@ async function reportPane() {
459
499
  let _stopping = false;
460
500
  let _pendingAck = [];
461
501
  let _paneTimer = null;
502
+ let _lastRunnerState = null;
462
503
 
463
504
  /** One receive→inject→ack-select cycle (extracted from loop() + exported so the #173b _pendingAck
464
505
  * fence is directly testable). Sets `_pendingAck` = the eventIds to ack on the NEXT poll. */
465
- export async function pollOnce(paneLive = paneIsLive) {
506
+ export async function pollOnce(paneLive = paneIsLive, readiness = runnerState, deliverEvent = injectEvent) {
466
507
  // next_event refreshes the portal's runner heartbeat. Never call it for a detached/orphan feeder: a
467
508
  // process without its owned tmux session cannot receive work, so polling would paint a false Live state
468
509
  // while every event stays unacknowledged. The supervisor recreates the session; until then the durable
@@ -472,7 +513,19 @@ export async function pollOnce(paneLive = paneIsLive) {
472
513
  await sleep(IDLE_DELAY_MS);
473
514
  return;
474
515
  }
475
- const { events = [], livenessProbe } = await nextEvent(_pendingAck.length ? _pendingAck : undefined);
516
+ const state = readiness();
517
+ if (state !== _lastRunnerState) {
518
+ console.log(`[feeder] runner state ${_lastRunnerState ?? "unknown"} → ${state}`);
519
+ _lastRunnerState = state;
520
+ }
521
+ // The server treats non-ready calls as status-only heartbeats: no drain and no ack. Keep any pending ack
522
+ // until readiness returns, and defensively ignore events from an older server that does not know the gate.
523
+ const canWork = state === "ready";
524
+ const { events = [], livenessProbe } = await nextEvent(canWork && _pendingAck.length ? _pendingAck : undefined, state);
525
+ if (!canWork) {
526
+ await sleep(IDLE_DELAY_MS);
527
+ return;
528
+ }
476
529
  _pendingAck = [];
477
530
  // #156/#173b: forward a liveness probe (if any) into the pane so the agent proves it can act.
478
531
  // Framed like an event for TRUST (synthetic `probe-<nonce>` eventId, control-plane source) but
@@ -490,7 +543,7 @@ export async function pollOnce(paneLive = paneIsLive) {
490
543
  for (const e of events) {
491
544
  // Inject in order; ack ONLY the ones that reached a live pane (no-phantom).
492
545
  // eslint-disable-next-line no-await-in-loop
493
- if (await injectEvent(e)) delivered.push(e.eventId);
546
+ if (await deliverEvent(e)) delivered.push(e.eventId);
494
547
  // eslint-disable-next-line no-await-in-loop
495
548
  else break; // pane went away mid-batch → stop; unacked events re-deliver in order.
496
549
  // eslint-disable-next-line no-await-in-loop
@@ -307,7 +307,7 @@ log "starting feeder (${FEEDER_JS}) → session '${TMUX_SESSION}'"
307
307
  # #152: pass AWP_BEARER_FILE (a 0600 PATH), NEVER the raw AWP_BEARER — the feeder reads the file, so the
308
308
  # bearer is not in the feeder process's environment (`ps eww`) or argv. AWP_MCP_URL is not a secret.
309
309
  AWP_MCP_URL="${AWP_MCP_URL}" AWP_BEARER_FILE="${AWP_BEARER_FILE}" TMUX_SESSION="${TMUX_SESSION}" \
310
- AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" \
310
+ AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" CLAUDE_BIN="${CLAUDE_BIN}" AR_OAUTH_TOKEN_FILE="${_OAUTH_TOKEN_FILE}" \
311
311
  node "${FEEDER_JS}" &
312
312
  FEEDER_PID=$!
313
313
  log "feeder pid ${FEEDER_PID}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Secure AgentRadar vendor setup and skill installer",
5
5
  "type": "module",
6
6
  "bin": {