agentradar-cli 0.1.3 → 0.1.4
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 =
|
|
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 (r.status !== 0 || 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
|
-
|
|
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
|
|
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
|
|
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}"
|