@scotthuang/agent-knock-knock 0.2.51 → 0.3.0-beta.2

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/src/cli.js CHANGED
@@ -14,7 +14,7 @@ import { defaultClaudeSettingsPath, loadTrustedClaudeTokenjuiceLaunchers } from
14
14
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
15
15
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
16
16
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
17
- import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
17
+ import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, isExecutorKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
18
18
  import { executorBootstrapPrompt } from "./bootstrap.js";
19
19
  import { redactString, writeRuntimeLog } from "./runtime-log.js";
20
20
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
@@ -23,10 +23,12 @@ import { planFork, planTakeover } from "./session-takeover-planner.js";
23
23
  import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
24
24
  import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
25
25
  import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
26
- import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
26
+ import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
27
27
  import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
28
28
  import { evaluateApprovalPolicy } from "./approval-policy.js";
29
- import { evaluateDoctorCapabilities } from "./doctor-capabilities.js";
29
+ import { evaluateDoctorCapabilities, runDoctorCapabilityProbes } from "./doctor-capabilities.js";
30
+ import { runOpenClawChainDiagnostics } from "./openclaw-doctor.js";
31
+ import { resolveSessionSelector, sessionShortRef } from "./session-selector.js";
30
32
  const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
31
33
  const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
32
34
  const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
@@ -57,6 +59,18 @@ const CONVERSATION_STATUSES = new Set([
57
59
  "cancelled",
58
60
  "cancelling"
59
61
  ]);
62
+ const SESSION_SELECTOR_COMMANDS = new Set([
63
+ "status",
64
+ "describe",
65
+ "summary",
66
+ "send",
67
+ "approve",
68
+ "cancel",
69
+ "renew",
70
+ "retry-callback",
71
+ "recover",
72
+ "close"
73
+ ]);
60
74
  class InlineCodexSessionAdapter {
61
75
  threads;
62
76
  processes;
@@ -113,6 +127,7 @@ catch (error) {
113
127
  process.exit(1);
114
128
  }
115
129
  async function runCommand(commandName, options) {
130
+ await resolveConversationSelectorOption(commandName, options);
116
131
  if (commandName === "help" || commandName === "--help" || commandName === "-h") {
117
132
  usage();
118
133
  }
@@ -196,7 +211,24 @@ async function runCommand(commandName, options) {
196
211
  function runInstallOpenClaw(options) {
197
212
  const root = packageRootDir();
198
213
  const skillOnly = options.skillOnly === true;
199
- const needsOpenClaw = !skillOnly || options.noRestart !== true;
214
+ const workspace = options.workspace === undefined
215
+ ? undefined
216
+ : canonicalWorkspace(options.workspace);
217
+ const defaultAgent = optionalExecutorKind(options.defaultAgent);
218
+ const selectedMode = options.mode === undefined
219
+ ? undefined
220
+ : parseDoctorMode(options.mode);
221
+ if (selectedMode === "tmux" && defaultAgent === "cursor") {
222
+ throw new Error("--default-agent cursor requires --mode acpx or --mode all");
223
+ }
224
+ if (skillOnly &&
225
+ (workspace !== undefined ||
226
+ defaultAgent !== undefined ||
227
+ selectedMode !== undefined ||
228
+ options.verify === true)) {
229
+ throw new Error("--skill-only cannot be combined with --workspace, --default-agent, --mode, or --verify");
230
+ }
231
+ const needsOpenClaw = !skillOnly || options.noRestart !== true || options.verify === true;
200
232
  const openclawBin = needsOpenClaw
201
233
  ? options.openclawBin ?? resolveExecutable("openclaw")
202
234
  : options.openclawBin;
@@ -210,12 +242,35 @@ function runInstallOpenClaw(options) {
210
242
  path: root,
211
243
  mode: pluginInstall.mode
212
244
  });
213
- runCheckedCommand(openclawBin, ["plugins", "enable", "agent-knock-knock"], {
214
- label: "openclaw plugins enable"
215
- });
245
+ const configOperations = [
246
+ {
247
+ path: "plugins.entries.agent-knock-knock.enabled",
248
+ value: true
249
+ },
250
+ ...(workspace === undefined
251
+ ? []
252
+ : [{
253
+ path: "plugins.entries.agent-knock-knock.config.workspace",
254
+ value: workspace
255
+ }]),
256
+ ...(defaultAgent === undefined
257
+ ? []
258
+ : [{
259
+ path: "plugins.entries.agent-knock-knock.config.defaultAgent",
260
+ value: defaultAgent
261
+ }]),
262
+ ...(selectedMode === undefined
263
+ ? []
264
+ : [{
265
+ path: "plugins.entries.agent-knock-knock.config.mode",
266
+ value: selectedMode
267
+ }])
268
+ ];
269
+ runCheckedCommand(openclawBin, ["config", "set", "--batch-json", JSON.stringify(configOperations)], { label: "openclaw config set" });
216
270
  steps.push({
217
- name: "plugin_enabled",
218
- plugin: "agent-knock-knock"
271
+ name: "plugin_configured",
272
+ plugin: "agent-knock-knock",
273
+ updated: configOperations.map((operation) => operation.path)
219
274
  });
220
275
  }
221
276
  fs.mkdirSync(path.dirname(skillDest), { recursive: true });
@@ -232,17 +287,105 @@ function runInstallOpenClaw(options) {
232
287
  name: "gateway_restarted"
233
288
  });
234
289
  }
