codex-chatgpt-control 0.1.0-alpha.1 → 0.2.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.
Files changed (242) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +7 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +192 -15
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -0
  8. package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
  9. package/contracts/v1/fixtures/files-preflight-success.json +32 -0
  10. package/contracts/v1/fixtures/help-root.json +1 -1
  11. package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
  12. package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
  13. package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
  14. package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
  15. package/contracts/v1/fixtures/responses-basic-success.json +11 -0
  16. package/contracts/v1/fixtures/run-basic-success.json +15 -0
  17. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
  18. package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
  19. package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
  20. package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
  21. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  22. package/contracts/v1/manifest.json +30 -0
  23. package/contracts/v1/parity-suite.json +72 -6
  24. package/contracts/v1/schemas/backend-request.schema.json +3 -0
  25. package/contracts/v1/schemas/command-result.schema.json +48 -0
  26. package/contracts/v1/schemas/response.schema.json +28 -0
  27. package/contracts/v1/schemas/run-result.schema.json +84 -0
  28. package/contracts/v1/surface-drift-policy.json +210 -0
  29. package/dist/codex-chatgpt-control-backend.mjs +5137 -488
  30. package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
  31. package/dist/src/backend/client.d.ts +6 -1
  32. package/dist/src/backend/client.js +5 -0
  33. package/dist/src/backend/protocol.d.ts +1 -1
  34. package/dist/src/backend/protocol.js +7 -0
  35. package/dist/src/backend/session.js +14 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/downloads.js +2 -1
  38. package/dist/src/browser/page-state.js +6 -3
  39. package/dist/src/client.d.ts +16 -1
  40. package/dist/src/client.js +86 -7
  41. package/dist/src/commands/artifacts.d.ts +5 -0
  42. package/dist/src/commands/artifacts.js +432 -0
  43. package/dist/src/commands/context.js +5 -2
  44. package/dist/src/commands/doctor.d.ts +10 -2
  45. package/dist/src/commands/doctor.js +242 -14
  46. package/dist/src/commands/files.d.ts +2 -1
  47. package/dist/src/commands/files.js +352 -18
  48. package/dist/src/commands/messages.d.ts +2 -1
  49. package/dist/src/commands/messages.js +232 -40
  50. package/dist/src/commands/modes.js +202 -20
  51. package/dist/src/commands/project-sources.d.ts +10 -0
  52. package/dist/src/commands/project-sources.js +643 -0
  53. package/dist/src/commands/registry.js +76 -14
  54. package/dist/src/commands/reports.d.ts +9 -0
  55. package/dist/src/commands/reports.js +83 -6
  56. package/dist/src/commands/response-actions.js +33 -0
  57. package/dist/src/commands/sequence.js +14 -0
  58. package/dist/src/commands/threads.js +2 -1
  59. package/dist/src/commands/timeouts.d.ts +2 -0
  60. package/dist/src/commands/timeouts.js +18 -0
  61. package/dist/src/diagnostics/blockers.d.ts +54 -0
  62. package/dist/src/diagnostics/blockers.js +299 -0
  63. package/dist/src/dom/artifacts.d.ts +7 -0
  64. package/dist/src/dom/artifacts.js +198 -0
  65. package/dist/src/dom/generation-state.d.ts +8 -0
  66. package/dist/src/dom/generation-state.js +103 -0
  67. package/dist/src/dom/locale/am.d.ts +28 -0
  68. package/dist/src/dom/locale/am.js +28 -0
  69. package/dist/src/dom/locale/ar.d.ts +28 -0
  70. package/dist/src/dom/locale/ar.js +28 -0
  71. package/dist/src/dom/locale/bg.d.ts +28 -0
  72. package/dist/src/dom/locale/bg.js +28 -0
  73. package/dist/src/dom/locale/bn.d.ts +28 -0
  74. package/dist/src/dom/locale/bn.js +28 -0
  75. package/dist/src/dom/locale/bs.d.ts +28 -0
  76. package/dist/src/dom/locale/bs.js +28 -0
  77. package/dist/src/dom/locale/ca.d.ts +28 -0
  78. package/dist/src/dom/locale/ca.js +28 -0
  79. package/dist/src/dom/locale/cs.d.ts +28 -0
  80. package/dist/src/dom/locale/cs.js +28 -0
  81. package/dist/src/dom/locale/da.d.ts +28 -0
  82. package/dist/src/dom/locale/da.js +28 -0
  83. package/dist/src/dom/locale/de.d.ts +30 -0
  84. package/dist/src/dom/locale/de.js +30 -0
  85. package/dist/src/dom/locale/el.d.ts +28 -0
  86. package/dist/src/dom/locale/el.js +28 -0
  87. package/dist/src/dom/locale/en.d.ts +50 -0
  88. package/dist/src/dom/locale/en.js +56 -0
  89. package/dist/src/dom/locale/es-419.d.ts +28 -0
  90. package/dist/src/dom/locale/es-419.js +28 -0
  91. package/dist/src/dom/locale/es-ES.d.ts +28 -0
  92. package/dist/src/dom/locale/es-ES.js +28 -0
  93. package/dist/src/dom/locale/et.d.ts +28 -0
  94. package/dist/src/dom/locale/et.js +28 -0
  95. package/dist/src/dom/locale/fa.d.ts +28 -0
  96. package/dist/src/dom/locale/fa.js +28 -0
  97. package/dist/src/dom/locale/fi.d.ts +28 -0
  98. package/dist/src/dom/locale/fi.js +28 -0
  99. package/dist/src/dom/locale/fr-CA.d.ts +29 -0
  100. package/dist/src/dom/locale/fr-CA.js +29 -0
  101. package/dist/src/dom/locale/fr-FR.d.ts +30 -0
  102. package/dist/src/dom/locale/fr-FR.js +30 -0
  103. package/dist/src/dom/locale/gu.d.ts +28 -0
  104. package/dist/src/dom/locale/gu.js +28 -0
  105. package/dist/src/dom/locale/hi.d.ts +28 -0
  106. package/dist/src/dom/locale/hi.js +28 -0
  107. package/dist/src/dom/locale/hr.d.ts +28 -0
  108. package/dist/src/dom/locale/hr.js +28 -0
  109. package/dist/src/dom/locale/hu.d.ts +28 -0
  110. package/dist/src/dom/locale/hu.js +28 -0
  111. package/dist/src/dom/locale/hy.d.ts +28 -0
  112. package/dist/src/dom/locale/hy.js +28 -0
  113. package/dist/src/dom/locale/id.d.ts +28 -0
  114. package/dist/src/dom/locale/id.js +28 -0
  115. package/dist/src/dom/locale/index.d.ts +79 -0
  116. package/dist/src/dom/locale/index.js +202 -0
  117. package/dist/src/dom/locale/is.d.ts +28 -0
  118. package/dist/src/dom/locale/is.js +28 -0
  119. package/dist/src/dom/locale/it.d.ts +27 -0
  120. package/dist/src/dom/locale/it.js +27 -0
  121. package/dist/src/dom/locale/ja.d.ts +27 -0
  122. package/dist/src/dom/locale/ja.js +27 -0
  123. package/dist/src/dom/locale/ka.d.ts +28 -0
  124. package/dist/src/dom/locale/ka.js +28 -0
  125. package/dist/src/dom/locale/kk.d.ts +28 -0
  126. package/dist/src/dom/locale/kk.js +28 -0
  127. package/dist/src/dom/locale/kn.d.ts +28 -0
  128. package/dist/src/dom/locale/kn.js +28 -0
  129. package/dist/src/dom/locale/ko.d.ts +28 -0
  130. package/dist/src/dom/locale/ko.js +28 -0
  131. package/dist/src/dom/locale/lt.d.ts +28 -0
  132. package/dist/src/dom/locale/lt.js +28 -0
  133. package/dist/src/dom/locale/lv.d.ts +28 -0
  134. package/dist/src/dom/locale/lv.js +28 -0
  135. package/dist/src/dom/locale/mk.d.ts +28 -0
  136. package/dist/src/dom/locale/mk.js +28 -0
  137. package/dist/src/dom/locale/ml.d.ts +28 -0
  138. package/dist/src/dom/locale/ml.js +28 -0
  139. package/dist/src/dom/locale/mn.d.ts +28 -0
  140. package/dist/src/dom/locale/mn.js +28 -0
  141. package/dist/src/dom/locale/mr.d.ts +28 -0
  142. package/dist/src/dom/locale/mr.js +28 -0
  143. package/dist/src/dom/locale/ms.d.ts +28 -0
  144. package/dist/src/dom/locale/ms.js +28 -0
  145. package/dist/src/dom/locale/my.d.ts +28 -0
  146. package/dist/src/dom/locale/my.js +28 -0
  147. package/dist/src/dom/locale/nb.d.ts +28 -0
  148. package/dist/src/dom/locale/nb.js +28 -0
  149. package/dist/src/dom/locale/nl.d.ts +28 -0
  150. package/dist/src/dom/locale/nl.js +28 -0
  151. package/dist/src/dom/locale/pa.d.ts +28 -0
  152. package/dist/src/dom/locale/pa.js +28 -0
  153. package/dist/src/dom/locale/pl.d.ts +28 -0
  154. package/dist/src/dom/locale/pl.js +28 -0
  155. package/dist/src/dom/locale/pt-BR.d.ts +28 -0
  156. package/dist/src/dom/locale/pt-BR.js +28 -0
  157. package/dist/src/dom/locale/pt-PT.d.ts +28 -0
  158. package/dist/src/dom/locale/pt-PT.js +28 -0
  159. package/dist/src/dom/locale/ro.d.ts +28 -0
  160. package/dist/src/dom/locale/ro.js +28 -0
  161. package/dist/src/dom/locale/ru.d.ts +28 -0
  162. package/dist/src/dom/locale/ru.js +28 -0
  163. package/dist/src/dom/locale/sk.d.ts +28 -0
  164. package/dist/src/dom/locale/sk.js +28 -0
  165. package/dist/src/dom/locale/sl.d.ts +28 -0
  166. package/dist/src/dom/locale/sl.js +28 -0
  167. package/dist/src/dom/locale/so.d.ts +28 -0
  168. package/dist/src/dom/locale/so.js +28 -0
  169. package/dist/src/dom/locale/sq.d.ts +28 -0
  170. package/dist/src/dom/locale/sq.js +28 -0
  171. package/dist/src/dom/locale/sr.d.ts +28 -0
  172. package/dist/src/dom/locale/sr.js +28 -0
  173. package/dist/src/dom/locale/sv.d.ts +28 -0
  174. package/dist/src/dom/locale/sv.js +28 -0
  175. package/dist/src/dom/locale/sw.d.ts +28 -0
  176. package/dist/src/dom/locale/sw.js +28 -0
  177. package/dist/src/dom/locale/ta.d.ts +28 -0
  178. package/dist/src/dom/locale/ta.js +28 -0
  179. package/dist/src/dom/locale/te.d.ts +28 -0
  180. package/dist/src/dom/locale/te.js +28 -0
  181. package/dist/src/dom/locale/th.d.ts +28 -0
  182. package/dist/src/dom/locale/th.js +28 -0
  183. package/dist/src/dom/locale/tl.d.ts +28 -0
  184. package/dist/src/dom/locale/tl.js +28 -0
  185. package/dist/src/dom/locale/tr.d.ts +28 -0
  186. package/dist/src/dom/locale/tr.js +28 -0
  187. package/dist/src/dom/locale/types.d.ts +45 -0
  188. package/dist/src/dom/locale/types.js +1 -0
  189. package/dist/src/dom/locale/uk.d.ts +28 -0
  190. package/dist/src/dom/locale/uk.js +28 -0
  191. package/dist/src/dom/locale/ur.d.ts +28 -0
  192. package/dist/src/dom/locale/ur.js +28 -0
  193. package/dist/src/dom/locale/vi.d.ts +28 -0
  194. package/dist/src/dom/locale/vi.js +28 -0
  195. package/dist/src/dom/locale/zh-HK.d.ts +28 -0
  196. package/dist/src/dom/locale/zh-HK.js +28 -0
  197. package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
  198. package/dist/src/dom/locale/zh-Hans.js +28 -0
  199. package/dist/src/dom/locale/zh-TW.d.ts +29 -0
  200. package/dist/src/dom/locale/zh-TW.js +29 -0
  201. package/dist/src/dom/locale-labels.d.ts +26 -0
  202. package/dist/src/dom/locale-labels.js +26 -0
  203. package/dist/src/dom/menus.d.ts +6 -0
  204. package/dist/src/dom/menus.js +34 -8
  205. package/dist/src/dom/message-format.d.ts +2 -1
  206. package/dist/src/dom/message-format.js +67 -10
  207. package/dist/src/dom/messages.d.ts +2 -1
  208. package/dist/src/dom/messages.js +2 -4
  209. package/dist/src/dom/selectors.d.ts +1 -0
  210. package/dist/src/dom/selectors.js +42 -15
  211. package/dist/src/index.d.ts +5 -0
  212. package/dist/src/index.js +5 -0
  213. package/dist/src/platform/local-paths.d.ts +4 -0
  214. package/dist/src/platform/local-paths.js +20 -0
  215. package/dist/src/runner/interruptions.js +15 -4
  216. package/dist/src/runner/responses.js +18 -1
  217. package/dist/src/runner/result.js +15 -0
  218. package/dist/src/runner/resume.js +1 -0
  219. package/dist/src/runner/types.d.ts +3 -0
  220. package/dist/src/safety/risk.d.ts +7 -0
  221. package/dist/src/safety/risk.js +7 -0
  222. package/dist/src/safety/untrusted-output.d.ts +73 -0
  223. package/dist/src/safety/untrusted-output.js +199 -0
  224. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  225. package/dist/src/scripts/apply-intelligence-locale-captures.js +184 -0
  226. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  227. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  228. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  229. package/dist/src/scripts/check-surface-drift.js +11 -0
  230. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  231. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  232. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  233. package/dist/src/testing/surface-drift.d.ts +66 -0
  234. package/dist/src/testing/surface-drift.js +363 -0
  235. package/dist/src/types.d.ts +201 -2
  236. package/package.json +2 -1
  237. package/references/backend-protocol.md +93 -1
  238. package/references/language-coverage.md +142 -0
  239. package/references/localization.md +247 -0
  240. package/references/python-parity.md +63 -1
  241. package/references/safety.md +2 -0
  242. package/references/troubleshooting.md +58 -0
