@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.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+
8
+ const options = parseArgs(process.argv.slice(2));
9
+ if (
10
+ process.env.AKK_RUN_LIVE_ACPX_SMOKE !== "1" ||
11
+ options.confirmLive !== true
12
+ ) {
13
+ fail(
14
+ "Refusing to run a credentialed ACPX turn. Set AKK_RUN_LIVE_ACPX_SMOKE=1 and pass --confirm-live."
15
+ );
16
+ }
17
+
18
+ const agent = String(options.agent ?? "codex").toLowerCase();
19
+ if (!["codex", "claude", "cursor"].includes(agent)) {
20
+ fail("--agent must be codex, claude, or cursor");
21
+ }
22
+ const workspace = fs.realpathSync(path.resolve(String(options.workspace ?? process.cwd())));
23
+ if (!fs.statSync(workspace).isDirectory()) {
24
+ fail("--workspace must be a directory");
25
+ }
26
+
27
+ const acpxBin = String(options.acpxBin ?? "acpx");
28
+ const nonce = randomUUID();
29
+ const session = `akk-smoke-${agent}-${nonce.slice(0, 8)}`;
30
+ const selector = agent === "codex"
31
+ ? [
32
+ "--agent",
33
+ process.env.AKK_CODEX_ACPX_AGENT_COMMAND?.trim() ||
34
+ "npx -y @agentclientprotocol/codex-acp@1.1.7"
35
+ ]
36
+ : [agent];
37
+ const prompt = [
38
+ "This is an Agent Knock Knock live ACPX smoke test.",
39
+ "Do not use tools and do not modify files.",
40
+ `Reply with this nonce and nothing else: ${nonce}`
41
+ ].join(" ");
42
+
43
+ process.stderr.write(
44
+ `LIVE ACPX SMOKE: this sends one real ${agent} turn and may use credentials or incur cost. Session: ${session}\n`
45
+ );
46
+
47
+ try {
48
+ run(acpxBin, [...selector, "sessions", "new", "--name", session], workspace);
49
+ const response = run(
50
+ acpxBin,
51
+ [
52
+ "--deny-all",
53
+ "--allowed-tools",
54
+ "",
55
+ "--max-turns",
56
+ "1",
57
+ "--ttl",
58
+ "30",
59
+ ...selector,
60
+ ...(agent === "codex" ? ["prompt"] : []),
61
+ "-s",
62
+ session,
63
+ prompt
64
+ ],
65
+ workspace,
66
+ Number(options.timeoutSeconds ?? 180) * 1000
67
+ );
68
+ if (!response.includes(nonce)) {
69
+ fail("ACPX smoke response did not contain the nonce");
70
+ }
71
+ process.stdout.write(`${JSON.stringify({
72
+ ok: true,
73
+ agent,
74
+ session,
75
+ nonce_verified: true
76
+ }, null, 2)}\n`);
77
+ } finally {
78
+ const closed = spawnSync(
79
+ acpxBin,
80
+ [...selector, "sessions", "close", session],
81
+ {
82
+ cwd: workspace,
83
+ encoding: "utf8",
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ timeout: 30_000
86
+ }
87
+ );
88
+ if (closed.status !== 0) {
89
+ process.stderr.write(
90
+ `Warning: disposable ACPX session ${session} could not be closed automatically.\n`
91
+ );
92
+ }
93
+ }
94
+
95
+ function run(command, args, cwd, timeout = 30_000) {
96
+ const result = spawnSync(command, args, {
97
+ cwd,
98
+ encoding: "utf8",
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ timeout,
101
+ maxBuffer: 1024 * 1024
102
+ });
103
+ if (result.error) {
104
+ fail(`${command} failed to start: ${result.error.message}`);
105
+ }
106
+ if (result.status !== 0) {
107
+ fail(
108
+ String(result.stderr || result.stdout || `${command} exited with status ${result.status}`)
109
+ .trim()
110
+ .slice(0, 1000)
111
+ );
112
+ }
113
+ return String(result.stdout || result.stderr || "");
114
+ }
115
+
116
+ function parseArgs(argv) {
117
+ const parsed = {};
118
+ for (let index = 0; index < argv.length; index += 1) {
119
+ const argument = argv[index];
120
+ if (!argument.startsWith("--")) {
121
+ fail(`unexpected argument: ${argument}`);
122
+ }
123
+ const key = argument
124
+ .slice(2)
125
+ .replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase());
126
+ const next = argv[index + 1];
127
+ if (next === undefined || next.startsWith("--")) {
128
+ parsed[key] = true;
129
+ } else {
130
+ parsed[key] = next;
131
+ index += 1;
132
+ }
133
+ }
134
+ return parsed;
135
+ }
136
+
137
+ function fail(message) {
138
+ throw new Error(message);
139
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const options = parseArgs(process.argv.slice(2));
9
+ if (
10
+ process.env.AKK_RUN_LIVE_TMUX_SMOKE !== "1" ||
11
+ options.confirmLive !== true
12
+ ) {
13
+ fail(
14
+ "Refusing to send a real tmux turn. Set AKK_RUN_LIVE_TMUX_SMOKE=1 and pass --confirm-live."
15
+ );
16
+ }
17
+
18
+ const target = requiredString(options.target, "--target is required");
19
+ const expectedPanePid = Number(requiredString(
20
+ options.expectedPanePid,
21
+ "--expected-pane-pid is required"
22
+ ));
23
+ if (!Number.isSafeInteger(expectedPanePid) || expectedPanePid <= 1) {
24
+ fail("--expected-pane-pid must be a positive integer");
25
+ }
26
+ const agent = String(options.agent ?? "codex").toLowerCase();
27
+ if (!["codex", "claude"].includes(agent)) {
28
+ fail("--agent must be codex or claude");
29
+ }
30
+
31
+ const packageRoot = path.resolve(
32
+ path.dirname(fileURLToPath(import.meta.url)),
33
+ ".."
34
+ );
35
+ const cliPath = path.join(packageRoot, "dist", "src", "cli.js");
36
+ const list = runCli(cliPath, ["list", "--terminal-debug"]);
37
+ const candidates = Array.isArray(list.terminal_controlled)
38
+ ? list.terminal_controlled
39
+ : [];
40
+ const matches = candidates.filter((candidate) =>
41
+ candidate?.agent === agent &&
42
+ candidate?.terminal_control?.target === target &&
43
+ Number(candidate?.terminal_control?.panePid) === expectedPanePid
44
+ );
45
+ if (matches.length !== 1) {
46
+ fail(
47
+ `Expected one ${agent} terminal at ${target} with pane PID ${expectedPanePid}; found ${matches.length}.`
48
+ );
49
+ }
50
+ const selected = matches[0];
51
+ if (
52
+ selected.activity_state !== "idle" ||
53
+ selected.commands?.send !== true
54
+ ) {
55
+ fail(
56
+ `Refusing to send: ${target} is not a verified idle, actionable ${agent} pane.`
57
+ );
58
+ }
59
+
60
+ const nonce = randomUUID();
61
+ const message = String(
62
+ options.message ??
63
+ `AKK live tmux smoke ${nonce}: reply with the nonce only and do not modify files.`
64
+ );
65
+ process.stderr.write(
66
+ `LIVE TMUX SMOKE: sending one real turn to ${target} (pane PID ${expectedPanePid}).\n`
67
+ );
68
+ const result = runCli(cliPath, [
69
+ "send",
70
+ "--conversation",
71
+ String(selected.id),
72
+ "--message",
73
+ message
74
+ ]);
75
+ process.stdout.write(`${JSON.stringify({
76
+ ok: true,
77
+ agent,
78
+ target,
79
+ pane_pid: expectedPanePid,
80
+ conversation_id: result.conversation?.conversation_id ?? selected.id,
81
+ nonce
82
+ }, null, 2)}\n`);
83
+
84
+ function runCli(cliPath, args) {
85
+ const result = spawnSync(process.execPath, [cliPath, ...args], {
86
+ encoding: "utf8",
87
+ stdio: ["ignore", "pipe", "pipe"],
88
+ timeout: 60_000,
89
+ maxBuffer: 10 * 1024 * 1024
90
+ });
91
+ if (result.error) {
92
+ fail(`agent-knock-knock ${args[0]} failed to start: ${result.error.message}`);
93
+ }
94
+ if (result.status !== 0) {
95
+ fail(
96
+ String(
97
+ result.stderr ||
98
+ result.stdout ||
99
+ `agent-knock-knock ${args[0]} exited with status ${result.status}`
100
+ ).trim().slice(0, 2000)
101
+ );
102
+ }
103
+ try {
104
+ return JSON.parse(result.stdout);
105
+ } catch {
106
+ fail(`agent-knock-knock ${args[0]} returned malformed JSON`);
107
+ }
108
+ }
109
+
110
+ function parseArgs(argv) {
111
+ const parsed = {};
112
+ for (let index = 0; index < argv.length; index += 1) {
113
+ const argument = argv[index];
114
+ if (!argument.startsWith("--")) {
115
+ fail(`unexpected argument: ${argument}`);
116
+ }
117
+ const key = argument
118
+ .slice(2)
119
+ .replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase());
120
+ const next = argv[index + 1];
121
+ if (next === undefined || next.startsWith("--")) {
122
+ parsed[key] = true;
123
+ } else {
124
+ parsed[key] = next;
125
+ index += 1;
126
+ }
127
+ }
128
+ return parsed;
129
+ }
130
+
131
+ function requiredString(value, message) {
132
+ if (typeof value !== "string" || value.trim() === "") {
133
+ fail(message);
134
+ }
135
+ return value;
136
+ }
137
+
138
+ function fail(message) {
139
+ throw new Error(message);
140
+ }
@@ -34,12 +34,15 @@ Slash command forms:
34
34
  - `/akk claude <task>`: delegate to Claude.