290
+ const pendingRestart = !skillOnly && options.noRestart === true;
291
+ const verification = options.verify === true
292
+ ? buildDoctorReport({
293
+ ...options,
294
+ openclawBin,
295
+ ...(workspace ? { workspace } : {}),
296
+ mode: selectedMode ?? "all"
297
+ })
298
+ : undefined;
299
+ const ready = verification
300
+ ? verification.ok === true && !pendingRestart
301
+ : false;
302
+ const nextActions = installNextActions({
303
+ pendingRestart,
304
+ verification,
305
+ mode: selectedMode ?? "all",
306
+ agent: defaultAgent ?? "codex"
307
+ });
235
308
  printJson({
236
309
  installed: true,
310
+ ready,
311
+ pending_restart: pendingRestart,
237
312
  mode: skillOnly ? "skill_only" : "full",
313
+ execution_mode: selectedMode ?? null,
314
+ default_agent: defaultAgent ?? null,
315
+ workspace: workspace ?? null,
238
316
  package_root: root,
239
317
  openclaw_bin: openclawBin ?? null,
240
318
  steps,
241
- next: options.noRestart === true
242
- ? "Restart the OpenClaw Gateway before using Agent Knock Knock."
243
- : "Agent Knock Knock is installed. Try: AKK list"
319
+ ...(verification ? { verification } : {}),
320
+ next_actions: nextActions
244
321
  });
245
322
  }
