@scotthuang/agent-knock-knock 0.13.7 → 0.13.8

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 (97) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/callback-outbox-policy.d.ts +1 -1
  3. package/dist/src/callback-outbox-policy.js +7 -3
  4. package/dist/src/callback-outbox-policy.js.map +1 -1
  5. package/dist/src/callback-outbox-service.js +3 -0
  6. package/dist/src/callback-outbox-service.js.map +1 -1
  7. package/dist/src/cli-core.js +1 -1
  8. package/dist/src/codex-astra-composer-proof.d.ts +10 -0
  9. package/dist/src/codex-astra-composer-proof.js +149 -0
  10. package/dist/src/codex-astra-composer-proof.js.map +1 -0
  11. package/dist/src/codex-async-question-adapter.d.ts +156 -0
  12. package/dist/src/codex-async-question-adapter.js +756 -0
  13. package/dist/src/codex-async-question-adapter.js.map +1 -0
  14. package/dist/src/codex-terminal-agent-adapter.js +1 -1
  15. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  16. package/dist/src/host-profile-callback-transport.d.ts +4 -2
  17. package/dist/src/host-profile-callback-transport.js +9 -6
  18. package/dist/src/host-profile-callback-transport.js.map +1 -1
  19. package/dist/src/semantic-tool-command-helpers.js +2 -0
  20. package/dist/src/semantic-tool-command-helpers.js.map +1 -1
  21. package/dist/src/semantic-tool-private-authority.js +4 -1
  22. package/dist/src/semantic-tool-private-authority.js.map +1 -1
  23. package/dist/src/semantic-tool-runtime.js +1 -1
  24. package/dist/src/semantic-tool-runtime.js.map +1 -1
  25. package/dist/src/semantic-tool-schemas.d.ts +5 -0
  26. package/dist/src/semantic-tool-schemas.js +6 -1
  27. package/dist/src/semantic-tool-schemas.js.map +1 -1
  28. package/dist/src/terminal-agent-adapter.d.ts +9 -0
  29. package/dist/src/terminal-agent-adapter.js +10 -0
  30. package/dist/src/terminal-agent-adapter.js.map +1 -1
  31. package/dist/src/terminal-agent-bridge.d.ts +2 -0
  32. package/dist/src/terminal-agent-bridge.js +9 -11
  33. package/dist/src/terminal-agent-bridge.js.map +1 -1
  34. package/dist/src/terminal-command-cli-ports.d.ts +1 -0
  35. package/dist/src/terminal-command-dispatch-preparation.d.ts +1 -0
  36. package/dist/src/terminal-command-dispatch-preparation.js +2 -1
  37. package/dist/src/terminal-command-dispatch-preparation.js.map +1 -1
  38. package/dist/src/terminal-composer-classifier.d.ts +9 -1
  39. package/dist/src/terminal-composer-classifier.js +19 -1
  40. package/dist/src/terminal-composer-classifier.js.map +1 -1
  41. package/dist/src/terminal-composer-readiness-bridge.d.ts +14 -0
  42. package/dist/src/terminal-composer-readiness-bridge.js +21 -0
  43. package/dist/src/terminal-composer-readiness-bridge.js.map +1 -0
  44. package/dist/src/terminal-dispatch-execution.js +11 -0
  45. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  46. package/dist/src/terminal-handoff-cli-adapter.js +4 -1
  47. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
  48. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -1
  49. package/dist/src/terminal-identity-authority-cli-adapter.js +15 -16
  50. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  51. package/dist/src/terminal-interaction-core.d.ts +30 -5
  52. package/dist/src/terminal-interaction-core.js +94 -8
  53. package/dist/src/terminal-interaction-core.js.map +1 -1
  54. package/dist/src/terminal-interaction-protocol.d.ts +9 -1
  55. package/dist/src/terminal-interaction-protocol.js +71 -9
  56. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  57. package/dist/src/terminal-interaction-response-bridge.d.ts +19 -6
  58. package/dist/src/terminal-interaction-response-bridge.js +345 -17
  59. package/dist/src/terminal-interaction-response-bridge.js.map +1 -1
  60. package/dist/src/terminal-list-renderer.js +4 -3
  61. package/dist/src/terminal-list-renderer.js.map +1 -1
  62. package/dist/src/terminal-monitor-application-service.d.ts +4 -0
  63. package/dist/src/terminal-monitor-application-service.js +61 -7
  64. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  65. package/dist/src/terminal-monitor-cli-adapter.d.ts +2 -0
  66. package/dist/src/terminal-monitor-cli-adapter.js +16 -3
  67. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  68. package/dist/src/terminal-monitor-interaction-store.d.ts +9 -0
  69. package/dist/src/terminal-monitor-interaction-store.js +65 -0
  70. package/dist/src/terminal-monitor-interaction-store.js.map +1 -0
  71. package/dist/src/terminal-monitor-state-cli-adapter.js +11 -12
  72. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  73. package/dist/src/terminal-questionnaire-adapter.d.ts +1 -0
  74. package/dist/src/terminal-questionnaire-adapter.js +1 -1
  75. package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
  76. package/dist/src/terminal-submission-acceptance.d.ts +12 -0
  77. package/dist/src/terminal-submission-acceptance.js +152 -0
  78. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  79. package/dist/src/terminal-text-submission-bridge.d.ts +1 -1
  80. package/dist/src/terminal-text-submission-bridge.js +1 -1
  81. package/dist/src/terminal-text-submission-bridge.js.map +1 -1
  82. package/dist/src/terminal-watch-cli-adapter.js +61 -78
  83. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  84. package/dist/src/terminal-watch-codec.js +1 -1
  85. package/dist/src/terminal-watch-codec.js.map +1 -1
  86. package/dist/src/terminal-watch-interaction-runtime.d.ts +10 -0
  87. package/dist/src/terminal-watch-interaction-runtime.js +88 -0
  88. package/dist/src/terminal-watch-interaction-runtime.js.map +1 -0
  89. package/dist/src/terminal-watch-record.d.ts +1 -1
  90. package/dist/src/terminal-watch-service.d.ts +7 -0
  91. package/dist/src/terminal-watch-service.js +88 -35
  92. package/dist/src/terminal-watch-service.js.map +1 -1
  93. package/docs/host-bridge-profiles.md +9 -0
  94. package/docs/quickstart-herdr.md +4 -2
  95. package/docs/quickstart-tmux.md +5 -3
  96. package/package.json +1 -1
  97. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +9 -9