35
35
  - `/akk cursor <task>`: delegate to Cursor.
36
36
  - `/akk list`: list open AKK sessions.
37
- - `/akk status <conversation-id>`: inspect one AKK session.
38
- - `/akk describe <conversation-id>`: summarize what one AKK-managed, native, or terminal-controlled session is about.
39
- - `/akk send <conversation-id> <message>`: send a follow-up to one open AKK session.
40
- - `/akk cancel <conversation-id>`: request cooperative cancellation of the current in-flight prompt for one AKK session without closing it.
41
- - `/akk recover <conversation-id>`: recover a session that is waiting for a recovery decision by starting a new agent session with AKK's saved protocol history summary.
42
- - `/akk close <conversation-id> [reason]`: close one AKK session.
37
+ - `/akk doctor [tmux|acpx|all]`: verify the configured AKK mode and OpenClaw integration.
38
+ - `/akk status [session-selector]`: inspect one AKK session. Omitting the selector works only when exactly one actionable session exists.
39
+ - `/akk describe [session-selector]`: summarize what one AKK-managed, native, or terminal-controlled session is about.
40
+ - `/akk send <session-selector>: <message>`: send a follow-up to one open AKK session.
41
+ - `/akk cancel <session-selector>`: request cooperative cancellation of the current in-flight prompt for one AKK session without closing it.
42
+ - `/akk recover <session-selector>`: recover a session that is waiting for a recovery decision by starting a new agent session with AKK's saved protocol history summary.
43
+ - `/akk close <session-selector> [reason]`: close one AKK session.
44
+
45
+ A session selector may be an authoritative full ID, an `@short-ref` returned by `AKK list`, `only`, `latest`, an agent name (`codex`, `claude`, or `cursor`), or `<agent>:latest`. Selectors fail closed when the target is missing or ambiguous. Use `latest` only when the user explicitly asks for the newest session; never guess which session they meant.
43
46
 
