@scotthuang/agent-knock-knock 0.2.10 → 0.2.11
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/CHANGELOG.md +12 -0
- package/README.md +7 -13
- package/dist/src/cli.js +97 -88
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -44
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-takeover-planner.d.ts +2 -14
- package/dist/src/session-takeover-planner.js +0 -23
- package/dist/src/session-takeover-planner.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.11 - 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added terminal-controlled `AKK cancel` support by sending Control-C to the controlled tmux pane.
|
|
8
|
+
- Added structured terminal reachability, screen, and approval state to `AKK status` for terminal-controlled sessions.
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Removed the public `agent discover` CLI path and `agent_knock_knock_agent_discover` OpenClaw tool; use `AKK list` for active/local session visibility.
|
|
13
|
+
- Removed the legacy `safe_resume` takeover strategy; native takeover now uses active process-based terminate, terminal-control, or fork paths.
|
|
14
|
+
|
|
3
15
|
## 0.2.10 - 2026-06-25
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -117,9 +117,13 @@ AKK Codex: review the current branch and propose a small fix
|
|
|
117
117
|
AKK Claude: review the latest commit
|
|
118
118
|
AKK Cursor: fix the flaky UI test
|
|
119
119
|
akk list
|
|
120
|
+
akk status <conversation-id>
|
|
120
121
|
akk send <conversation-id>: continue with the smaller implementation
|
|
121
122
|
akk cancel <conversation-id>
|
|
123
|
+
akk recover <conversation-id>
|
|
122
124
|
akk close <conversation-id>
|
|
125
|
+
AKK terminal takeover Codex <native-session-id>
|
|
126
|
+
AKK approve <conversation-id>
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
Surfaces that support OpenClaw native commands can use the same workflow through `/akk`:
|
|
@@ -154,32 +158,23 @@ Optional default-agent config:
|
|
|
154
158
|
|
|
155
159
|
## Native Codex Takeover
|
|
156
160
|
|
|
157
|
-
Experimental: AKK can
|
|
161
|
+
Experimental: AKK can list and take over Codex CLI sessions that were started outside AKK. This is useful when you started Codex in a terminal, left the machine, and want OpenClaw to continue managing that work from chat.
|
|
158
162
|
|
|
159
163
|
List current AKK-managed and local agent work:
|
|
160
164
|
|
|
161
165
|
```text
|
|
162
166
|
AKK list
|
|
163
|
-
AKK Codex active
|
|
164
167
|
```
|
|
165
168
|
|
|
166
169
|
`AKK list` returns separate groups for:
|
|
167
170
|
|
|
168
171
|
- `delegated`: AKK-managed background tasks.
|
|
169
172
|
- `native`: local native sessions that AKK can discover but cannot directly control.
|
|
170
|
-
- `terminal_controlled`: local sessions running in a controllable terminal provider
|
|
171
|
-
|
|
172
|
-
Lower-level discovery prompts remain available for historical session and capability inspection:
|
|
173
|
-
|
|
174
|
-
```text
|
|
175
|
-
AKK Codex sessions
|
|
176
|
-
AKK Codex capabilities
|
|
177
|
-
```
|
|
173
|
+
- `terminal_controlled`: local sessions running in a controllable terminal provider. The current provider is tmux. These entries include terminal metadata, command capabilities, and concise approval state when a visible approval prompt is detected.
|
|
178
174
|
|
|
179
175
|
Takeover prompts:
|
|
180
176
|
|
|
181
177
|
```text
|
|
182
|
-
AKK safe resume Codex <native-session-id>
|
|
183
178
|
AKK takeover Codex <native-session-id>
|
|
184
179
|
AKK terminal takeover Codex <native-session-id>
|
|
185
180
|
AKK fork takeover Codex <native-session-id>
|
|
@@ -188,9 +183,8 @@ AKK approve <conversation-id>
|
|
|
188
183
|
|
|
189
184
|
Strategies:
|
|
190
185
|
|
|
191
|
-
- `safe resume`: attach a stopped/inactive Codex session to AKK.
|
|
192
186
|
- `takeover`: stop an active matching Codex CLI after explicit confirmation, then resume it under AKK.
|
|
193
|
-
- `terminal takeover`: attach an active Codex CLI running inside
|
|
187
|
+
- `terminal takeover`: attach an active Codex CLI running inside a controllable terminal provider after explicit confirmation. AKK sends follow-ups directly to the terminal pane, can send Control-C for cancel, and can approve the currently visible Codex approval prompt.
|
|
194
188
|
- `fork takeover`: keep the original Codex CLI running, ask OpenClaw to summarize bounded source context, then create a new AKK-managed fork from that summary.
|
|
195
189
|
|
|
196
190
|
For long-running local Codex sessions that you may want OpenClaw to operate from WeChat, run Codex inside tmux. AKK does not require tmux, but tmux enables terminal-control takeover without stopping the original process:
|
package/dist/src/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
|
13
13
|
import { writeRuntimeLog } from "./runtime-log.js";
|
|
14
14
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
15
15
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
16
|
-
import { planFork,
|
|
16
|
+
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
17
17
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl } from "./terminal-control-provider.js";
|
|
18
18
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
19
19
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
@@ -99,7 +99,7 @@ async function runCommand(commandName, options) {
|
|
|
99
99
|
await runApprove(options);
|
|
100
100
|
}
|
|
101
101
|
else if (commandName === "cancel") {
|
|
102
|
-
runCancel(options);
|
|
102
|
+
await runCancel(options);
|
|
103
103
|
}
|
|
104
104
|
else if (commandName === "recover") {
|
|
105
105
|
runRecover(options);
|
|
@@ -210,47 +210,13 @@ function runDoctor(options) {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
async function runAgent(options) {
|
|
213
|
-
const agentCommand = required(options.agentCommand, "agent subcommand is required:
|
|
214
|
-
if (agentCommand === "discover") {
|
|
215
|
-
printJson(await runAgentDiscover(options));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
213
|
+
const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
|
|
218
214
|
if (agentCommand === "takeover") {
|
|
219
215
|
printJson(await runAgentTakeover(options));
|
|
220
216
|
return;
|
|
221
217
|
}
|
|
222
218
|
throw new Error(`unsupported agent subcommand: ${agentCommand}`);
|
|
223
219
|
}
|
|
224
|
-
async function runAgentDiscover(options) {
|
|
225
|
-
const agent = required(options.agent, "--agent is required");
|
|
226
|
-
const scope = required(options.scope, "--scope is required");
|
|
227
|
-
const provider = createAgentSessionProvider(agent, options);
|
|
228
|
-
const capabilities = await provider.getCapabilities();
|
|
229
|
-
if (scope === "capabilities") {
|
|
230
|
-
return {
|
|
231
|
-
agent,
|
|
232
|
-
scope,
|
|
233
|
-
capabilities
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
if (scope === "sessions") {
|
|
237
|
-
return {
|
|
238
|
-
agent,
|
|
239
|
-
scope,
|
|
240
|
-
capabilities,
|
|
241
|
-
sessions: await provider.listHistoricalSessions()
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
if (scope === "active") {
|
|
245
|
-
return {
|
|
246
|
-
agent,
|
|
247
|
-
scope,
|
|
248
|
-
capabilities,
|
|
249
|
-
active: await listActiveSessionsWithTerminalControl(provider, options)
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
throw new Error(`unsupported discover scope: ${scope}`);
|
|
253
|
-
}
|
|
254
220
|
async function runAgentTakeover(options) {
|
|
255
221
|
const agent = required(options.agent, "--agent is required");
|
|
256
222
|
const sessionId = required(options.sessionId, "--session-id is required");
|
|
@@ -270,36 +236,6 @@ async function runAgentTakeover(options) {
|
|
|
270
236
|
}
|
|
271
237
|
};
|
|
272
238
|
}
|
|
273
|
-
if (strategy === "safe_resume") {
|
|
274
|
-
const plan = planSafeResume(session, await listActiveSessionsWithTerminalControl(provider, options));
|
|
275
|
-
if (plan.allowed && options.createConversation) {
|
|
276
|
-
const modelInfo = await provider.getSessionModel(session.id);
|
|
277
|
-
const attached = createNativeSessionConversation({
|
|
278
|
-
agent,
|
|
279
|
-
strategy,
|
|
280
|
-
session,
|
|
281
|
-
modelInfo,
|
|
282
|
-
options
|
|
283
|
-
});
|
|
284
|
-
return {
|
|
285
|
-
agent,
|
|
286
|
-
sessionId,
|
|
287
|
-
strategy,
|
|
288
|
-
status: "attached",
|
|
289
|
-
sideEffectsExecuted: true,
|
|
290
|
-
plan,
|
|
291
|
-
...attached
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
agent,
|
|
296
|
-
sessionId,
|
|
297
|
-
strategy,
|
|
298
|
-
status: plan.allowed ? "ready" : "blocked",
|
|
299
|
-
sideEffectsExecuted: false,
|
|
300
|
-
plan
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
239
|
if (strategy === "terminate_then_resume") {
|
|
304
240
|
const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
|
|
305
241
|
const plan = planTakeover(session, activeSessions);
|
|
@@ -402,7 +338,7 @@ async function runAgentTakeover(options) {
|
|
|
402
338
|
plan,
|
|
403
339
|
error: {
|
|
404
340
|
code: "terminal_target_unavailable",
|
|
405
|
-
message: `No matching
|
|
341
|
+
message: `No matching terminal-controlled Codex process was found for ${terminalTarget}`
|
|
406
342
|
}
|
|
407
343
|
};
|
|
408
344
|
}
|
|
@@ -1419,7 +1355,6 @@ function nativeListEntry(session, activeSessions) {
|
|
|
1419
1355
|
confidence: session.confidence,
|
|
1420
1356
|
reason: session.reason,
|
|
1421
1357
|
commands: {
|
|
1422
|
-
safe_resume: Boolean(session.sessionId),
|
|
1423
1358
|
terminate_then_resume: true,
|
|
1424
1359
|
fork: Boolean(session.sessionId),
|
|
1425
1360
|
terminal_control_attach: false,
|
|
@@ -1458,7 +1393,7 @@ async function terminalControlledListEntry(session, activeSessions, options) {
|
|
|
1458
1393
|
capture_screen: true,
|
|
1459
1394
|
approve: approvalState.approvable === true,
|
|
1460
1395
|
status: true,
|
|
1461
|
-
cancel:
|
|
1396
|
+
cancel: true,
|
|
1462
1397
|
close: false
|
|
1463
1398
|
}
|
|
1464
1399
|
};
|
|
@@ -1540,21 +1475,8 @@ async function runStatus(options) {
|
|
|
1540
1475
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1541
1476
|
if (terminalControl) {
|
|
1542
1477
|
result.terminal_control = terminalControl;
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1546
|
-
socketPath: terminalControl.socketPath
|
|
1547
|
-
});
|
|
1548
|
-
result.terminal_screen = {
|
|
1549
|
-
excerpt: screenExcerpt(screen),
|
|
1550
|
-
approval: detectCodexApprovalPrompt(screen)
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
catch (error) {
|
|
1554
|
-
result.terminal_screen = {
|
|
1555
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1478
|
+
result.terminal_status = await terminalStatusForControl(terminalControl, options);
|
|
1479
|
+
result.terminal_screen = result.terminal_status.screen;
|
|
1558
1480
|
}
|
|
1559
1481
|
printJson(result);
|
|
1560
1482
|
runtimeLog("info", "task_status_read", {
|
|
@@ -1566,6 +1488,48 @@ async function runStatus(options) {
|
|
|
1566
1488
|
trace: Boolean(options.trace)
|
|
1567
1489
|
});
|
|
1568
1490
|
}
|
|
1491
|
+
async function terminalStatusForControl(terminalControl, options) {
|
|
1492
|
+
try {
|
|
1493
|
+
const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
|
|
1494
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1495
|
+
socketPath: terminalControl.socketPath
|
|
1496
|
+
});
|
|
1497
|
+
const approval = detectCodexApprovalPrompt(screen);
|
|
1498
|
+
const blocked = isCodexApprovalPromptVisible(screen);
|
|
1499
|
+
return {
|
|
1500
|
+
provider: terminalControl.kind,
|
|
1501
|
+
target: terminalControl.target,
|
|
1502
|
+
reachable: true,
|
|
1503
|
+
approval_state: {
|
|
1504
|
+
scanned: true,
|
|
1505
|
+
blocked,
|
|
1506
|
+
approvable: approval.approvable,
|
|
1507
|
+
key: approval.approvable ? approval.key : undefined,
|
|
1508
|
+
label: approval.approvable ? approval.label : undefined,
|
|
1509
|
+
reason: approval.approvable ? undefined : approval.reason
|
|
1510
|
+
},
|
|
1511
|
+
screen: {
|
|
1512
|
+
excerpt: screenExcerpt(screen),
|
|
1513
|
+
approval
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
return {
|
|
1519
|
+
provider: terminalControl.kind,
|
|
1520
|
+
target: terminalControl.target,
|
|
1521
|
+
reachable: false,
|
|
1522
|
+
approval_state: {
|
|
1523
|
+
scanned: false,
|
|
1524
|
+
blocked: false,
|
|
1525
|
+
approvable: false
|
|
1526
|
+
},
|
|
1527
|
+
screen: {
|
|
1528
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1569
1533
|
async function runSend(options) {
|
|
1570
1534
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
1571
1535
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
@@ -2370,12 +2334,26 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
2370
2334
|
budget: budgetAction(nextConversation)
|
|
2371
2335
|
};
|
|
2372
2336
|
}
|
|
2373
|
-
function runCancel(options) {
|
|
2337
|
+
async function runCancel(options) {
|
|
2374
2338
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2375
2339
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
2376
2340
|
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
2377
2341
|
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
2378
2342
|
}
|
|
2343
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2344
|
+
? conversation.native_session_takeover
|
|
2345
|
+
: undefined;
|
|
2346
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
2347
|
+
if (terminalControl) {
|
|
2348
|
+
await runTerminalControlCancel({
|
|
2349
|
+
options,
|
|
2350
|
+
conversation,
|
|
2351
|
+
statePath,
|
|
2352
|
+
logPath,
|
|
2353
|
+
terminalControl
|
|
2354
|
+
});
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2379
2357
|
const executor = executorForConversation(conversation);
|
|
2380
2358
|
const acpxPath = resolveExecutable("acpx");
|
|
2381
2359
|
const executorEnv = environmentForExecutor(executor, {
|
|
@@ -2434,6 +2412,38 @@ function runCancel(options) {
|
|
|
2434
2412
|
budget: budgetAction(nextConversation)
|
|
2435
2413
|
});
|
|
2436
2414
|
}
|
|
2415
|
+
async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
|
|
2416
|
+
const provider = createTerminalControlProvider(options);
|
|
2417
|
+
await provider.sendKeys(terminalControl.target, ["C-c"], {
|
|
2418
|
+
socketPath: terminalControl.socketPath
|
|
2419
|
+
});
|
|
2420
|
+
const now = new Date().toISOString();
|
|
2421
|
+
appendEvent(logPath, {
|
|
2422
|
+
ts: now,
|
|
2423
|
+
conversation_id: conversation.conversation_id,
|
|
2424
|
+
event: "terminal_cancel_requested",
|
|
2425
|
+
terminal_control: terminalControl,
|
|
2426
|
+
key: "C-c"
|
|
2427
|
+
});
|
|
2428
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
2429
|
+
conversation_id: conversation.conversation_id,
|
|
2430
|
+
terminal_target: terminalControl.target,
|
|
2431
|
+
key: "C-c"
|
|
2432
|
+
});
|
|
2433
|
+
const nextConversation = {
|
|
2434
|
+
...conversation,
|
|
2435
|
+
terminal_cancel_requested_at: now,
|
|
2436
|
+
updated_at: now
|
|
2437
|
+
};
|
|
2438
|
+
saveState(statePath, nextConversation);
|
|
2439
|
+
printJson({
|
|
2440
|
+
conversation: nextConversation,
|
|
2441
|
+
cancel_requested: true,
|
|
2442
|
+
terminal_control: terminalControl,
|
|
2443
|
+
key: "C-c",
|
|
2444
|
+
budget: budgetAction(nextConversation)
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2437
2447
|
function runRecover(options) {
|
|
2438
2448
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
2439
2449
|
}
|
|
@@ -4059,8 +4069,7 @@ function usage() {
|
|
|
4059
4069
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
4060
4070
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
4061
4071
|
agent-knock-knock doctor
|
|
4062
|
-
agent-knock-knock agent
|
|
4063
|
-
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
4072
|
+
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
4064
4073
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
4065
4074
|
agent-knock-knock transcript --log <file> [--include-raw]
|
|
4066
4075
|
agent-knock-knock transcript --conversation <dir> [--include-raw]
|