323
+ function canonicalWorkspace(value) {
324
+ const requested = path.resolve(String(required(value, "--workspace is required")));
325
+ let canonical;
326
+ let stat;
327
+ try {
328
+ canonical = fs.realpathSync(requested);
329
+ stat = fs.statSync(canonical);
330
+ }
331
+ catch {
332
+ throw new Error(`--workspace does not exist: ${requested}`);
333
+ }
334
+ if (!stat.isDirectory()) {
335
+ throw new Error(`--workspace must be a directory: ${requested}`);
336
+ }
337
+ return canonical;
338
+ }
339
+ function optionalExecutorKind(value) {
340
+ if (value === undefined) {
341
+ return undefined;
342
+ }
343
+ const normalized = String(value).trim().toLowerCase();
344
+ if (!isExecutorKind(normalized)) {
345
+ throw new Error(`--default-agent must be one of: ${EXECUTOR_KINDS.join(", ")}`);
346
+ }
347
+ return normalized;
348
+ }
349
+ function installNextActions({ pendingRestart, verification, mode, agent }) {
350
+ if (pendingRestart) {
351
+ return [
352
+ {
353
+ action: "apply_plugin_changes",
354
+ command: "openclaw gateway restart"
355
+ },
356
+ {
357
+ action: "verify",
358
+ command: `agent-knock-knock doctor --mode ${mode}`
359
+ }
360
+ ];
361
+ }
362
+ if (verification && verification.ok !== true) {
363
+ const chain = isRecord(verification.openclaw) ? verification.openclaw : {};
364
+ const checks = Array.isArray(chain.checks) ? chain.checks : [];
365
+ const remediation = checks.flatMap((check) => isRecord(check) && Array.isArray(check.remediation)
366
+ ? check.remediation.filter((command) => typeof command === "string")
367
+ : []);
368
+ return [...new Set(remediation)].map((command) => ({
369
+ action: "repair",
370
+ command
371
+ }));
372
+ }
373
+ if (!verification) {
374
+ return [{
375
+ action: "verify",
376
+ command: `agent-knock-knock doctor --mode ${mode}`
377
+ }];
378
+ }
379
+ return mode === "tmux"
380
+ ? [{
381
+ action: "start_agent",
382
+ command: `tmux new -s akk-${agent} ${agent}`
383
+ }]
384
+ : [{
385
+ action: "delegate",
386
+ command: `/akk ${agent} <task>`
387
+ }];
388
+ }
246
389
  async function runClaudeHook(options) {
247
390
  const rawInput = fs.readFileSync(0, "utf8");
248
391
  let input;
@@ -321,18 +464,41 @@ function installOpenClawPlugin(openclawBin, root) {
321
464
  return { mode: "replaced" };
322
465
  }
323
466
  function runDoctor(options) {
324
- const commands = ["node", "openclaw", "tmux", "acpx", "codex", "claude", "cursor"];
325
- const checks = commands.map((commandName) => {
326
- const check = executableCheck(commandName);
327
- return commandName === "node"
328
- ? {
329
- ...check,
330
- version: process.versions.node,
331
- version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
332
- minimum_version: MINIMUM_NODE_VERSION
333
- }
334
- : check;
335
- });
467
+ const report = buildDoctorReport(options);
468
+ printJson(report);
469
+ if (!report.ok) {
470
+ process.exitCode = 1;
471
+ }
472
+ }
473
+ function buildDoctorReport(options) {
474
+ const mode = parseDoctorMode(options.mode ?? "all");
475
+ const timeoutMs = options.timeoutMs === undefined
476
+ ? undefined
477
+ : positiveMilliseconds(options.timeoutMs, "--timeout-ms");
478
+ const openclawBin = String(options.openclawBin ?? resolveOptionalExecutable("openclaw"));
479
+ const executables = {
480
+ openclaw: openclawBin,
481
+ ...(options.tmuxBin ? { tmux: String(options.tmuxBin) } : {}),
482
+ ...(options.acpxBin ? { acpx: String(options.acpxBin) } : {}),
483
+ ...(options.codexBin ? { codex: String(options.codexBin) } : {}),
484
+ ...(options.claudeBin ? { claude: String(options.claudeBin) } : {}),
485
+ ...(options.cursorBin ? { cursor: String(options.cursorBin) } : {})
486
+ };
487
+ const checks = [
488
+ {
489
+ command: "node",
490
+ status: "ok",
491
+ available: true,
492
+ executable: process.execPath,
493
+ version: process.versions.node,
494
+ version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
495
+ minimum_version: MINIMUM_NODE_VERSION
496
+ },
497
+ ...runDoctorCapabilityProbes({
498
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
499
+ executables
500
+ }, mode)
501
+ ];
336
502
  const root = packageRootDir();
337
503
  const packageFiles = [
338
504
  "dist/src/cli.js",
@@ -346,29 +512,78 @@ function runDoctor(options) {
346
512
  exists: fs.existsSync(filePath)
347
513
  };
348
514
  });
349
- const capabilities = evaluateDoctorCapabilities(checks);
515
+ const capabilities = evaluateDoctorCapabilities(checks, mode);
350
516
  const filesOk = packageFiles.every((check) => check.exists);
351
- const ok = capabilities.coreOk && filesOk && capabilities.transportOk;
352
- printJson({
517
+ const openclaw = runOpenClawChainDiagnostics({
518
+ openclawBin,
519
+ ...(options.workspace ? { workspace: String(options.workspace) } : {}),
520
+ ...(timeoutMs === undefined ? {} : { timeoutMs })
521
+ });
522
+ const selectedAgent = optionalExecutorKind(options.defaultAgent) ??
523
+ openclaw.default_agent ??
524
+ "codex";
525
+ const selectedAgentReady = mode === "tmux"
526
+ ? capabilities.tmux.status === "ready" &&
527
+ capabilities.tmux.agents.includes(selectedAgent)
528
+ : mode === "acpx"
529
+ ? capabilities.acpx.status === "ready" &&
530
+ capabilities.acpx.agents.includes(selectedAgent)
531
+ : (capabilities.tmux.status === "ready" &&
532
+ capabilities.tmux.agents.includes(selectedAgent)) || (capabilities.acpx.status === "ready" &&
533
+ capabilities.acpx.agents.includes(selectedAgent));
534
+ const ok = capabilities.readiness === "ready" &&
535
+ selectedAgentReady &&
536
+ filesOk &&
537
+ openclaw.ready;
538
+ return {
353
539
  ok,
540
+ readiness: ok
541
+ ? "ready"
542
+ : capabilities.readiness === "not_ready"
543
+ ? "not_ready"
544
+ : "partially_ready",
545
+ selected_mode: mode,
546
+ selected_agent: selectedAgent
547
+ ? {
548
+ agent: selectedAgent,
549
+ ready: selectedAgentReady
550
+ }
551
+ : null,
354
552
  package_root: root,
355
553
  checks,
356
554
  package_files: packageFiles,
357
555
  capabilities: {
358
- tmux: capabilities.tmux,
359
- acp: capabilities.acp
556
+ tmux: {
557
+ ...capabilities.tmux,
558
+ checked: mode !== "acpx"
559
+ },
560
+ acpx: {
561
+ ...capabilities.acpx,
562
+ checked: mode !== "tmux"
563
+ }
360
564
  },
565
+ openclaw,
361
566
  notes: [
362
567
  `Node.js ${MINIMUM_NODE_VERSION}+ and OpenClaw are required.`,
363
568
  "Choose tmux (recommended), ACPX/ACP, or install both.",
364
569
  "tmux supports Codex and Claude Code; ACPX supports Codex, Claude Code, and Cursor.",
365
570
  "Claude tmux completion is hook-free and fails closed unless the local transcript schema is verified."
366
- ],
367
- options
368
- });
369
- if (!ok) {
370
- process.exitCode = 1;
571
+ ]
572
+ };
573
+ }
574
+ function parseDoctorMode(value) {
575
+ const normalized = String(value).trim().toLowerCase();
576
+ if (normalized === "tmux" || normalized === "acpx" || normalized === "all") {
577
+ return normalized;
371
578
  }
579
+ throw new Error("--mode must be one of: tmux, acpx, all");
580
+ }
581
+ function positiveMilliseconds(value, optionName) {
582
+ const parsed = Number(value);
583
+ if (!Number.isFinite(parsed) || parsed <= 0) {
584
+ throw new Error(`${optionName} must be a positive number`);
585
+ }
586
+ return Math.ceil(parsed);
372
587
  }
