@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.
- package/CHANGELOG.md +17 -0
- package/dist/src/claude-injected-paste-proof.d.ts +59 -0
- package/dist/src/claude-injected-paste-proof.js +153 -0
- package/dist/src/claude-injected-paste-proof.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.js +20 -1
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +50 -35
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
- package/dist/src/codex-terminal-agent-adapter.js +40 -4
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/host-adapter.d.ts +1 -1
- package/dist/src/host-adapter.js +1 -1
- package/dist/src/host-bridge-tools.js +2 -2
- package/dist/src/openclaw-plugin-command-adapter.js +155 -24
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
- package/dist/src/openclaw-plugin-helpers.js +28 -7
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
- package/dist/src/openclaw-plugin-schemas.js +108 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
- package/dist/src/openclaw-private-authority-offers.js +12 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +34 -0
- package/dist/src/terminal-agent-adapter.js +54 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +76 -1
- package/dist/src/terminal-agent-bridge.js +873 -153
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
- package/dist/src/terminal-command-cli-adapter.js +46 -8
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +15 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-interaction-authority.d.ts +33 -0
- package/dist/src/terminal-interaction-authority.js +3 -0
- package/dist/src/terminal-interaction-authority.js.map +1 -0
- package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
- package/dist/src/terminal-interaction-cli-adapter.js +354 -0
- package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
- package/dist/src/terminal-interaction-protocol.d.ts +111 -0
- package/dist/src/terminal-interaction-protocol.js +432 -0
- package/dist/src/terminal-interaction-protocol.js.map +1 -0
- package/dist/src/terminal-list-renderer.js +31 -2
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js +12 -3
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
- package/dist/src/terminal-mutation-cli-runtime.js +41 -0
- package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
- package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
- package/dist/src/terminal-questionnaire-adapter.js +677 -0
- package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
- package/docs/host-bridge-profiles.md +2 -2
- package/docs/quickstart-herdr.md +1 -1
- package/docs/quickstart-tmux.md +3 -1
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- 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
|