@scotthuang/agent-knock-knock 0.12.28 → 0.12.30

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/claude-injected-paste-proof.d.ts +59 -0
  3. package/dist/src/claude-injected-paste-proof.js +153 -0
  4. package/dist/src/claude-injected-paste-proof.js.map +1 -0
  5. package/dist/src/claude-terminal-agent-adapter.js +20 -1
  6. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  7. package/dist/src/cli-core.js +50 -35
  8. package/dist/src/cli-core.js.map +1 -1
  9. package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
  10. package/dist/src/codex-terminal-agent-adapter.js +40 -4
  11. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  12. package/dist/src/host-adapter.d.ts +1 -1
  13. package/dist/src/host-adapter.js +1 -1
  14. package/dist/src/host-bridge-tools.js +2 -2
  15. package/dist/src/openclaw-plugin-command-adapter.js +155 -24
  16. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  17. package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
  18. package/dist/src/openclaw-plugin-helpers.js +28 -7
  19. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  20. package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
  21. package/dist/src/openclaw-plugin-schemas.js +108 -0
  22. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  23. package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
  24. package/dist/src/openclaw-private-authority-offers.js +12 -0
  25. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  26. package/dist/src/terminal-agent-adapter.d.ts +34 -0
  27. package/dist/src/terminal-agent-adapter.js +54 -0
  28. package/dist/src/terminal-agent-adapter.js.map +1 -1
  29. package/dist/src/terminal-agent-bridge.d.ts +76 -1
  30. package/dist/src/terminal-agent-bridge.js +873 -153
  31. package/dist/src/terminal-agent-bridge.js.map +1 -1
  32. package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
  33. package/dist/src/terminal-command-cli-adapter.js +46 -8
  34. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  35. package/dist/src/terminal-dispatch-execution.js +15 -0
  36. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  37. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
  38. package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
  39. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  40. package/dist/src/terminal-interaction-authority.d.ts +33 -0
  41. package/dist/src/terminal-interaction-authority.js +3 -0
  42. package/dist/src/terminal-interaction-authority.js.map +1 -0
  43. package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
  44. package/dist/src/terminal-interaction-cli-adapter.js +354 -0
  45. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
  46. package/dist/src/terminal-interaction-protocol.d.ts +111 -0
  47. package/dist/src/terminal-interaction-protocol.js +432 -0
  48. package/dist/src/terminal-interaction-protocol.js.map +1 -0
  49. package/dist/src/terminal-list-renderer.js +31 -2
  50. package/dist/src/terminal-list-renderer.js.map +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js +12 -3
  52. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  53. package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
  54. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
  55. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  56. package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
  57. package/dist/src/terminal-mutation-cli-runtime.js +41 -0
  58. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
  59. package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
  60. package/dist/src/terminal-questionnaire-adapter.js +677 -0
  61. package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
  62. package/docs/host-bridge-profiles.md +2 -2
  63. package/docs/quickstart-herdr.md +1 -1
  64. package/docs/quickstart-tmux.md +3 -1
  65. package/openclaw.plugin.json +4 -0
  66. package/package.json +1 -1
  67. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +11 -8