@@ -0,0 +1,756 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * This adapter is deliberately version-bound. Later Codex releases gained
4
+ * different durable answer identities; callers must not silently reuse this
5
+ * TUI contract for an unverified release.
6
+ */
7
+ export const CODEX_ASYNC_QUESTION_PROFILES = Object.freeze({
8
+ "0.154.0": "codex/0.154.0/request-user-input-async-v1",
9
+ "0.155.1": "codex/0.155.1/request-user-input-async-v1"
10
+ });
11
+ export const CODEX_ASYNC_QUESTION_LIMITS = Object.freeze({
12
+ maxRecords: 4_096,
13
+ maxArgumentsBytes: 64 * 1024,
14
+ maxQuestions: 16,
15
+ maxOptions: 32,
16
+ maxTitleCharacters: 4_096,
17
+ maxOptionCharacters: 512,
18
+ maxIdentifierCharacters: 256,
19
+ maxScreenCharacters: 128 * 1024,
20
+ maxScreenLines: 160
21
+ });
22
+ const ANSI_SEQUENCE_PATTERN = /[\u001B\u009B](?:(?:\[[0-?]*[ -/]*[@-~])|(?:\][^\u0007]*(?:\u0007|\u001B\\))|.)/gu;
23
+ const UNSAFE_CONTROL_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/u;
24
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
25
+ export function codexAsyncQuestionProfile(version) {
26
+ return Object.hasOwn(CODEX_ASYNC_QUESTION_PROFILES, version)
27
+ ? CODEX_ASYNC_QUESTION_PROFILES[version]
28
+ : undefined;
29
+ }
30
+ /**
31
+ * Parse already-bounded, already-read rollout records. `function_call_output`
32
+ * with `{accepted:true}` is deliberately ignored: request_user_input_async
33
+ * returns immediately, so that output does not mean the user answered.
34
+ */
35
+ export function parseCodexAsyncQuestionRecordCandidates(input) {
36
+ if (!codexAsyncQuestionProfile(input.version)) {
37
+ return { status: "unsupported_version" };
38
+ }
39
+ if (input.records.length > CODEX_ASYNC_QUESTION_LIMITS.maxRecords) {
40
+ return { status: "ambiguous", reason: "record_limit_exceeded" };
41
+ }
42
+ const evidence = [];
43
+ const itemIds = new Set();
44
+ for (const value of input.records) {
45
+ if (!isRecord(value) || value.type !== "response_item")
46
+ continue;
47
+ const payload = isRecord(value.payload) ? value.payload : undefined;
48
+ if (!payload || payload.type !== "function_call" ||
49
+ payload.name !== "request_user_input_async") {
50
+ continue;
51
+ }
52
+ const itemId = boundedIdentifier(payload.call_id);
53
+ const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
54
+ ? payload.internal_chat_message_metadata_passthrough
55
+ : undefined;
56
+ const turnId = boundedIdentifier(metadata?.turn_id) ??
57
+ boundedIdentifier(input.fallbackTurnId);
58
+ if (!itemId || !turnId) {
59
+ return { status: "ambiguous", reason: "invalid_identity" };
60
+ }
61
+ if (itemIds.has(itemId)) {
62
+ return { status: "ambiguous", reason: "duplicate_item_id" };
63
+ }
64
+ const questions = parseArguments(payload.arguments);
65
+ if (!questions) {
66
+ return { status: "ambiguous", reason: "invalid_arguments" };
67
+ }
68
+ itemIds.add(itemId);
69
+ evidence.push({ itemId, turnId, questions });
70
+ }
71
+ return evidence.length === 0
72
+ ? { status: "none" }
73
+ : { status: "candidates", evidence };
74
+ }
75
+ export function inspectCodexAsyncQuestion(input) {
76
+ const profile = codexAsyncQuestionProfile(input.version);
77
+ if (!profile) {
78
+ return { state: "ambiguous", reason: "unsupported_version" };
79
+ }
80
+ const captured = captureLines(input.screen);
81
+ if (captured.status !== "ok") {
82
+ return { state: "ambiguous", reason: captured.reason, profile };
83
+ }
84
+ const normalizedEvidence = normalizeEvidence(input.evidence ?? [], profile);
85
+ if (!normalizedEvidence) {
86
+ return { state: "ambiguous", reason: "invalid_durable_evidence", profile };
87
+ }
88
+ const collapsed = parseCollapsed(captured.lines);
89
+ const expandedMarker = hasExpandedMarker(captured.lines);
90
+ if (collapsed.status === "exact" && !expandedMarker) {
91
+ return inspectCollapsedQuestion(collapsed, normalizedEvidence, profile);
92
+ }
93
+ if (collapsed.status === "partial" && !expandedMarker) {
94
+ return { state: "ambiguous", reason: "partial_or_unknown_surface", profile };
95
+ }
96
+ const expanded = parseExpanded(captured.lines, normalizedEvidence, profile);
97
+ if (expanded.status === "none") {
98
+ return collapsed.status === "none"
99
+ ? { state: "absent", reason: "no_async_question_surface" }
100
+ : { state: "ambiguous", reason: "partial_or_unknown_surface", profile };
101
+ }
102
+ if (expanded.status === "ambiguous") {
103
+ return { state: "ambiguous", reason: expanded.reason, profile };
104
+ }
105
+ return inspectExpandedQuestion(expanded, profile);
106
+ }
107
+ /**
108
+ * Owner-private pre-submit proof after one text injection. The public observer
109
+ * continues to reject drafts. Text must be wholly visible on one row: terminal
110
+ * wrapping cannot prove the original buffer's whitespace or hidden contents.
111
+ */
112
+ export function verifyCodexAsyncQuestionInjectedText(input) {
113
+ const text = input.expectedText;
114
+ if (!text || text !== text.trim() || /[\r\n\t]/u.test(text) ||
115
+ UNSAFE_CONTROL_PATTERN.test(text) || [...text].length > 4_096) {
116
+ return false;
117
+ }
118
+ const captured = captureLines(input.screen);
119
+ if (captured.status !== "ok")
120
+ return false;
121
+ let matches = 0;
122
+ for (const [index, line] of captured.lines.entries()) {
123
+ for (const replacement of injectedTextPlaceholderRows(line, text)) {
124
+ const restored = [...captured.lines];
125
+ restored[index] = replacement;
126
+ const inspection = inspectCodexAsyncQuestion({
127
+ version: input.version,
128
+ screen: restored.join("\n"),
129
+ evidence: input.evidence
130
+ });
131
+ if (inspection.state === "expanded" &&
132
+ inspection.match.question.response_kind === "free_text" &&
133
+ inspection.match.question.question_id === input.expectedQuestionId) {
134
+ matches += 1;
135
+ }
136
+ }
137
+ }
138
+ return matches === 1;
139
+ }
140
+ function injectedTextPlaceholderRows(line, text) {
141
+ if (line.trimEnd() === ` ${text}`) {
142
+ return text === "Type your answer" ? [] : [" Type your answer"];
143
+ }
144
+ const optionPrefix = /^ {2}› [1-9]\d*\. /u.exec(line)?.[0];
145
+ if (!optionPrefix || line.slice(optionPrefix.length).trimEnd() !== text) {
146
+ return [];
147
+ }
148
+ return ["Other", "Other (write an answer)"]
149
+ .filter((placeholder) => placeholder !== text)
150
+ .map((placeholder) => `${optionPrefix}${placeholder}`);
151
+ }
152
+ /** Withdraw-only observation; this never grants main-Composer input authority. */
153
+ export function codexAsyncQuestionMainComposerVisible(input) {
154
+ const captured = captureLines(input.screen);
155
+ if (captured.status !== "ok" ||
156
+ inspectCodexAsyncQuestion(input).state !== "absent") {
157
+ return false;
158
+ }
159
+ const footer = captured.lines.at(-1)?.trim() ?? "";
160
+ return /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u
161
+ .test(footer) && captured.lines.slice(0, -1).some((line) => /^[›»](?:\s|$)/u.test(line));
162
+ }
163
+ function inspectCollapsedQuestion(collapsed, evidence, profile) {
164
+ const knownRemaining = evidence.length > 0 &&
165
+ evidence.every((entry) => entry.remainingCount !== undefined)
166
+ ? evidence.reduce((total, entry) => total + entry.remainingCount, 0)
167
+ : undefined;
168
+ if (knownRemaining !== undefined && knownRemaining < collapsed.pendingCount) {
169
+ return { state: "ambiguous", reason: "pending_count_mismatch", profile };
170
+ }
171
+ const match = collapsedMatch(evidence, collapsed.pendingCount, profile);
172
+ const exactDigest = sha256("codex-async-question-region", collapsed.exactLines.join("\n"));
173
+ return {
174
+ state: "collapsed",
175
+ profile,
176
+ pending_count: collapsed.pendingCount,
177
+ ...(collapsed.countdownSeconds === undefined
178
+ ? {}
179
+ : { countdown_seconds: collapsed.countdownSeconds }),
180
+ ...(match ? { match } : {}),
181
+ prompt_evidence: {
182
+ profile,
183
+ exact_region_sha256: exactDigest,
184
+ semantic_sha256: sha256("codex-async-question-semantic", {
185
+ kind: "collapsed",
186
+ pending_count: collapsed.pendingCount,
187
+ match: match?.question.question_id
188
+ })
189
+ },
190
+ owner_private_action_plan: {
191
+ kind: "open_async_question_editor",
192
+ binding: collapsed.binding,
193
+ expected_pending_count: collapsed.pendingCount,
194
+ expected_region_sha256: exactDigest
195
+ }
196
+ };
197
+ }
198
+ function inspectExpandedQuestion(expanded, profile) {
199
+ const exactDigest = sha256("codex-async-question-region", expanded.exactLines.join("\n"));
200
+ const question = expanded.match.question;
201
+ const options = question.options ?? [];
202
+ const selectedOption = expanded.match.selected_option_id;
203
+ const choicePlans = options.map((option, index) => ({
204
+ option_id: option.option_id,
205
+ native_ordinal: index + 1,
206
+ intent: option.kind === "free_text_entry"
207
+ ? "open_free_text"
208
+ : "submit_choice"
209
+ }));
210
+ const freeTextEntry = options.findIndex((option) => option.kind === "free_text_entry");
211
+ return {
212
+ state: "expanded",
213
+ profile,
214
+ current_step: expanded.currentStep,
215
+ total_steps: expanded.totalSteps,
216
+ match: expanded.match,
217
+ prompt_evidence: {
218
+ profile,
219
+ exact_region_sha256: exactDigest,
220
+ semantic_sha256: sha256("codex-async-question-semantic", {
221
+ source_id: expanded.match.source_id,
222
+ question_id: question.question_id,
223
+ selected_option_id: selectedOption
224
+ })
225
+ },
226
+ owner_private_action_plan: {
227
+ kind: "answer_async_question",
228
+ question_id: question.question_id,
229
+ choices: choicePlans,
230
+ free_text: freeTextEntry >= 0
231
+ ? {
232
+ intent: "select_other_then_enter_text",
233
+ native_ordinal: freeTextEntry + 1,
234
+ max_characters: 4_096
235
+ }
236
+ : {
237
+ intent: "enter_free_text",
238
+ max_characters: 4_096
239
+ },
240
+ delivery: {
241
+ steer_current_turn: "submit",
242
+ queue_next_turn: "queue"
243
+ },
244
+ expected_region_sha256: exactDigest
245
+ }
246
+ };
247
+ }
248
+ function normalizeEvidence(values, profile) {
249
+ const sourceIds = new Set();
250
+ const normalized = [];
251
+ for (const value of values) {
252
+ const entry = normalizeEvidenceEntry(value, profile);
253
+ if (!entry || sourceIds.has(entry.sourceId))
254
+ return undefined;
255
+ sourceIds.add(entry.sourceId);
256
+ normalized.push(entry);
257
+ }
258
+ return normalized;
259
+ }
260
+ function normalizeEvidenceEntry(value, profile) {
261
+ const itemId = boundedIdentifier(value.itemId);
262
+ const turnId = boundedIdentifier(value.turnId);
263
+ if (!itemId || !turnId || !validQuestionArray(value.questions)) {
264
+ return undefined;
265
+ }
266
+ if (!validOptionalProgress(value.currentIndex, 0, value.questions.length - 1) ||
267
+ !validOptionalProgress(value.remainingCount, 1, value.questions.length)) {
268
+ return undefined;
269
+ }
270
+ const sourceId = `cai_${sha256("codex-async-question-source", {
271
+ profile,
272
+ item_id: itemId,
273
+ turn_id: turnId
274
+ }).slice(0, 40)}`;
275
+ const questions = [];
276
+ for (const [index, question] of value.questions.entries()) {
277
+ const normalized = normalizeQuestion(question, sourceId, index);
278
+ if (!normalized)
279
+ return undefined;
280
+ questions.push(normalized);
281
+ }
282
+ return {
283
+ itemId,
284
+ turnId,
285
+ sourceId,
286
+ ...(value.currentIndex === undefined
287
+ ? {}
288
+ : { currentIndex: value.currentIndex }),
289
+ ...(value.remainingCount === undefined
290
+ ? {}
291
+ : { remainingCount: value.remainingCount }),
292
+ questions
293
+ };
294
+ }
295
+ function validOptionalProgress(value, minimum, maximum) {
296
+ return value === undefined ||
297
+ (Number.isSafeInteger(value) && value >= minimum && value <= maximum);
298
+ }
299
+ function normalizeQuestion(question, sourceId, sourceIndex) {
300
+ const title = safeSemanticText(question.title, CODEX_ASYNC_QUESTION_LIMITS.maxTitleCharacters);
301
+ const modelOptions = parseOptionLabels(question.options ?? []);
302
+ if (!title || !modelOptions)
303
+ return undefined;
304
+ const normalizedModelOptions = modelOptions.map(normalizeText);
305
+ if (new Set(normalizedModelOptions).size !== modelOptions.length) {
306
+ return undefined;
307
+ }
308
+ const questionId = `question_${sha256("codex-async-question", {
309
+ source_id: sourceId,
310
+ source_index: sourceIndex,
311
+ title: normalizeText(title),
312
+ options: normalizedModelOptions
313
+ }).slice(0, 40)}`;
314
+ const semanticOptions = buildSemanticOptions(questionId, modelOptions);
315
+ return {
316
+ sourceIndex,
317
+ title,
318
+ normalizedTitle: normalizeText(title),
319
+ modelOptions,
320
+ normalizedModelOptions,
321
+ semantic: {
322
+ question_id: questionId,
323
+ prompt: title,
324
+ response_kind: modelOptions.length > 0 ? "single_select" : "free_text",
325
+ required: true,
326
+ ...(semanticOptions.length > 0 ? { options: semanticOptions } : {})
327
+ }
328
+ };
329
+ }
330
+ function nativeOtherLabel(normalizedModelOptions) {
331
+ return normalizedModelOptions.some((label) => label.toLocaleLowerCase("en-US") === "other") ? "Other (write an answer)" : "Other";
332
+ }
333
+ function buildSemanticOptions(questionId, modelOptions) {
334
+ const result = modelOptions.map((label, optionIndex) => ({
335
+ option_id: `option_${sha256("codex-async-question-option", {
336
+ question_id: questionId,
337
+ option_index: optionIndex,
338
+ label: normalizeText(label)
339
+ }).slice(0, 40)}`,
340
+ label,
341
+ kind: "suggested"
342
+ }));
343
+ if (modelOptions.length > 0) {
344
+ result.push({
345
+ option_id: `option_${sha256("codex-async-question-option", {
346
+ question_id: questionId,
347
+ option_index: modelOptions.length,
348
+ kind: "free_text_entry"
349
+ }).slice(0, 40)}`,
350
+ label: nativeOtherLabel(modelOptions.map(normalizeText)),
351
+ kind: "free_text_entry"
352
+ });
353
+ }
354
+ return result;
355
+ }
356
+ function collapsedMatch(evidence, pendingCount, profile) {
357
+ if (evidence.length === 0 ||
358
+ !evidence.every((entry) => entry.currentIndex !== undefined && entry.remainingCount !== undefined) ||
359
+ evidence.reduce((sum, entry) => sum + entry.remainingCount, 0) <
360
+ pendingCount)
361
+ return undefined;
362
+ const ordered = evidence.flatMap((entry) => entry.questions
363
+ .slice(entry.currentIndex)
364
+ .map((question) => ({ entry, question })));
365
+ const consumedCount = ordered.length - pendingCount;
366
+ const current = ordered[consumedCount];
367
+ if (!current || consumedCount < 0)
368
+ return undefined;
369
+ // The profile participates through sourceId/questionId. Keeping the argument
370
+ // explicit makes accidental cross-version reuse visible to reviewers.
371
+ void profile;
372
+ return {
373
+ source_id: current.entry.sourceId,
374
+ source_question_index: current.question.sourceIndex,
375
+ question: current.question.semantic
376
+ };
377
+ }
378
+ function captureLines(screen) {
379
+ if (typeof screen !== "string" ||
380
+ screen.length > CODEX_ASYNC_QUESTION_LIMITS.maxScreenCharacters) {
381
+ return { status: "error", reason: "capture_limit_exceeded" };
382
+ }
383
+ const plain = screen.replace(ANSI_SEQUENCE_PATTERN, "")
384
+ .replace(/\r\n?/gu, "\n")
385
+ .replace(/\u00a0/gu, " ");
386
+ if (UNSAFE_CONTROL_PATTERN.test(plain)) {
387
+ return { status: "error", reason: "unsafe_control" };
388
+ }
389
+ const lines = plain.split("\n")
390
+ .slice(-CODEX_ASYNC_QUESTION_LIMITS.maxScreenLines);
391
+ while (lines.length > 0 && lines.at(-1)?.trim().length === 0)
392
+ lines.pop();
393
+ return { status: "ok", lines };
394
+ }
395
+ function parseCollapsed(lines) {
396
+ let header = -1;
397
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
398
+ if (lines[index] === "• Queued follow-up inputs") {
399
+ header = index;
400
+ break;
401
+ }
402
+ }
403
+ if (header < 0) {
404
+ return lines.some((line) => /^\s*\?\s+\d+\s+questions?/iu.test(line) ||
405
+ /\bto answer\s*$/iu.test(line))
406
+ ? { status: "partial" }
407
+ : { status: "none" };
408
+ }
409
+ let summary = -1;
410
+ let summaryMatch = null;
411
+ for (let index = header + 1; index < lines.length; index += 1) {
412
+ const match = /^ \? ([1-9]\d{0,2}) (question|questions)(?: · ([1-9]\d{0,2})s)?$/u
413
+ .exec(lines[index]);
414
+ if (match) {
415
+ summary = index;
416
+ summaryMatch = match;
417
+ break;
418
+ }
419
+ }
420
+ if (summary < 0 || !summaryMatch) {
421
+ const positive = lines.slice(header + 1).some((line) => /^\s*\?/u.test(line) || /\bto answer\s*$/iu.test(line));
422
+ return positive ? { status: "partial" } : { status: "none" };
423
+ }
424
+ const pendingCount = Number(summaryMatch[1]);
425
+ if ((pendingCount === 1) !== (summaryMatch[2] === "question"))
426
+ return { status: "partial" };
427
+ let hint = summary + 1;
428
+ while (hint < lines.length && lines[hint].trim().length === 0)
429
+ hint += 1;
430
+ const hintLine = lines[hint] ?? "";
431
+ const binding = hintLine === " shift + ← to answer"
432
+ ? "shift_left"
433
+ : hintLine === " ⌥ + ↑ to answer"
434
+ ? "alt_up"
435
+ : undefined;
436
+ if (!binding)
437
+ return { status: "partial" };
438
+ return {
439
+ status: "exact",
440
+ pendingCount,
441
+ ...(summaryMatch[3] === undefined
442
+ ? {}
443
+ : { countdownSeconds: Number(summaryMatch[3]) }),
444
+ binding,
445
+ exactLines: lines.slice(header, hint + 1)
446
+ };
447
+ }
448
+ function hasExpandedMarker(lines) {
449
+ return lines.some((line) => /^ {2}enter submit\b/u.test(line) ||
450
+ /^ {2}\d+ of(?: \d+)?/u.test(line) ||
451
+ /^ {2}(?:ente|ctrl)…\s*$/u.test(line) ||
452
+ /^ {2}Expand terminal to read the entire option\s*$/u.test(line));
453
+ }
454
+ function parseExpanded(lines, evidence, profile) {
455
+ let footerStart = -1;
456
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
457
+ if (/^ {2}\S.*\bsubmit\b/u.test(lines[index])) {
458
+ footerStart = index;
459
+ break;
460
+ }
461
+ }
462
+ if (footerStart < 0) {
463
+ if (lines.some((line) => /^ {2}Expand terminal to read the entire option\s*$/u.test(line)))
464
+ return { status: "ambiguous", reason: "clipped_question" };
465
+ return hasExpandedMarker(lines)
466
+ ? { status: "ambiguous", reason: "partial_or_unknown_surface" }
467
+ : { status: "none" };
468
+ }
469
+ const footerLines = lines.slice(footerStart).filter((line) => line.trim());
470
+ const footer = footerLines.map((line) => normalizeText(line)).join(" ");
471
+ if (/\boption \d+\/\d+\b/iu.test(footer) ||
472
+ lines.slice(Math.max(0, footerStart - 5), footerStart).some((line) => normalizeText(line) === "Expand terminal to read the entire option")) {
473
+ return { status: "ambiguous", reason: "clipped_question" };
474
+ }
475
+ if (!/^enter submit ctrl \+ \] skip ⌥ \+ ↓ (?:main prompt|prev question)(?: (?:⌥ \+ ↑|shift \+ ←) (?:next question|queued messages))?$/u
476
+ .test(footer)) {
477
+ return { status: "ambiguous", reason: "unsupported_keymap" };
478
+ }
479
+ if (evidence.length === 0) {
480
+ return { status: "ambiguous", reason: "question_match_missing" };
481
+ }
482
+ const matches = [];
483
+ const content = lines.slice(0, footerStart);
484
+ const maximumPending = evidence.reduce((total, source) => total + source.questions.length, 0);
485
+ for (const source of evidence) {
486
+ for (const question of source.questions) {
487
+ for (let start = 0; start < content.length; start += 1) {
488
+ if (!lineCanStartText(content[start]))
489
+ continue;
490
+ const consumed = consumeWrappedText(content, start, question.normalizedTitle);
491
+ if (consumed < 0)
492
+ continue;
493
+ const parsed = parseQuestionBody(content, consumed, question, source, start, footerStart, maximumPending);
494
+ if (parsed)
495
+ matches.push(parsed);
496
+ }
497
+ }
498
+ }
499
+ const unique = uniqueExpandedMatches(matches);
500
+ if (unique.length === 0) {
501
+ const draftLikely = evidence.some((source) => source.questions.some((question) => content.some((line) => normalizeText(line) === question.normalizedTitle)));
502
+ return {
503
+ status: "ambiguous",
504
+ reason: draftLikely ? "existing_answer_draft" : "question_match_missing"
505
+ };
506
+ }
507
+ if (unique.length !== 1) {
508
+ return { status: "ambiguous", reason: "question_match_ambiguous" };
509
+ }
510
+ const exact = unique[0];
511
+ void profile;
512
+ return {
513
+ status: "exact",
514
+ currentStep: exact.currentStep,
515
+ totalSteps: exact.totalSteps,
516
+ match: exact.match,
517
+ exactLines: lines.slice(exact.start, lines.length)
518
+ };
519
+ }
520
+ function parseQuestionBody(lines, bodyStart, question, source, questionStart, footerStart, maximumPending) {
521
+ let cursor = bodyStart;
522
+ while (cursor < lines.length && lines[cursor].trim().length === 0)
523
+ cursor += 1;
524
+ const match = parseAnswerRegion(lines.slice(cursor), question, source);
525
+ const progress = progressForQuestion(lines, questionStart, maximumPending);
526
+ if (!match || !progress)
527
+ return undefined;
528
+ return {
529
+ start: progress.start,
530
+ end: footerStart,
531
+ currentStep: progress.current,
532
+ totalSteps: progress.total,
533
+ match
534
+ };
535
+ }
536
+ function parseAnswerRegion(lines, question, source) {
537
+ const match = {
538
+ source_id: source.sourceId,
539
+ source_question_index: question.sourceIndex,
540
+ question: question.semantic
541
+ };
542
+ if (question.modelOptions.length === 0) {
543
+ return normalizeText(lines[0] ?? "") === "Type your answer" &&
544
+ lines.slice(1).every((line) => line.trim().length === 0)
545
+ ? match
546
+ : undefined;
547
+ }
548
+ const selectedOption = parseSelectedOption(lines, question);
549
+ if (!selectedOption)
550
+ return undefined;
551
+ if (selectedOption.kind !== "free_text_entry") {
552
+ return { ...match, selected_option_id: selectedOption.option_id };
553
+ }
554
+ return {
555
+ ...match,
556
+ question: {
557
+ question_id: `question_${sha256("codex-async-question-free-text", {
558
+ source_id: source.sourceId,
559
+ source_index: question.sourceIndex,
560
+ parent_question_id: question.semantic.question_id
561
+ }).slice(0, 40)}`,
562
+ prompt: question.semantic.prompt,
563
+ response_kind: "free_text",
564
+ required: true
565
+ }
566
+ };
567
+ }
568
+ function parseSelectedOption(lines, question) {
569
+ const rows = parseOptionRows(lines);
570
+ const expectedLabels = [
571
+ ...question.normalizedModelOptions,
572
+ normalizeText(nativeOtherLabel(question.normalizedModelOptions))
573
+ ];
574
+ if (!rows || rows.length !== expectedLabels.length ||
575
+ rows.some((row, index) => row.number !== index + 1 || row.label !== expectedLabels[index]) || rows.filter((row) => row.selected).length !== 1) {
576
+ return undefined;
577
+ }
578
+ return question.semantic.options?.[rows.findIndex((row) => row.selected)];
579
+ }
580
+ function parseOptionRows(lines) {
581
+ const rows = [];
582
+ let cursor = 0;
583
+ while (cursor < lines.length) {
584
+ if (lines[cursor].trim().length === 0) {
585
+ cursor += 1;
586
+ continue;
587
+ }
588
+ const row = /^ {2}(›| ) (\d+)\. (\S.*)$/u.exec(lines[cursor]);
589
+ if (!row)
590
+ return undefined;
591
+ cursor += 1;
592
+ let label = row[3];
593
+ while (cursor < lines.length && /^ {7,}\S/u.test(lines[cursor])) {
594
+ label += ` ${lines[cursor].trim()}`;
595
+ cursor += 1;
596
+ }
597
+ rows.push({
598
+ number: Number(row[2]),
599
+ selected: row[1] === "›",
600
+ label: normalizeText(label)
601
+ });
602
+ }
603
+ return rows;
604
+ }
605
+ function progressForQuestion(lines, questionStart, maximumPending) {
606
+ let previous = questionStart - 1;
607
+ while (previous >= 0 && lines[previous].trim().length === 0)
608
+ previous -= 1;
609
+ const match = previous >= 0
610
+ ? /^ {2}([1-9]\d*) of ([1-9]\d*)\s*$/u.exec(lines[previous])
611
+ : null;
612
+ if (!match) {
613
+ if (previous >= 0 && /^ {2}\d+ of\b/u.test(lines[previous])) {
614
+ return undefined;
615
+ }
616
+ return { start: questionStart, current: 1, total: 1 };
617
+ }
618
+ const current = Number(match[1]);
619
+ const total = Number(match[2]);
620
+ if (current > total || total > maximumPending) {
621
+ return undefined;
622
+ }
623
+ // Codex removes answered questions from its local pending queue. This counter
624
+ // describes that mutable queue, not the index/count in the durable item.
625
+ // Identity comes from the uniquely matched title and complete option region.
626
+ return { start: previous, current, total };
627
+ }
628
+ function uniqueExpandedMatches(values) {
629
+ const unique = new Map();
630
+ for (const value of values) {
631
+ const key = [
632
+ value.match.source_id,
633
+ value.match.question.question_id,
634
+ value.start,
635
+ value.end
636
+ ].join(":");
637
+ unique.set(key, value);
638
+ }
639
+ return [...unique.values()];
640
+ }
641
+ function consumeWrappedText(lines, start, expected) {
642
+ let combined = "";
643
+ for (let index = start; index < lines.length; index += 1) {
644
+ const line = normalizeText(lines[index]);
645
+ if (!line)
646
+ return -1;
647
+ combined = combined ? `${combined} ${line}` : line;
648
+ if (combined === expected)
649
+ return index + 1;
650
+ if (!expected.startsWith(`${combined} `))
651
+ return -1;
652
+ }
653
+ return -1;
654
+ }
655
+ function lineCanStartText(line) {
656
+ return /^ {2}\S/u.test(line) &&
657
+ !/^ {2}(?:\d+ of \d+|[› ] \d+\.|enter submit|\?|↳)/u.test(line);
658
+ }
659
+ function parseArguments(value) {
660
+ if (typeof value !== "string" || value.length === 0 ||
661
+ Buffer.byteLength(value, "utf8") >
662
+ CODEX_ASYNC_QUESTION_LIMITS.maxArgumentsBytes)
663
+ return undefined;
664
+ let parsed;
665
+ try {
666
+ parsed = JSON.parse(value);
667
+ }
668
+ catch {
669
+ return undefined;
670
+ }
671
+ if (!isRecord(parsed) || Object.keys(parsed).some((key) => key !== "questions")) {
672
+ return undefined;
673
+ }
674
+ const questions = parsed.questions;
675
+ if (!validQuestionArray(questions))
676
+ return undefined;
677
+ const result = [];
678
+ for (const value of questions) {
679
+ const question = parseArgumentQuestion(value);
680
+ if (!question)
681
+ return undefined;
682
+ result.push(question);
683
+ }
684
+ return result;
685
+ }
686
+ function validQuestionArray(value) {
687
+ return Array.isArray(value) && value.length > 0 &&
688
+ value.length <= CODEX_ASYNC_QUESTION_LIMITS.maxQuestions;
689
+ }
690
+ function parseArgumentQuestion(value) {
691
+ if (!isRecord(value) ||
692
+ Object.keys(value).some((key) => key !== "title" && key !== "options")) {
693
+ return undefined;
694
+ }
695
+ const title = safeSemanticText(value.title, CODEX_ASYNC_QUESTION_LIMITS.maxTitleCharacters);
696
+ if (!title)
697
+ return undefined;
698
+ if (value.options === undefined || value.options === null)
699
+ return { title };
700
+ const options = parseOptionLabels(value.options);
701
+ return options && options.length > 0 ? { title, options } : undefined;
702
+ }
703
+ function parseOptionLabels(value) {
704
+ if (!Array.isArray(value) ||
705
+ value.length > CODEX_ASYNC_QUESTION_LIMITS.maxOptions) {
706
+ return undefined;
707
+ }
708
+ const result = [];
709
+ for (const option of value) {
710
+ const label = safeSemanticText(option, CODEX_ASYNC_QUESTION_LIMITS.maxOptionCharacters);
711
+ if (!label)
712
+ return undefined;
713
+ result.push(label);
714
+ }
715
+ return result;
716
+ }
717
+ function boundedIdentifier(value) {
718
+ if (typeof value !== "string")
719
+ return undefined;
720
+ const trimmed = value.trim();
721
+ return trimmed.length > 0 &&
722
+ trimmed.length <= CODEX_ASYNC_QUESTION_LIMITS.maxIdentifierCharacters &&
723
+ IDENTIFIER_PATTERN.test(trimmed)
724
+ ? trimmed
725
+ : undefined;
726
+ }
727
+ function safeSemanticText(value, maxCharacters) {
728
+ if (typeof value !== "string" || UNSAFE_CONTROL_PATTERN.test(value)) {
729
+ return undefined;
730
+ }
731
+ const trimmed = value.trim().normalize("NFC");
732
+ return trimmed.length > 0 && [...trimmed].length <= maxCharacters
733
+ ? trimmed
734
+ : undefined;
735
+ }
736
+ function normalizeText(value) {
737
+ return value.normalize("NFC").trim().replace(/\s+/gu, " ");
738
+ }
739
+ function sha256(domain, value) {
740
+ return createHash("sha256")
741
+ .update(`${domain}\0${typeof value === "string" ? value : stableJson(value)}`)
742
+ .digest("hex");
743
+ }
744
+ function stableJson(value) {
745
+ if (Array.isArray(value)) {
746
+ return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
747
+ }
748
+ if (isRecord(value)) {
749
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
750
+ }
751
+ return JSON.stringify(value);
752
+ }
753
+ function isRecord(value) {
754
+ return typeof value === "object" && value !== null && !Array.isArray(value);
755
+ }
756
+ //# sourceMappingURL=codex-async-question-adapter.js.map