@@ -0,0 +1,643 @@
1
+ import { localeLabels, anyLabelPattern } from "../dom/locale-labels.js";
2
+ import { resultError, resultOk } from "../errors.js";
3
+ import { contextFromPage } from "./context.js";
4
+ import { preflightFiles } from "./files.js";
5
+ import { bootstrap } from "./session.js";
6
+ const CHATGPT_ORIGIN = "https://chatgpt.com";
7
+ const DEFAULT_PROJECT_SOURCE_BATCH_SIZE = 10;
8
+ const PROJECT_SOURCE_CANDIDATE_LIMIT = 20;
9
+ export function normalizeProjectSourcesUrl(value) {
10
+ let parsed;
11
+ try {
12
+ parsed = new URL(value);
13
+ }
14
+ catch {
15
+ throw new Error("ChatGPT Project URL must be an absolute URL.");
16
+ }
17
+ if (parsed.protocol !== "https:") {
18
+ throw new Error("ChatGPT Project URL must use https.");
19
+ }
20
+ if (parsed.hostname !== "chatgpt.com") {
21
+ throw new Error("ChatGPT Project URL must be on chatgpt.com.");
22
+ }
23
+ const segments = parsed.pathname.split("/").filter(Boolean);
24
+ const gIndex = segments.indexOf("g");
25
+ const handle = gIndex >= 0 ? segments[gIndex + 1] : undefined;
26
+ if (handle === undefined || !handle.startsWith("g-p-")) {
27
+ throw new Error("ChatGPT Project URL must include a Project path such as /g/g-p-.../project.");
28
+ }
29
+ const { projectId, projectSlug } = splitProjectHandle(handle);
30
+ const normalized = {
31
+ projectId,
32
+ url: `${CHATGPT_ORIGIN}/g/${handle}/project`
33
+ };
34
+ if (projectSlug !== undefined) {
35
+ normalized.projectSlug = projectSlug;
36
+ }
37
+ return normalized;
38
+ }
39
+ export async function buildProjectSourceAddPlan(env, args) {
40
+ let project;
41
+ try {
42
+ project = normalizeProjectSourcesUrl(args.projectUrl);
43
+ }
44
+ catch (error) {
45
+ return resultError(error instanceof Error ? error : new Error(String(error)));
46
+ }
47
+ const preflightArgs = { paths: args.files };
48
+ if (args.maxBytesPerFile !== undefined)
49
+ preflightArgs.maxBytesPerFile = args.maxBytesPerFile;
50
+ if (args.maxTotalBytes !== undefined)
51
+ preflightArgs.maxTotalBytes = args.maxTotalBytes;
52
+ const preflight = await preflightFiles(env, preflightArgs);
53
+ if (!preflight.ok || preflight.data === undefined) {
54
+ return preflight;
55
+ }
56
+ const files = preflight.data.files.map((file, index) => ({
57
+ ...file,
58
+ displayPath: args.files[index] ?? file.path
59
+ }));
60
+ const batchSize = normalizedBatchSize(args.batchSize);
61
+ const batches = [];
62
+ for (let offset = 0; offset < files.length; offset += batchSize) {
63
+ const batchFiles = files.slice(offset, offset + batchSize);
64
+ batches.push({
65
+ index: batches.length,
66
+ files: batchFiles,
67
+ totalBytes: batchFiles.reduce((sum, file) => sum + file.bytes, 0)
68
+ });
69
+ }
70
+ return resultOk({
71
+ ...project,
72
+ projectUrl: project.url,
73
+ operation: "append_add",
74
+ dryRun: true,
75
+ files,
76
+ batches,
77
+ totalBytes: preflight.data.totalBytes
78
+ }, { timestamp: preflight.context.timestamp }, preflight.warnings);
79
+ }
80
+ export async function listProjectSources(env, args) {
81
+ const opened = await openProjectSourcesUI(env, args);
82
+ if (!opened.ok || opened.data === undefined) {
83
+ return opened;
84
+ }
85
+ return readProjectSourcesFromCurrentPage(env, opened.data, "project_sources_list_unavailable");
86
+ }
87
+ export async function addProjectSources(env, args) {
88
+ const plan = await buildProjectSourceAddPlan(env, args);
89
+ if (!plan.ok || plan.data === undefined) {
90
+ return plan;
91
+ }
92
+ if (args.confirmMutation !== true) {
93
+ return {
94
+ ok: false,
95
+ status: "needs_confirmation",
96
+ data: plan.data,
97
+ warnings: plan.warnings,
98
+ blocker: {
99
+ kind: "confirmation",
100
+ code: "project_sources_add_confirmation_required",
101
+ fieldPath: "confirmMutation",
102
+ message: "Adding files to a ChatGPT Project Sources list mutates visible project state. Re-run with confirmMutation: true after user approval.",
103
+ remediation: [
104
+ {
105
+ label: "Confirm Project Sources add",
106
+ instruction: "Ask the user to confirm this append-only Project Sources add operation for the listed local file names.",
107
+ userActionRequired: true
108
+ }
109
+ ],
110
+ resumable: true
111
+ },
112
+ context: plan.context
113
+ };
114
+ }
115
+ const opened = await openProjectSourcesUI(env, args);
116
+ if (!opened.ok || opened.data === undefined) {
117
+ return opened;
118
+ }
119
+ const before = await readProjectSourcesFromCurrentPage(env, opened.data, "project_sources_list_unavailable");
120
+ if (!before.ok || before.data === undefined) {
121
+ return before;
122
+ }
123
+ const page = env.page;
124
+ if (page === undefined) {
125
+ return resultError(new Error("No active ChatGPT Project page is available for Project Sources upload."), opened.context);
126
+ }
127
+ for (const batch of plan.data.batches) {
128
+ const upload = await uploadProjectSourceBatch(page, batch, args.timeoutMs ?? 120000);
129
+ if (!upload.ok) {
130
+ return upload;
131
+ }
132
+ }
133
+ await page.waitForTimeout?.(1000);
134
+ const after = await readProjectSourcesFromCurrentPage(env, opened.data, "project_sources_after_add_unavailable");
135
+ if (!after.ok || after.data === undefined) {
136
+ return after;
137
+ }
138
+ return resultOk({
139
+ ...plan.data,
140
+ dryRun: false,
141
+ before: before.data.sources,
142
+ after: after.data.sources,
143
+ added: diffProjectSourceNames(before.data.sources, after.data.sources)
144
+ }, await contextFromPage(page), [...plan.warnings, ...before.warnings, ...after.warnings]);
145
+ }
146
+ export function diffProjectSourceNames(before, after) {
147
+ const remaining = new Map();
148
+ for (const source of before) {
149
+ const key = sourceNameKey(source.name);
150
+ remaining.set(key, (remaining.get(key) ?? 0) + 1);
151
+ }
152
+ const added = [];
153
+ for (const source of after) {
154
+ const key = sourceNameKey(source.name);
155
+ const count = remaining.get(key) ?? 0;
156
+ if (count > 0) {
157
+ remaining.set(key, count - 1);
158
+ }
159
+ else {
160
+ added.push(source);
161
+ }
162
+ }
163
+ return added;
164
+ }
165
+ export function extractProjectSourcesFromHtml(html) {
166
+ const sources = [];
167
+ const sourceBlockPattern = /<(?:div|li|article|tr)\b(?<attrs>[^>]*(?:data-testid|aria-label|class)=["'][^"']*source[^"']*["'][^>]*)>(?<body>[\s\S]*?)<\/(?:div|li|article|tr)>/gi;
168
+ for (const match of html.matchAll(sourceBlockPattern)) {
169
+ const body = match.groups?.body ?? "";
170
+ const texts = extractChildTexts(body, ["span", "td", "button", "a"]);
171
+ const name = texts.map(sourceNameFromCandidateText).find((text) => text !== undefined);
172
+ if (name === undefined) {
173
+ continue;
174
+ }
175
+ const statusText = texts.find(text => text !== name && normalizeProjectSourceStatus(text) !== "unknown");
176
+ sources.push({ name, status: normalizeProjectSourceStatus(statusText ?? "") });
177
+ }
178
+ sources.push(...extractSourcesSectionRowsFromHtml(html));
179
+ return dedupeAdjacentSources(sources);
180
+ }
181
+ export function safeProjectSourceCandidatesFromHtml(html) {
182
+ const candidates = [];
183
+ const interactivePattern = /<(button|a)\b(?<attrs>[^>]*)>(?<body>[\s\S]*?)<\/\1>/gi;
184
+ for (const match of html.matchAll(interactivePattern)) {
185
+ const tag = match[1]?.toLowerCase();
186
+ const attrs = match.groups?.attrs ?? "";
187
+ const text = normalizeText(stripTags(match.groups?.body ?? ""));
188
+ const label = normalizeText(attr(attrs, "aria-label") ?? attr(attrs, "title") ?? text);
189
+ if (label.length === 0 || label.length > 120) {
190
+ continue;
191
+ }
192
+ const roleAttr = attr(attrs, "role");
193
+ const role = roleAttr ?? (tag === "a" ? "link" : "button");
194
+ candidates.push({ label, role });
195
+ }
196
+ return dedupeCandidates(candidates).slice(0, PROJECT_SOURCE_CANDIDATE_LIMIT);
197
+ }
198
+ async function openProjectSourcesUI(env, args) {
199
+ let project;
200
+ try {
201
+ project = normalizeProjectSourcesUrl(args.projectUrl);
202
+ }
203
+ catch (error) {
204
+ return resultError(error instanceof Error ? error : new Error(String(error)));
205
+ }
206
+ if (env.page === undefined) {
207
+ const boot = await bootstrap(env, bootstrapArgsForProject(project.url, args));
208
+ if (!boot.ok) {
209
+ return boot;
210
+ }
211
+ }
212
+ const page = env.page;
213
+ if (page === undefined) {
214
+ return resultError(new Error("No active ChatGPT page is available."), { timestamp: new Date().toISOString() });
215
+ }
216
+ try {
217
+ const currentUrl = await Promise.resolve(page.url?.()).catch(() => undefined);
218
+ if (!sameProjectPageUrl(currentUrl, project.url) && typeof page.goto === "function") {
219
+ await page.goto(project.url, { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
220
+ await page.waitForTimeout?.(500);
221
+ }
222
+ await clickSourcesTabIfAvailable(page, args.timeoutMs ?? 30000);
223
+ return resultOk(project, await contextFromPage(page));
224
+ }
225
+ catch (error) {
226
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
227
+ }
228
+ }
229
+ function bootstrapArgsForProject(url, args) {
230
+ const boot = { url };
231
+ if (args.preferExistingTab !== undefined) {
232
+ boot.preferExistingTab = args.preferExistingTab;
233
+ }
234
+ if (args.existingTab === true) {
235
+ boot.existingTab = {
236
+ target: { type: "url", url },
237
+ ifMissing: "block",
238
+ ifMultiple: "block",
239
+ requireChatGPT: true
240
+ };
241
+ }
242
+ else if (args.existingTab !== undefined) {
243
+ boot.existingTab = args.existingTab;
244
+ }
245
+ return boot;
246
+ }
247
+ async function readProjectSourcesFromCurrentPage(env, project, driftCode) {
248
+ const page = env.page;
249
+ if (page === undefined) {
250
+ return resultError(new Error("No active ChatGPT Project page is available for Project Sources listing."), project);
251
+ }
252
+ const snapshot = await readProjectSourcesSnapshot(page);
253
+ if (snapshot.uiPresent) {
254
+ return resultOk({ ...project, sources: snapshot.sources }, await contextFromPage(page));
255
+ }
256
+ return {
257
+ ok: false,
258
+ status: "unsupported",
259
+ warnings: [],
260
+ blocker: {
261
+ kind: "selector_drift",
262
+ code: driftCode,
263
+ message: "The visible ChatGPT Project Sources UI could not be identified without reading source contents.",
264
+ candidates: snapshot.candidates,
265
+ resumable: true
266
+ },
267
+ context: await contextFromPage(page)
268
+ };
269
+ }
270
+ async function readProjectSourcesSnapshot(page) {
271
+ if (typeof page.evaluate === "function") {
272
+ try {
273
+ const raw = await page.evaluate(() => {
274
+ const normalize = (value) => value.replace(/\s+/g, " ").trim();
275
+ const textOf = (element) => normalize(element.innerText ?? element.textContent ?? "");
276
+ const statusFor = (text) => {
277
+ if (/\b(ready|added|available|synced)\b/i.test(text))
278
+ return "ready";
279
+ if (/\b(processing|uploading|adding|pending|in progress)\b/i.test(text))
280
+ return "processing";
281
+ if (/\b(failed|error|unsupported)\b/i.test(text))
282
+ return "failed";
283
+ return "unknown";
284
+ };
285
+ const excludedLabel = (text) => /^(ready|processing|uploading|failed|error|add sources?|sources?|newest|all|source actions)$/i.test(text)
286
+ || /^(sort|filter) sources?:/i.test(text);
287
+ const sourceNameFromCandidate = (text) => {
288
+ const normalized = normalize(text).replace(/\s+(Document|File|PDF|Image|Spreadsheet|Text|Code|CSV|Markdown)\s+·.*$/i, "");
289
+ if (normalized.length === 0 || normalized.length > 160 || excludedLabel(normalized))
290
+ return undefined;
291
+ return normalized;
292
+ };
293
+ const sourceNodes = Array.from(document.querySelectorAll([
294
+ "li[data-testid*='source' i]",
295
+ "article[data-testid*='source' i]",
296
+ "tr[data-testid*='source' i]",
297
+ "div[data-testid*='source' i]",
298
+ "li[aria-label*='source' i]",
299
+ "article[aria-label*='source' i]",
300
+ "tr[aria-label*='source' i]",
301
+ "div[aria-label*='source' i]",
302
+ "li[class*='source' i]",
303
+ "article[class*='source' i]",
304
+ "tr[class*='source' i]",
305
+ "div[class*='source' i]"
306
+ ].join(", "))).filter(node => {
307
+ const tag = node.tagName.toLowerCase();
308
+ const role = node.getAttribute("role") ?? "";
309
+ return tag !== "button"
310
+ && tag !== "a"
311
+ && !/^(button|tab|menuitem|option|dialog|menu)$/i.test(role)
312
+ && node.closest("[role='dialog'], [role='menu']") === null;
313
+ });
314
+ const sourcesFromNodes = sourceNodes.flatMap(node => {
315
+ const children = Array.from(node.querySelectorAll("span, td, button, a"))
316
+ .map(textOf)
317
+ .filter(Boolean);
318
+ const name = children.map(sourceNameFromCandidate).find(Boolean);
319
+ if (!name)
320
+ return [];
321
+ const statusText = children.find(child => child !== name && statusFor(child) !== "unknown") ?? "";
322
+ return [{ name, status: statusFor(statusText) }];
323
+ });
324
+ const sourcesFromSection = Array.from(document.querySelectorAll("section[aria-label]"))
325
+ .filter(section => /sources?/i.test(section.getAttribute("aria-label") ?? "") && section.closest("[role='dialog'], [role='menu']") === null)
326
+ .flatMap(section => Array.from(section.querySelectorAll("[aria-label], button")).flatMap(element => {
327
+ const role = element.getAttribute("role") ?? "";
328
+ if (/^(tab|menuitem|option)$/i.test(role))
329
+ return [];
330
+ const aria = normalize(element.getAttribute("aria-label") ?? "");
331
+ const text = textOf(element);
332
+ const name = sourceNameFromCandidate(aria) ?? sourceNameFromCandidate(text);
333
+ if (!name)
334
+ return [];
335
+ const status = statusFor(text);
336
+ return [{ name, status }];
337
+ }));
338
+ const sources = [...sourcesFromNodes, ...sourcesFromSection];
339
+ const candidates = Array.from(document.querySelectorAll("[role='tab'], button, a"))
340
+ .map(element => {
341
+ const label = normalize(element.getAttribute("aria-label") ?? element.getAttribute("title") ?? textOf(element));
342
+ const role = element.getAttribute("role") ?? (element.tagName.toLowerCase() === "a" ? "link" : "button");
343
+ return { label, role };
344
+ })
345
+ .filter(candidate => candidate.label.length > 0 && candidate.label.length <= 120)
346
+ .slice(0, 20);
347
+ const activeSourceTab = Array.from(document.querySelectorAll("[role='tab'], button"))
348
+ .some(element => {
349
+ const label = textOf(element) || element.getAttribute("aria-label") || "";
350
+ return /sources?/i.test(label) && element.getAttribute("aria-selected") === "true";
351
+ });
352
+ const emptyState = /\bno sources\b/i.test(document.body?.innerText ?? "");
353
+ return {
354
+ sources,
355
+ uiPresent: sources.length > 0 || activeSourceTab || emptyState,
356
+ candidates
357
+ };
358
+ });
359
+ return normalizeSnapshot(raw);
360
+ }
361
+ catch {
362
+ // Fall through to HTML extraction.
363
+ }
364
+ }
365
+ if (typeof page.content === "function") {
366
+ const html = await page.content();
367
+ const sources = extractProjectSourcesFromHtml(html);
368
+ const activeSourceTab = /role=["']tab["'][^>]*aria-selected=["']true["'][^>]*>\s*Sources\s*</i.test(html)
369
+ || /aria-label=["']Sources["'][^>]*aria-selected=["']true["']/i.test(html);
370
+ const emptyState = /\bno sources\b/i.test(stripInteractiveHtml(html));
371
+ return {
372
+ sources,
373
+ uiPresent: sources.length > 0 || activeSourceTab || emptyState,
374
+ candidates: safeProjectSourceCandidatesFromHtml(html)
375
+ };
376
+ }
377
+ return { sources: [], uiPresent: false, candidates: [] };
378
+ }
379
+ function normalizeSnapshot(raw) {
380
+ if (raw === null || typeof raw !== "object") {
381
+ return { sources: [], uiPresent: false, candidates: [] };
382
+ }
383
+ const record = raw;
384
+ const sources = Array.isArray(record.sources)
385
+ ? record.sources.flatMap(item => isRecord(item) && typeof item.name === "string"
386
+ ? [{ name: normalizeText(item.name), status: normalizeProjectSourceStatus(String(item.status ?? "")) }]
387
+ : [])
388
+ : [];
389
+ const candidates = Array.isArray(record.candidates)
390
+ ? dedupeCandidates(record.candidates.flatMap(item => {
391
+ if (!isRecord(item) || typeof item.label !== "string") {
392
+ return [];
393
+ }
394
+ const candidate = { label: normalizeText(item.label) };
395
+ if (typeof item.role === "string") {
396
+ candidate.role = item.role;
397
+ }
398
+ return [candidate];
399
+ }))
400
+ : [];
401
+ return {
402
+ sources: dedupeAdjacentSources(sources.filter(source => source.name.length > 0)),
403
+ uiPresent: record.uiPresent === true,
404
+ candidates: candidates.slice(0, PROJECT_SOURCE_CANDIDATE_LIMIT)
405
+ };
406
+ }
407
+ async function uploadProjectSourceBatch(page, batch, timeoutMs) {
408
+ const paths = batch.files.map(file => file.path);
409
+ try {
410
+ const directInput = page.locator?.("input[type='file']");
411
+ if (directInput !== undefined && typeof directInput.setInputFiles === "function" && await locatorCount(directInput) > 0) {
412
+ await directInput.setInputFiles(paths);
413
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
414
+ }
415
+ if (typeof page.waitForEvent !== "function") {
416
+ throw new Error("The active Project Sources page does not expose file chooser events.");
417
+ }
418
+ let chooserPromise = waitForFileChooser(page, timeoutMs);
419
+ await clickProjectSourceControl(page, localeLabels.projectSourcesAddSource, "button", timeoutMs);
420
+ const opened = await raceFileChooserOpen(chooserPromise, page, 300);
421
+ if (!opened) {
422
+ chooserPromise = waitForFileChooser(page, timeoutMs);
423
+ await clickProjectSourceControl(page, localeLabels.projectSourcesUploadFiles, "button", timeoutMs);
424
+ }
425
+ const chooser = await chooserPromise;
426
+ await chooser.setFiles(paths);
427
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
428
+ }
429
+ catch (error) {
430
+ return {
431
+ ok: false,
432
+ status: "blocked",
433
+ warnings: [],
434
+ blocker: {
435
+ kind: "permission",
436
+ code: "project_sources_upload_unavailable",
437
+ message: `Project Sources file upload could not be completed through visible file chooser controls: ${error instanceof Error ? error.message : String(error)}`,
438
+ remediation: [
439
+ {
440
+ label: "Use visible Project Sources UI",
441
+ instruction: "Open the Project Sources tab, click Add source, choose the local file upload option, and retry after the browser file chooser is available.",
442
+ userActionRequired: true
443
+ }
444
+ ],
445
+ resumable: true
446
+ },
447
+ context: await contextFromPage(page)
448
+ };
449
+ }
450
+ }
451
+ async function clickSourcesTabIfAvailable(page, timeoutMs) {
452
+ try {
453
+ await clickProjectSourceControl(page, localeLabels.projectSourcesTab, "tab", timeoutMs);
454
+ }
455
+ catch {
456
+ // Listing will report selector_drift with safe candidates if the tab is not discoverable.
457
+ }
458
+ }
459
+ async function clickProjectSourceControl(page, labels, role, timeoutMs) {
460
+ const locator = page.getByRole?.(role, { name: anyLabelPattern(labels) });
461
+ if (locator !== undefined && await locatorCount(locator) > 0) {
462
+ await (locator.first?.() ?? locator).click?.({ timeout: Math.min(timeoutMs, 10000) });
463
+ await page.waitForTimeout?.(250);
464
+ return;
465
+ }
466
+ const selector = labels
467
+ .map(label => `button[aria-label*='${cssString(label)}'], [role='${role}'][aria-label*='${cssString(label)}']`)
468
+ .join(", ");
469
+ const fallback = page.locator?.(selector);
470
+ if (fallback !== undefined && await locatorCount(fallback) > 0) {
471
+ await (fallback.first?.() ?? fallback).click?.({ timeout: Math.min(timeoutMs, 10000) });
472
+ await page.waitForTimeout?.(250);
473
+ return;
474
+ }
475
+ throw new Error(`Project Sources ${role} was not available for labels: ${labels.join(", ")}`);
476
+ }
477
+ async function waitForFileChooser(page, timeoutMs) {
478
+ const rawChooser = await page.waitForEvent?.("filechooser", { timeout: timeoutMs, timeoutMs });
479
+ if (rawChooser === null || typeof rawChooser !== "object" || typeof rawChooser.setFiles !== "function") {
480
+ throw new Error("Project Sources file chooser did not expose setFiles().");
481
+ }
482
+ return rawChooser;
483
+ }
484
+ async function raceFileChooserOpen(chooserPromise, page, waitMs) {
485
+ return Promise.race([
486
+ chooserPromise.then(() => true, () => false),
487
+ (page.waitForTimeout?.(waitMs) ?? new Promise(resolve => setTimeout(resolve, waitMs))).then(() => false)
488
+ ]);
489
+ }
490
+ async function locatorCount(locator) {
491
+ if (typeof locator.count !== "function") {
492
+ return 0;
493
+ }
494
+ return locator.count();
495
+ }
496
+ function normalizedBatchSize(value) {
497
+ if (value === undefined) {
498
+ return DEFAULT_PROJECT_SOURCE_BATCH_SIZE;
499
+ }
500
+ return Number.isInteger(value) && value > 0 ? value : DEFAULT_PROJECT_SOURCE_BATCH_SIZE;
501
+ }
502
+ function splitProjectHandle(handle) {
503
+ const match = /^(g-p-[0-9a-f]{16,})(?:-(.+))?$/i.exec(handle);
504
+ if (match === null) {
505
+ return { projectId: handle };
506
+ }
507
+ const result = { projectId: match[1] };
508
+ if (match[2] !== undefined && match[2].length > 0) {
509
+ result.projectSlug = match[2];
510
+ }
511
+ return result;
512
+ }
513
+ function sameProjectPageUrl(current, expected) {
514
+ if (current === undefined) {
515
+ return false;
516
+ }
517
+ try {
518
+ const currentUrl = new URL(current);
519
+ const expectedUrl = new URL(expected);
520
+ return currentUrl.origin === expectedUrl.origin
521
+ && trimTrailingSlash(currentUrl.pathname) === trimTrailingSlash(expectedUrl.pathname);
522
+ }
523
+ catch {
524
+ return false;
525
+ }
526
+ }
527
+ function trimTrailingSlash(value) {
528
+ return value.endsWith("/") ? value.slice(0, -1) : value;
529
+ }
530
+ function sourceNameKey(value) {
531
+ return normalizeText(value).toLocaleLowerCase();
532
+ }
533
+ function extractChildTexts(html, tags) {
534
+ const tagPattern = tags.join("|");
535
+ const pattern = new RegExp(`<(${tagPattern})\\b[^>]*>([\\s\\S]*?)<\\/\\1>`, "gi");
536
+ return Array.from(html.matchAll(pattern))
537
+ .map(match => normalizeText(stripTags(match[2] ?? "")))
538
+ .filter(Boolean);
539
+ }
540
+ function extractAttrValues(html, name) {
541
+ const pattern = new RegExp(`${name}=["']([^"']+)["']`, "gi");
542
+ return Array.from(html.matchAll(pattern))
543
+ .map(match => normalizeText(match[1] ?? ""))
544
+ .filter(Boolean);
545
+ }
546
+ function looksLikeSourceName(text) {
547
+ return text.length > 0
548
+ && text.length <= 160
549
+ && !/^(ready|processing|uploading|failed|error|add sources?|sources?|newest|all|source actions)$/i.test(text)
550
+ && !/^(sort|filter) sources?:/i.test(text);
551
+ }
552
+ function sourceNameFromCandidateText(text) {
553
+ const name = normalizeText(text)
554
+ .replace(/\s+(Document|File|PDF|Image|Spreadsheet|Text|Code|CSV|Markdown)\s+·.*$/i, "");
555
+ return looksLikeSourceName(name) ? name : undefined;
556
+ }
557
+ function extractSourcesSectionRowsFromHtml(html) {
558
+ const sources = [];
559
+ const sectionPattern = /<section\b(?<attrs>[^>]*aria-label=["']Sources["'][^>]*)>(?<body>[\s\S]*?)<\/section>/gi;
560
+ for (const match of html.matchAll(sectionPattern)) {
561
+ const body = match.groups?.body ?? "";
562
+ const texts = [
563
+ ...extractAttrValues(body, "aria-label"),
564
+ ...extractChildTexts(body, ["button"])
565
+ ];
566
+ for (const text of texts) {
567
+ const name = sourceNameFromCandidateText(text);
568
+ if (name !== undefined) {
569
+ sources.push({ name, status: normalizeProjectSourceStatus(text) });
570
+ }
571
+ }
572
+ }
573
+ return dedupeAdjacentSources(sources);
574
+ }
575
+ function normalizeProjectSourceStatus(value) {
576
+ if (/\b(ready|added|available|synced)\b/i.test(value))
577
+ return "ready";
578
+ if (/\b(processing|uploading|adding|pending|in progress)\b/i.test(value))
579
+ return "processing";
580
+ if (/\b(failed|error|unsupported)\b/i.test(value))
581
+ return "failed";
582
+ return "unknown";
583
+ }
584
+ function dedupeAdjacentSources(sources) {
585
+ const deduped = [];
586
+ for (const source of sources) {
587
+ const previous = deduped.at(-1);
588
+ if (previous?.name === source.name && previous.status === source.status) {
589
+ continue;
590
+ }
591
+ deduped.push(source);
592
+ }
593
+ return deduped;
594
+ }
595
+ function dedupeCandidates(candidates) {
596
+ const seen = new Set();
597
+ const deduped = [];
598
+ for (const candidate of candidates) {
599
+ const label = normalizeText(candidate.label);
600
+ if (label.length === 0) {
601
+ continue;
602
+ }
603
+ const role = candidate.role === undefined ? undefined : normalizeText(candidate.role);
604
+ const key = `${role ?? ""}\0${label}`;
605
+ if (seen.has(key)) {
606
+ continue;
607
+ }
608
+ seen.add(key);
609
+ const item = { label };
610
+ if (role !== undefined && role.length > 0) {
611
+ item.role = role;
612
+ }
613
+ deduped.push(item);
614
+ }
615
+ return deduped;
616
+ }
617
+ function attr(attrs, name) {
618
+ const pattern = new RegExp(`${name}=["']([^"']+)["']`, "i");
619
+ return pattern.exec(attrs)?.[1];
620
+ }
621
+ function stripInteractiveHtml(html) {
622
+ return html.replace(/<(button|a)\b[\s\S]*?<\/\1>/gi, " ");
623
+ }
624
+ function stripTags(value) {
625
+ return value.replace(/<[^>]+>/g, " ");
626
+ }
627
+ function normalizeText(value) {
628
+ return decodeEntities(value).replace(/\s+/g, " ").trim();
629
+ }
630
+ function decodeEntities(value) {
631
+ return value
632
+ .replace(/&amp;/g, "&")
633
+ .replace(/&lt;/g, "<")
634
+ .replace(/&gt;/g, ">")
635
+ .replace(/&quot;/g, "\"")
636
+ .replace(/&#39;/g, "'");
637
+ }
638
+ function cssString(value) {
639
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
640
+ }
641
+ function isRecord(value) {
642
+ return value !== null && typeof value === "object" && !Array.isArray(value);
643
+ }