@@ -0,0 +1,677 @@
1
+ import { createHash } from "node:crypto";
2
+ export const NATIVE_QUESTIONNAIRE_PROFILES = Object.freeze({
3
+ claude: "claude-code/2.1.263/ask-user-question-v1",
4
+ codex: "codex/0.153.4/request-user-input-v1"
5
+ });
6
+ const CLAUDE_VERSION = "2.1.263";
7
+ const CODEX_VERSION = "0.153.4";
8
+ const MAX_CAPTURE_CHARACTERS = 128 * 1024;
9
+ const MAX_CAPTURE_LINES = 240;
10
+ const MAX_TEXT_ANSWER_CHARACTERS = 4_096;
11
+ // Claude collapses bracketed-paste payloads at 800 characters. Until the
12
+ // custom-text editor has the same placeholder proof as managed Send, keep a
13
+ // semantic response strictly below that boundary.
14
+ const MAX_CLAUDE_TEXT_ANSWER_CHARACTERS = 799;
15
+ const ANSI_SEQUENCE_PATTERN = /[\u001B\u009B](?:(?:\[[0-?]*[ -/]*[@-~])|(?:\][^\u0007]*(?:\u0007|\u001B\\))|.)/gu;
16
+ const UNSAFE_CONTROL_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/u;
17
+ const SECRET_INPUT_PATTERN = new RegExp([
18
+ "password",
19
+ "passphrase",
20
+ "api[ _-]?key",
21
+ "secret",
22
+ "access[ _-]?token",
23
+ "credential",
24
+ "private[ _-]?key",
25
+ "one[ _-]?time(?:[ _-](?:password|code))?",
26
+ "verification[ _-]?code",
27
+ "recovery[ _-]?code",
28
+ "seed[ _-]?phrase",
29
+ "mnemonic",
30
+ "two[ _-]?factor",
31
+ "auth(?:entication)?[ _-]?code",
32
+ "(?:^|[^A-Za-z0-9])(?:pin|otp|2fa|mfa)(?:[^A-Za-z0-9]|$)",
33
+ "密码",
34
+ "口令",
35
+ "密钥",
36
+ "令牌",
37
+ "验证码",
38
+ "助记词",
39
+ "恢复码"
40
+ ].join("|"), "iu");
41
+ const CLAUDE_SELECTION_FOOTER = "Enter to select · Tab/Arrow keys to navigate · Esc to cancel";
42
+ const CLAUDE_SINGLE_SELECTION_FOOTER = "Enter to select · ↑/↓ to navigate · Esc to cancel";
43
+ const CLAUDE_CUSTOM_TEXT_FOOTER = "Enter to select · ↑/↓ to navigate · ctrl+g to edit in Vim · Esc to cancel";
44
+ const CODEX_OPTIONS_FOOTER = " tab to add notes | enter to submit answer | esc to interrupt";
45
+ const CODEX_MULTI_OPTIONS_FOOTER = " tab to add notes | enter to submit answer | ←/→ to navigate questions | esc to interrupt";
46
+ const CODEX_FREE_TEXT_FOOTER = " enter to submit answer | esc to interrupt";
47
+ const CODEX_MULTI_FREE_TEXT_FOOTER = " enter to submit all | ctrl + p / ctrl + n change question | esc to interrupt";
48
+ const CODEX_UNANSWERED_FOOTER = " Press enter to confirm or esc to go back";
49
+ export function inspectNativeQuestionnaire(options) {
50
+ const screen = normalizedScreenLines(options.screen);
51
+ return options.agent === "claude"
52
+ ? inspectClaudeQuestionnaire(options, screen)
53
+ : inspectCodexQuestionnaire(options, screen);
54
+ }
55
+ function normalizedScreenLines(screen) {
56
+ const tail = screen.length > MAX_CAPTURE_CHARACTERS
57
+ ? screen.slice(-MAX_CAPTURE_CHARACTERS)
58
+ : screen;
59
+ const withoutAnsi = tail
60
+ .replace(ANSI_SEQUENCE_PATTERN, "")
61
+ .replaceAll("\u00a0", " ")
62
+ .replaceAll("\r", "");
63
+ const hadUnsafeControl = UNSAFE_CONTROL_PATTERN.test(withoutAnsi);
64
+ const allLines = withoutAnsi.split("\n").map((line) => line.trimEnd());
65
+ while (allLines.at(-1) === "") {
66
+ allLines.pop();
67
+ }
68
+ return {
69
+ lines: allLines.slice(-MAX_CAPTURE_LINES),
70
+ hadUnsafeControl
71
+ };
72
+ }
73
+ function fingerprint(profile, region) {
74
+ return createHash("sha256")
75
+ .update(`${profile}\0${region}`, "utf8")
76
+ .digest("hex");
77
+ }
78
+ function promptEvidence(profile, lines, start, end, footer) {
79
+ const exactRegion = lines.slice(start, end + 1).join("\n");
80
+ return {
81
+ profile,
82
+ exact_region: exactRegion,
83
+ sha256: fingerprint(profile, exactRegion),
84
+ ...(footer === undefined ? {} : { footer })
85
+ };
86
+ }
87
+ function semanticId(prefix, ...parts) {
88
+ const digest = createHash("sha256")
89
+ .update(JSON.stringify(parts), "utf8")
90
+ .digest("hex")
91
+ .slice(0, 20);
92
+ return `${prefix}_${digest}`;
93
+ }
94
+ function hasClaudeQuestionnaireCandidate(lines) {
95
+ const tail = lines.at(-1) ?? "";
96
+ return tail.includes("Enter to select ·") || tail === " 2. Cancel";
97
+ }
98
+ function hasCodexQuestionnaireCandidate(lines) {
99
+ const tail = lines.at(-1) ?? "";
100
+ return tail.includes("esc to interrupt") ||
101
+ /^ Press enter to confirm or esc to go back$/u.test(tail);
102
+ }
103
+ function candidateBounds(lines, marker) {
104
+ let start = -1;
105
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
106
+ if (marker(lines[index] ?? "")) {
107
+ start = index;
108
+ break;
109
+ }
110
+ }
111
+ return {
112
+ start: Math.max(0, start),
113
+ end: Math.max(0, lines.length - 1)
114
+ };
115
+ }
116
+ function fallbackQuestion(agent, lines, bounds) {
117
+ const prompt = lines.slice(bounds.start, bounds.end + 1)
118
+ .find((line) => line.trim().length > 0)
119
+ ?.trim() ?? "Unrecognized native questionnaire";
120
+ return {
121
+ question_id: semanticId("question", agent, prompt, bounds.start),
122
+ prompt,
123
+ response_kind: "confirm",
124
+ required: true
125
+ };
126
+ }
127
+ function manualCandidate(agent, profile, lines, bounds, reason, question, currentStep = 1, totalSteps = 1) {
128
+ return {
129
+ status: "manual_required",
130
+ reason,
131
+ agent,
132
+ profile,
133
+ current_step: currentStep,
134
+ total_steps: totalSteps,
135
+ question: question ?? fallbackQuestion(agent, lines, bounds),
136
+ prompt_evidence: promptEvidence(profile, lines, bounds.start, bounds.end),
137
+ action_plan: { kind: "manual_only" }
138
+ };
139
+ }
140
+ function isSecretQuestion(explicitSecret, question) {
141
+ return explicitSecret === true ||
142
+ SECRET_INPUT_PATTERN.test(question.prompt) ||
143
+ question.options?.some((option) => SECRET_INPUT_PATTERN.test(option.label) ||
144
+ (option.description !== undefined &&
145
+ SECRET_INPUT_PATTERN.test(option.description))) === true;
146
+ }
147
+ function inspectClaudeQuestionnaire(options, screen) {
148
+ if (!hasClaudeQuestionnaireCandidate(screen.lines)) {
149
+ return { status: "none", agent: "claude", reason: "no_questionnaire_surface" };
150
+ }
151
+ const bounds = candidateBounds(screen.lines, (line) => /^← [☐☒]/u.test(line) || /^ [☐☒] \S/u.test(line));
152
+ if (options.version !== CLAUDE_VERSION) {
153
+ return manualCandidate("claude", `claude-code/${options.version}/unsupported-questionnaire`, screen.lines, bounds, "unsupported_version");
154
+ }
155
+ if (screen.hadUnsafeControl) {
156
+ return manualCandidate("claude", NATIVE_QUESTIONNAIRE_PROFILES.claude, screen.lines, bounds, "changed_shape");
157
+ }
158
+ const finalReview = parseClaudeFinalReview(screen.lines);
159
+ if (finalReview) {
160
+ return options.secret === true ||
161
+ SECRET_INPUT_PATTERN.test(finalReview.prompt_evidence.exact_region)
162
+ ? {
163
+ ...finalReview,
164
+ status: "manual_required",
165
+ reason: "secret_input",
166
+ action_plan: { kind: "manual_only" }
167
+ }
168
+ : finalReview;
169
+ }
170
+ const customTextEdit = parseClaudeCustomTextEdit(screen.lines);
171
+ if (customTextEdit) {
172
+ return isSecretQuestion(options.secret, customTextEdit.question)
173
+ ? {
174
+ ...customTextEdit,
175
+ status: "manual_required",
176
+ reason: "secret_input",
177
+ action_plan: { kind: "manual_only" }
178
+ }
179
+ : customTextEdit;
180
+ }
181
+ const choiceRegion = parseClaudeChoiceRegion(screen.lines);
182
+ if (!choiceRegion) {
183
+ return manualCandidate("claude", NATIVE_QUESTIONNAIRE_PROFILES.claude, screen.lines, bounds, "changed_shape");
184
+ }
185
+ return claudeChoiceInspection(choiceRegion, screen.lines, options.secret);
186
+ }
187
+ function parseClaudeHeader(line) {
188
+ const single = /^ ☐ (\S.*)$/u.exec(line);
189
+ if (single?.[1] && single[1].trim() === single[1]) {
190
+ return {
191
+ tabs: [{ complete: false, label: single[1] }],
192
+ currentStep: 1,
193
+ totalSteps: 1,
194
+ finalReview: false
195
+ };
196
+ }
197
+ const match = /^← (.+) ✔ Submit →$/u.exec(line);
198
+ if (!match?.[1]) {
199
+ return undefined;
200
+ }
201
+ const inner = match[1];
202
+ const tokenPattern = /([☐☒]) ([^☐☒]+?)(?= [☐☒] |$)/gu;
203
+ const tabs = [...inner.matchAll(tokenPattern)].map((token) => ({
204
+ complete: token[1] === "☒",
205
+ label: token[2]?.trim() ?? ""
206
+ }));
207
+ if (tabs.length < 1 ||
208
+ tabs.some((tab) => tab.label.length === 0) ||
209
+ tabs.map((tab) => `${tab.complete ? "☒" : "☐"} ${tab.label}`).join(" ") !== inner) {
210
+ return undefined;
211
+ }
212
+ const firstIncomplete = tabs.findIndex((tab) => !tab.complete);
213
+ if (firstIncomplete >= 0 &&
214
+ tabs.slice(firstIncomplete).some((tab) => tab.complete)) {
215
+ return undefined;
216
+ }
217
+ const totalSteps = tabs.length + 1;
218
+ return {
219
+ tabs,
220
+ currentStep: firstIncomplete < 0 ? totalSteps : firstIncomplete + 1,
221
+ totalSteps,
222
+ finalReview: firstIncomplete < 0
223
+ };
224
+ }
225
+ function parseClaudeOptionLines(lines, kind) {
226
+ const options = [];
227
+ for (const line of lines) {
228
+ if (line.length === 0) {
229
+ continue;
230
+ }
231
+ const selected = /^❯ ([1-9]\d*)\. (.+)$/u.exec(line);
232
+ const unselected = /^ ([1-9]\d*)\. (.+)$/u.exec(line);
233
+ const row = selected ?? unselected;
234
+ if (row?.[1] && row[2]) {
235
+ options.push({
236
+ number: Number(row[1]),
237
+ selected: Boolean(selected),
238
+ label: row[2]
239
+ });
240
+ continue;
241
+ }
242
+ const description = kind === "multi_select"
243
+ ? /^ {2,5}(\S.*)$/u.exec(line)?.[1]
244
+ : /^ {5}(\S.*)$/u.exec(line)?.[1];
245
+ const previous = options.at(-1);
246
+ if (!description || !previous || previous.description !== undefined) {
247
+ return undefined;
248
+ }
249
+ options[options.length - 1] = { ...previous, description };
250
+ }
251
+ if (options.length === 0 ||
252
+ options.some((option, index) => option.number !== index + 1) ||
253
+ options.filter((option) => option.selected).length !== 1) {
254
+ return undefined;
255
+ }
256
+ return options;
257
+ }
258
+ function lastIndexMatching(lines, predicate, before = lines.length) {
259
+ for (let index = Math.min(before, lines.length) - 1; index >= 0; index -= 1) {
260
+ if (predicate(lines[index] ?? "")) {
261
+ return index;
262
+ }
263
+ }
264
+ return -1;
265
+ }
266
+ function parseClaudeChoiceRegion(lines) {
267
+ const end = lines.length - 1;
268
+ const footer = lines[end] ?? "";
269
+ if (end < 0 ||
270
+ (footer !== CLAUDE_SELECTION_FOOTER &&
271
+ footer !== CLAUDE_SINGLE_SELECTION_FOOTER)) {
272
+ return undefined;
273
+ }
274
+ const footerGap = lines[end - 1] === "" ? 1 : 0;
275
+ const chatIndex = end - 1 - footerGap;
276
+ const chatMatch = /^ ([1-9]\d*)\. Chat about this$/u.exec(lines[chatIndex] ?? "");
277
+ const separatorIndex = chatIndex - 1;
278
+ if (!chatMatch?.[1] || !/^─{8,}$/u.test(lines[separatorIndex] ?? "")) {
279
+ return undefined;
280
+ }
281
+ const start = lastIndexMatching(lines, (line) => parseClaudeHeader(line) !== undefined, separatorIndex);
282
+ const parsedHeader = start >= 0
283
+ ? parseClaudeHeader(lines[start] ?? "")
284
+ : undefined;
285
+ const hasHeaderGap = lines[start + 1] === "";
286
+ const promptIndex = start + (hasHeaderGap ? 2 : 1);
287
+ const prompt = lines[promptIndex] ?? "";
288
+ const optionStart = promptIndex + (lines[promptIndex + 1] === "" ? 2 : 1);
289
+ if (!parsedHeader ||
290
+ prompt.length === 0 ||
291
+ prompt !== prompt.trim() ||
292
+ (parsedHeader.totalSteps === 1) !==
293
+ (footer === CLAUDE_SINGLE_SELECTION_FOOTER)) {
294
+ return undefined;
295
+ }
296
+ const optionLines = lines.slice(optionStart, separatorIndex);
297
+ const multiSelect = optionLines.some((line) => /^(?:❯ | )[1-9]\d*\. \[(?: |✔)\] /u.test(line));
298
+ const options = parseClaudeOptionLines(optionLines, multiSelect ? "multi_select" : "single_select");
299
+ const expectedCustomLabel = multiSelect
300
+ ? "[ ] Type something"
301
+ : "Type something.";
302
+ if (!options ||
303
+ Number(chatMatch[1]) !== options.length + 1 ||
304
+ options.at(-1)?.label !== expectedCustomLabel ||
305
+ options.length < 3 ||
306
+ options.length > 5 ||
307
+ options.at(-1)?.number !== options.length) {
308
+ return undefined;
309
+ }
310
+ const header = parsedHeader.finalReview
311
+ ? {
312
+ ...parsedHeader,
313
+ currentStep: parsedHeader.tabs.length,
314
+ finalReview: false
315
+ }
316
+ : parsedHeader;
317
+ return { start, end, footer, header, prompt, options };
318
+ }
319
+ function normalizedOptions(profile, prompt, rows) {
320
+ return rows.map((row) => ({
321
+ option_id: semanticId("option", profile, prompt, row.number, row.label),
322
+ label: row.label,
323
+ ...(row.description === undefined ? {} : { description: row.description })
324
+ }));
325
+ }
326
+ function claudeChoiceInspection(region, lines, explicitSecret) {
327
+ const multiSelect = region.options.every((option) => /^\[(?: |\u2714)\] /u.test(option.label));
328
+ const mixedKinds = !multiSelect && region.options.some((option) => /^\[[^\]]*\] /u.test(option.label));
329
+ const options = normalizedOptions(NATIVE_QUESTIONNAIRE_PROFILES.claude, region.prompt, region.options);
330
+ const question = {
331
+ question_id: semanticId("question", NATIVE_QUESTIONNAIRE_PROFILES.claude, region.prompt, region.header.currentStep),
332
+ prompt: region.prompt,
333
+ response_kind: multiSelect ? "multi_select" : "single_select",
334
+ required: true,
335
+ options
336
+ };
337
+ const evidence = promptEvidence(NATIVE_QUESTIONNAIRE_PROFILES.claude, lines, region.start, region.end, region.footer);
338
+ const base = {
339
+ agent: "claude",
340
+ profile: NATIVE_QUESTIONNAIRE_PROFILES.claude,
341
+ current_step: region.header.currentStep,
342
+ total_steps: region.header.totalSteps,
343
+ question,
344
+ prompt_evidence: evidence
345
+ };
346
+ if (mixedKinds) {
347
+ return { ...base, status: "manual_required", reason: "changed_shape", action_plan: { kind: "manual_only" } };
348
+ }
349
+ if (isSecretQuestion(explicitSecret, question)) {
350
+ return { ...base, status: "manual_required", reason: "secret_input", action_plan: { kind: "manual_only" } };
351
+ }
352
+ if (multiSelect) {
353
+ return { ...base, status: "manual_required", reason: "unsupported_multi_select", action_plan: { kind: "manual_only" } };
354
+ }
355
+ if (!region.options[0]?.selected) {
356
+ return { ...base, status: "manual_required", reason: "ambiguous_surface", action_plan: { kind: "manual_only" } };
357
+ }
358
+ return {
359
+ ...base,
360
+ status: "actionable",
361
+ action_plan: {
362
+ kind: "single_select",
363
+ choices: options.map((option, index) => ({
364
+ option_id: option.option_id,
365
+ outcome: index === options.length - 1
366
+ ? "open_custom_text"
367
+ : "submit_or_advance",
368
+ stages: [{ kind: "key", key: String(index + 1) }]
369
+ }))
370
+ }
371
+ };
372
+ }
373
+ function parseClaudeFinalReview(lines) {
374
+ const readyIndex = lastIndexMatching(lines, (line) => line === "Ready to submit your answers?");
375
+ const afterReady = lines.slice(readyIndex + 1).filter((line) => line !== "");
376
+ if (readyIndex < 2 ||
377
+ afterReady.length !== 2 ||
378
+ afterReady[0] !== "❯ 1. Submit answers" ||
379
+ afterReady[1] !== " 2. Cancel") {
380
+ return undefined;
381
+ }
382
+ const start = lastIndexMatching(lines, (line) => parseClaudeHeader(line) !== undefined, readyIndex);
383
+ const header = start >= 0 ? parseClaudeHeader(lines[start] ?? "") : undefined;
384
+ const reviewLines = lines.slice(start + 1, readyIndex)
385
+ .filter((line) => line !== "");
386
+ if (!header?.finalReview || reviewLines[0] !== "Review your answers") {
387
+ return undefined;
388
+ }
389
+ const answerRows = reviewLines.slice(1);
390
+ if (answerRows.length !== header.tabs.length * 2 ||
391
+ answerRows.some((line, index) => index % 2 === 0
392
+ ? !/^ ● \S.+\?$/u.test(line)
393
+ : !/^ → \S.*$/u.test(line))) {
394
+ return undefined;
395
+ }
396
+ const end = lines.length - 1;
397
+ const question = {
398
+ question_id: semanticId("question", NATIVE_QUESTIONNAIRE_PROFILES.claude, "final-review", header.totalSteps),
399
+ prompt: "Ready to submit your answers?",
400
+ response_kind: "confirm",
401
+ required: true
402
+ };
403
+ return {
404
+ status: "actionable",
405
+ agent: "claude",
406
+ profile: NATIVE_QUESTIONNAIRE_PROFILES.claude,
407
+ current_step: header.currentStep,
408
+ total_steps: header.totalSteps,
409
+ question,
410
+ prompt_evidence: promptEvidence(NATIVE_QUESTIONNAIRE_PROFILES.claude, lines, start, end),
411
+ action_plan: {
412
+ kind: "confirm",
413
+ confirm_stages: [{ kind: "key", key: "1" }],
414
+ cancel_stages: [{ kind: "key", key: "2" }]
415
+ }
416
+ };
417
+ }
418
+ function parseClaudeCustomTextEdit(lines) {
419
+ const editIndex = lastIndexMatching(lines, (line) => line.includes("ctrl+g to edit in Vim"));
420
+ if (editIndex < 0 ||
421
+ editIndex !== lines.length - 1 ||
422
+ lines[editIndex] !== CLAUDE_CUSTOM_TEXT_FOOTER) {
423
+ return undefined;
424
+ }
425
+ const start = lastIndexMatching(lines, (line) => parseClaudeHeader(line) !== undefined, editIndex);
426
+ const header = start >= 0 ? parseClaudeHeader(lines[start] ?? "") : undefined;
427
+ const promptIndex = start + (lines[start + 1] === "" ? 2 : 1);
428
+ const prompt = lines[promptIndex] ?? "";
429
+ const optionStart = promptIndex + (lines[promptIndex + 1] === "" ? 2 : 1);
430
+ const footerGap = lines[editIndex - 1] === "" ? 1 : 0;
431
+ const chatIndex = editIndex - 1 - footerGap;
432
+ const chat = /^ ([1-9]\d*)\. Chat about this$/u.exec(lines[chatIndex] ?? "");
433
+ const separatorIndex = chatIndex - 1;
434
+ const options = parseClaudeOptionLines(lines.slice(optionStart, separatorIndex), "single_select");
435
+ if (!header ||
436
+ header.finalReview ||
437
+ prompt.length === 0 ||
438
+ prompt !== prompt.trim() ||
439
+ !chat?.[1] ||
440
+ !/^\u2500{8,}$/u.test(lines[separatorIndex] ?? "") ||
441
+ !options ||
442
+ options.length < 3 ||
443
+ options.length > 5 ||
444
+ Number(chat[1]) !== options.length + 1 ||
445
+ options.at(-1)?.number !== options.length ||
446
+ options.at(-1)?.selected !== true ||
447
+ options.at(-1)?.label !== "Type something.") {
448
+ return undefined;
449
+ }
450
+ const question = {
451
+ question_id: semanticId("question", NATIVE_QUESTIONNAIRE_PROFILES.claude, prompt, "custom-text"),
452
+ prompt,
453
+ response_kind: "free_text",
454
+ required: true
455
+ };
456
+ return {
457
+ status: "actionable",
458
+ agent: "claude",
459
+ profile: NATIVE_QUESTIONNAIRE_PROFILES.claude,
460
+ current_step: header.currentStep,
461
+ total_steps: header.totalSteps,
462
+ question,
463
+ prompt_evidence: promptEvidence(NATIVE_QUESTIONNAIRE_PROFILES.claude, lines, start, editIndex),
464
+ action_plan: {
465
+ kind: "free_text",
466
+ stages: [
467
+ {
468
+ kind: "answer_text",
469
+ single_line: true,
470
+ max_characters: MAX_CLAUDE_TEXT_ANSWER_CHARACTERS
471
+ },
472
+ { kind: "key", key: "C-m" }
473
+ ]
474
+ }
475
+ };
476
+ }
477
+ function inspectCodexQuestionnaire(options, screen) {
478
+ if (!hasCodexQuestionnaireCandidate(screen.lines)) {
479
+ return { status: "none", agent: "codex", reason: "no_questionnaire_surface" };
480
+ }
481
+ const bounds = candidateBounds(screen.lines, (line) => /^ Question \d+\/\d+ \(/u.test(line) ||
482
+ line === " Submit with unanswered questions?");
483
+ if (options.version !== CODEX_VERSION) {
484
+ return manualCandidate("codex", `codex/${options.version}/unsupported-questionnaire`, screen.lines, bounds, "unsupported_version");
485
+ }
486
+ if (screen.hadUnsafeControl) {
487
+ return manualCandidate("codex", NATIVE_QUESTIONNAIRE_PROFILES.codex, screen.lines, bounds, "changed_shape");
488
+ }
489
+ const unanswered = parseCodexUnansweredConfirmation(screen.lines);
490
+ if (unanswered) {
491
+ return options.secret
492
+ ? { ...unanswered, status: "manual_required", reason: "secret_input", action_plan: { kind: "manual_only" } }
493
+ : unanswered;
494
+ }
495
+ const region = parseCodexQuestionRegion(screen.lines);
496
+ if (!region) {
497
+ return manualCandidate("codex", NATIVE_QUESTIONNAIRE_PROFILES.codex, screen.lines, bounds, "changed_shape");
498
+ }
499
+ return codexQuestionInspection(region, screen.lines, options.secret);
500
+ }
501
+ function parseCodexHeader(line) {
502
+ const match = /^ Question ([1-3])\/([1-3]) \(([0-3]+) unanswered\)$/u.exec(line);
503
+ if (!match?.[1] || !match[2] || !match[3]) {
504
+ return undefined;
505
+ }
506
+ const currentStep = Number(match[1]);
507
+ const totalSteps = Number(match[2]);
508
+ const unanswered = Number(match[3]);
509
+ return currentStep <= totalSteps && unanswered <= totalSteps
510
+ ? { currentStep, totalSteps }
511
+ : undefined;
512
+ }
513
+ function parseCodexOptionLines(lines) {
514
+ const nonBlank = lines.filter((line) => line.length > 0);
515
+ const options = nonBlank.map((line) => {
516
+ const match = /^ (› | )([1-4])\. (.+)$/u.exec(line);
517
+ if (!match?.[2] || !match[3]) {
518
+ return undefined;
519
+ }
520
+ const contentParts = match[3].split(/ {2,}/u);
521
+ if (contentParts.length > 2 || !contentParts[0]) {
522
+ return undefined;
523
+ }
524
+ return {
525
+ number: Number(match[2]),
526
+ selected: match[1] === "› ",
527
+ label: contentParts[0],
528
+ ...(contentParts[1] ? { description: contentParts[1] } : {})
529
+ };
530
+ });
531
+ if (options.length < 2 ||
532
+ options.length > 4 ||
533
+ options.some((option) => option === undefined)) {
534
+ return undefined;
535
+ }
536
+ const rows = options;
537
+ if (rows.some((option, index) => option.number !== index + 1) ||
538
+ rows.filter((option) => option.selected).length !== 1) {
539
+ return undefined;
540
+ }
541
+ return rows;
542
+ }
543
+ function parseCodexQuestionRegion(lines) {
544
+ const end = lines.length - 1;
545
+ const footer = lines[end] ?? "";
546
+ const headerIndex = lastIndexMatching(lines, (line) => parseCodexHeader(line) !== undefined, end);
547
+ const header = headerIndex >= 0 ? parseCodexHeader(lines[headerIndex] ?? "") : undefined;
548
+ const promptLine = lines[headerIndex + 1] ?? "";
549
+ if (!header || !/^ \S/u.test(promptLine)) {
550
+ return undefined;
551
+ }
552
+ const prompt = promptLine.slice(2);
553
+ const body = lines.slice(headerIndex + 2, end);
554
+ const freeTextLines = body.filter((line) => line.length > 0);
555
+ if ((footer === CODEX_FREE_TEXT_FOOTER ||
556
+ (header.totalSteps > 1 && footer === CODEX_MULTI_FREE_TEXT_FOOTER)) &&
557
+ freeTextLines.length === 1 &&
558
+ freeTextLines[0] === " › Type your answer (optional)") {
559
+ return {
560
+ start: headerIndex,
561
+ end,
562
+ footer,
563
+ header,
564
+ prompt,
565
+ freeText: true
566
+ };
567
+ }
568
+ const expectedFooter = header.totalSteps === 1
569
+ ? CODEX_OPTIONS_FOOTER
570
+ : CODEX_MULTI_OPTIONS_FOOTER;
571
+ if (footer !== expectedFooter) {
572
+ return undefined;
573
+ }
574
+ const options = parseCodexOptionLines(body);
575
+ return options
576
+ ? {
577
+ start: headerIndex,
578
+ end,
579
+ footer,
580
+ header,
581
+ prompt,
582
+ options,
583
+ freeText: false
584
+ }
585
+ : undefined;
586
+ }
587
+ function codexQuestionInspection(region, lines, explicitSecret) {
588
+ const options = region.options
589
+ ? normalizedOptions(NATIVE_QUESTIONNAIRE_PROFILES.codex, region.prompt, region.options)
590
+ : undefined;
591
+ const question = {
592
+ question_id: semanticId("question", NATIVE_QUESTIONNAIRE_PROFILES.codex, region.prompt, region.header.currentStep),
593
+ prompt: region.prompt,
594
+ response_kind: region.freeText ? "free_text" : "single_select",
595
+ required: !region.freeText,
596
+ ...(options === undefined ? {} : { options })
597
+ };
598
+ const base = {
599
+ agent: "codex",
600
+ profile: NATIVE_QUESTIONNAIRE_PROFILES.codex,
601
+ current_step: region.header.currentStep,
602
+ total_steps: region.header.totalSteps,
603
+ question,
604
+ prompt_evidence: promptEvidence(NATIVE_QUESTIONNAIRE_PROFILES.codex, lines, region.start, region.end, region.footer)
605
+ };
606
+ if (isSecretQuestion(explicitSecret, question)) {
607
+ return { ...base, status: "manual_required", reason: "secret_input", action_plan: { kind: "manual_only" } };
608
+ }
609
+ if (region.freeText) {
610
+ return {
611
+ ...base,
612
+ status: "actionable",
613
+ action_plan: {
614
+ kind: "free_text",
615
+ stages: [
616
+ {
617
+ kind: "answer_text",
618
+ single_line: true,
619
+ max_characters: MAX_TEXT_ANSWER_CHARACTERS
620
+ },
621
+ { kind: "key", key: "C-m" }
622
+ ]
623
+ }
624
+ };
625
+ }
626
+ return {
627
+ ...base,
628
+ status: "actionable",
629
+ action_plan: {
630
+ kind: "single_select",
631
+ choices: (options ?? []).map((option, index) => ({
632
+ option_id: option.option_id,
633
+ outcome: "submit_or_advance",
634
+ stages: [{ kind: "key", key: String(index + 1) }]
635
+ }))
636
+ }
637
+ };
638
+ }
639
+ function parseCodexUnansweredConfirmation(lines) {
640
+ const end = lines.length - 1;
641
+ if (lines[end] !== CODEX_UNANSWERED_FOOTER) {
642
+ return undefined;
643
+ }
644
+ const start = lastIndexMatching(lines, (line) => line === " Submit with unanswered questions?", end);
645
+ if (start < 0) {
646
+ return undefined;
647
+ }
648
+ const countMatch = /^ ([1-3]) unanswered questions?$/u.exec(lines[start + 1] ?? "");
649
+ const body = lines.slice(start + 2, end).filter((line) => line.length > 0);
650
+ if (!countMatch?.[1] ||
651
+ body.length !== 2 ||
652
+ body[0] !== ` › 1. Proceed Submit with ${countMatch[1]} unanswered questions.` ||
653
+ body[1] !== " 2. Go back Return to the first unanswered question.") {
654
+ return undefined;
655
+ }
656
+ const prompt = "Submit with unanswered questions?";
657
+ return {
658
+ status: "actionable",
659
+ agent: "codex",
660
+ profile: NATIVE_QUESTIONNAIRE_PROFILES.codex,
661
+ current_step: 1,
662
+ total_steps: 1,
663
+ question: {
664
+ question_id: semanticId("question", NATIVE_QUESTIONNAIRE_PROFILES.codex, prompt, countMatch[1]),
665
+ prompt,
666
+ response_kind: "confirm",
667
+ required: true
668
+ },
669
+ prompt_evidence: promptEvidence(NATIVE_QUESTIONNAIRE_PROFILES.codex, lines, start, end, CODEX_UNANSWERED_FOOTER),
670
+ action_plan: {
671
+ kind: "confirm",
672
+ confirm_stages: [{ kind: "key", key: "C-m" }],
673
+ cancel_stages: [{ kind: "key", key: "Escape" }]
674
+ }
675
+ };
676
+ }
677
+ //# sourceMappingURL=terminal-questionnaire-adapter.js.map