44
47
  Natural-language forms:
45
48
 
@@ -48,13 +51,13 @@ Natural-language forms:
48
51
  - `AKK Claude: <task>`: call `agent_knock_knock_delegate` with `agent="claude"`.
49
52
  - `AKK Cursor: <task>`: call `agent_knock_knock_delegate` with `agent="cursor"`.
50
53
  - `AKK list`, `akk list`, questions such as "what AKK sessions are open", "which Codex or Claude sessions are currently running", "terminal-controlled sessions", or requests to list active local coding-agent work: call `agent_knock_knock_list`.
51
- - `AKK status <conversation-id>` or requests to view current output, execution result, terminal screen, or "what is it doing now": call `agent_knock_knock_status`. For `terminal_controlled` entries, status internally captures the terminal pane and returns `terminal_screen`; do not call tmux or shell commands directly to inspect the pane unless AKK status fails.
52
- - `AKK describe <conversation-id>`, `AKK summary <conversation-id>`, or requests such as "what is this session about", "what was this task doing", "remind me what this drawing/session is for", or "这个会话/绘画大概在做什么": call `agent_knock_knock_describe`. Prefer this over direct terminal/tmux inspection because AKK can combine saved conversation history, agent-specific structured history when available, and terminal-screen fallback with explicit confidence.
53
- - `AKK send <conversation-id>: <message>` or follow-up requests for an existing open agent session: call `agent_knock_knock_send`. Also use `agent_knock_knock_send` when the user says to send/tell/ask/forward/add/continue a message or task to a listed AKK session, a listed native session, a terminal-controlled entry, a tmux target such as `my-work:0.1`, or "the one from the list". If the target comes from a `terminal_controlled` entry in `AKK list`, use that entry's `id` directly; AKK submits only when the pane is at a verified idle prompt. For Claude Code, this background send returns the managed conversation ID required to bind later screen approval to the current message and process; use that returned ID for subsequent actions. Do not call `agent_knock_knock_delegate` for these requests unless the user explicitly asks for a new independent session.
54
- - `AKK cancel <conversation-id>` or requests to stop the current running work: call `agent_knock_knock_cancel`. If the target is terminal-controlled, use its listed `id` directly. AKK uses the adapter's interrupt key (`Control-C` for Codex or `Escape` for Claude Code) and leaves the tmux pane open.
55
- - `AKK renew <conversation-id>` or requests to extend/restart monitoring for a stalled but still-live terminal task: call `agent_knock_knock_renew`. This is only for an AKK-managed terminal bridge conversation already marked `stalled`; it does not send a message or key to Codex. Pass `minutes` only when the user requests a specific new inactivity timeout.
56
- - `AKK recover <conversation-id>`: call `agent_knock_knock_recover`.
57
- - `AKK close <conversation-id>`: call `agent_knock_knock_close`.
54
+ - `AKK status <session-selector>` or requests to view current output, execution result, terminal screen, or "what is it doing now": call `agent_knock_knock_status`. For `terminal_controlled` entries, status internally captures the terminal pane and returns `terminal_screen`; do not call tmux or shell commands directly to inspect the pane unless AKK status fails.
55
+ - `AKK describe <session-selector>`, `AKK summary <session-selector>`, or requests such as "what is this session about", "what was this task doing", "remind me what this drawing/session is for", or "这个会话/绘画大概在做什么": call `agent_knock_knock_describe`. Prefer this over direct terminal/tmux inspection because AKK can combine saved conversation history, agent-specific structured history when available, and terminal-screen fallback with explicit confidence.
56
+ - `AKK send <session-selector>: <message>` or follow-up requests for an existing open agent session: call `agent_knock_knock_send`. Also use `agent_knock_knock_send` when the user says to send/tell/ask/forward/add/continue a message or task to a listed AKK session, a listed native session, a terminal-controlled entry, a tmux target such as `my-work:0.1`, or "the one from the list". If the target comes from a `terminal_controlled` entry in `AKK list`, use that entry's authoritative `id` directly; its `short_ref` is only a stable user-facing selector. AKK submits only when the pane is at a verified idle prompt. For Claude Code, this background send returns the managed conversation ID required to bind later screen approval to the current message and process; use that returned ID for subsequent actions. Do not call `agent_knock_knock_delegate` for these requests unless the user explicitly asks for a new independent session.
57
+ - `AKK cancel <session-selector>` or requests to stop the current running work: call `agent_knock_knock_cancel`. If the target is terminal-controlled, use its listed `id` directly. AKK uses the adapter's interrupt key (`Control-C` for Codex or `Escape` for Claude Code) and leaves the tmux pane open.
58
+ - `AKK renew <session-selector>` or requests to extend/restart monitoring for a stalled but still-live terminal task: call `agent_knock_knock_renew`. This is only for an AKK-managed terminal bridge conversation already marked `stalled`; it does not send a message or key to Codex. Pass `minutes` only when the user requests a specific new inactivity timeout.
59
+ - `AKK recover <session-selector>`: call `agent_knock_knock_recover`.
60
+ - `AKK close <session-selector>`: call `agent_knock_knock_close`.
58
61
  - `AKK takeover Codex <session-id>` or requests to take over an active Codex CLI session: call `agent_knock_knock_agent_takeover` with `agent="codex"` and `strategy="terminate_then_resume"`.
