@scotthuang/agent-knock-knock 0.1.0
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 +104 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/src/acpx-output.d.ts +18 -0
- package/dist/src/acpx-output.js +223 -0
- package/dist/src/acpx-output.js.map +1 -0
- package/dist/src/bootstrap.d.ts +8 -0
- package/dist/src/bootstrap.js +45 -0
- package/dist/src/bootstrap.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2364 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/executors.d.ts +101 -0
- package/dist/src/executors.js +118 -0
- package/dist/src/executors.js.map +1 -0
- package/dist/src/openclaw-plugin.d.ts +7 -0
- package/dist/src/openclaw-plugin.js +916 -0
- package/dist/src/openclaw-plugin.js.map +1 -0
- package/dist/src/protocol.d.ts +96 -0
- package/dist/src/protocol.js +329 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/runtime-log.d.ts +37 -0
- package/dist/src/runtime-log.js +149 -0
- package/dist/src/runtime-log.js.map +1 -0
- package/dist/src/store.d.ts +35 -0
- package/dist/src/store.js +121 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/transcript.d.ts +24 -0
- package/dist/src/transcript.js +88 -0
- package/dist/src/transcript.js.map +1 -0
- package/openclaw.plugin.json +150 -0
- package/package.json +63 -0
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +181 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
5
|
+
import { EXECUTOR_KINDS, executorDefinitionForAlias, executorDefinitionForKind, parseLeadingExecutorAlias } from "./executors.js";
|
|
6
|
+
const CALLBACK_METHOD = "agent-knock-knock.callback";
|
|
7
|
+
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
const defaultBinPath = path.join(pluginRoot, "dist", "src", "cli.js");
|
|
9
|
+
const delegateParameters = {
|
|
10
|
+
type: "object",
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
required: ["request"],
|
|
13
|
+
properties: {
|
|
14
|
+
agent: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: EXECUTOR_KINDS,
|
|
17
|
+
description: "Coding agent to delegate to. Defaults to plugin config defaultAgent, falling back to codex when unset. Explicit user agent requests override the default."
|
|
18
|
+
},
|
|
19
|
+
request: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Implementation task for the coding agent."
|
|
22
|
+
},
|
|
23
|
+
workspace: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Workspace path for Claude Code. Defaults to plugin config or the current process directory."
|
|
26
|
+
},
|
|
27
|
+
storeDir: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Conversation store directory. Defaults to <workspace>/.agent-knock-knock/conversations."
|
|
30
|
+
},
|
|
31
|
+
claudeSession: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Claude Code session name."
|
|
34
|
+
},
|
|
35
|
+
codexSession: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Codex session name."
|
|
38
|
+
},
|
|
39
|
+
cursorSession: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Cursor session name."
|
|
42
|
+
},
|
|
43
|
+
codexAllProxy: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "ALL_PROXY value used when launching Codex through ACPX."
|
|
46
|
+
},
|
|
47
|
+
cursorAllProxy: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "ALL_PROXY value used when launching Cursor through ACPX."
|
|
50
|
+
},
|
|
51
|
+
codexModel: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "ACPX model id used when launching Codex."
|
|
54
|
+
},
|
|
55
|
+
cursorModel: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "ACPX model id used when launching Cursor."
|
|
58
|
+
},
|
|
59
|
+
model: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "ACPX model id used when launching the selected coding agent."
|
|
62
|
+
},
|
|
63
|
+
allProxy: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "ALL_PROXY value used when launching the selected coding agent through ACPX."
|
|
66
|
+
},
|
|
67
|
+
session: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Explicit coding agent session name."
|
|
70
|
+
},
|
|
71
|
+
openclawSession: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "OpenClaw session label recorded in the protocol state."
|
|
74
|
+
},
|
|
75
|
+
softLimit: {
|
|
76
|
+
type: "number",
|
|
77
|
+
description: "Soft response-requiring round limit."
|
|
78
|
+
},
|
|
79
|
+
hardLimit: {
|
|
80
|
+
type: "number",
|
|
81
|
+
description: "Hard response-requiring round limit."
|
|
82
|
+
},
|
|
83
|
+
idleTimeoutMinutes: {
|
|
84
|
+
type: "number",
|
|
85
|
+
description: "Minutes an idle AKK session remains open before lazy cleanup closes it."
|
|
86
|
+
},
|
|
87
|
+
agentTimeoutMinutes: {
|
|
88
|
+
type: "number",
|
|
89
|
+
description: "Minutes AKK waits for a coding-agent callback before marking the task stalled."
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const listParameters = {
|
|
94
|
+
type: "object",
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
agent: {
|
|
98
|
+
type: "string",
|
|
99
|
+
enum: EXECUTOR_KINDS
|
|
100
|
+
},
|
|
101
|
+
status: {
|
|
102
|
+
type: "string"
|
|
103
|
+
},
|
|
104
|
+
all: {
|
|
105
|
+
type: "boolean"
|
|
106
|
+
},
|
|
107
|
+
storeDir: {
|
|
108
|
+
type: "string"
|
|
109
|
+
},
|
|
110
|
+
idleTimeoutMinutes: {
|
|
111
|
+
type: "number"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const statusParameters = {
|
|
116
|
+
type: "object",
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
required: ["conversation_id"],
|
|
119
|
+
properties: {
|
|
120
|
+
conversation_id: {
|
|
121
|
+
type: "string"
|
|
122
|
+
},
|
|
123
|
+
storeDir: {
|
|
124
|
+
type: "string"
|
|
125
|
+
},
|
|
126
|
+
idleTimeoutMinutes: {
|
|
127
|
+
type: "number"
|
|
128
|
+
},
|
|
129
|
+
agentTimeoutMinutes: {
|
|
130
|
+
type: "number"
|
|
131
|
+
},
|
|
132
|
+
trace: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
description: "Include a safe executor trace summary with tool calls, permission requests, monitor events, and redacted thinking markers."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const sendParameters = {
|
|
139
|
+
type: "object",
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
required: ["conversation_id", "message"],
|
|
142
|
+
properties: {
|
|
143
|
+
conversation_id: {
|
|
144
|
+
type: "string"
|
|
145
|
+
},
|
|
146
|
+
message: {
|
|
147
|
+
type: "string"
|
|
148
|
+
},
|
|
149
|
+
type: {
|
|
150
|
+
type: "string",
|
|
151
|
+
enum: ["answer", "task", "control", "error"]
|
|
152
|
+
},
|
|
153
|
+
allProxy: {
|
|
154
|
+
type: "string"
|
|
155
|
+
},
|
|
156
|
+
model: {
|
|
157
|
+
type: "string"
|
|
158
|
+
},
|
|
159
|
+
storeDir: {
|
|
160
|
+
type: "string"
|
|
161
|
+
},
|
|
162
|
+
idleTimeoutMinutes: {
|
|
163
|
+
type: "number"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const cancelParameters = {
|
|
168
|
+
type: "object",
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
required: ["conversation_id"],
|
|
171
|
+
properties: {
|
|
172
|
+
conversation_id: {
|
|
173
|
+
type: "string"
|
|
174
|
+
},
|
|
175
|
+
allProxy: {
|
|
176
|
+
type: "string"
|
|
177
|
+
},
|
|
178
|
+
storeDir: {
|
|
179
|
+
type: "string"
|
|
180
|
+
},
|
|
181
|
+
idleTimeoutMinutes: {
|
|
182
|
+
type: "number"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const recoveryParameters = {
|
|
187
|
+
type: "object",
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
required: ["conversation_id"],
|
|
190
|
+
properties: {
|
|
191
|
+
conversation_id: {
|
|
192
|
+
type: "string"
|
|
193
|
+
},
|
|
194
|
+
session: {
|
|
195
|
+
type: "string"
|
|
196
|
+
},
|
|
197
|
+
allProxy: {
|
|
198
|
+
type: "string"
|
|
199
|
+
},
|
|
200
|
+
model: {
|
|
201
|
+
type: "string"
|
|
202
|
+
},
|
|
203
|
+
storeDir: {
|
|
204
|
+
type: "string"
|
|
205
|
+
},
|
|
206
|
+
idleTimeoutMinutes: {
|
|
207
|
+
type: "number"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const closeParameters = {
|
|
212
|
+
type: "object",
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
required: ["conversation_id"],
|
|
215
|
+
properties: {
|
|
216
|
+
conversation_id: {
|
|
217
|
+
type: "string"
|
|
218
|
+
},
|
|
219
|
+
reason: {
|
|
220
|
+
type: "string"
|
|
221
|
+
},
|
|
222
|
+
storeDir: {
|
|
223
|
+
type: "string"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
export default definePluginEntry({
|
|
228
|
+
id: "agent-knock-knock",
|
|
229
|
+
name: "Agent Knock Knock",
|
|
230
|
+
description: "Agent Knock Knock (AKK/akk) delegates OpenClaw coding work to local Codex, Claude, or Cursor agents. Use this for AKK, akk, Agent Knock Knock, Codex delegation, Claude delegation, Cursor delegation, task listing, follow-up messages, status, recovery, restart, cancel requests, and close requests. Default delegation target comes from plugin config defaultAgent and falls back to Codex when unset; explicit user agent requests override it.",
|
|
231
|
+
register(api) {
|
|
232
|
+
api.registerGatewayMethod(CALLBACK_METHOD, async ({ params, respond }) => {
|
|
233
|
+
try {
|
|
234
|
+
const result = await handleCallback(api, params);
|
|
235
|
+
respond(true, result);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
api.logger.warn?.(`agent-knock-knock callback failed: ${message}`);
|
|
240
|
+
respond(false, undefined, {
|
|
241
|
+
code: "AGENT_KNOCK_KNOCK_CALLBACK_FAILED",
|
|
242
|
+
message
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}, { scope: "operator.write" });
|
|
246
|
+
api.registerCommand?.({
|
|
247
|
+
name: "akk",
|
|
248
|
+
description: "Delegate coding work to Agent Knock Knock, list tasks, send follow-ups, cancel running work, and close tasks.",
|
|
249
|
+
acceptsArgs: true,
|
|
250
|
+
requireAuth: true,
|
|
251
|
+
nativeProgressMessages: {
|
|
252
|
+
default: "AKK is handling the request..."
|
|
253
|
+
},
|
|
254
|
+
agentPromptGuidance: [
|
|
255
|
+
"Use /akk <task> to delegate coding work to Agent Knock Knock. /akk uses the plugin-configured defaultAgent and falls back to Codex when unset; use /akk claude <task>, /akk cursor <task>, or /akk codex <task> when the user explicitly requests that agent."
|
|
256
|
+
],
|
|
257
|
+
handler: async (ctx) => handleAkkCommand(api, ctx)
|
|
258
|
+
});
|
|
259
|
+
api.registerTool((toolContext) => ({
|
|
260
|
+
name: "agent_knock_knock_delegate",
|
|
261
|
+
description: "Delegate an implementation task to a local coding agent. Use this when the user says AKK, akk, Agent Knock Knock, asks to hand work to Codex, Claude, or Cursor, or asks OpenClaw to start a background coding-agent task. If the user says AKK without an explicit agent, omit the agent parameter so the plugin-configured defaultAgent is used, falling back to Codex when unset. The tool starts the coding agent in the background and returns only protocol metadata, not raw terminal output.",
|
|
262
|
+
parameters: delegateParameters,
|
|
263
|
+
async execute(_toolCallId, params) {
|
|
264
|
+
const result = runDelegate(api, params, toolContext);
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: JSON.stringify(result, null, 2)
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}), { name: "agent_knock_knock_delegate", optional: true });
|
|
275
|
+
registerCliTool(api, {
|
|
276
|
+
name: "agent_knock_knock_list",
|
|
277
|
+
description: "List open or historical Agent Knock Knock coding-agent sessions. Use this for AKK list, akk list, current AKK tasks, or asking which Codex/Claude/Cursor sessions are open. Idle sessions are complete for now but can receive follow-up sends until they are closed or time out.",
|
|
278
|
+
parameters: listParameters,
|
|
279
|
+
buildArgs: (params) => {
|
|
280
|
+
const args = ["list"];
|
|
281
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
282
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
283
|
+
pushOptional(args, "--agent", stringValue(params.agent));
|
|
284
|
+
pushOptional(args, "--status", stringValue(params.status));
|
|
285
|
+
if (params.all === true) {
|
|
286
|
+
args.push("--all");
|
|
287
|
+
}
|
|
288
|
+
return args;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
registerCliTool(api, {
|
|
292
|
+
name: "agent_knock_knock_status",
|
|
293
|
+
description: "Get detailed status for one Agent Knock Knock coding-agent task.",
|
|
294
|
+
parameters: statusParameters,
|
|
295
|
+
buildArgs: (params) => {
|
|
296
|
+
const args = ["status", "--conversation", requiredString(params.conversation_id, "conversation_id")];
|
|
297
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
298
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
299
|
+
if (params.trace === true) {
|
|
300
|
+
args.push("--trace");
|
|
301
|
+
}
|
|
302
|
+
return args;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
registerCliTool(api, {
|
|
306
|
+
name: "agent_knock_knock_send",
|
|
307
|
+
description: "Send a follow-up message to an existing open Agent Knock Knock coding-agent session. Use this for AKK follow-up requests such as sending another instruction to an idle, waiting, or running Codex, Claude, or Cursor session.",
|
|
308
|
+
parameters: sendParameters,
|
|
309
|
+
buildArgs: (params) => {
|
|
310
|
+
const args = [
|
|
311
|
+
"send",
|
|
312
|
+
"--conversation",
|
|
313
|
+
requiredString(params.conversation_id, "conversation_id"),
|
|
314
|
+
"--message",
|
|
315
|
+
requiredString(params.message, "message"),
|
|
316
|
+
"--background"
|
|
317
|
+
];
|
|
318
|
+
pushOptional(args, "--type", stringValue(params.type));
|
|
319
|
+
pushOptional(args, "--all-proxy", stringValue(params.allProxy) ?? stringValue(api.pluginConfig?.codexAllProxy) ?? stringValue(api.pluginConfig?.allProxy));
|
|
320
|
+
pushOptional(args, "--model", stringValue(params.model) ?? stringValue(api.pluginConfig?.codexModel) ?? stringValue(api.pluginConfig?.model));
|
|
321
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
322
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
323
|
+
pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(api.pluginConfig?.agentTimeoutMinutes));
|
|
324
|
+
return args;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
registerCliTool(api, {
|
|
328
|
+
name: "agent_knock_knock_cancel",
|
|
329
|
+
description: "Request cooperative cancellation of the current in-flight prompt for an existing Agent Knock Knock Codex, Claude, or Cursor session. This does not close the AKK session; use close when the session should no longer be reused.",
|
|
330
|
+
parameters: cancelParameters,
|
|
331
|
+
buildArgs: (params) => {
|
|
332
|
+
const args = ["cancel", "--conversation", requiredString(params.conversation_id, "conversation_id")];
|
|
333
|
+
pushOptional(args, "--all-proxy", stringValue(params.allProxy) ?? stringValue(api.pluginConfig?.codexAllProxy) ?? stringValue(api.pluginConfig?.allProxy));
|
|
334
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
335
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
336
|
+
return args;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
registerCliTool(api, {
|
|
340
|
+
name: "agent_knock_knock_recover",
|
|
341
|
+
description: "Recover an Agent Knock Knock task whose coding-agent session is unavailable by starting a new session with AKK's saved protocol history summary plus the pending message. Use only after the user chooses recovery.",
|
|
342
|
+
parameters: recoveryParameters,
|
|
343
|
+
buildArgs: (params) => buildRecoveryArgs(api, "recover", params)
|
|
344
|
+
});
|
|
345
|
+
registerCliTool(api, {
|
|
346
|
+
name: "agent_knock_knock_restart",
|
|
347
|
+
description: "Restart an Agent Knock Knock task whose coding-agent session is unavailable by starting a new session with only the pending message. Use only after the user chooses restart instead of history recovery.",
|
|
348
|
+
parameters: recoveryParameters,
|
|
349
|
+
buildArgs: (params) => buildRecoveryArgs(api, "restart", params)
|
|
350
|
+
});
|
|
351
|
+
registerCliTool(api, {
|
|
352
|
+
name: "agent_knock_knock_close",
|
|
353
|
+
description: "Close an Agent Knock Knock coding-agent task without terminating the underlying ACPX session. Use this for AKK close requests.",
|
|
354
|
+
parameters: closeParameters,
|
|
355
|
+
buildArgs: (params) => {
|
|
356
|
+
const args = ["close", "--conversation", requiredString(params.conversation_id, "conversation_id")];
|
|
357
|
+
pushOptional(args, "--reason", stringValue(params.reason));
|
|
358
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
359
|
+
return args;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
async function handleAkkCommand(api, ctx) {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = parseAkkCommand(ctx.args);
|
|
367
|
+
if (parsed.action === "help") {
|
|
368
|
+
return { text: akkUsageText() };
|
|
369
|
+
}
|
|
370
|
+
if (parsed.action === "delegate") {
|
|
371
|
+
const result = runDelegate(api, {
|
|
372
|
+
agent: parsed.agent,
|
|
373
|
+
request: parsed.request
|
|
374
|
+
}, {
|
|
375
|
+
sessionKey: ctx.sessionKey
|
|
376
|
+
});
|
|
377
|
+
return { text: formatDelegateCommandResult(result) };
|
|
378
|
+
}
|
|
379
|
+
if (parsed.action === "list") {
|
|
380
|
+
const result = runCli(api, ["list"]);
|
|
381
|
+
return { text: formatListCommandResult(result) };
|
|
382
|
+
}
|
|
383
|
+
if (parsed.action === "status") {
|
|
384
|
+
const result = runCli(api, ["status", "--conversation", parsed.conversationId]);
|
|
385
|
+
return { text: formatStatusCommandResult(result) };
|
|
386
|
+
}
|
|
387
|
+
if (parsed.action === "send") {
|
|
388
|
+
const args = [
|
|
389
|
+
"send",
|
|
390
|
+
"--conversation",
|
|
391
|
+
parsed.conversationId,
|
|
392
|
+
"--message",
|
|
393
|
+
parsed.message,
|
|
394
|
+
"--background"
|
|
395
|
+
];
|
|
396
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
397
|
+
pushOptional(args, "--all-proxy", stringValue(config.codexAllProxy) ?? stringValue(config.allProxy));
|
|
398
|
+
pushOptional(args, "--model", stringValue(config.codexModel) ?? stringValue(config.model));
|
|
399
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
|
|
400
|
+
const result = runCli(api, args);
|
|
401
|
+
return { text: formatSendCommandResult(result) };
|
|
402
|
+
}
|
|
403
|
+
if (parsed.action === "cancel") {
|
|
404
|
+
const args = [
|
|
405
|
+
"cancel",
|
|
406
|
+
"--conversation",
|
|
407
|
+
parsed.conversationId
|
|
408
|
+
];
|
|
409
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
410
|
+
pushOptional(args, "--all-proxy", stringValue(config.codexAllProxy) ?? stringValue(config.allProxy));
|
|
411
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
|
|
412
|
+
const result = runCli(api, args);
|
|
413
|
+
return { text: formatCancelCommandResult(result) };
|
|
414
|
+
}
|
|
415
|
+
if (parsed.action === "recover" || parsed.action === "restart") {
|
|
416
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
417
|
+
const args = [
|
|
418
|
+
parsed.action,
|
|
419
|
+
"--conversation",
|
|
420
|
+
parsed.conversationId,
|
|
421
|
+
"--background"
|
|
422
|
+
];
|
|
423
|
+
pushOptional(args, "--all-proxy", stringValue(config.codexAllProxy) ?? stringValue(config.allProxy));
|
|
424
|
+
pushOptional(args, "--model", stringValue(config.codexModel) ?? stringValue(config.model));
|
|
425
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
|
|
426
|
+
const result = runCli(api, args);
|
|
427
|
+
return { text: formatRecoveryCommandResult(result, parsed.action) };
|
|
428
|
+
}
|
|
429
|
+
if (parsed.action === "close") {
|
|
430
|
+
const result = runCli(api, [
|
|
431
|
+
"close",
|
|
432
|
+
"--conversation",
|
|
433
|
+
parsed.conversationId,
|
|
434
|
+
"--reason",
|
|
435
|
+
parsed.reason
|
|
436
|
+
]);
|
|
437
|
+
return { text: formatCloseCommandResult(result) };
|
|
438
|
+
}
|
|
439
|
+
return { text: akkUsageText(), isError: true };
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
443
|
+
return {
|
|
444
|
+
text: `AKK command failed: ${message}`,
|
|
445
|
+
isError: true
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function parseAkkCommand(args) {
|
|
450
|
+
const input = String(args ?? "").trim();
|
|
451
|
+
if (!input || input === "help" || input === "-h" || input === "--help") {
|
|
452
|
+
return { action: "help" };
|
|
453
|
+
}
|
|
454
|
+
const { token, rest } = takeToken(input);
|
|
455
|
+
const action = String(token).toLowerCase();
|
|
456
|
+
if (action === "list" || action === "ls" || action === "tasks") {
|
|
457
|
+
return { action: "list" };
|
|
458
|
+
}
|
|
459
|
+
if (action === "status" || action === "show") {
|
|
460
|
+
const { token: conversationId } = takeRequiredToken(rest, "Usage: /akk status <conversation-id>");
|
|
461
|
+
return { action: "status", conversationId };
|
|
462
|
+
}
|
|
463
|
+
if (action === "send" || action === "reply") {
|
|
464
|
+
const { token: conversationId, rest: message } = takeRequiredToken(rest, "Usage: /akk send <conversation-id> <message>");
|
|
465
|
+
const body = message.trim();
|
|
466
|
+
if (!body) {
|
|
467
|
+
throw new Error("Usage: /akk send <conversation-id> <message>");
|
|
468
|
+
}
|
|
469
|
+
return { action: "send", conversationId, message: body };
|
|
470
|
+
}
|
|
471
|
+
if (action === "cancel" || action === "stop") {
|
|
472
|
+
const { token: conversationId } = takeRequiredToken(rest, "Usage: /akk cancel <conversation-id>");
|
|
473
|
+
return { action: "cancel", conversationId };
|
|
474
|
+
}
|
|
475
|
+
if (action === "recover") {
|
|
476
|
+
const { token: conversationId } = takeRequiredToken(rest, "Usage: /akk recover <conversation-id>");
|
|
477
|
+
return { action: "recover", conversationId };
|
|
478
|
+
}
|
|
479
|
+
if (action === "restart") {
|
|
480
|
+
const { token: conversationId } = takeRequiredToken(rest, "Usage: /akk restart <conversation-id>");
|
|
481
|
+
return { action: "restart", conversationId };
|
|
482
|
+
}
|
|
483
|
+
if (action === "close" || action === "done") {
|
|
484
|
+
const { token: conversationId, rest: reason } = takeRequiredToken(rest, "Usage: /akk close <conversation-id> [reason]");
|
|
485
|
+
return { action: "close", conversationId, reason: reason.trim() || "Closed from /akk command" };
|
|
486
|
+
}
|
|
487
|
+
const executorDefinition = executorDefinitionForAlias(action);
|
|
488
|
+
if (executorDefinition) {
|
|
489
|
+
const request = rest.trim();
|
|
490
|
+
if (!request) {
|
|
491
|
+
throw new Error(`Usage: /akk ${executorDefinition.kind} <task>`);
|
|
492
|
+
}
|
|
493
|
+
return { action: "delegate", agent: executorDefinition.kind, request };
|
|
494
|
+
}
|
|
495
|
+
return { action: "delegate", agent: "codex", request: input };
|
|
496
|
+
}
|
|
497
|
+
function takeRequiredToken(input, usage) {
|
|
498
|
+
const parsed = takeToken(input);
|
|
499
|
+
if (!parsed.token) {
|
|
500
|
+
throw new Error(usage);
|
|
501
|
+
}
|
|
502
|
+
return parsed;
|
|
503
|
+
}
|
|
504
|
+
function takeToken(input) {
|
|
505
|
+
const value = String(input ?? "").trimStart();
|
|
506
|
+
const match = value.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
507
|
+
if (!match) {
|
|
508
|
+
return { token: "", rest: "" };
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
token: match[1],
|
|
512
|
+
rest: match[2] ?? ""
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function akkUsageText() {
|
|
516
|
+
return [
|
|
517
|
+
"AKK usage:",
|
|
518
|
+
"/akk <task>",
|
|
519
|
+
"/akk codex <task>",
|
|
520
|
+
"/akk claude <task>",
|
|
521
|
+
"/akk list",
|
|
522
|
+
"/akk status <conversation-id>",
|
|
523
|
+
"/akk send <conversation-id> <message>",
|
|
524
|
+
"/akk cancel <conversation-id>",
|
|
525
|
+
"/akk recover <conversation-id>",
|
|
526
|
+
"/akk restart <conversation-id>",
|
|
527
|
+
"/akk close <conversation-id> [reason]"
|
|
528
|
+
].join("\n");
|
|
529
|
+
}
|
|
530
|
+
function formatDelegateCommandResult(result) {
|
|
531
|
+
const agent = executorDisplayName(result.agent);
|
|
532
|
+
return [
|
|
533
|
+
`AKK 已交给 ${agent}。`,
|
|
534
|
+
`conversation: ${result.conversation_id ?? "unknown"}`,
|
|
535
|
+
`session: ${result.session ?? "unknown"}`,
|
|
536
|
+
`status: ${result.conversation_status ?? result.status ?? "unknown"}`,
|
|
537
|
+
"结果会通过 OpenClaw 回调返回当前会话。"
|
|
538
|
+
].join("\n");
|
|
539
|
+
}
|
|
540
|
+
function executorDisplayName(kind) {
|
|
541
|
+
try {
|
|
542
|
+
return executorDefinitionForKind(String(kind ?? "codex")).displayName;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return String(kind ?? "agent");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function formatListCommandResult(result) {
|
|
549
|
+
const tasks = Array.isArray(result.tasks) ? result.tasks : [];
|
|
550
|
+
if (!tasks.length) {
|
|
551
|
+
return "AKK 当前没有打开的会话。";
|
|
552
|
+
}
|
|
553
|
+
return [
|
|
554
|
+
`AKK open sessions (${tasks.length}):`,
|
|
555
|
+
...tasks.slice(0, 20).map(formatTaskLine)
|
|
556
|
+
].join("\n");
|
|
557
|
+
}
|
|
558
|
+
function formatStatusCommandResult(result) {
|
|
559
|
+
const summary = result.summary ?? result.conversation ?? {};
|
|
560
|
+
const lines = [
|
|
561
|
+
`AKK status: ${summary.conversation_id ?? "unknown"}`,
|
|
562
|
+
`agent: ${summary.agent ?? summary.executor?.kind ?? "unknown"}`,
|
|
563
|
+
`status: ${summary.status ?? "unknown"}`,
|
|
564
|
+
`session: ${summary.session ?? summary.executor?.session ?? "unknown"}`
|
|
565
|
+
];
|
|
566
|
+
if (summary.request) {
|
|
567
|
+
lines.push(`request: ${truncateText(summary.request, 180)}`);
|
|
568
|
+
}
|
|
569
|
+
return lines.join("\n");
|
|
570
|
+
}
|
|
571
|
+
function formatSendCommandResult(result) {
|
|
572
|
+
const conversation = result.conversation ?? {};
|
|
573
|
+
return [
|
|
574
|
+
"AKK follow-up sent.",
|
|
575
|
+
`conversation: ${conversation.conversation_id ?? "unknown"}`,
|
|
576
|
+
`status: ${conversation.status ?? "unknown"}`,
|
|
577
|
+
`launched: ${result.launched === true ? "yes" : "no"}`
|
|
578
|
+
].join("\n");
|
|
579
|
+
}
|
|
580
|
+
function formatCancelCommandResult(result) {
|
|
581
|
+
const conversation = result.conversation ?? {};
|
|
582
|
+
return [
|
|
583
|
+
"AKK cancel requested.",
|
|
584
|
+
`conversation: ${conversation.conversation_id ?? "unknown"}`,
|
|
585
|
+
`agent: ${result.executor?.kind ?? conversation.executor?.kind ?? "unknown"}`,
|
|
586
|
+
`session: ${result.executor?.session ?? conversation.executor?.session ?? "unknown"}`,
|
|
587
|
+
`status: ${conversation.status ?? "unknown"}`
|
|
588
|
+
].join("\n");
|
|
589
|
+
}
|
|
590
|
+
function formatRecoveryCommandResult(result, action) {
|
|
591
|
+
const conversation = result.conversation ?? {};
|
|
592
|
+
return [
|
|
593
|
+
action === "recover" ? "AKK recovery started." : "AKK restart started.",
|
|
594
|
+
`conversation: ${conversation.conversation_id ?? "unknown"}`,
|
|
595
|
+
`agent: ${result.executor?.kind ?? conversation.executor?.kind ?? "unknown"}`,
|
|
596
|
+
`session: ${result.executor?.session ?? conversation.executor?.session ?? "unknown"}`,
|
|
597
|
+
`status: ${conversation.status ?? "unknown"}`
|
|
598
|
+
].join("\n");
|
|
599
|
+
}
|
|
600
|
+
function formatCloseCommandResult(result) {
|
|
601
|
+
const conversation = result.conversation ?? {};
|
|
602
|
+
return [
|
|
603
|
+
"AKK session closed.",
|
|
604
|
+
`conversation: ${conversation.conversation_id ?? "unknown"}`,
|
|
605
|
+
`status: ${conversation.status ?? "unknown"}`
|
|
606
|
+
].join("\n");
|
|
607
|
+
}
|
|
608
|
+
function formatTaskLine(task) {
|
|
609
|
+
return [
|
|
610
|
+
task.conversation_id ?? "unknown",
|
|
611
|
+
task.agent ?? task.executor?.kind ?? "agent",
|
|
612
|
+
task.status ?? "unknown",
|
|
613
|
+
truncateText(task.request ?? "", 90)
|
|
614
|
+
].filter(Boolean).join(" | ");
|
|
615
|
+
}
|
|
616
|
+
function runDelegate(api, params, toolContext) {
|
|
617
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
618
|
+
const binPath = stringValue(config.binPath) ?? defaultBinPath;
|
|
619
|
+
const workspace = stringValue(params.workspace) ?? stringValue(config.workspace) ?? process.cwd();
|
|
620
|
+
const rawRequest = requiredString(params.request, "request");
|
|
621
|
+
const prefixedRequest = stringValue(params.agent) ? undefined : parseLeadingExecutorAlias(rawRequest);
|
|
622
|
+
const request = prefixedRequest?.request ?? rawRequest;
|
|
623
|
+
const agent = executorDefinitionForKind(stringValue(params.agent) ?? prefixedRequest?.kind ?? stringValue(config.defaultAgent) ?? "codex").kind;
|
|
624
|
+
const executorDefinition = executorDefinitionForKind(agent);
|
|
625
|
+
const agentSession = stringValue(params.session) ??
|
|
626
|
+
firstStringForKeys(params, executorDefinition.sessionConfigKeys) ??
|
|
627
|
+
firstStringForKeys(config, executorDefinition.sessionConfigKeys);
|
|
628
|
+
const allProxy = stringValue(params.allProxy) ??
|
|
629
|
+
firstStringForKeys(params, executorDefinition.proxyConfigKeys) ??
|
|
630
|
+
firstStringForKeys(config, executorDefinition.proxyConfigKeys) ??
|
|
631
|
+
stringValue(config.allProxy);
|
|
632
|
+
const model = stringValue(params.model) ??
|
|
633
|
+
firstStringForKeys(params, executorDefinition.modelConfigKeys) ??
|
|
634
|
+
firstStringForKeys(config, executorDefinition.modelConfigKeys) ??
|
|
635
|
+
stringValue(config.model);
|
|
636
|
+
const openclawSession = stringValue(toolContext?.sessionKey) ??
|
|
637
|
+
stringValue(config.openclawSession) ??
|
|
638
|
+
stringValue(params.openclawSession) ??
|
|
639
|
+
"agent:main:main";
|
|
640
|
+
const args = [
|
|
641
|
+
binPath,
|
|
642
|
+
"delegate",
|
|
643
|
+
"--agent",
|
|
644
|
+
agent,
|
|
645
|
+
"--request",
|
|
646
|
+
request,
|
|
647
|
+
"--workspace",
|
|
648
|
+
workspace,
|
|
649
|
+
"--background"
|
|
650
|
+
];
|
|
651
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(config.storeDir));
|
|
652
|
+
pushOptional(args, "--session", agentSession);
|
|
653
|
+
pushOptional(args, "--all-proxy", allProxy);
|
|
654
|
+
pushOptional(args, "--model", model);
|
|
655
|
+
pushOptional(args, "--openclaw-session", openclawSession);
|
|
656
|
+
pushOptional(args, "--gateway-url", stringValue(config.gatewayUrl));
|
|
657
|
+
pushOptional(args, "--token", stringValue(config.gatewayToken));
|
|
658
|
+
pushOptional(args, "--gateway-method", CALLBACK_METHOD);
|
|
659
|
+
pushOptional(args, "--gateway-session", openclawSession);
|
|
660
|
+
pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
|
|
661
|
+
pushOptional(args, "--callback-command", stringValue(config.callbackCommand));
|
|
662
|
+
pushOptional(args, "--soft-limit", numberString(params.softLimit) ?? numberString(config.softLimit));
|
|
663
|
+
pushOptional(args, "--hard-limit", numberString(params.hardLimit) ?? numberString(config.hardLimit));
|
|
664
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
|
|
665
|
+
pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(config.agentTimeoutMinutes));
|
|
666
|
+
const spawned = spawnSync(process.execPath, args, {
|
|
667
|
+
encoding: "utf8",
|
|
668
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
669
|
+
cwd: workspace
|
|
670
|
+
});
|
|
671
|
+
if (spawned.error) {
|
|
672
|
+
throw new Error(`agent-knock-knock delegate failed to start: ${spawned.error.message}`);
|
|
673
|
+
}
|
|
674
|
+
if (spawned.status !== 0) {
|
|
675
|
+
throw new Error(cleanError(spawned.stderr || spawned.stdout || `agent-knock-knock delegate exited with status ${spawned.status}`));
|
|
676
|
+
}
|
|
677
|
+
const parsed = parseJson(spawned.stdout);
|
|
678
|
+
const conversationId = parsed.conversation?.conversation_id;
|
|
679
|
+
const statePath = parsed.paths?.statePath;
|
|
680
|
+
const logPath = parsed.paths?.logPath;
|
|
681
|
+
return {
|
|
682
|
+
status: "async_pending",
|
|
683
|
+
delegation_status: "delegated",
|
|
684
|
+
conversation_id: conversationId,
|
|
685
|
+
conversation_status: parsed.conversation?.status,
|
|
686
|
+
state_path: statePath,
|
|
687
|
+
event_log_path: logPath,
|
|
688
|
+
agent,
|
|
689
|
+
executor: parsed.conversation?.executor,
|
|
690
|
+
session: parsed.conversation?.executor?.session ?? parsed.conversation?.claude_session,
|
|
691
|
+
claude_session: parsed.conversation?.claude_session,
|
|
692
|
+
openclaw_session: openclawSession,
|
|
693
|
+
launched: parsed.launched === true,
|
|
694
|
+
background: parsed.background === true,
|
|
695
|
+
pid: parsed.pid ?? null,
|
|
696
|
+
callback_method: CALLBACK_METHOD,
|
|
697
|
+
openclaw_next_action: {
|
|
698
|
+
action: "yield",
|
|
699
|
+
reason: "The delegated coding agent is running asynchronously. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
|
|
700
|
+
do_not: "Do not inspect event logs, process lists, session internals, files, stdout, or stderr while waiting. Follow-up communication must only use structured callbacks from agent-knock-knock.",
|
|
701
|
+
expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
|
|
702
|
+
},
|
|
703
|
+
note: "The coding agent was launched in the background. This is an async delegation; OpenClaw should yield now and wait for the scheduled callback turn."
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function registerCliTool(api, { name, description, parameters, buildArgs }) {
|
|
707
|
+
api.registerTool(() => ({
|
|
708
|
+
name,
|
|
709
|
+
description,
|
|
710
|
+
parameters,
|
|
711
|
+
async execute(_toolCallId, params) {
|
|
712
|
+
const result = runCli(api, buildArgs(isRecord(params) ? params : {}));
|
|
713
|
+
return {
|
|
714
|
+
content: [
|
|
715
|
+
{
|
|
716
|
+
type: "text",
|
|
717
|
+
text: JSON.stringify(result, null, 2)
|
|
718
|
+
}
|
|
719
|
+
]
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}), { name, optional: true });
|
|
723
|
+
}
|
|
724
|
+
function buildRecoveryArgs(api, command, params) {
|
|
725
|
+
const args = [command, "--conversation", requiredString(params.conversation_id, "conversation_id"), "--background"];
|
|
726
|
+
pushOptional(args, "--session", stringValue(params.session));
|
|
727
|
+
pushOptional(args, "--all-proxy", stringValue(params.allProxy) ?? stringValue(api.pluginConfig?.codexAllProxy) ?? stringValue(api.pluginConfig?.allProxy));
|
|
728
|
+
pushOptional(args, "--model", stringValue(params.model) ?? stringValue(api.pluginConfig?.codexModel) ?? stringValue(api.pluginConfig?.model));
|
|
729
|
+
pushOptional(args, "--store-dir", stringValue(params.storeDir) ?? stringValue(api.pluginConfig?.storeDir));
|
|
730
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
731
|
+
return args;
|
|
732
|
+
}
|
|
733
|
+
function runCli(api, cliArgs, { cwd = process.cwd() } = {}) {
|
|
734
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
735
|
+
const binPath = stringValue(config.binPath) ?? defaultBinPath;
|
|
736
|
+
const spawned = spawnSync(process.execPath, [binPath, ...cliArgs], {
|
|
737
|
+
encoding: "utf8",
|
|
738
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
739
|
+
cwd
|
|
740
|
+
});
|
|
741
|
+
if (spawned.error) {
|
|
742
|
+
throw new Error(`agent-knock-knock ${cliArgs[0]} failed to start: ${spawned.error.message}`);
|
|
743
|
+
}
|
|
744
|
+
if (spawned.status !== 0) {
|
|
745
|
+
throw new Error(cleanError(spawned.stderr || spawned.stdout || `agent-knock-knock ${cliArgs[0]} exited with status ${spawned.status}`));
|
|
746
|
+
}
|
|
747
|
+
return parseJson(spawned.stdout);
|
|
748
|
+
}
|
|
749
|
+
async function handleCallback(api, params) {
|
|
750
|
+
if (!isRecord(params)) {
|
|
751
|
+
throw new Error("callback params must be an object");
|
|
752
|
+
}
|
|
753
|
+
const message = isRecord(params.message) ? params.message : undefined;
|
|
754
|
+
const conversation = isRecord(params.conversation) ? params.conversation : undefined;
|
|
755
|
+
const sessionKey = stringValue(params.sessionKey) ??
|
|
756
|
+
stringValue(conversation?.openclaw_session) ??
|
|
757
|
+
stringValue(message?.metadata?.openclaw_session);
|
|
758
|
+
if (!sessionKey) {
|
|
759
|
+
throw new Error("callback params.sessionKey is required");
|
|
760
|
+
}
|
|
761
|
+
if (!message) {
|
|
762
|
+
throw new Error("callback params.message is required");
|
|
763
|
+
}
|
|
764
|
+
const conversationId = stringValue(message.conversation_id) ?? stringValue(conversation?.conversation_id);
|
|
765
|
+
const messageId = stringValue(message.id) ?? `${conversationId ?? "unknown"}:${stringValue(message.type) ?? "message"}:${Date.now()}`;
|
|
766
|
+
const formatted = formatCallbackInjection({ conversation, message, statePath: stringValue(params.statePath) });
|
|
767
|
+
const injection = await api.session.workflow.enqueueNextTurnInjection({
|
|
768
|
+
sessionKey,
|
|
769
|
+
text: formatted,
|
|
770
|
+
idempotencyKey: `agent-knock-knock:${conversationId ?? "unknown"}:${messageId}`,
|
|
771
|
+
placement: "append_context",
|
|
772
|
+
ttlMs: 24 * 60 * 60 * 1000,
|
|
773
|
+
metadata: {
|
|
774
|
+
kind: "agent-knock-knock-callback",
|
|
775
|
+
conversation_id: conversationId,
|
|
776
|
+
message_id: messageId,
|
|
777
|
+
message_type: stringValue(message.type) ?? "unknown",
|
|
778
|
+
state_path: stringValue(params.statePath),
|
|
779
|
+
log_path: stringValue(params.logPath)
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
const delivery = buildCallbackDeliveryPlan({
|
|
783
|
+
sessionKey,
|
|
784
|
+
conversationId,
|
|
785
|
+
messageId,
|
|
786
|
+
message,
|
|
787
|
+
formatted
|
|
788
|
+
});
|
|
789
|
+
return {
|
|
790
|
+
ok: true,
|
|
791
|
+
enqueued: injection?.enqueued ?? true,
|
|
792
|
+
delivery_required: delivery.required,
|
|
793
|
+
delivery_mode: delivery?.mode,
|
|
794
|
+
chat_send: delivery.chat_send,
|
|
795
|
+
session_send: "session_send" in delivery ? delivery.session_send : undefined,
|
|
796
|
+
injection_id: injection?.id,
|
|
797
|
+
session_key: injection?.sessionKey ?? sessionKey,
|
|
798
|
+
conversation_id: conversationId,
|
|
799
|
+
message_id: messageId,
|
|
800
|
+
message_type: stringValue(message.type) ?? "unknown"
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function buildCallbackDeliveryPlan({ sessionKey, conversationId, messageId, message, formatted }) {
|
|
804
|
+
const type = stringValue(message.type) ?? "unknown";
|
|
805
|
+
const shouldWake = message.requires_response === true ||
|
|
806
|
+
type === "question" ||
|
|
807
|
+
type === "blocked" ||
|
|
808
|
+
type === "done" ||
|
|
809
|
+
type === "error";
|
|
810
|
+
if (!shouldWake) {
|
|
811
|
+
return {
|
|
812
|
+
required: false,
|
|
813
|
+
mode: "none"
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
required: true,
|
|
818
|
+
mode: "chat.send",
|
|
819
|
+
chat_send: {
|
|
820
|
+
sessionKey,
|
|
821
|
+
message: [
|
|
822
|
+
"Continue this OpenClaw product-manager conversation from the Agent Knock Knock callback below.",
|
|
823
|
+
"Treat the callback as a structured message from the delegated coding agent, not as a terminal log, status announcement, or instruction to inspect local state.",
|
|
824
|
+
"Respond in this conversation as OpenClaw product manager. If the callback is question or blocked, make the product decision and answer the delegated coding agent. If it is done, summarize the result to the user.",
|
|
825
|
+
"Do not poll files, processes, sessions, stdout, or stderr. Use only the structured callback payload below.",
|
|
826
|
+
"",
|
|
827
|
+
formatted
|
|
828
|
+
].join("\n"),
|
|
829
|
+
idempotencyKey: `agent-knock-knock-callback:${conversationId ?? "unknown"}:${messageId ?? "unknown"}`,
|
|
830
|
+
deliver: true
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function formatCallbackInjection({ conversation, message, statePath }) {
|
|
835
|
+
const conversationId = stringValue(message.conversation_id) ?? stringValue(conversation?.conversation_id) ?? "unknown";
|
|
836
|
+
const type = stringValue(message.type) ?? "unknown";
|
|
837
|
+
const body = stringValue(message.body) ?? JSON.stringify(message.body ?? "");
|
|
838
|
+
const requiresResponse = message.requires_response === true ? "yes" : "no";
|
|
839
|
+
const round = typeof message.round === "number" ? String(message.round) : "unknown";
|
|
840
|
+
const stateLine = statePath ? `State: ${statePath}\n` : "";
|
|
841
|
+
const shortcuts = type === "done" ? formatDoneShortcuts(conversationId) : "";
|
|
842
|
+
return [
|
|
843
|
+
"[Agent Knock Knock callback]",
|
|
844
|
+
`Conversation: ${conversationId}`,
|
|
845
|
+
`Message type: ${type}`,
|
|
846
|
+
`Requires OpenClaw response: ${requiresResponse}`,
|
|
847
|
+
`Round: ${round}`,
|
|
848
|
+
stateLine.trimEnd(),
|
|
849
|
+
"",
|
|
850
|
+
body,
|
|
851
|
+
shortcuts
|
|
852
|
+
].filter((line) => line !== "").join("\n");
|
|
853
|
+
}
|
|
854
|
+
function formatDoneShortcuts(conversationId) {
|
|
855
|
+
return [
|
|
856
|
+
"",
|
|
857
|
+
"[AKK convenience commands]",
|
|
858
|
+
"When summarizing this result to the user, include these short next-step commands:",
|
|
859
|
+
"- `AKK list` lists open AKK sessions.",
|
|
860
|
+
`- \`AKK send ${conversationId}: <message>\` sends a follow-up to this same AKK session.`,
|
|
861
|
+
`- \`AKK status ${conversationId}\` shows this session status.`,
|
|
862
|
+
`- \`AKK cancel ${conversationId}\` requests cancellation of current running work without closing this AKK session.`,
|
|
863
|
+
`- \`AKK close ${conversationId}\` closes this AKK session.`
|
|
864
|
+
].join("\n");
|
|
865
|
+
}
|
|
866
|
+
function pushOptional(args, flag, value) {
|
|
867
|
+
if (value !== undefined && value !== "") {
|
|
868
|
+
args.push(flag, value);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function isRecord(value) {
|
|
872
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
873
|
+
}
|
|
874
|
+
function stringValue(value) {
|
|
875
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
876
|
+
}
|
|
877
|
+
function firstStringForKeys(source, keys) {
|
|
878
|
+
if (!isRecord(source)) {
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
for (const key of keys) {
|
|
882
|
+
const value = stringValue(source[key]);
|
|
883
|
+
if (value) {
|
|
884
|
+
return value;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
function numberString(value) {
|
|
890
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : undefined;
|
|
891
|
+
}
|
|
892
|
+
function truncateText(value, maxLength) {
|
|
893
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
894
|
+
if (text.length <= maxLength) {
|
|
895
|
+
return text;
|
|
896
|
+
}
|
|
897
|
+
return `${text.slice(0, Math.max(0, maxLength - 1))}...`;
|
|
898
|
+
}
|
|
899
|
+
function requiredString(value, name) {
|
|
900
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
901
|
+
throw new Error(`${name} is required`);
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
function parseJson(text) {
|
|
906
|
+
try {
|
|
907
|
+
return JSON.parse(text);
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
throw new Error(`agent-knock-knock delegate returned invalid JSON: ${error.message}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function cleanError(text) {
|
|
914
|
+
return String(text).trim().slice(0, 2000);
|
|
915
|
+
}
|
|
916
|
+
//# sourceMappingURL=openclaw-plugin.js.map
|