thehood 0.1.0-preview.0

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 (274) hide show
  1. package/CODE_OF_CONDUCT.md +21 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/PRIVACY.md +49 -0
  5. package/README.md +264 -0
  6. package/SECURITY.md +31 -0
  7. package/dist/bridges/chatgptWebBridge.d.ts +2 -0
  8. package/dist/bridges/chatgptWebBridge.js +981 -0
  9. package/dist/bridges/chatgptWebBridge.js.map +1 -0
  10. package/dist/cli/args.d.ts +9 -0
  11. package/dist/cli/args.js +82 -0
  12. package/dist/cli/args.js.map +1 -0
  13. package/dist/cli/format.d.ts +56 -0
  14. package/dist/cli/format.js +752 -0
  15. package/dist/cli/format.js.map +1 -0
  16. package/dist/cli/main.d.ts +2 -0
  17. package/dist/cli/main.js +996 -0
  18. package/dist/cli/main.js.map +1 -0
  19. package/dist/cli/mcpConfig.d.ts +36 -0
  20. package/dist/cli/mcpConfig.js +98 -0
  21. package/dist/cli/mcpConfig.js.map +1 -0
  22. package/dist/index.d.ts +37 -0
  23. package/dist/index.js +38 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/mcp/protocol.d.ts +44 -0
  26. package/dist/mcp/protocol.js +33 -0
  27. package/dist/mcp/protocol.js.map +1 -0
  28. package/dist/mcp/server.d.ts +1 -0
  29. package/dist/mcp/server.js +106 -0
  30. package/dist/mcp/server.js.map +1 -0
  31. package/dist/mcp/tools.d.ts +10 -0
  32. package/dist/mcp/tools.js +2200 -0
  33. package/dist/mcp/tools.js.map +1 -0
  34. package/dist/mcp/validation.d.ts +8 -0
  35. package/dist/mcp/validation.js +67 -0
  36. package/dist/mcp/validation.js.map +1 -0
  37. package/dist/providers/chatgptWeb.d.ts +2 -0
  38. package/dist/providers/chatgptWeb.js +26 -0
  39. package/dist/providers/chatgptWeb.js.map +1 -0
  40. package/dist/providers/claudeCode.d.ts +4 -0
  41. package/dist/providers/claudeCode.js +32 -0
  42. package/dist/providers/claudeCode.js.map +1 -0
  43. package/dist/providers/codexCli.d.ts +6 -0
  44. package/dist/providers/codexCli.js +25 -0
  45. package/dist/providers/codexCli.js.map +1 -0
  46. package/dist/providers/codexCliModels.d.ts +23 -0
  47. package/dist/providers/codexCliModels.js +147 -0
  48. package/dist/providers/codexCliModels.js.map +1 -0
  49. package/dist/providers/localCommand.d.ts +26 -0
  50. package/dist/providers/localCommand.js +614 -0
  51. package/dist/providers/localCommand.js.map +1 -0
  52. package/dist/providers/markdownPayload.d.ts +7 -0
  53. package/dist/providers/markdownPayload.js +29 -0
  54. package/dist/providers/markdownPayload.js.map +1 -0
  55. package/dist/providers/responseSchema.d.ts +3 -0
  56. package/dist/providers/responseSchema.js +187 -0
  57. package/dist/providers/responseSchema.js.map +1 -0
  58. package/dist/providers/router.d.ts +3 -0
  59. package/dist/providers/router.js +21 -0
  60. package/dist/providers/router.js.map +1 -0
  61. package/dist/providers/stub.d.ts +2 -0
  62. package/dist/providers/stub.js +177 -0
  63. package/dist/providers/stub.js.map +1 -0
  64. package/dist/providers/types.d.ts +37 -0
  65. package/dist/providers/types.js +2 -0
  66. package/dist/providers/types.js.map +1 -0
  67. package/dist/runtime/agentBoard.d.ts +79 -0
  68. package/dist/runtime/agentBoard.js +166 -0
  69. package/dist/runtime/agentBoard.js.map +1 -0
  70. package/dist/runtime/agentBoardArtifact.d.ts +9 -0
  71. package/dist/runtime/agentBoardArtifact.js +171 -0
  72. package/dist/runtime/agentBoardArtifact.js.map +1 -0
  73. package/dist/runtime/agentRunner.d.ts +17 -0
  74. package/dist/runtime/agentRunner.js +92 -0
  75. package/dist/runtime/agentRunner.js.map +1 -0
  76. package/dist/runtime/approvalInbox.d.ts +54 -0
  77. package/dist/runtime/approvalInbox.js +143 -0
  78. package/dist/runtime/approvalInbox.js.map +1 -0
  79. package/dist/runtime/approvalPolicy.d.ts +11 -0
  80. package/dist/runtime/approvalPolicy.js +58 -0
  81. package/dist/runtime/approvalPolicy.js.map +1 -0
  82. package/dist/runtime/artifacts.d.ts +23 -0
  83. package/dist/runtime/artifacts.js +48 -0
  84. package/dist/runtime/artifacts.js.map +1 -0
  85. package/dist/runtime/browserManager.d.ts +37 -0
  86. package/dist/runtime/browserManager.js +356 -0
  87. package/dist/runtime/browserManager.js.map +1 -0
  88. package/dist/runtime/canonicalMemory.d.ts +23 -0
  89. package/dist/runtime/canonicalMemory.js +134 -0
  90. package/dist/runtime/canonicalMemory.js.map +1 -0
  91. package/dist/runtime/chatGptPageReadiness.d.ts +16 -0
  92. package/dist/runtime/chatGptPageReadiness.js +74 -0
  93. package/dist/runtime/chatGptPageReadiness.js.map +1 -0
  94. package/dist/runtime/commandRunner.d.ts +18 -0
  95. package/dist/runtime/commandRunner.js +115 -0
  96. package/dist/runtime/commandRunner.js.map +1 -0
  97. package/dist/runtime/commandSafety.d.ts +7 -0
  98. package/dist/runtime/commandSafety.js +61 -0
  99. package/dist/runtime/commandSafety.js.map +1 -0
  100. package/dist/runtime/config.d.ts +10 -0
  101. package/dist/runtime/config.js +107 -0
  102. package/dist/runtime/config.js.map +1 -0
  103. package/dist/runtime/crewLanes.d.ts +2 -0
  104. package/dist/runtime/crewLanes.js +123 -0
  105. package/dist/runtime/crewLanes.js.map +1 -0
  106. package/dist/runtime/criticPolicy.d.ts +17 -0
  107. package/dist/runtime/criticPolicy.js +50 -0
  108. package/dist/runtime/criticPolicy.js.map +1 -0
  109. package/dist/runtime/defaults.d.ts +5 -0
  110. package/dist/runtime/defaults.js +100 -0
  111. package/dist/runtime/defaults.js.map +1 -0
  112. package/dist/runtime/directives.d.ts +3 -0
  113. package/dist/runtime/directives.js +218 -0
  114. package/dist/runtime/directives.js.map +1 -0
  115. package/dist/runtime/doctor.d.ts +36 -0
  116. package/dist/runtime/doctor.js +185 -0
  117. package/dist/runtime/doctor.js.map +1 -0
  118. package/dist/runtime/errors.d.ts +20 -0
  119. package/dist/runtime/errors.js +41 -0
  120. package/dist/runtime/errors.js.map +1 -0
  121. package/dist/runtime/externalTransfer.d.ts +20 -0
  122. package/dist/runtime/externalTransfer.js +156 -0
  123. package/dist/runtime/externalTransfer.js.map +1 -0
  124. package/dist/runtime/fanout.d.ts +64 -0
  125. package/dist/runtime/fanout.js +263 -0
  126. package/dist/runtime/fanout.js.map +1 -0
  127. package/dist/runtime/gitEvidence.d.ts +10 -0
  128. package/dist/runtime/gitEvidence.js +80 -0
  129. package/dist/runtime/gitEvidence.js.map +1 -0
  130. package/dist/runtime/handoffs.d.ts +32 -0
  131. package/dist/runtime/handoffs.js +100 -0
  132. package/dist/runtime/handoffs.js.map +1 -0
  133. package/dist/runtime/ids.d.ts +2 -0
  134. package/dist/runtime/ids.js +4 -0
  135. package/dist/runtime/ids.js.map +1 -0
  136. package/dist/runtime/localStateIgnore.d.ts +9 -0
  137. package/dist/runtime/localStateIgnore.js +98 -0
  138. package/dist/runtime/localStateIgnore.js.map +1 -0
  139. package/dist/runtime/loop.d.ts +14 -0
  140. package/dist/runtime/loop.js +1863 -0
  141. package/dist/runtime/loop.js.map +1 -0
  142. package/dist/runtime/loopRecommendation.d.ts +109 -0
  143. package/dist/runtime/loopRecommendation.js +566 -0
  144. package/dist/runtime/loopRecommendation.js.map +1 -0
  145. package/dist/runtime/loopResponsibilities.d.ts +2 -0
  146. package/dist/runtime/loopResponsibilities.js +395 -0
  147. package/dist/runtime/loopResponsibilities.js.map +1 -0
  148. package/dist/runtime/loopRunner.d.ts +28 -0
  149. package/dist/runtime/loopRunner.js +81 -0
  150. package/dist/runtime/loopRunner.js.map +1 -0
  151. package/dist/runtime/operatorNextActions.d.ts +2 -0
  152. package/dist/runtime/operatorNextActions.js +344 -0
  153. package/dist/runtime/operatorNextActions.js.map +1 -0
  154. package/dist/runtime/paths.d.ts +9 -0
  155. package/dist/runtime/paths.js +14 -0
  156. package/dist/runtime/paths.js.map +1 -0
  157. package/dist/runtime/permissions.d.ts +9 -0
  158. package/dist/runtime/permissions.js +73 -0
  159. package/dist/runtime/permissions.js.map +1 -0
  160. package/dist/runtime/progressPacket.d.ts +12 -0
  161. package/dist/runtime/progressPacket.js +512 -0
  162. package/dist/runtime/progressPacket.js.map +1 -0
  163. package/dist/runtime/protectedPaths.d.ts +6 -0
  164. package/dist/runtime/protectedPaths.js +48 -0
  165. package/dist/runtime/protectedPaths.js.map +1 -0
  166. package/dist/runtime/providers.d.ts +13 -0
  167. package/dist/runtime/providers.js +60 -0
  168. package/dist/runtime/providers.js.map +1 -0
  169. package/dist/runtime/reconciliation.d.ts +17 -0
  170. package/dist/runtime/reconciliation.js +283 -0
  171. package/dist/runtime/reconciliation.js.map +1 -0
  172. package/dist/runtime/redaction.d.ts +1 -0
  173. package/dist/runtime/redaction.js +5 -0
  174. package/dist/runtime/redaction.js.map +1 -0
  175. package/dist/runtime/remoteRepoContext.d.ts +77 -0
  176. package/dist/runtime/remoteRepoContext.js +316 -0
  177. package/dist/runtime/remoteRepoContext.js.map +1 -0
  178. package/dist/runtime/repoContext.d.ts +50 -0
  179. package/dist/runtime/repoContext.js +399 -0
  180. package/dist/runtime/repoContext.js.map +1 -0
  181. package/dist/runtime/repoGateway.d.ts +64 -0
  182. package/dist/runtime/repoGateway.js +308 -0
  183. package/dist/runtime/repoGateway.js.map +1 -0
  184. package/dist/runtime/responseContracts.d.ts +3 -0
  185. package/dist/runtime/responseContracts.js +86 -0
  186. package/dist/runtime/responseContracts.js.map +1 -0
  187. package/dist/runtime/reviewLanes.d.ts +2 -0
  188. package/dist/runtime/reviewLanes.js +343 -0
  189. package/dist/runtime/reviewLanes.js.map +1 -0
  190. package/dist/runtime/reviewRouting.d.ts +51 -0
  191. package/dist/runtime/reviewRouting.js +152 -0
  192. package/dist/runtime/reviewRouting.js.map +1 -0
  193. package/dist/runtime/revisionPacket.d.ts +38 -0
  194. package/dist/runtime/revisionPacket.js +144 -0
  195. package/dist/runtime/revisionPacket.js.map +1 -0
  196. package/dist/runtime/revisionTrail.d.ts +2 -0
  197. package/dist/runtime/revisionTrail.js +162 -0
  198. package/dist/runtime/revisionTrail.js.map +1 -0
  199. package/dist/runtime/role-assignment.d.ts +4 -0
  200. package/dist/runtime/role-assignment.js +21 -0
  201. package/dist/runtime/role-assignment.js.map +1 -0
  202. package/dist/runtime/roleRoster.d.ts +28 -0
  203. package/dist/runtime/roleRoster.js +96 -0
  204. package/dist/runtime/roleRoster.js.map +1 -0
  205. package/dist/runtime/runInsights.d.ts +121 -0
  206. package/dist/runtime/runInsights.js +305 -0
  207. package/dist/runtime/runInsights.js.map +1 -0
  208. package/dist/runtime/runMonitor.d.ts +33 -0
  209. package/dist/runtime/runMonitor.js +143 -0
  210. package/dist/runtime/runMonitor.js.map +1 -0
  211. package/dist/runtime/runtime.d.ts +15 -0
  212. package/dist/runtime/runtime.js +199 -0
  213. package/dist/runtime/runtime.js.map +1 -0
  214. package/dist/runtime/runtimeInfo.d.ts +9 -0
  215. package/dist/runtime/runtimeInfo.js +76 -0
  216. package/dist/runtime/runtimeInfo.js.map +1 -0
  217. package/dist/runtime/store.d.ts +4 -0
  218. package/dist/runtime/store.js +48 -0
  219. package/dist/runtime/store.js.map +1 -0
  220. package/dist/runtime/summons.d.ts +25 -0
  221. package/dist/runtime/summons.js +403 -0
  222. package/dist/runtime/summons.js.map +1 -0
  223. package/dist/runtime/teamPresets.d.ts +14 -0
  224. package/dist/runtime/teamPresets.js +153 -0
  225. package/dist/runtime/teamPresets.js.map +1 -0
  226. package/dist/runtime/types.d.ts +505 -0
  227. package/dist/runtime/types.js +28 -0
  228. package/dist/runtime/types.js.map +1 -0
  229. package/dist/runtime/validationCommands.d.ts +18 -0
  230. package/dist/runtime/validationCommands.js +106 -0
  231. package/dist/runtime/validationCommands.js.map +1 -0
  232. package/dist/tui/dashboard.d.ts +41 -0
  233. package/dist/tui/dashboard.js +1115 -0
  234. package/dist/tui/dashboard.js.map +1 -0
  235. package/docs/ARCHITECTURE.md +277 -0
  236. package/docs/CLI_SPEC.md +396 -0
  237. package/docs/CODEX_SETUP.md +288 -0
  238. package/docs/COMPLETION_CONTRACT.md +52 -0
  239. package/docs/CONTRIBUTOR_GUIDE.md +70 -0
  240. package/docs/DEMO.md +62 -0
  241. package/docs/GLOSSARY.md +46 -0
  242. package/docs/GOAL_LOOP_SCHEDULE.md +50 -0
  243. package/docs/KNOWN_LIMITATIONS.md +29 -0
  244. package/docs/LICENSING.md +21 -0
  245. package/docs/LOOP_RECIPES.md +290 -0
  246. package/docs/LOOP_SELECTION_UX.md +118 -0
  247. package/docs/MCP_SPEC.md +689 -0
  248. package/docs/MEMORY_AND_RECONCILIATION.md +222 -0
  249. package/docs/NPM_PUBLISHING.md +51 -0
  250. package/docs/OPEN_DECISIONS.md +81 -0
  251. package/docs/PROMPT_SCHEMAS.md +411 -0
  252. package/docs/PROVIDER_ADAPTERS.md +323 -0
  253. package/docs/PROVIDER_MATRIX.md +21 -0
  254. package/docs/PUBLIC_REPO_READINESS.md +49 -0
  255. package/docs/RESEARCH_NOTES.md +92 -0
  256. package/docs/ROADMAP.md +94 -0
  257. package/docs/ROLE_CONTRACTS.md +252 -0
  258. package/docs/RUNTIME_LOOP.md +240 -0
  259. package/docs/SECURITY_AND_PRIVACY.md +161 -0
  260. package/docs/TESTING_AND_VERIFICATION.md +180 -0
  261. package/docs/TRUST_MODEL.md +65 -0
  262. package/docs/decisions/0001-runtime-first-cli-and-mcp.md +23 -0
  263. package/docs/decisions/0002-provider-neutral-role-mapping.md +43 -0
  264. package/docs/decisions/0003-separate-implementation-and-verification.md +27 -0
  265. package/docs/product/README.md +14 -0
  266. package/docs/product/model-selection.md +88 -0
  267. package/docs/product/positioning.md +37 -0
  268. package/docs/product/pro-usage-modes.md +70 -0
  269. package/docs/product/roadmap.md +57 -0
  270. package/docs/product/role-policy.md +89 -0
  271. package/docs/product/runtime-invariants.md +44 -0
  272. package/docs/release/v0.1.0-preview.0.md +48 -0
  273. package/examples/stub-demo/README.md +25 -0
  274. package/package.json +55 -0