373
588
  function versionAtLeast(version, minimum) {
374
589
  const parsed = version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
@@ -2160,12 +2375,12 @@ async function runList(options) {
2160
2375
  const includeAll = Boolean(options.all);
2161
2376
  const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
2162
2377
  const statusFilter = options.status;
2163
- const conversations = listConversations(storeDir)
2164
- .map((conversation) => summarizeConversation(conversation))
2378
+ const storedConversations = listConversations(storeDir)
2165
2379
  .filter((conversation) => includeAll || isActiveStatus(conversation.status))
2166
- .filter((conversation) => !agentFilter || conversation.agent === agentFilter)
2380
+ .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
2167
2381
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
2168
- const delegated = conversations.map(delegatedListEntry);
2382
+ const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
2383
+ const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
2169
2384
  const nativeScan = await buildNativeListGroups({ options, agentFilter, statusFilter });
2170
2385
  printJson({
2171
2386
  store_dir: storeDir,
@@ -2274,23 +2489,26 @@ async function terminalControlDiagnostics(provider) {
2274
2489
  paneCount: (await provider.listPanes()).length
2275
2490
  };
2276
2491
  }
2277
- function delegatedListEntry(task) {
2492
+ function delegatedListEntry(task, { terminalBridge = false } = {}) {
2278
2493
  return {
2279
2494
  ...task,
2280
2495
  id: task.conversation_id,
2496
+ short_ref: sessionShortRef(task.conversation_id),
2281
2497
  source: "akk_delegate",
2282
2498
  commands: {
2283
2499
  send: canSendDelegated(task.status),
2284
2500
  cancel: isWaitingForAgent(task.status),
2285
2501
  close: task.status !== "closed",
2286
2502
  status: true,
2287
- approve: false
2503
+ approve: terminalBridge && isActiveStatus(task.status)
2288
2504
  }
2289
2505
  };
2290
2506
  }
2291
2507
  function nativeListEntry(session, activeSessions) {
2508
+ const id = `native:${session.agent}:${session.pid}`;
2292
2509
  return {
2293
- id: `native:${session.agent}:${session.pid}`,
2510
+ id,
2511
+ short_ref: sessionShortRef(id),
2294
2512
  source: "native_active",
2295
2513
  agent: session.agent,
2296
2514
  status: "active",
@@ -2328,6 +2546,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2328
2546
  });
2329
2547
  return {
2330
2548
  id: bridge.terminalConversationId(session),
2549
+ short_ref: sessionShortRef(bridge.terminalConversationId(session)),
2331
2550
  source: "terminal_control",
2332
2551
  agent: session.agent,
2333
2552
  status: "active",
@@ -2421,6 +2640,120 @@ function childPidsForRoot(root, processes) {
2421
2640
  function canSendDelegated(status) {
2422
2641
  return !["failed", "closed", "cancelled"].includes(status);
2423
2642
  }
2643
+ async function resolveConversationSelectorOption(commandName, options) {
2644
+ if (!SESSION_SELECTOR_COMMANDS.has(String(commandName ?? "")) ||
2645
+ options.state) {
2646
+ return;
2647
+ }
2648
+ const supplied = stringValue(options.conversation ?? options.conversationId)?.trim();
2649
+ if (supplied && !isSessionSelectorSyntax(supplied)) {
2650
+ // Full authoritative IDs keep their existing command-specific validation
2651
+ // path. This avoids a discovery scan before option validation and preserves
2652
+ // precise downstream errors for closed or currently non-actionable state.
2653
+ return;
2654
+ }
2655
+ const candidates = await sessionSelectorCandidates(commandName, options);
2656
+ const resolution = resolveSessionSelector(supplied, candidates, {
2657
+ operation: commandName
2658
+ });
2659
+ options.conversation = resolution.id;
2660
+ delete options.conversationId;
2661
+ }
2662
+ function isSessionSelectorSyntax(value) {
2663
+ return (/^(?:only|latest|codex|claude|cursor|(?:codex|claude|cursor):latest)$/iu.test(value) ||
2664
+ /^@[0-9a-f]+$/iu.test(value));
2665
+ }
2666
+ async function sessionSelectorCandidates(commandName, options) {
2667
+ const storeDir = storeDirFromOptions(options);
2668
+ cleanupIdleConversations(storeDir, options);
2669
+ const storedConversations = listConversations(storeDir);
2670
+ const managed = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
2671
+ const managedTerminalKeys = new Set(storedConversations
2672
+ .filter((conversation) => isActiveStatus(conversation.status))
2673
+ .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
2674
+ ? conversation.native_session_takeover
2675
+ : undefined)))
2676
+ .filter((key) => key !== undefined));
2677
+ const nativeScan = await buildNativeListGroups({
2678
+ options: {
2679
+ ...options,
2680
+ noApprovalScan: commandName === "approve"
2681
+ ? options.noApprovalScan
2682
+ : true
2683
+ },
2684
+ agentFilter: undefined,
2685
+ statusFilter: undefined
2686
+ });
2687
+ const observedAtMs = Date.now();
2688
+ return [
2689
+ ...managed,
2690
+ ...nativeScan.terminalControlled.filter((entry) => {
2691
+ const key = terminalControlSelectorKey(entry.terminal_control);
2692
+ return key === undefined || !managedTerminalKeys.has(key);
2693
+ }),
2694
+ ...nativeScan.native
2695
+ ].map((entry) => ({
2696
+ id: String(entry.id),
2697
+ agent: resolveExecutor({ kind: entry.agent }).kind,
2698
+ actionable: sessionEntrySupportsCommand(entry, commandName),
2699
+ ...sessionEntryRecency(entry, observedAtMs),
2700
+ source: stringValue(entry.source),
2701
+ status: stringValue(entry.status),
2702
+ workspace: stringValue(entry.workspace ?? entry.cwd),
2703
+ label: stringValue(entry.request ?? entry.command)
2704
+ }));
2705
+ }
2706
+ function terminalControlSelectorKey(value) {
2707
+ if (!isRecord(value)) {
2708
+ return undefined;
2709
+ }
2710
+ const target = stringValue(value.target);
2711
+ const panePid = Number(value.panePid);
2712
+ if (!target || !Number.isSafeInteger(panePid) || panePid <= 1) {
2713
+ return undefined;
2714
+ }
2715
+ return JSON.stringify({
2716
+ target,
2717
+ pane_pid: panePid,
2718
+ socket_path: stringValue(value.socketPath) ?? null
2719
+ });
2720
+ }
2721
+ function sessionEntrySupportsCommand(entry, commandName) {
2722
+ if (commandName === "summary") {
2723
+ commandName = "describe";
2724
+ }
2725
+ const commands = isRecord(entry.commands) ? entry.commands : {};
2726
+ if (typeof commands[commandName] === "boolean") {
2727
+ return commands[commandName] === true;
2728
+ }
2729
+ if (commandName === "describe") {
2730
+ return commands.status === true || entry.source === "native_active";
2731
+ }
2732
+ if (entry.source !== "akk_delegate") {
2733
+ return false;
2734
+ }
2735
+ if (commandName === "renew") {
2736
+ return entry.status === "stalled";
2737
+ }
2738
+ if (commandName === "retry-callback") {
2739
+ return ["callback_pending", "callback_failed"].includes(entry.status);
2740
+ }
2741
+ if (commandName === "recover") {
2742
+ return entry.status === "needs_recovery";
2743
+ }
2744
+ return false;
2745
+ }
2746
+ function sessionEntryRecency(entry, observedAtMs) {
2747
+ const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
2748
+ if (Number.isFinite(timestamp)) {
2749
+ return { updatedAtMs: timestamp };
2750
+ }
2751
+ const elapsedSeconds = parseProcessElapsedSeconds(entry.elapsed);
2752
+ if (elapsedSeconds !== undefined) {
2753
+ return { updatedAtMs: observedAtMs - elapsedSeconds * 1000 };
2754
+ }
2755
+ return {};
2756
+ }
2424
2757
  async function resolveTerminalConversationFromOptions(options) {
2425
2758
  return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
2426
2759
  }
@@ -9654,18 +9987,18 @@ function usage() {
9654
9987
  agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
9655
9988
  agent-knock-knock delegate --request <text> [--agent ${agentList}] [--store-dir <dir>] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--token <gateway-token>] [--send|--background]
9656
9989
  agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
9657
- agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
9658
- agent-knock-knock describe --conversation <id> [--store-dir <dir>]
9659
- agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
9660
- agent-knock-knock approve --conversation <id>
9661
- agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
9662
- agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
9990
+ agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--trace]
9991
+ agent-knock-knock describe [--conversation <id|selector>] [--store-dir <dir>]
9992
+ agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
9993
+ agent-knock-knock approve [--conversation <id|selector>]
9994
+ agent-knock-knock cancel [--conversation <id|selector>] [--all-proxy <url>]
9995
+ agent-knock-knock renew [--conversation <id|selector>] [--minutes <inactivity-minutes>]
9663
9996
  agent-knock-knock reconcile-monitors [--store-dir <dir>]
9664
9997
  agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
9665
9998
  agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
9666
9999
  agent-knock-knock close --conversation <id> [--reason <text>]
9667
- agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
9668
- agent-knock-knock doctor
10000
+ agent-knock-knock install-openclaw [--workspace <path>] [--default-agent ${agentList}] [--mode tmux|acpx|all] [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
10001
+ agent-knock-knock doctor [--mode tmux|acpx|all] [--workspace <path>] [--openclaw-bin <path>]
9669
10002
  agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
9670
10003
  agent-knock-knock callback --state <file> --message-json <json> [--record-only]
9671
10004
  agent-knock-knock transcript --log <file> [--include-raw]