59
62
  - `AKK terminal takeover Codex <session-id>` or requests to take over a Codex CLI that is running inside a controllable terminal provider without stopping it: call `agent_knock_knock_agent_takeover` with `agent="codex"` and `strategy="terminal_control"`.
60
63
  - `AKK approve <conversation-id>` or requests to approve a terminal-controlled permission or command prompt: this is the manual path. First call `agent_knock_knock_status` and show the detected request details to the user. For hookless Claude, the callback intentionally omits the raw command; require the user to personally inspect the named live tmux pane and never approve from a hash or summary alone. Only after the user explicitly approves that exact request, call `agent_knock_knock_approve` with its `approval_state.fingerprint` as `expected_approval_fingerprint`. Codex and Claude Code both use the current visible prompt; Claude manual approval is limited to a strict one-time `Yes` Bash dialog in the current AKK-managed turn. A trusted, default-disabled plugin `autoApprove` policy may act independently when the Claude command, workspace, screen, and local transcript evidence match exactly; the model cannot configure that policy. If an AKK callback says a terminal bridge session is waiting for approval, pass its fingerprint the same way; deny it with `agent_knock_knock_cancel`. Unknown, stale, expired, ambiguous, persistent-permission, or changed requests must never be approved. Codex visible-prompt approval can also work without managed state; Claude requires the managed conversation returned by a prior background send.
