codex-chatgpt-control 0.1.0-alpha.1
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 +5 -0
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/contracts/v1/fixtures/backend-capabilities.json +54 -0
- package/contracts/v1/fixtures/backend-error-event-missing-stream-input.json +10 -0
- package/contracts/v1/fixtures/backend-error-missing-run-input.json +10 -0
- package/contracts/v1/fixtures/backend-runner-plan-request.json +12 -0
- package/contracts/v1/fixtures/backend-version.json +10 -0
- package/contracts/v1/fixtures/command-descriptors.json +803 -0
- package/contracts/v1/fixtures/describe-runner-run.json +31 -0
- package/contracts/v1/fixtures/doctor-bridge-upload.json +34 -0
- package/contracts/v1/fixtures/help-root.json +6 -0
- package/contracts/v1/fixtures/named-plan-two-turn.json +39 -0
- package/contracts/v1/fixtures/output-json-parse-success.json +115 -0
- package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +43 -0
- package/contracts/v1/fixtures/report-redaction-default.json +16 -0
- package/contracts/v1/fixtures/reports-create-redacted.json +16 -0
- package/contracts/v1/fixtures/reports-summarize-redacted.json +26 -0
- package/contracts/v1/fixtures/responses-basic-success.json +27 -0
- package/contracts/v1/fixtures/responses-hidden-instructions-unsupported.json +22 -0
- package/contracts/v1/fixtures/responses-unknown-field-unsupported.json +22 -0
- package/contracts/v1/fixtures/responses-unsupported-previous-response-id.json +22 -0
- package/contracts/v1/fixtures/responses-unsupported-temperature.json +22 -0
- package/contracts/v1/fixtures/run-basic-success.json +36 -0
- package/contracts/v1/fixtures/run-browser-bridge-blocker.json +180 -0
- package/contracts/v1/fixtures/run-file-attach-success.json +63 -0
- package/contracts/v1/fixtures/run-report-redacted.json +52 -0
- package/contracts/v1/fixtures/run-selector-drift-blocker.json +60 -0
- package/contracts/v1/fixtures/run-timeout-partial.json +81 -0
- package/contracts/v1/fixtures/run-two-turn-success.json +83 -0
- package/contracts/v1/fixtures/run-upload-permission-blocker.json +56 -0
- package/contracts/v1/fixtures/runner-budget-blocker.json +106 -0
- package/contracts/v1/fixtures/runner-full-agent-config.json +50 -0
- package/contracts/v1/fixtures/runner-input-items-and-files-plan.json +53 -0
- package/contracts/v1/fixtures/runner-metadata-only-plan.json +31 -0
- package/contracts/v1/fixtures/runner-visible-prefix-plan.json +28 -0
- package/contracts/v1/fixtures/runner-visible-setup-plan.json +37 -0
- package/contracts/v1/fixtures/stream-basic.ndjson +3 -0
- package/contracts/v1/fixtures/stream-blocked.ndjson +2 -0
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +3 -0
- package/contracts/v1/fixtures/workflow-ask-success.json +67 -0
- package/contracts/v1/manifest.json +210 -0
- package/contracts/v1/parity-suite.json +635 -0
- package/contracts/v1/schemas/agent.schema.json +52 -0
- package/contracts/v1/schemas/backend-event.schema.json +74 -0
- package/contracts/v1/schemas/backend-request.schema.json +59 -0
- package/contracts/v1/schemas/backend-response.schema.json +42 -0
- package/contracts/v1/schemas/capabilities.schema.json +33 -0
- package/contracts/v1/schemas/command-descriptor.schema.json +31 -0
- package/contracts/v1/schemas/command-result.schema.json +99 -0
- package/contracts/v1/schemas/manifest.schema.json +69 -0
- package/contracts/v1/schemas/response.schema.json +51 -0
- package/contracts/v1/schemas/run-result.schema.json +138 -0
- package/contracts/v1/schemas/sequence-plan.schema.json +44 -0
- package/contracts/v1/schemas/stream-event.schema.json +25 -0
- package/dist/codex-chatgpt-control-backend.mjs +5284 -0
- package/dist/codex-chatgpt-control.bundle.mjs +6013 -0
- package/dist/src/backend/client.d.ts +129 -0
- package/dist/src/backend/client.js +415 -0
- package/dist/src/backend/protocol.d.ts +74 -0
- package/dist/src/backend/protocol.js +128 -0
- package/dist/src/backend/session.d.ts +11 -0
- package/dist/src/backend/session.js +223 -0
- package/dist/src/backend/stdio-server.d.ts +9 -0
- package/dist/src/backend/stdio-server.js +88 -0
- package/dist/src/browser/attach.d.ts +8 -0
- package/dist/src/browser/attach.js +456 -0
- package/dist/src/browser/clipboard.d.ts +2 -0
- package/dist/src/browser/clipboard.js +26 -0
- package/dist/src/browser/downloads.d.ts +7 -0
- package/dist/src/browser/downloads.js +29 -0
- package/dist/src/browser/page-state.d.ts +17 -0
- package/dist/src/browser/page-state.js +66 -0
- package/dist/src/client.d.ts +167 -0
- package/dist/src/client.js +666 -0
- package/dist/src/commands/confirmations.d.ts +9 -0
- package/dist/src/commands/confirmations.js +33 -0
- package/dist/src/commands/context.d.ts +2 -0
- package/dist/src/commands/context.js +32 -0
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +120 -0
- package/dist/src/commands/files.d.ts +4 -0
- package/dist/src/commands/files.js +314 -0
- package/dist/src/commands/helpers.d.ts +16 -0
- package/dist/src/commands/helpers.js +128 -0
- package/dist/src/commands/messages.d.ts +16 -0
- package/dist/src/commands/messages.js +538 -0
- package/dist/src/commands/modes.d.ts +9 -0
- package/dist/src/commands/modes.js +323 -0
- package/dist/src/commands/output.d.ts +5 -0
- package/dist/src/commands/output.js +31 -0
- package/dist/src/commands/registry.d.ts +17 -0
- package/dist/src/commands/registry.js +275 -0
- package/dist/src/commands/reports.d.ts +14 -0
- package/dist/src/commands/reports.js +47 -0
- package/dist/src/commands/response-actions.d.ts +2 -0
- package/dist/src/commands/response-actions.js +141 -0
- package/dist/src/commands/sequence.d.ts +9 -0
- package/dist/src/commands/sequence.js +228 -0
- package/dist/src/commands/session.d.ts +2 -0
- package/dist/src/commands/session.js +25 -0
- package/dist/src/commands/threads.d.ts +6 -0
- package/dist/src/commands/threads.js +300 -0
- package/dist/src/dom/fixtures.d.ts +1 -0
- package/dist/src/dom/fixtures.js +8 -0
- package/dist/src/dom/menus.d.ts +8 -0
- package/dist/src/dom/menus.js +40 -0
- package/dist/src/dom/message-format.d.ts +30 -0
- package/dist/src/dom/message-format.js +593 -0
- package/dist/src/dom/messages.d.ts +40 -0
- package/dist/src/dom/messages.js +127 -0
- package/dist/src/dom/selectors.d.ts +20 -0
- package/dist/src/dom/selectors.js +72 -0
- package/dist/src/dom/visible-text.d.ts +5 -0
- package/dist/src/dom/visible-text.js +27 -0
- package/dist/src/errors.d.ts +32 -0
- package/dist/src/errors.js +127 -0
- package/dist/src/index.d.ts +34 -0
- package/dist/src/index.js +34 -0
- package/dist/src/logger.d.ts +15 -0
- package/dist/src/logger.js +25 -0
- package/dist/src/runner/agent.d.ts +2 -0
- package/dist/src/runner/agent.js +17 -0
- package/dist/src/runner/index.d.ts +7 -0
- package/dist/src/runner/index.js +7 -0
- package/dist/src/runner/interruptions.d.ts +3 -0
- package/dist/src/runner/interruptions.js +65 -0
- package/dist/src/runner/responses.d.ts +30 -0
- package/dist/src/runner/responses.js +171 -0
- package/dist/src/runner/result.d.ts +3 -0
- package/dist/src/runner/result.js +128 -0
- package/dist/src/runner/resume.d.ts +11 -0
- package/dist/src/runner/resume.js +26 -0
- package/dist/src/runner/stream.d.ts +13 -0
- package/dist/src/runner/stream.js +66 -0
- package/dist/src/runner/types.d.ts +264 -0
- package/dist/src/runner/types.js +1 -0
- package/dist/src/safety/blockers.d.ts +7 -0
- package/dist/src/safety/blockers.js +60 -0
- package/dist/src/safety/redaction.d.ts +2 -0
- package/dist/src/safety/redaction.js +18 -0
- package/dist/src/safety/report-redaction.d.ts +8 -0
- package/dist/src/safety/report-redaction.js +74 -0
- package/dist/src/safety/risk.d.ts +26 -0
- package/dist/src/safety/risk.js +28 -0
- package/dist/src/scripts/backend-server.d.ts +2 -0
- package/dist/src/scripts/backend-server.js +7 -0
- package/dist/src/scripts/check-parity-suite.d.ts +1 -0
- package/dist/src/scripts/check-parity-suite.js +10 -0
- package/dist/src/scripts/continue-thread.d.ts +27 -0
- package/dist/src/scripts/continue-thread.js +388 -0
- package/dist/src/scripts/live-smoke/harness.d.ts +11 -0
- package/dist/src/scripts/live-smoke/harness.js +151 -0
- package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
- package/dist/src/scripts/live-smoke/scenarios.js +648 -0
- package/dist/src/scripts/live-smoke/types.d.ts +53 -0
- package/dist/src/scripts/live-smoke/types.js +1 -0
- package/dist/src/scripts/live-smoke-module.d.ts +3 -0
- package/dist/src/scripts/live-smoke-module.js +2 -0
- package/dist/src/scripts/live-smoke.d.ts +1 -0
- package/dist/src/scripts/live-smoke.js +54 -0
- package/dist/src/scripts/smoke-attach-file.d.ts +1 -0
- package/dist/src/scripts/smoke-attach-file.js +39 -0
- package/dist/src/scripts/smoke-hi.d.ts +1 -0
- package/dist/src/scripts/smoke-hi.js +25 -0
- package/dist/src/scripts/smoke-search-open-read.d.ts +1 -0
- package/dist/src/scripts/smoke-search-open-read.js +41 -0
- package/dist/src/testing/parity-suite.d.ts +12 -0
- package/dist/src/testing/parity-suite.js +213 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.js +1 -0
- package/package.json +81 -0
- package/references/agents-runner.md +32 -0
- package/references/backend-protocol.md +238 -0
- package/references/python-parity.md +168 -0
- package/references/responses-adapter.md +28 -0
- package/references/safety.md +14 -0
- package/references/streaming.md +15 -0
- package/references/troubleshooting.md +91 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { decodeBasicEntities, normalizeLineBreaks, normalizeWhitespace } from "./visible-text.js";
|
|
2
|
+
const VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
3
|
+
const SKIPPED_TAGS = new Set(["button", "nav", "script", "style", "svg"]);
|
|
4
|
+
const BLOCK_TAGS = new Set([
|
|
5
|
+
"article",
|
|
6
|
+
"blockquote",
|
|
7
|
+
"div",
|
|
8
|
+
"h1",
|
|
9
|
+
"h2",
|
|
10
|
+
"h3",
|
|
11
|
+
"h4",
|
|
12
|
+
"h5",
|
|
13
|
+
"h6",
|
|
14
|
+
"li",
|
|
15
|
+
"ol",
|
|
16
|
+
"p",
|
|
17
|
+
"pre",
|
|
18
|
+
"section",
|
|
19
|
+
"table",
|
|
20
|
+
"tbody",
|
|
21
|
+
"td",
|
|
22
|
+
"tfoot",
|
|
23
|
+
"th",
|
|
24
|
+
"thead",
|
|
25
|
+
"tr",
|
|
26
|
+
"ul"
|
|
27
|
+
]);
|
|
28
|
+
export function normalizeResponseFormat(format) {
|
|
29
|
+
if (format === undefined || format === "markdown")
|
|
30
|
+
return "markdown";
|
|
31
|
+
if (format === "text")
|
|
32
|
+
return "normalized_text";
|
|
33
|
+
return format;
|
|
34
|
+
}
|
|
35
|
+
export function extractRoleMessageHtml(html) {
|
|
36
|
+
const root = parseHtmlFragment(html);
|
|
37
|
+
const messages = [];
|
|
38
|
+
walkElementsWithAncestors(root, [], (element, ancestors) => {
|
|
39
|
+
const role = element.attrs["data-message-author-role"];
|
|
40
|
+
if (role === "user" || role === "assistant") {
|
|
41
|
+
const metadataElement = [...ancestors]
|
|
42
|
+
.reverse()
|
|
43
|
+
.find(ancestor => ancestor.attrs["data-testid"]?.startsWith("conversation-turn")) ?? element;
|
|
44
|
+
messages.push({ role, html: serializeChildren(element), metadataHtml: serializeNode(metadataElement) });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return messages;
|
|
48
|
+
}
|
|
49
|
+
export function formatMessageHtml(html, requestedFormat = "markdown", maxChars, metadataHtml) {
|
|
50
|
+
const format = normalizeResponseFormat(requestedFormat);
|
|
51
|
+
const root = parseHtmlFragment(html);
|
|
52
|
+
const meaningfulChildren = stripIgnorableNodes(root.children);
|
|
53
|
+
const blocks = extractBlocks(meaningfulChildren);
|
|
54
|
+
const markdown = clamp(blocksToMarkdown(blocks), maxChars);
|
|
55
|
+
const visibleText = clamp(blocksToPlainText(blocks), maxChars);
|
|
56
|
+
const normalizedText = clamp(normalizeWhitespace(visibleText), maxChars);
|
|
57
|
+
const citations = collectCitations(meaningfulChildren);
|
|
58
|
+
const codeBlocks = blocks.flatMap(block => block.type === "code" ? [codeBlockFromBlock(block)] : []);
|
|
59
|
+
const tables = blocks.flatMap(block => block.type === "table" ? [tableFromBlock(block)] : []);
|
|
60
|
+
const metadata = extractResponseMetadata(metadataHtml ?? html);
|
|
61
|
+
const content = {
|
|
62
|
+
text: textForFormat(format, { markdown, visibleText, normalizedText, html }),
|
|
63
|
+
format,
|
|
64
|
+
source: "semantic_dom",
|
|
65
|
+
fidelity: fidelityForDomFormat(format)
|
|
66
|
+
};
|
|
67
|
+
const warnings = warningsForDomFormat(format);
|
|
68
|
+
if (warnings.length > 0)
|
|
69
|
+
content.warnings = warnings;
|
|
70
|
+
if (format === "markdown" || format === "all")
|
|
71
|
+
content.markdown = markdown;
|
|
72
|
+
if (format === "visible_text" || format === "all")
|
|
73
|
+
content.visibleText = visibleText;
|
|
74
|
+
if (format === "normalized_text" || format === "all")
|
|
75
|
+
content.normalizedText = normalizedText;
|
|
76
|
+
if (format === "html" || format === "all")
|
|
77
|
+
content.html = html;
|
|
78
|
+
if (format === "blocks" || format === "all")
|
|
79
|
+
content.blocks = blocks;
|
|
80
|
+
if ((format === "markdown" || format === "blocks" || format === "all") && citations.length > 0) {
|
|
81
|
+
content.citations = citations;
|
|
82
|
+
}
|
|
83
|
+
if ((format === "markdown" || format === "blocks" || format === "all") && codeBlocks.length > 0) {
|
|
84
|
+
content.codeBlocks = codeBlocks;
|
|
85
|
+
}
|
|
86
|
+
if ((format === "markdown" || format === "blocks" || format === "all") && tables.length > 0) {
|
|
87
|
+
content.tables = tables;
|
|
88
|
+
}
|
|
89
|
+
if (metadata.branch !== undefined)
|
|
90
|
+
content.branch = metadata.branch;
|
|
91
|
+
if (metadata.actions.length > 0)
|
|
92
|
+
content.actions = metadata.actions;
|
|
93
|
+
if (metadata.thoughtDurationText !== undefined)
|
|
94
|
+
content.thoughtDurationText = metadata.thoughtDurationText;
|
|
95
|
+
if (metadata.sourcesAvailable === true)
|
|
96
|
+
content.sourcesAvailable = true;
|
|
97
|
+
return content;
|
|
98
|
+
}
|
|
99
|
+
export function formatClipboardMarkdown(text, maxChars, requestedFormat = "markdown") {
|
|
100
|
+
const format = normalizeResponseFormat(requestedFormat);
|
|
101
|
+
const markdown = clamp(normalizeLineBreaks(text).trim(), maxChars);
|
|
102
|
+
const visibleText = markdown;
|
|
103
|
+
const normalizedText = clamp(normalizeWhitespace(markdown), maxChars);
|
|
104
|
+
const content = {
|
|
105
|
+
text: textForFormat(format, { markdown, visibleText, normalizedText, html: markdown }),
|
|
106
|
+
format,
|
|
107
|
+
source: "clipboard",
|
|
108
|
+
fidelity: "clipboard_markdown"
|
|
109
|
+
};
|
|
110
|
+
if (format === "markdown" || format === "all")
|
|
111
|
+
content.markdown = markdown;
|
|
112
|
+
if (format === "visible_text" || format === "all")
|
|
113
|
+
content.visibleText = visibleText;
|
|
114
|
+
if (format === "normalized_text" || format === "all")
|
|
115
|
+
content.normalizedText = normalizedText;
|
|
116
|
+
return content;
|
|
117
|
+
}
|
|
118
|
+
function fidelityForDomFormat(format) {
|
|
119
|
+
switch (format) {
|
|
120
|
+
case "markdown":
|
|
121
|
+
return "semantic_markdown";
|
|
122
|
+
case "visible_text":
|
|
123
|
+
return "visible_text";
|
|
124
|
+
case "normalized_text":
|
|
125
|
+
return "normalized_text";
|
|
126
|
+
case "html":
|
|
127
|
+
return "html";
|
|
128
|
+
case "blocks":
|
|
129
|
+
return "blocks";
|
|
130
|
+
case "all":
|
|
131
|
+
return "all";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function warningsForDomFormat(format) {
|
|
135
|
+
if (format !== "markdown" && format !== "all") {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
return ["Markdown was reconstructed from visible DOM semantics; use response.copy for clipboard Markdown when exact copy fidelity is required."];
|
|
139
|
+
}
|
|
140
|
+
function textForFormat(format, values) {
|
|
141
|
+
switch (format) {
|
|
142
|
+
case "markdown":
|
|
143
|
+
return values.markdown;
|
|
144
|
+
case "visible_text":
|
|
145
|
+
return values.visibleText;
|
|
146
|
+
case "normalized_text":
|
|
147
|
+
return values.normalizedText;
|
|
148
|
+
case "html":
|
|
149
|
+
return values.normalizedText;
|
|
150
|
+
case "blocks":
|
|
151
|
+
return values.markdown;
|
|
152
|
+
case "all":
|
|
153
|
+
return values.markdown;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function parseHtmlFragment(html) {
|
|
157
|
+
const root = { type: "element", tag: "#root", attrs: {}, children: [] };
|
|
158
|
+
const stack = [root];
|
|
159
|
+
const tokenRe = /<!--[\s\S]*?-->|<![^>]*>|<\/?[a-zA-Z][^>]*>|[^<]+/g;
|
|
160
|
+
for (const match of html.matchAll(tokenRe)) {
|
|
161
|
+
const token = match[0];
|
|
162
|
+
const parent = stack.at(-1) ?? root;
|
|
163
|
+
if (token.startsWith("<!--") || token.startsWith("<!")) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (token.startsWith("</")) {
|
|
167
|
+
const tag = /^<\/\s*([a-zA-Z0-9-]+)/.exec(token)?.[1]?.toLowerCase();
|
|
168
|
+
if (tag === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
while (stack.length > 1) {
|
|
171
|
+
const current = stack.pop();
|
|
172
|
+
if (current?.tag === tag)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (token.startsWith("<")) {
|
|
178
|
+
const tag = /^<\s*([a-zA-Z0-9-]+)/.exec(token)?.[1]?.toLowerCase();
|
|
179
|
+
if (tag === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
const element = {
|
|
182
|
+
type: "element",
|
|
183
|
+
tag,
|
|
184
|
+
attrs: parseAttrs(token),
|
|
185
|
+
children: []
|
|
186
|
+
};
|
|
187
|
+
parent.children.push(element);
|
|
188
|
+
if (!VOID_TAGS.has(tag) && !/\/\s*>$/.test(token)) {
|
|
189
|
+
stack.push(element);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
parent.children.push({ type: "text", text: decodeBasicEntities(token) });
|
|
194
|
+
}
|
|
195
|
+
return root;
|
|
196
|
+
}
|
|
197
|
+
function parseAttrs(token) {
|
|
198
|
+
const attrs = {};
|
|
199
|
+
const attrText = token.replace(/^<\s*[^\s/>]+/, "").replace(/\/?>$/, "");
|
|
200
|
+
const attrRe = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
201
|
+
for (const match of attrText.matchAll(attrRe)) {
|
|
202
|
+
const key = match[1]?.toLowerCase();
|
|
203
|
+
if (key === undefined)
|
|
204
|
+
continue;
|
|
205
|
+
attrs[key] = decodeBasicEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
206
|
+
}
|
|
207
|
+
return attrs;
|
|
208
|
+
}
|
|
209
|
+
function walkElements(element, visit) {
|
|
210
|
+
visit(element);
|
|
211
|
+
for (const child of element.children) {
|
|
212
|
+
if (child.type === "element")
|
|
213
|
+
walkElements(child, visit);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function walkElementsWithAncestors(element, ancestors, visit) {
|
|
217
|
+
visit(element, ancestors);
|
|
218
|
+
for (const child of element.children) {
|
|
219
|
+
if (child.type === "element")
|
|
220
|
+
walkElementsWithAncestors(child, [...ancestors, element], visit);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function serializeChildren(element) {
|
|
224
|
+
return element.children.map(serializeNode).join("");
|
|
225
|
+
}
|
|
226
|
+
function serializeNode(node) {
|
|
227
|
+
if (node.type === "text")
|
|
228
|
+
return escapeHtml(node.text);
|
|
229
|
+
const attrs = Object.entries(node.attrs)
|
|
230
|
+
.map(([key, value]) => value.length > 0 ? ` ${key}="${escapeAttr(value)}"` : ` ${key}`)
|
|
231
|
+
.join("");
|
|
232
|
+
if (VOID_TAGS.has(node.tag))
|
|
233
|
+
return `<${node.tag}${attrs}>`;
|
|
234
|
+
return `<${node.tag}${attrs}>${serializeChildren(node)}</${node.tag}>`;
|
|
235
|
+
}
|
|
236
|
+
function stripIgnorableNodes(nodes) {
|
|
237
|
+
return nodes.filter(node => {
|
|
238
|
+
if (node.type === "text")
|
|
239
|
+
return node.text.trim().length > 0;
|
|
240
|
+
return !SKIPPED_TAGS.has(node.tag) && nodeText(node).trim().length > 0;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function extractBlocks(nodes) {
|
|
244
|
+
const blocks = [];
|
|
245
|
+
for (const node of nodes) {
|
|
246
|
+
if (node.type === "text") {
|
|
247
|
+
const text = normalizeWhitespace(node.text);
|
|
248
|
+
if (text.length > 0)
|
|
249
|
+
blocks.push({ type: "paragraph", text });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (SKIPPED_TAGS.has(node.tag))
|
|
253
|
+
continue;
|
|
254
|
+
blocks.push(...elementToBlocks(node));
|
|
255
|
+
}
|
|
256
|
+
return blocks.filter(block => blockToPlainText(block).length > 0);
|
|
257
|
+
}
|
|
258
|
+
function elementToBlocks(element) {
|
|
259
|
+
if (/^h[1-6]$/.test(element.tag)) {
|
|
260
|
+
return [{ type: "heading", depth: Number(element.tag.slice(1)), text: inlineText(element.children) }];
|
|
261
|
+
}
|
|
262
|
+
if (element.tag === "p") {
|
|
263
|
+
return [{ type: "paragraph", text: inlineMarkdown(element.children) }];
|
|
264
|
+
}
|
|
265
|
+
if (element.tag === "ul" || element.tag === "ol") {
|
|
266
|
+
return [{
|
|
267
|
+
type: "list",
|
|
268
|
+
ordered: element.tag === "ol",
|
|
269
|
+
items: element.children
|
|
270
|
+
.filter((child) => child.type === "element" && child.tag === "li")
|
|
271
|
+
.map(item => markdownForListItem(item))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
}];
|
|
274
|
+
}
|
|
275
|
+
if (element.tag === "pre") {
|
|
276
|
+
const code = firstElement(element, "code") ?? element;
|
|
277
|
+
const language = languageFromClass(code.attrs.class);
|
|
278
|
+
const text = normalizeLineBreaks(nodeText(code)).replace(/^\n+|\n+$/g, "");
|
|
279
|
+
const block = language === undefined
|
|
280
|
+
? { type: "code", text }
|
|
281
|
+
: { type: "code", language, text };
|
|
282
|
+
return [block];
|
|
283
|
+
}
|
|
284
|
+
if (element.tag === "table") {
|
|
285
|
+
return [tableBlock(element)];
|
|
286
|
+
}
|
|
287
|
+
if (element.tag === "blockquote") {
|
|
288
|
+
return [{ type: "quote", text: inlineMarkdown(element.children) }];
|
|
289
|
+
}
|
|
290
|
+
if (element.tag === "br") {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
const childBlocks = extractBlocks(element.children);
|
|
294
|
+
if (childBlocks.length > 0 && hasBlockChild(element)) {
|
|
295
|
+
return childBlocks;
|
|
296
|
+
}
|
|
297
|
+
const text = inlineMarkdown(element.children);
|
|
298
|
+
return text.length > 0 ? [{ type: "paragraph", text }] : [];
|
|
299
|
+
}
|
|
300
|
+
function markdownForListItem(item) {
|
|
301
|
+
const childBlocks = extractBlocks(item.children);
|
|
302
|
+
if (childBlocks.length === 0)
|
|
303
|
+
return inlineMarkdown(item.children);
|
|
304
|
+
if (childBlocks.length === 1 && childBlocks[0]?.type === "paragraph")
|
|
305
|
+
return childBlocks[0].text;
|
|
306
|
+
return blocksToMarkdown(childBlocks);
|
|
307
|
+
}
|
|
308
|
+
function tableBlock(table) {
|
|
309
|
+
const rows = descendants(table, "tr")
|
|
310
|
+
.map(row => row.children.filter((child) => child.type === "element" && (child.tag === "th" || child.tag === "td")))
|
|
311
|
+
.filter(cells => cells.length > 0);
|
|
312
|
+
const firstHeaderRow = rows.find(cells => cells.some(cell => cell.tag === "th"));
|
|
313
|
+
const headers = (firstHeaderRow ?? rows[0] ?? []).map(cell => inlineText(cell.children));
|
|
314
|
+
const bodyRows = rows
|
|
315
|
+
.filter(cells => cells !== firstHeaderRow)
|
|
316
|
+
.map(cells => cells.map(cell => inlineText(cell.children)));
|
|
317
|
+
return { type: "table", headers, rows: bodyRows };
|
|
318
|
+
}
|
|
319
|
+
function inlineMarkdown(nodes) {
|
|
320
|
+
return normalizeInline(nodes.map(node => {
|
|
321
|
+
if (node.type === "text")
|
|
322
|
+
return node.text;
|
|
323
|
+
if (SKIPPED_TAGS.has(node.tag))
|
|
324
|
+
return "";
|
|
325
|
+
const child = inlineMarkdown(node.children);
|
|
326
|
+
switch (node.tag) {
|
|
327
|
+
case "a": {
|
|
328
|
+
const href = node.attrs.href;
|
|
329
|
+
if (href === undefined || href.length === 0)
|
|
330
|
+
return child;
|
|
331
|
+
const label = child.length > 0 ? child : href;
|
|
332
|
+
return `[${escapeMarkdownLinkText(label)}](${href})`;
|
|
333
|
+
}
|
|
334
|
+
case "code":
|
|
335
|
+
return `\`${nodeText(node).trim()}\``;
|
|
336
|
+
case "strong":
|
|
337
|
+
case "b":
|
|
338
|
+
return child.length > 0 ? `**${child}**` : "";
|
|
339
|
+
case "em":
|
|
340
|
+
case "i":
|
|
341
|
+
return child.length > 0 ? `*${child}*` : "";
|
|
342
|
+
case "br":
|
|
343
|
+
return "\n";
|
|
344
|
+
default:
|
|
345
|
+
return child;
|
|
346
|
+
}
|
|
347
|
+
}).join(""));
|
|
348
|
+
}
|
|
349
|
+
function inlineText(nodes) {
|
|
350
|
+
return normalizeInline(nodes.map(node => {
|
|
351
|
+
if (node.type === "text")
|
|
352
|
+
return node.text;
|
|
353
|
+
if (SKIPPED_TAGS.has(node.tag))
|
|
354
|
+
return "";
|
|
355
|
+
if (node.tag === "br")
|
|
356
|
+
return "\n";
|
|
357
|
+
return inlineText(node.children);
|
|
358
|
+
}).join(""));
|
|
359
|
+
}
|
|
360
|
+
function blocksToMarkdown(blocks) {
|
|
361
|
+
return blocks.map(blockToMarkdown).filter(Boolean).join("\n\n").trim();
|
|
362
|
+
}
|
|
363
|
+
function blockToMarkdown(block) {
|
|
364
|
+
switch (block.type) {
|
|
365
|
+
case "heading":
|
|
366
|
+
return `${"#".repeat(Math.min(Math.max(block.depth, 1), 6))} ${block.text}`;
|
|
367
|
+
case "paragraph":
|
|
368
|
+
return block.text;
|
|
369
|
+
case "list":
|
|
370
|
+
return block.items.map((item, index) => block.ordered ? `${index + 1}. ${item}` : `- ${item}`).join("\n");
|
|
371
|
+
case "code":
|
|
372
|
+
return `\`\`\`${block.language ?? ""}\n${block.text}\n\`\`\``;
|
|
373
|
+
case "table":
|
|
374
|
+
return tableToMarkdown(block);
|
|
375
|
+
case "quote":
|
|
376
|
+
return block.text.split("\n").map(line => `> ${line}`).join("\n");
|
|
377
|
+
case "unknown":
|
|
378
|
+
return block.text;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function tableToMarkdown(table) {
|
|
382
|
+
const width = Math.max(table.headers.length, ...table.rows.map(row => row.length), 1);
|
|
383
|
+
const headers = padCells(table.headers, width);
|
|
384
|
+
const rows = table.rows.map(row => padCells(row, width));
|
|
385
|
+
return [
|
|
386
|
+
markdownTableRow(headers),
|
|
387
|
+
markdownTableRow(headers.map(() => "---")),
|
|
388
|
+
...rows.map(markdownTableRow)
|
|
389
|
+
].join("\n");
|
|
390
|
+
}
|
|
391
|
+
function markdownTableRow(cells) {
|
|
392
|
+
return `| ${cells.map(cell => cell.replace(/\|/g, "\\|")).join(" | ")} |`;
|
|
393
|
+
}
|
|
394
|
+
function padCells(cells, width) {
|
|
395
|
+
return Array.from({ length: width }, (_, index) => cells[index] ?? "");
|
|
396
|
+
}
|
|
397
|
+
function blocksToPlainText(blocks) {
|
|
398
|
+
return blocks.map(blockToPlainText).filter(Boolean).join("\n").trim();
|
|
399
|
+
}
|
|
400
|
+
function blockToPlainText(block) {
|
|
401
|
+
switch (block.type) {
|
|
402
|
+
case "heading":
|
|
403
|
+
case "paragraph":
|
|
404
|
+
case "quote":
|
|
405
|
+
case "unknown":
|
|
406
|
+
return inlineMarkdownToPlainText(block.text);
|
|
407
|
+
case "list":
|
|
408
|
+
return block.items.map(inlineMarkdownToPlainText).join("\n");
|
|
409
|
+
case "code":
|
|
410
|
+
return block.text;
|
|
411
|
+
case "table":
|
|
412
|
+
return [block.headers.join(" "), ...block.rows.map(row => row.join(" "))].join("\n");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function inlineMarkdownToPlainText(text) {
|
|
416
|
+
return normalizeWhitespace(text
|
|
417
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
418
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
419
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
420
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1"));
|
|
421
|
+
}
|
|
422
|
+
function collectCitations(nodes) {
|
|
423
|
+
const citations = [];
|
|
424
|
+
for (const node of nodes) {
|
|
425
|
+
if (node.type === "text" || SKIPPED_TAGS.has(node.tag))
|
|
426
|
+
continue;
|
|
427
|
+
if (node.tag === "a" && node.attrs.href !== undefined && node.attrs.href.length > 0) {
|
|
428
|
+
const text = inlineText(node.children) || node.attrs.href;
|
|
429
|
+
citations.push({ text, href: node.attrs.href });
|
|
430
|
+
}
|
|
431
|
+
citations.push(...collectCitations(node.children));
|
|
432
|
+
}
|
|
433
|
+
return citations;
|
|
434
|
+
}
|
|
435
|
+
function extractResponseMetadata(html) {
|
|
436
|
+
const root = parseHtmlFragment(html);
|
|
437
|
+
const text = normalizeWhitespace(metadataNodeText(root));
|
|
438
|
+
const actions = collectResponseActions(root);
|
|
439
|
+
const branch = extractBranchState(text, actions);
|
|
440
|
+
const thoughtDurationText = /\bThought for\s+[^.。!?]+?(?=(?:\s+\d+\s*\/\s*\d+)|\s+Sources\b|$)/i.exec(text)?.[0];
|
|
441
|
+
const sourcesAvailable = actions.some(action => action.type === "sources") || /\bSources\b/i.test(text);
|
|
442
|
+
return {
|
|
443
|
+
...(branch === undefined ? {} : { branch }),
|
|
444
|
+
actions,
|
|
445
|
+
...(thoughtDurationText === undefined ? {} : { thoughtDurationText }),
|
|
446
|
+
...(sourcesAvailable ? { sourcesAvailable: true } : {})
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function collectResponseActions(root) {
|
|
450
|
+
const actions = [];
|
|
451
|
+
walkElements(root, element => {
|
|
452
|
+
if (element.tag !== "button" && element.tag !== "div")
|
|
453
|
+
return;
|
|
454
|
+
const ariaLabel = element.attrs["aria-label"];
|
|
455
|
+
const text = inlineText(element.children);
|
|
456
|
+
const label = normalizeWhitespace(ariaLabel ?? text);
|
|
457
|
+
const type = responseActionType(label);
|
|
458
|
+
if (type === undefined)
|
|
459
|
+
return;
|
|
460
|
+
const action = { type, label };
|
|
461
|
+
if (ariaLabel !== undefined)
|
|
462
|
+
action.ariaLabel = ariaLabel;
|
|
463
|
+
if (text.length > 0)
|
|
464
|
+
action.text = text;
|
|
465
|
+
if (element.attrs["data-testid"] !== undefined)
|
|
466
|
+
action.testId = element.attrs["data-testid"];
|
|
467
|
+
if (element.attrs.disabled !== undefined || element.attrs["aria-disabled"] === "true")
|
|
468
|
+
action.disabled = true;
|
|
469
|
+
actions.push(action);
|
|
470
|
+
});
|
|
471
|
+
return dedupeActions(actions);
|
|
472
|
+
}
|
|
473
|
+
function responseActionType(label) {
|
|
474
|
+
if (/^previous response$/i.test(label))
|
|
475
|
+
return "previous_response";
|
|
476
|
+
if (/^next response$/i.test(label))
|
|
477
|
+
return "next_response";
|
|
478
|
+
if (/^copy response$/i.test(label))
|
|
479
|
+
return "copy_response";
|
|
480
|
+
if (/^sources$/i.test(label) || /\bSources\b/.test(label))
|
|
481
|
+
return "sources";
|
|
482
|
+
if (/^good response$/i.test(label))
|
|
483
|
+
return "good_response";
|
|
484
|
+
if (/^bad response$/i.test(label))
|
|
485
|
+
return "bad_response";
|
|
486
|
+
if (/^more actions$/i.test(label))
|
|
487
|
+
return "more_actions";
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
function dedupeActions(actions) {
|
|
491
|
+
const seen = new Set();
|
|
492
|
+
const unique = [];
|
|
493
|
+
for (const action of actions) {
|
|
494
|
+
const key = `${action.type}:${action.label}:${action.testId ?? ""}`;
|
|
495
|
+
if (seen.has(key))
|
|
496
|
+
continue;
|
|
497
|
+
seen.add(key);
|
|
498
|
+
unique.push(action);
|
|
499
|
+
}
|
|
500
|
+
return unique;
|
|
501
|
+
}
|
|
502
|
+
function extractBranchState(text, actions) {
|
|
503
|
+
const match = /\b(\d+)\s*\/\s*(\d+)\b/.exec(text);
|
|
504
|
+
if (match === null)
|
|
505
|
+
return undefined;
|
|
506
|
+
const current = Number(match[1]);
|
|
507
|
+
const total = Number(match[2]);
|
|
508
|
+
const branch = { label: match[0] };
|
|
509
|
+
if (Number.isFinite(current))
|
|
510
|
+
branch.current = current;
|
|
511
|
+
if (Number.isFinite(total))
|
|
512
|
+
branch.total = total;
|
|
513
|
+
const previous = actions.find(action => action.type === "previous_response");
|
|
514
|
+
const next = actions.find(action => action.type === "next_response");
|
|
515
|
+
if (previous !== undefined)
|
|
516
|
+
branch.canGoPrevious = previous.disabled !== true;
|
|
517
|
+
if (next !== undefined)
|
|
518
|
+
branch.canGoNext = next.disabled !== true;
|
|
519
|
+
return branch;
|
|
520
|
+
}
|
|
521
|
+
function codeBlockFromBlock(block) {
|
|
522
|
+
return block.language === undefined ? { text: block.text } : { language: block.language, text: block.text };
|
|
523
|
+
}
|
|
524
|
+
function tableFromBlock(block) {
|
|
525
|
+
return { headers: block.headers, rows: block.rows };
|
|
526
|
+
}
|
|
527
|
+
function firstElement(element, tag) {
|
|
528
|
+
for (const child of element.children) {
|
|
529
|
+
if (child.type === "element") {
|
|
530
|
+
if (child.tag === tag)
|
|
531
|
+
return child;
|
|
532
|
+
const nested = firstElement(child, tag);
|
|
533
|
+
if (nested !== undefined)
|
|
534
|
+
return nested;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
function descendants(element, tag) {
|
|
540
|
+
const found = [];
|
|
541
|
+
walkElements(element, child => {
|
|
542
|
+
if (child.tag === tag)
|
|
543
|
+
found.push(child);
|
|
544
|
+
});
|
|
545
|
+
return found;
|
|
546
|
+
}
|
|
547
|
+
function hasBlockChild(element) {
|
|
548
|
+
return element.children.some(child => child.type === "element" && BLOCK_TAGS.has(child.tag));
|
|
549
|
+
}
|
|
550
|
+
function nodeText(node) {
|
|
551
|
+
if (node.type === "text")
|
|
552
|
+
return node.text;
|
|
553
|
+
if (SKIPPED_TAGS.has(node.tag))
|
|
554
|
+
return "";
|
|
555
|
+
if (node.tag === "br")
|
|
556
|
+
return "\n";
|
|
557
|
+
return node.children.map(nodeText).join("");
|
|
558
|
+
}
|
|
559
|
+
function metadataNodeText(node) {
|
|
560
|
+
if (node.type === "text")
|
|
561
|
+
return node.text;
|
|
562
|
+
if (node.tag === "script" || node.tag === "style" || node.tag === "svg")
|
|
563
|
+
return "";
|
|
564
|
+
if (node.tag === "br")
|
|
565
|
+
return "\n";
|
|
566
|
+
return node.children.map(metadataNodeText).join(" ");
|
|
567
|
+
}
|
|
568
|
+
function languageFromClass(className) {
|
|
569
|
+
return className?.split(/\s+/).find(name => name.startsWith("language-"))?.slice("language-".length);
|
|
570
|
+
}
|
|
571
|
+
function normalizeInline(text) {
|
|
572
|
+
return decodeBasicEntities(text)
|
|
573
|
+
.replace(/[ \t\r\n]+/g, " ")
|
|
574
|
+
.replace(/\s+([.,;:!?])/g, "$1")
|
|
575
|
+
.trim();
|
|
576
|
+
}
|
|
577
|
+
function clamp(text, maxChars) {
|
|
578
|
+
if (maxChars === undefined || text.length <= maxChars)
|
|
579
|
+
return text;
|
|
580
|
+
return text.slice(0, Math.max(0, maxChars));
|
|
581
|
+
}
|
|
582
|
+
function escapeMarkdownLinkText(text) {
|
|
583
|
+
return text.replace(/]/g, "\\]");
|
|
584
|
+
}
|
|
585
|
+
function escapeHtml(text) {
|
|
586
|
+
return text
|
|
587
|
+
.replace(/&/g, "&")
|
|
588
|
+
.replace(/</g, "<")
|
|
589
|
+
.replace(/>/g, ">");
|
|
590
|
+
}
|
|
591
|
+
function escapeAttr(text) {
|
|
592
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
593
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PageLike, ResponseAction, ResponseBlock, ResponseBranchState, ResponseCaptureFidelity, ResponseCaptureSource, ResponseCitation, ResponseCodeBlock, ResponseFormat, ResponseTable } from "../types.js";
|
|
2
|
+
export type MessageRole = "user" | "assistant";
|
|
3
|
+
export type ExtractedMessage = {
|
|
4
|
+
role: MessageRole;
|
|
5
|
+
text: string;
|
|
6
|
+
format: Exclude<ResponseFormat, "text">;
|
|
7
|
+
source?: ResponseCaptureSource;
|
|
8
|
+
fidelity?: ResponseCaptureFidelity;
|
|
9
|
+
warnings?: string[];
|
|
10
|
+
markdown?: string;
|
|
11
|
+
visibleText?: string;
|
|
12
|
+
normalizedText?: string;
|
|
13
|
+
html?: string;
|
|
14
|
+
blocks?: ResponseBlock[];
|
|
15
|
+
citations?: ResponseCitation[];
|
|
16
|
+
codeBlocks?: ResponseCodeBlock[];
|
|
17
|
+
tables?: ResponseTable[];
|
|
18
|
+
branch?: ResponseBranchState;
|
|
19
|
+
actions?: ResponseAction[];
|
|
20
|
+
thoughtDurationText?: string;
|
|
21
|
+
sourcesAvailable?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type ReadMessagesArgs = {
|
|
24
|
+
role?: MessageRole;
|
|
25
|
+
scope?: "visible" | "loaded";
|
|
26
|
+
format?: ResponseFormat;
|
|
27
|
+
maxChars?: number;
|
|
28
|
+
};
|
|
29
|
+
export type LatestMessageTextSnapshot = {
|
|
30
|
+
latestText?: string;
|
|
31
|
+
turnCount: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function extractMessagesFromHtml(html: string, args?: ReadMessagesArgs): ExtractedMessage[];
|
|
34
|
+
export declare function readMessages(page: PageLike, args?: ReadMessagesArgs): Promise<ExtractedMessage[]>;
|
|
35
|
+
export declare function readLatestMessage(page: PageLike, role?: MessageRole, format?: ResponseFormat, maxChars?: number): Promise<ExtractedMessage | undefined>;
|
|
36
|
+
export declare function readLatestMessageText(page: PageLike, role?: MessageRole): Promise<string | undefined>;
|
|
37
|
+
export declare function readLatestMessageTextSnapshot(page: PageLike, role: MessageRole): Promise<LatestMessageTextSnapshot>;
|
|
38
|
+
export declare function isTransientAssistantText(text: string): boolean;
|
|
39
|
+
export declare function countMessages(messages: ExtractedMessage[], role?: MessageRole): number;
|
|
40
|
+
export declare function countPageMessages(page: PageLike, role?: MessageRole): Promise<number>;
|