@@ -0,0 +1,981 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { classifyChatGptPageSnapshot, chatGptPageSnapshotExpression } from "../runtime/chatGptPageReadiness.js";
6
+ const defaultOptions = {
7
+ model: "chatgpt-pro",
8
+ schemaPath: "",
9
+ cdpUrl: process.env.THEHOOD_CHATGPT_WEB_CDP_URL ?? "http://127.0.0.1:9222",
10
+ freshUrl: process.env.THEHOOD_CHATGPT_WEB_FRESH_URL ?? "https://chatgpt.com/",
11
+ timeoutMs: Number(process.env.THEHOOD_CHATGPT_WEB_TIMEOUT_MS ?? 600_000),
12
+ commandTimeoutMs: Number(process.env.THEHOOD_CHATGPT_WEB_CDP_COMMAND_TIMEOUT_MS ?? 15_000),
13
+ promptSelector: process.env.THEHOOD_CHATGPT_WEB_PROMPT_SELECTOR ?? "#prompt-textarea,[contenteditable='true'],textarea",
14
+ sendSelector: process.env.THEHOOD_CHATGPT_WEB_SEND_SELECTOR ??
15
+ "button[data-testid='send-button'],button[aria-label*='Send'],button[aria-label*='send']",
16
+ responseSelector: process.env.THEHOOD_CHATGPT_WEB_RESPONSE_SELECTOR ?? "[data-message-author-role='assistant']",
17
+ newChatSelector: process.env.THEHOOD_CHATGPT_WEB_NEW_CHAT_SELECTOR ??
18
+ "a[href='/'],a[href='/?model=auto'],button[aria-label*='New chat'],a[aria-label*='New chat'],[data-testid='create-new-chat-button']",
19
+ reuseChat: process.env.THEHOOD_CHATGPT_WEB_REUSE_CHAT === "1",
20
+ runScopedTarget: process.env.THEHOOD_CHATGPT_WEB_RUN_SCOPED_TARGETS !== "0",
21
+ keepCreatedTarget: process.env.THEHOOD_CHATGPT_WEB_KEEP_TARGET === "1",
22
+ keepTargetOnFailure: process.env.THEHOOD_CHATGPT_WEB_KEEP_TARGET_ON_FAILURE !== "0",
23
+ allowUnverifiedModel: process.env.THEHOOD_CHATGPT_WEB_MODEL_CONFIRMED === "1" ||
24
+ process.env.THEHOOD_CHATGPT_WEB_ALLOW_UNVERIFIED_MODEL === "1",
25
+ sessionDir: process.env.THEHOOD_CHATGPT_WEB_SESSION_DIR ??
26
+ path.join(os.tmpdir(), "thehood-chatgpt-web-sessions")
27
+ };
28
+ const readStdin = async () => new Promise((resolve, reject) => {
29
+ let input = "";
30
+ process.stdin.setEncoding("utf8");
31
+ process.stdin.on("data", (chunk) => {
32
+ input += chunk;
33
+ });
34
+ process.stdin.on("end", () => resolve(input));
35
+ process.stdin.on("error", reject);
36
+ });
37
+ const parseArgs = (argv) => {
38
+ const options = { ...defaultOptions };
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const arg = argv[index];
41
+ if (arg === "--allow-unverified-model") {
42
+ options.allowUnverifiedModel = true;
43
+ continue;
44
+ }
45
+ if (arg === "--reuse-chat") {
46
+ options.reuseChat = true;
47
+ continue;
48
+ }
49
+ if (arg === "--no-run-scoped-target") {
50
+ options.runScopedTarget = false;
51
+ continue;
52
+ }
53
+ if (arg === "--keep-target") {
54
+ options.keepCreatedTarget = true;
55
+ continue;
56
+ }
57
+ if (arg === "--keep-target-on-failure") {
58
+ options.keepTargetOnFailure = true;
59
+ continue;
60
+ }
61
+ if (arg === "--close-target-on-failure") {
62
+ options.keepTargetOnFailure = false;
63
+ continue;
64
+ }
65
+ if (!arg?.startsWith("--")) {
66
+ throw new Error(`Unexpected argument: ${arg ?? ""}`);
67
+ }
68
+ const next = argv[index + 1];
69
+ if (!next || next.startsWith("--")) {
70
+ throw new Error(`${arg} requires a value.`);
71
+ }
72
+ switch (arg) {
73
+ case "--model":
74
+ options.model = next;
75
+ break;
76
+ case "--schema":
77
+ options.schemaPath = next;
78
+ break;
79
+ case "--cdp-url":
80
+ options.cdpUrl = next;
81
+ break;
82
+ case "--fresh-url":
83
+ options.freshUrl = next;
84
+ break;
85
+ case "--timeout-ms":
86
+ options.timeoutMs = Number(next);
87
+ break;
88
+ case "--command-timeout-ms":
89
+ options.commandTimeoutMs = Number(next);
90
+ break;
91
+ case "--prompt-selector":
92
+ options.promptSelector = next;
93
+ break;
94
+ case "--send-selector":
95
+ options.sendSelector = next;
96
+ break;
97
+ case "--response-selector":
98
+ options.responseSelector = next;
99
+ break;
100
+ case "--new-chat-selector":
101
+ options.newChatSelector = next;
102
+ break;
103
+ case "--session-dir":
104
+ options.sessionDir = next;
105
+ break;
106
+ default:
107
+ throw new Error(`Unknown option: ${arg}`);
108
+ }
109
+ index += 1;
110
+ }
111
+ if (!options.schemaPath) {
112
+ throw new Error("--schema is required.");
113
+ }
114
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
115
+ throw new Error("--timeout-ms must be a positive number.");
116
+ }
117
+ if (!Number.isFinite(options.commandTimeoutMs) || options.commandTimeoutMs <= 0) {
118
+ throw new Error("--command-timeout-ms must be a positive number.");
119
+ }
120
+ return options;
121
+ };
122
+ const readRequiredDataKey = async (schemaPath) => {
123
+ const raw = await fs.readFile(schemaPath, "utf8");
124
+ const schema = JSON.parse(raw);
125
+ const key = schema.properties?.data?.required?.[0];
126
+ if (!key) {
127
+ throw new Error(`Could not infer AgentResponse data key from schema: ${schemaPath}`);
128
+ }
129
+ return key;
130
+ };
131
+ const isJsonObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
132
+ const extractExpectedDirectiveAck = (prompt) => {
133
+ const marker = "Runtime directive:";
134
+ const markerIndex = prompt.lastIndexOf(marker);
135
+ if (markerIndex === -1) {
136
+ return undefined;
137
+ }
138
+ const rawDirective = prompt.slice(markerIndex + marker.length).trim();
139
+ if (!rawDirective) {
140
+ return undefined;
141
+ }
142
+ try {
143
+ const directive = JSON.parse(rawDirective);
144
+ const ack = isJsonObject(directive.directiveAck)
145
+ ? directive.directiveAck
146
+ : isJsonObject(directive.variables?.directiveAck)
147
+ ? directive.variables.directiveAck
148
+ : undefined;
149
+ if (typeof ack?.runId === "string" &&
150
+ typeof ack.nonce === "string" &&
151
+ typeof ack.responseField === "string") {
152
+ return {
153
+ runId: ack.runId,
154
+ nonce: ack.nonce,
155
+ responseField: ack.responseField
156
+ };
157
+ }
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ return undefined;
163
+ };
164
+ const withDirectiveAck = (payload, expectedAck) => {
165
+ if (!expectedAck) {
166
+ return payload;
167
+ }
168
+ return {
169
+ ...payload,
170
+ [expectedAck.responseField]: {
171
+ runId: expectedAck.runId,
172
+ nonce: expectedAck.nonce,
173
+ responseField: expectedAck.responseField
174
+ }
175
+ };
176
+ };
177
+ const payloadForKey = (requiredDataKey, summary, expectedAck) => {
178
+ switch (requiredDataKey) {
179
+ case "decision":
180
+ return withDirectiveAck({
181
+ action: "request_approval",
182
+ reason: summary
183
+ }, expectedAck);
184
+ case "implementationResult":
185
+ return withDirectiveAck({
186
+ status: "blocked",
187
+ changedFiles: [],
188
+ commandsRun: [],
189
+ unresolvedRisks: [summary]
190
+ }, expectedAck);
191
+ case "qaResult":
192
+ return withDirectiveAck({
193
+ verdict: "blocked",
194
+ summary,
195
+ suggestedCommands: [],
196
+ risks: [summary]
197
+ }, expectedAck);
198
+ case "verificationResult":
199
+ return withDirectiveAck({
200
+ verdict: "ask_user",
201
+ summary,
202
+ failedCriteria: ["chatgpt_web_bridge"],
203
+ risks: [summary],
204
+ nextAction: "user"
205
+ }, expectedAck);
206
+ case "critiqueResult":
207
+ return withDirectiveAck({
208
+ verdict: "unclear",
209
+ blockingConcerns: [summary],
210
+ nonBlockingConcerns: []
211
+ }, expectedAck);
212
+ default:
213
+ return withDirectiveAck({
214
+ summary
215
+ }, expectedAck);
216
+ }
217
+ };
218
+ const fallback = (requiredDataKey, summary, status = "blocked", expectedAck) => ({
219
+ status,
220
+ summary,
221
+ data: {
222
+ [requiredDataKey]: payloadForKey(requiredDataKey, summary, expectedAck)
223
+ }
224
+ });
225
+ const repairMissingClosingBraces = (text) => {
226
+ const trimmed = text.trim();
227
+ if (!trimmed.startsWith("{")) {
228
+ return undefined;
229
+ }
230
+ let depth = 0;
231
+ let inString = false;
232
+ let escaped = false;
233
+ for (const char of trimmed) {
234
+ if (inString) {
235
+ if (escaped) {
236
+ escaped = false;
237
+ continue;
238
+ }
239
+ if (char === "\\") {
240
+ escaped = true;
241
+ continue;
242
+ }
243
+ if (char === "\"") {
244
+ inString = false;
245
+ }
246
+ continue;
247
+ }
248
+ if (char === "\"") {
249
+ inString = true;
250
+ continue;
251
+ }
252
+ if (char === "{") {
253
+ depth += 1;
254
+ continue;
255
+ }
256
+ if (char === "}") {
257
+ depth -= 1;
258
+ if (depth < 0) {
259
+ return undefined;
260
+ }
261
+ }
262
+ }
263
+ if (inString || depth <= 0 || depth > 3) {
264
+ return undefined;
265
+ }
266
+ return `${trimmed}${"}".repeat(depth)}`;
267
+ };
268
+ const tryParseJson = (text) => {
269
+ try {
270
+ return JSON.parse(text);
271
+ }
272
+ catch {
273
+ const repaired = repairMissingClosingBraces(text);
274
+ if (!repaired) {
275
+ return undefined;
276
+ }
277
+ try {
278
+ return JSON.parse(repaired);
279
+ }
280
+ catch {
281
+ return undefined;
282
+ }
283
+ }
284
+ };
285
+ const fencedCodeBlocks = (text) => {
286
+ const blocks = [];
287
+ const pattern = /```(?:json)?\s*([\s\S]*?)```/gi;
288
+ let match;
289
+ while ((match = pattern.exec(text)) !== null) {
290
+ const block = match[1]?.trim();
291
+ if (block) {
292
+ blocks.push(block);
293
+ }
294
+ }
295
+ return blocks;
296
+ };
297
+ const balancedJsonObjects = (text) => {
298
+ const objects = [];
299
+ for (let start = 0; start < text.length; start += 1) {
300
+ if (text[start] !== "{") {
301
+ continue;
302
+ }
303
+ let depth = 0;
304
+ let inString = false;
305
+ let escaped = false;
306
+ for (let index = start; index < text.length; index += 1) {
307
+ const char = text[index];
308
+ if (inString) {
309
+ if (escaped) {
310
+ escaped = false;
311
+ continue;
312
+ }
313
+ if (char === "\\") {
314
+ escaped = true;
315
+ continue;
316
+ }
317
+ if (char === "\"") {
318
+ inString = false;
319
+ }
320
+ continue;
321
+ }
322
+ if (char === "\"") {
323
+ inString = true;
324
+ continue;
325
+ }
326
+ if (char === "{") {
327
+ depth += 1;
328
+ continue;
329
+ }
330
+ if (char === "}") {
331
+ depth -= 1;
332
+ if (depth === 0) {
333
+ objects.push(text.slice(start, index + 1));
334
+ break;
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return objects;
340
+ };
341
+ const jsonCandidateStrings = (text) => {
342
+ const trimmed = text.trim();
343
+ if (!trimmed) {
344
+ return [];
345
+ }
346
+ const lines = trimmed
347
+ .split("\n")
348
+ .map((line) => line.trim())
349
+ .filter(Boolean)
350
+ .reverse();
351
+ return [
352
+ trimmed,
353
+ ...fencedCodeBlocks(trimmed).reverse(),
354
+ ...balancedJsonObjects(trimmed).reverse(),
355
+ ...lines
356
+ ];
357
+ };
358
+ const isAgentResponse = (value) => {
359
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
360
+ return false;
361
+ }
362
+ const candidate = value;
363
+ return ((candidate.status === "ok" || candidate.status === "blocked" || candidate.status === "failed") &&
364
+ typeof candidate.summary === "string" &&
365
+ candidate.data !== null &&
366
+ typeof candidate.data === "object" &&
367
+ !Array.isArray(candidate.data));
368
+ };
369
+ const hasRequiredDataKey = (response, requiredDataKey) => {
370
+ if (!requiredDataKey) {
371
+ return true;
372
+ }
373
+ const payload = response.data[requiredDataKey];
374
+ return payload !== null && typeof payload === "object" && !Array.isArray(payload);
375
+ };
376
+ const directiveAckError = (response, requiredDataKey, expectedAck) => {
377
+ if (!expectedAck || !requiredDataKey) {
378
+ return undefined;
379
+ }
380
+ const payload = response.data[requiredDataKey];
381
+ if (!isJsonObject(payload)) {
382
+ return undefined;
383
+ }
384
+ const ack = payload[expectedAck.responseField];
385
+ if (!isJsonObject(ack)) {
386
+ return `AgentResponse data.${requiredDataKey}.${expectedAck.responseField} is missing.`;
387
+ }
388
+ if (ack.runId !== expectedAck.runId || ack.nonce !== expectedAck.nonce) {
389
+ return `AgentResponse acknowledged a stale directive for run ${String(ack.runId)}.`;
390
+ }
391
+ return undefined;
392
+ };
393
+ const unwrapAgentResponse = (value, requiredDataKey) => {
394
+ if (isAgentResponse(value)) {
395
+ return value;
396
+ }
397
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
398
+ return value;
399
+ }
400
+ const candidate = value;
401
+ if (typeof candidate.result === "string") {
402
+ return parseAgentResponse(candidate.result, requiredDataKey) ?? value;
403
+ }
404
+ if (candidate.result !== undefined) {
405
+ return candidate.result;
406
+ }
407
+ if (typeof candidate.message === "string") {
408
+ return parseAgentResponse(candidate.message, requiredDataKey) ?? value;
409
+ }
410
+ return value;
411
+ };
412
+ const parseAgentResponse = (text, requiredDataKey) => parseAgentResponseCandidate(text, requiredDataKey).response;
413
+ const parseAgentResponseCandidate = (text, requiredDataKey, expectedAck) => {
414
+ for (const candidate of jsonCandidateStrings(text)) {
415
+ const parsed = tryParseJson(candidate);
416
+ const unwrapped = unwrapAgentResponse(parsed, requiredDataKey);
417
+ if (isAgentResponse(unwrapped) && hasRequiredDataKey(unwrapped, requiredDataKey)) {
418
+ const ackError = directiveAckError(unwrapped, requiredDataKey, expectedAck);
419
+ return ackError ? { ackError } : { response: unwrapped };
420
+ }
421
+ }
422
+ return {};
423
+ };
424
+ const listTargets = async (cdpUrl) => {
425
+ const response = await fetch(new URL("/json/list", cdpUrl));
426
+ if (!response.ok) {
427
+ throw new Error(`Chrome DevTools returned ${response.status}.`);
428
+ }
429
+ return (await response.json());
430
+ };
431
+ const isChatGptTarget = (candidate) => {
432
+ const url = candidate.url ?? "";
433
+ return Boolean(candidate.webSocketDebuggerUrl &&
434
+ (url.includes("chatgpt.com") || url.includes("chat.openai.com")));
435
+ };
436
+ const findChatGptTarget = async (cdpUrl) => {
437
+ const targets = await listTargets(cdpUrl);
438
+ const target = targets.find(isChatGptTarget);
439
+ if (!target?.webSocketDebuggerUrl) {
440
+ throw new Error("No ChatGPT tab with a DevTools websocket was found.");
441
+ }
442
+ return target;
443
+ };
444
+ const createChatGptTarget = async (cdpUrl, freshUrl) => {
445
+ const response = await fetch(new URL(`/json/new?${encodeURIComponent(freshUrl)}`, cdpUrl), {
446
+ method: "PUT"
447
+ });
448
+ if (!response.ok) {
449
+ throw new Error(`Chrome DevTools could not create a fresh ChatGPT target: ${response.status}.`);
450
+ }
451
+ const target = await response.json();
452
+ if (!target.webSocketDebuggerUrl) {
453
+ throw new Error("Chrome DevTools created a target without a websocket URL.");
454
+ }
455
+ return target;
456
+ };
457
+ const closeChromeTarget = async (cdpUrl, target) => {
458
+ if (!target?.id) {
459
+ return;
460
+ }
461
+ await fetch(new URL(`/json/close/${encodeURIComponent(target.id)}`, cdpUrl)).catch(() => undefined);
462
+ };
463
+ const runScopedSessionId = (options, expectedAck) => {
464
+ if (options.reuseChat || !options.runScopedTarget) {
465
+ return undefined;
466
+ }
467
+ return expectedAck?.runId ?? process.env.THEHOOD_RUN_ID;
468
+ };
469
+ const safeSessionFileName = (runId) => `${runId.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`;
470
+ const sessionFilePath = (options, runId) => path.join(options.sessionDir, safeSessionFileName(runId));
471
+ const readTargetSession = async (options, runId) => {
472
+ try {
473
+ const parsed = JSON.parse(await fs.readFile(sessionFilePath(options, runId), "utf8"));
474
+ if (parsed.schemaVersion === 1 && parsed.runId === runId && typeof parsed.targetId === "string") {
475
+ return parsed;
476
+ }
477
+ }
478
+ catch {
479
+ return undefined;
480
+ }
481
+ return undefined;
482
+ };
483
+ const removeTargetSession = async (options, runId) => {
484
+ await fs.rm(sessionFilePath(options, runId), { force: true }).catch(() => undefined);
485
+ };
486
+ const findRunScopedTarget = async (options, runId) => {
487
+ const session = await readTargetSession(options, runId);
488
+ if (!session) {
489
+ return undefined;
490
+ }
491
+ const targets = await listTargets(options.cdpUrl);
492
+ const target = targets.find((candidate) => candidate.id === session.targetId);
493
+ if (target && isChatGptTarget(target)) {
494
+ return target;
495
+ }
496
+ await removeTargetSession(options, runId);
497
+ return undefined;
498
+ };
499
+ const writeTargetSession = async (options, runId, target) => {
500
+ if (!target.id) {
501
+ return;
502
+ }
503
+ const filePath = sessionFilePath(options, runId);
504
+ const existing = await readTargetSession(options, runId);
505
+ const now = new Date().toISOString();
506
+ const session = {
507
+ schemaVersion: 1,
508
+ runId,
509
+ targetId: target.id,
510
+ createdAt: existing?.createdAt ?? now,
511
+ updatedAt: now
512
+ };
513
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
514
+ await fs.writeFile(filePath, `${JSON.stringify(session, null, 2)}\n`, "utf8");
515
+ };
516
+ const shouldCloseCreatedTarget = (options, parsedResponse, runScopedTarget) => !options.reuseChat &&
517
+ !runScopedTarget &&
518
+ !options.keepCreatedTarget &&
519
+ (parsedResponse || !options.keepTargetOnFailure);
520
+ const connectCdp = async (webSocketUrl, commandTimeoutMs) => {
521
+ let nextId = 1;
522
+ const pending = new Map();
523
+ const socket = new WebSocket(webSocketUrl);
524
+ await new Promise((resolve, reject) => {
525
+ const timer = setTimeout(() => {
526
+ reject(new Error(`Could not connect to Chrome DevTools websocket within ${commandTimeoutMs}ms.`));
527
+ socket.close();
528
+ }, commandTimeoutMs);
529
+ socket.addEventListener("open", () => {
530
+ clearTimeout(timer);
531
+ resolve();
532
+ });
533
+ socket.addEventListener("error", () => {
534
+ clearTimeout(timer);
535
+ reject(new Error("Could not connect to Chrome DevTools websocket."));
536
+ });
537
+ });
538
+ socket.addEventListener("message", (event) => {
539
+ const message = JSON.parse(String(event.data));
540
+ if (!message.id) {
541
+ return;
542
+ }
543
+ const callbacks = pending.get(message.id);
544
+ if (!callbacks) {
545
+ return;
546
+ }
547
+ pending.delete(message.id);
548
+ clearTimeout(callbacks.timer);
549
+ if (message.error) {
550
+ callbacks.reject(new Error(message.error.message ?? "Chrome DevTools command failed."));
551
+ return;
552
+ }
553
+ callbacks.resolve(message.result);
554
+ });
555
+ socket.addEventListener("close", () => {
556
+ for (const callbacks of pending.values()) {
557
+ clearTimeout(callbacks.timer);
558
+ callbacks.reject(new Error("Chrome DevTools websocket closed."));
559
+ }
560
+ pending.clear();
561
+ });
562
+ const send = (method, params) => {
563
+ const id = nextId;
564
+ nextId += 1;
565
+ return new Promise((resolve, reject) => {
566
+ const timer = setTimeout(() => {
567
+ pending.delete(id);
568
+ reject(new Error(`Chrome DevTools command ${method} timed out after ${commandTimeoutMs}ms.`));
569
+ socket.close();
570
+ }, commandTimeoutMs);
571
+ pending.set(id, {
572
+ resolve,
573
+ reject,
574
+ timer
575
+ });
576
+ try {
577
+ socket.send(JSON.stringify({
578
+ id,
579
+ method,
580
+ params
581
+ }));
582
+ }
583
+ catch (error) {
584
+ clearTimeout(timer);
585
+ pending.delete(id);
586
+ reject(error instanceof Error ? error : new Error(String(error)));
587
+ }
588
+ });
589
+ };
590
+ return {
591
+ async evaluate(expression) {
592
+ const result = await send("Runtime.evaluate", {
593
+ expression,
594
+ awaitPromise: true,
595
+ returnByValue: true
596
+ });
597
+ if (result.exceptionDetails) {
598
+ throw new Error(result.exceptionDetails.text ?? "Browser evaluation failed.");
599
+ }
600
+ return result.result?.value;
601
+ },
602
+ async navigate(url) {
603
+ await send("Page.navigate", {
604
+ url
605
+ });
606
+ },
607
+ close() {
608
+ socket.close();
609
+ }
610
+ };
611
+ };
612
+ const promptEditorReadyExpression = (promptSelector) => `
613
+ (() => {
614
+ const promptSelectors = ${JSON.stringify(promptSelector)}.split(',').map((selector) => selector.trim()).filter(Boolean);
615
+ return promptSelectors.some((selector) => Boolean(document.querySelector(selector)));
616
+ })()
617
+ `;
618
+ const assistantSnapshotExpression = (responseSelector) => `
619
+ (() => {
620
+ const selected = Array.from(document.querySelectorAll(${JSON.stringify(responseSelector)}));
621
+ const fallbackNodes = selected.length > 0
622
+ ? []
623
+ : Array.from(document.querySelectorAll('[data-message-author-role="assistant"], [data-testid^="conversation-turn-"]'))
624
+ .filter((node) => {
625
+ const role = node.getAttribute('data-message-author-role')
626
+ || node.querySelector('[data-message-author-role]')?.getAttribute('data-message-author-role');
627
+ return role === 'assistant';
628
+ });
629
+ const nodes = selected.length > 0 ? selected : fallbackNodes;
630
+ return nodes.map((node) => (node.textContent || '').trim()).filter(Boolean);
631
+ })()
632
+ `;
633
+ const chatGptErrorExpression = () => `
634
+ (() => {
635
+ const text = document.body?.innerText || '';
636
+ const messages = [
637
+ 'Message delivery timed out',
638
+ 'Something went wrong',
639
+ 'There was an error generating a response',
640
+ 'Unable to load conversation'
641
+ ];
642
+ return messages.find((message) => text.includes(message)) || null;
643
+ })()
644
+ `;
645
+ const sendPromptExpression = (prompt, promptSelector, sendSelector) => `
646
+ (async () => {
647
+ const promptSelectors = ${JSON.stringify(promptSelector)}.split(',').map((selector) => selector.trim()).filter(Boolean);
648
+ const sendSelectors = ${JSON.stringify(sendSelector)}.split(',').map((selector) => selector.trim()).filter(Boolean);
649
+ const prompt = ${JSON.stringify(prompt)};
650
+ const editor = promptSelectors.map((selector) => document.querySelector(selector)).find(Boolean);
651
+
652
+ if (!editor) {
653
+ return { ok: false, error: 'prompt editor not found' };
654
+ }
655
+
656
+ editor.focus();
657
+
658
+ if ('value' in editor) {
659
+ const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(editor), 'value')?.set;
660
+ if (setter) {
661
+ setter.call(editor, prompt);
662
+ } else {
663
+ editor.value = prompt;
664
+ }
665
+ } else {
666
+ editor.textContent = prompt;
667
+ const selection = window.getSelection();
668
+ const range = document.createRange();
669
+ range.selectNodeContents(editor);
670
+ range.collapse(false);
671
+ selection.removeAllRanges();
672
+ selection.addRange(range);
673
+ }
674
+
675
+ editor.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, inputType: 'insertText', data: prompt }));
676
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: prompt }));
677
+ editor.dispatchEvent(new Event('change', { bubbles: true }));
678
+
679
+ const findEnabledButton = () => sendSelectors
680
+ .map((selector) => document.querySelector(selector))
681
+ .find((candidate) => candidate && !candidate.disabled && candidate.getAttribute('aria-disabled') !== 'true');
682
+ let button = findEnabledButton();
683
+
684
+ for (let attempt = 0; !button && attempt < 50; attempt += 1) {
685
+ await new Promise((resolve) => setTimeout(resolve, 100));
686
+ button = findEnabledButton();
687
+ }
688
+
689
+ if (button) {
690
+ button.click();
691
+ return { ok: true, method: 'button' };
692
+ }
693
+
694
+ editor.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter', code: 'Enter' }));
695
+ editor.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Enter', code: 'Enter' }));
696
+ await new Promise((resolve) => setTimeout(resolve, 300));
697
+ editor.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter', code: 'Enter', metaKey: true }));
698
+ editor.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Enter', code: 'Enter', metaKey: true }));
699
+ await new Promise((resolve) => setTimeout(resolve, 300));
700
+
701
+ return { ok: true, method: 'keyboard' };
702
+ })()
703
+ `;
704
+ const clickNewChatExpression = (newChatSelector) => `
705
+ (() => {
706
+ const selectors = ${JSON.stringify(newChatSelector)}.split(',').map((selector) => selector.trim()).filter(Boolean);
707
+ const selected = selectors
708
+ .map((selector) => document.querySelector(selector))
709
+ .find(Boolean);
710
+
711
+ const textMatch = selected || Array.from(document.querySelectorAll('a,button'))
712
+ .find((node) => /new chat/i.test(node.getAttribute('aria-label') || node.textContent || ''));
713
+
714
+ if (!textMatch) {
715
+ return false;
716
+ }
717
+
718
+ textMatch.click();
719
+ return true;
720
+ })()
721
+ `;
722
+ const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
723
+ const loginRequiredMessage = "ChatGPT Web session requires login in the TheHood-managed browser profile. Run thehood browser start, sign in to ChatGPT in that window, then retry.";
724
+ const waitForPromptEditor = async (client, promptSelector, timeoutMs) => {
725
+ const deadline = Date.now() + timeoutMs;
726
+ while (Date.now() < deadline) {
727
+ try {
728
+ if (await client.evaluate(promptEditorReadyExpression(promptSelector))) {
729
+ return true;
730
+ }
731
+ }
732
+ catch {
733
+ // Navigation can briefly invalidate the execution context.
734
+ }
735
+ await sleep(500);
736
+ }
737
+ return false;
738
+ };
739
+ const waitForChatGptPageReadiness = async (client, promptSelector, timeoutMs) => {
740
+ const deadline = Date.now() + timeoutMs;
741
+ let latest;
742
+ while (Date.now() < deadline) {
743
+ try {
744
+ const snapshot = await client.evaluate(chatGptPageSnapshotExpression(promptSelector));
745
+ latest = classifyChatGptPageSnapshot(snapshot);
746
+ if (latest.authRequired || latest.composerReady) {
747
+ return latest;
748
+ }
749
+ }
750
+ catch {
751
+ // Navigation can briefly invalidate the execution context.
752
+ }
753
+ await sleep(500);
754
+ }
755
+ return latest;
756
+ };
757
+ const assistantCount = async (client, responseSelector) => (await client.evaluate(assistantSnapshotExpression(responseSelector))).length;
758
+ const waitForEmptyAssistantHistory = async (client, responseSelector, timeoutMs) => {
759
+ const deadline = Date.now() + timeoutMs;
760
+ let emptySince;
761
+ while (Date.now() < deadline) {
762
+ try {
763
+ if (await assistantCount(client, responseSelector) === 0) {
764
+ emptySince ??= Date.now();
765
+ if (Date.now() - emptySince >= 1_000) {
766
+ return true;
767
+ }
768
+ }
769
+ else {
770
+ emptySince = undefined;
771
+ }
772
+ }
773
+ catch {
774
+ // Navigation can briefly invalidate the execution context.
775
+ emptySince = undefined;
776
+ }
777
+ await sleep(500);
778
+ }
779
+ return false;
780
+ };
781
+ const ensureFreshComposer = async (client, promptSelector, responseSelector, newChatSelector, timeoutMs) => {
782
+ if (!await waitForPromptEditor(client, promptSelector, Math.min(timeoutMs, 30_000))) {
783
+ return { ok: false, reason: "ChatGPT Web bridge could not open a fresh ChatGPT composer." };
784
+ }
785
+ if (await waitForEmptyAssistantHistory(client, responseSelector, 2_000)) {
786
+ return { ok: true };
787
+ }
788
+ const clicked = await client.evaluate(clickNewChatExpression(newChatSelector)).catch(() => false);
789
+ if (!clicked) {
790
+ return {
791
+ ok: false,
792
+ reason: "ChatGPT Web bridge found existing assistant messages and could not locate a New Chat control."
793
+ };
794
+ }
795
+ if (!await waitForPromptEditor(client, promptSelector, Math.min(timeoutMs, 30_000))) {
796
+ return { ok: false, reason: "ChatGPT Web bridge clicked New Chat but the composer did not become ready." };
797
+ }
798
+ if (!await waitForEmptyAssistantHistory(client, responseSelector, 10_000)) {
799
+ return {
800
+ ok: false,
801
+ reason: "ChatGPT Web bridge could not verify an empty composer after opening New Chat."
802
+ };
803
+ }
804
+ return { ok: true };
805
+ };
806
+ const waitForAgentResponse = async (client, responseSelector, previousResponses, requiredDataKey, expectedAck, timeoutMs) => {
807
+ const startedAt = Date.now();
808
+ const deadline = Date.now() + timeoutMs;
809
+ const previousText = new Set(previousResponses);
810
+ let diagnostics = {
811
+ assistantCount: previousResponses.length,
812
+ candidateCount: 0,
813
+ latestLength: previousResponses.at(-1)?.length ?? 0,
814
+ elapsedMs: 0
815
+ };
816
+ while (Date.now() < deadline) {
817
+ let responses;
818
+ try {
819
+ responses = await client.evaluate(assistantSnapshotExpression(responseSelector));
820
+ }
821
+ catch (error) {
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ return {
824
+ bridgeError: `Chrome DevTools evaluation failed: ${message}`,
825
+ diagnostics
826
+ };
827
+ }
828
+ const newByPosition = responses.slice(previousResponses.length);
829
+ const changedOrNew = responses.filter((response, index) => index >= previousResponses.length || !previousText.has(response));
830
+ const candidates = Array.from(new Set([...newByPosition, ...changedOrNew])).reverse();
831
+ diagnostics = {
832
+ assistantCount: responses.length,
833
+ candidateCount: candidates.length,
834
+ latestLength: responses.at(-1)?.length ?? 0,
835
+ elapsedMs: Date.now() - startedAt
836
+ };
837
+ for (const candidate of candidates) {
838
+ const { response, ackError } = parseAgentResponseCandidate(candidate, requiredDataKey, expectedAck);
839
+ if (response) {
840
+ return {
841
+ response,
842
+ diagnostics
843
+ };
844
+ }
845
+ if (ackError) {
846
+ return {
847
+ bridgeError: `ChatGPT returned stale or unacknowledged AgentResponse JSON: ${ackError}`,
848
+ diagnostics
849
+ };
850
+ }
851
+ }
852
+ let browserError;
853
+ try {
854
+ browserError = await client.evaluate(chatGptErrorExpression());
855
+ }
856
+ catch (error) {
857
+ const message = error instanceof Error ? error.message : String(error);
858
+ return {
859
+ bridgeError: `Chrome DevTools evaluation failed: ${message}`,
860
+ diagnostics
861
+ };
862
+ }
863
+ if (browserError) {
864
+ return {
865
+ browserError,
866
+ diagnostics
867
+ };
868
+ }
869
+ await sleep(1_000);
870
+ }
871
+ return {
872
+ diagnostics: {
873
+ ...diagnostics,
874
+ elapsedMs: Date.now() - startedAt
875
+ }
876
+ };
877
+ };
878
+ const run = async () => {
879
+ const options = parseArgs(process.argv.slice(2));
880
+ const requiredDataKey = await readRequiredDataKey(options.schemaPath);
881
+ const prompt = await readStdin();
882
+ const expectedAck = extractExpectedDirectiveAck(prompt);
883
+ if (!options.allowUnverifiedModel) {
884
+ return fallback(requiredDataKey, `ChatGPT Web bridge requires explicit model confirmation for ${options.model}. Set THEHOOD_CHATGPT_WEB_MODEL_CONFIRMED=1 or pass --allow-unverified-model after selecting the model in ChatGPT.`, "blocked", expectedAck);
885
+ }
886
+ if (typeof WebSocket === "undefined") {
887
+ return fallback(requiredDataKey, "This Node.js runtime does not provide WebSocket support.", "failed", expectedAck);
888
+ }
889
+ const sessionRunId = runScopedSessionId(options, expectedAck);
890
+ let target;
891
+ let client;
892
+ let parsedResponse = false;
893
+ let createdRunScopedTarget = false;
894
+ let reusedRunScopedTarget = false;
895
+ try {
896
+ if (options.reuseChat) {
897
+ target = await findChatGptTarget(options.cdpUrl);
898
+ }
899
+ else {
900
+ const sessionTarget = sessionRunId ? await findRunScopedTarget(options, sessionRunId) : undefined;
901
+ target = sessionTarget ?? await createChatGptTarget(options.cdpUrl, options.freshUrl);
902
+ reusedRunScopedTarget = Boolean(sessionTarget);
903
+ createdRunScopedTarget = Boolean(sessionRunId && !sessionTarget);
904
+ if (sessionRunId && target.id) {
905
+ await writeTargetSession(options, sessionRunId, target);
906
+ }
907
+ }
908
+ client = await connectCdp(target.webSocketDebuggerUrl ?? "", options.commandTimeoutMs);
909
+ }
910
+ catch (error) {
911
+ const message = error instanceof Error ? error.message : String(error);
912
+ return fallback(requiredDataKey, `ChatGPT Web bridge failed fast: ${message}`, "failed", expectedAck);
913
+ }
914
+ try {
915
+ if (!options.reuseChat && !reusedRunScopedTarget) {
916
+ await client.navigate(options.freshUrl);
917
+ }
918
+ const readiness = await waitForChatGptPageReadiness(client, options.promptSelector, Math.min(options.timeoutMs, 10_000));
919
+ if (readiness?.authRequired) {
920
+ return fallback(requiredDataKey, loginRequiredMessage, "blocked", expectedAck);
921
+ }
922
+ if (!options.reuseChat && !reusedRunScopedTarget) {
923
+ const freshness = await ensureFreshComposer(client, options.promptSelector, options.responseSelector, options.newChatSelector, options.timeoutMs);
924
+ if (!freshness.ok) {
925
+ const latestReadiness = await waitForChatGptPageReadiness(client, options.promptSelector, 1_000);
926
+ if (latestReadiness?.authRequired) {
927
+ return fallback(requiredDataKey, loginRequiredMessage, "blocked", expectedAck);
928
+ }
929
+ return fallback(requiredDataKey, freshness.reason, "blocked", expectedAck);
930
+ }
931
+ }
932
+ const before = await client.evaluate(assistantSnapshotExpression(options.responseSelector));
933
+ const sent = await client.evaluate(sendPromptExpression(prompt, options.promptSelector, options.sendSelector));
934
+ if (!sent.ok) {
935
+ return fallback(requiredDataKey, `ChatGPT Web bridge could not send prompt: ${sent.error ?? "unknown error"}`, "blocked", expectedAck);
936
+ }
937
+ const result = await waitForAgentResponse(client, options.responseSelector, before, requiredDataKey, expectedAck, options.timeoutMs);
938
+ if (result.response) {
939
+ parsedResponse = true;
940
+ return result.response;
941
+ }
942
+ if (result.browserError) {
943
+ return fallback(requiredDataKey, `ChatGPT reported: ${result.browserError}.`, "blocked", expectedAck);
944
+ }
945
+ if (result.bridgeError) {
946
+ return fallback(requiredDataKey, `ChatGPT Web bridge failed fast: ${result.bridgeError}.`, "failed", expectedAck);
947
+ }
948
+ return fallback(requiredDataKey, [
949
+ "ChatGPT Web bridge timed out waiting for AgentResponse JSON.",
950
+ `Observed ${result.diagnostics.assistantCount} assistant message(s),`,
951
+ `${result.diagnostics.candidateCount} changed/new candidate(s),`,
952
+ `latest visible response length ${result.diagnostics.latestLength}.`
953
+ ].join(" "), "blocked", expectedAck);
954
+ }
955
+ finally {
956
+ client.close();
957
+ if (shouldCloseCreatedTarget(options, parsedResponse, Boolean(sessionRunId))) {
958
+ await closeChromeTarget(options.cdpUrl, target);
959
+ }
960
+ else if (createdRunScopedTarget && sessionRunId && !target.id) {
961
+ await removeTargetSession(options, sessionRunId);
962
+ }
963
+ }
964
+ };
965
+ run()
966
+ .then((response) => {
967
+ process.stdout.write(`${JSON.stringify(response)}\n`);
968
+ })
969
+ .catch(async (error) => {
970
+ const message = error instanceof Error ? error.message : String(error);
971
+ let requiredDataKey = "decision";
972
+ try {
973
+ const options = parseArgs(process.argv.slice(2));
974
+ requiredDataKey = await readRequiredDataKey(options.schemaPath);
975
+ }
976
+ catch {
977
+ // Keep the generic decision fallback.
978
+ }
979
+ process.stdout.write(`${JSON.stringify(fallback(requiredDataKey, message, "failed"))}\n`);
980
+ });
981
+ //# sourceMappingURL=chatgptWebBridge.js.map