@@ -62,11 +65,11 @@ Natural-language forms:
62
65
 
63
66
  Session reuse rule:
64
67
 
65
- - If the user asks to continue, add, follow up, "send another task", "let it also", "tell it", "ask Codex/Claude/Cursor to also", "再让它", "继续让它", "给刚才那个", or otherwise refers to an existing AKK agent, reuse the most recent matching open AKK session instead of creating a new delegation.
66
- - When the user gives a follow-up without a `conversation_id`, first call `agent_knock_knock_list`, choose the most recent open session that matches the requested agent if one is named, and then call `agent_knock_knock_send` with that `conversation_id`.
68
+ - If the user asks to continue, add, follow up, "send another task", "let it also", "tell it", "ask Codex/Claude/Cursor to also", "再让它", "继续让它", "给刚才那个", or otherwise refers to an existing AKK agent, reuse it only when the referenced open session is uniquely identifiable.
69
+ - When the user gives a follow-up without a `conversation_id`, first call `agent_knock_knock_list`. Reuse the session only when exactly one actionable row matches the user's wording. If multiple rows match, show their `short_ref`, agent, and description and ask the user to choose. If the user explicitly says latest/newest, use `latest` or `<agent>:latest`; AKK will still reject recency ties.
67
70
  - After `AKK list`, if the user refers to any listed row by id, tmux target, pane number, session name, ordinal ("first/second/that one"), cwd, or visible description and asks to send or assign work to it, resolve that listed row and call `agent_knock_knock_send`. For `terminal_controlled` rows, pass the row's `id` such as `terminal:v2:tmux:codex:my-work:0.1:38140`; do not create a new AKK delegation.
68
71
  - `idle` means the agent completed the previous round but the AKK session is still open and should be reused for follow-ups.
69
- - `send` automatically falls back to AKK replay recovery when the previous coding-agent session is unavailable. If AKK still reports `needs_recovery`, ask the user to choose `AKK recover <conversation-id>`, `AKK close <conversation-id>`, or starting a new independent AKK delegation.
72
+ - `send` automatically falls back to AKK replay recovery when the previous coding-agent session is unavailable. If AKK still reports `needs_recovery`, ask the user to choose `AKK recover <session-selector>`, `AKK close <session-selector>`, or starting a new independent AKK delegation.
70
73
  - Call `agent_knock_knock_delegate` only when the user clearly asks for a new independent AKK task/session, names a different agent that does not already have a suitable open session, or there is no matching open session to reuse.
71
74
 
72
75
  Useful examples:
@@ -116,13 +119,13 @@ If the plugin tool is unavailable, stop and report that Agent Knock Knock is not
116
119
 
117
120
  ## Recovery Decisions
118
121
 
119
- `AKK send <conversation-id>: <message>` automatically falls back to AKK replay recovery when the previous ACPX session is unavailable. This starts a fresh ACPX session, gives the coding agent AKK's saved protocol history summary, and includes the pending message.
122
+ `AKK send <session-selector>: <message>` automatically falls back to AKK replay recovery when the previous ACPX session is unavailable. This starts a fresh ACPX session, gives the coding agent AKK's saved protocol history summary, and includes the pending message.
120
123
 
121
124
  When AKK still reports that a conversation is `needs_recovery`, do not automatically recover, start a replacement task, or close it. Explain the options and let the user choose:
122
125
 
123
- - `AKK recover <conversation-id>`: use `agent_knock_knock_recover` to start a new coding-agent session with AKK's saved protocol history summary plus the pending message.
124
- - `AKK close <conversation-id>`: use `agent_knock_knock_close` to close the task.
125
- - `AKK renew <conversation-id>`: use `agent_knock_knock_renew` when a live terminal bridge task was marked stalled and monitoring should resume without injecting another task.
126
+ - `AKK recover <session-selector>`: use `agent_knock_knock_recover` to start a new coding-agent session with AKK's saved protocol history summary plus the pending message.
127
+ - `AKK close <session-selector>`: use `agent_knock_knock_close` to close the task.
128
+ - `AKK renew <session-selector>`: use `agent_knock_knock_renew` when a live terminal bridge task was marked stalled and monitoring should resume without injecting another task.
126
129
  - Start a new independent `AKK <task>` delegation if the old task should not be recovered.
127
130
 
128
131
  Make clear that `recover` is AKK replay recovery, not guaranteed native coding-agent session resume.