@steipete/oracle 0.9.0 → 0.11.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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,16 +1,16 @@
1
- import path from 'node:path';
2
- import { randomUUID } from 'node:crypto';
3
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { delay } from './utils.js';
7
- const DEVTOOLS_ACTIVE_PORT_FILENAME = 'DevToolsActivePort';
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { delay } from "./utils.js";
7
+ const DEVTOOLS_ACTIVE_PORT_FILENAME = "DevToolsActivePort";
8
8
  const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
9
9
  DEVTOOLS_ACTIVE_PORT_FILENAME,
10
- path.join('Default', DEVTOOLS_ACTIVE_PORT_FILENAME),
10
+ path.join("Default", DEVTOOLS_ACTIVE_PORT_FILENAME),
11
11
  ];
12
- const CHROME_PID_FILENAME = 'chrome.pid';
13
- const ORACLE_PROFILE_LOCK_FILENAME = 'oracle-automation.lock';
12
+ const CHROME_PID_FILENAME = "chrome.pid";
13
+ const ORACLE_PROFILE_LOCK_FILENAME = "oracle-automation.lock";
14
14
  const execFileAsync = promisify(execFile);
15
15
  export function getDevToolsActivePortPaths(userDataDir) {
16
16
  return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
@@ -18,9 +18,9 @@ export function getDevToolsActivePortPaths(userDataDir) {
18
18
  export async function readDevToolsPort(userDataDir) {
19
19
  for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
20
20
  try {
21
- const raw = await readFile(candidate, 'utf8');
21
+ const raw = await readFile(candidate, "utf8");
22
22
  const firstLine = raw.split(/\r?\n/u)[0]?.trim();
23
- const port = Number.parseInt(firstLine ?? '', 10);
23
+ const port = Number.parseInt(firstLine ?? "", 10);
24
24
  if (Number.isFinite(port)) {
25
25
  return port;
26
26
  }
@@ -36,7 +36,7 @@ export async function writeDevToolsActivePort(userDataDir, port) {
36
36
  for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
37
37
  try {
38
38
  await mkdir(path.dirname(candidate), { recursive: true });
39
- await writeFile(candidate, contents, 'utf8');
39
+ await writeFile(candidate, contents, "utf8");
40
40
  }
41
41
  catch {
42
42
  // best effort
@@ -46,7 +46,7 @@ export async function writeDevToolsActivePort(userDataDir, port) {
46
46
  export async function readChromePid(userDataDir) {
47
47
  const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
48
48
  try {
49
- const raw = (await readFile(pidPath, 'utf8')).trim();
49
+ const raw = (await readFile(pidPath, "utf8")).trim();
50
50
  const pid = Number.parseInt(raw, 10);
51
51
  if (!Number.isFinite(pid) || pid <= 0) {
52
52
  return null;
@@ -63,12 +63,83 @@ export async function writeChromePid(userDataDir, pid) {
63
63
  const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
64
64
  try {
65
65
  await mkdir(path.dirname(pidPath), { recursive: true });
66
- await writeFile(pidPath, `${Math.trunc(pid)}\n`, 'utf8');
66
+ await writeFile(pidPath, `${Math.trunc(pid)}\n`, "utf8");
67
67
  }
68
68
  catch {
69
69
  // best effort
70
70
  }
71
71
  }
72
+ export async function findRunningChromeDebugTargetForProfile(userDataDir) {
73
+ if (process.platform === "win32") {
74
+ return null;
75
+ }
76
+ try {
77
+ const { stdout } = await execFileAsync("ps", ["-ax", "-o", "pid=", "-o", "command="], {
78
+ maxBuffer: 10 * 1024 * 1024,
79
+ });
80
+ return findChromeDebugTargetForProfileFromProcessList(String(stdout ?? ""), userDataDir);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function findChromeDebugTargetForProfileFromProcessList(processList, userDataDir) {
87
+ for (const line of processList.split("\n")) {
88
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
89
+ if (!match)
90
+ continue;
91
+ const pid = Number.parseInt(match[1] ?? "", 10);
92
+ const command = match[2] ?? "";
93
+ const lower = command.toLowerCase();
94
+ if (!Number.isFinite(pid) || pid <= 0)
95
+ continue;
96
+ if (!lower.includes("chrome") && !lower.includes("chromium"))
97
+ continue;
98
+ if (!lower.includes("user-data-dir") || !command.includes(userDataDir))
99
+ continue;
100
+ const portMatch = command.match(/--remote-debugging-port(?:=|\s+)(\d+)/);
101
+ const port = Number.parseInt(portMatch?.[1] ?? "", 10);
102
+ if (!Number.isFinite(port) || port <= 0)
103
+ continue;
104
+ return { pid, port };
105
+ }
106
+ return null;
107
+ }
108
+ export function findChromeDebugTargetForProfileFromProcessListForTest(processList, userDataDir) {
109
+ return findChromeDebugTargetForProfileFromProcessList(processList, userDataDir);
110
+ }
111
+ export async function terminateRecordedChromeForProfile(userDataDir, logger) {
112
+ const pid = await readChromePid(userDataDir);
113
+ if (!pid || !isProcessAlive(pid)) {
114
+ return false;
115
+ }
116
+ const command = await readProcessCommand(pid);
117
+ if (!isChromeCommandForUserDataDir(command, userDataDir)) {
118
+ logger?.(`Recorded Chrome pid ${pid} does not match ${userDataDir}; skipping termination`);
119
+ return false;
120
+ }
121
+ try {
122
+ process.kill(pid, "SIGTERM");
123
+ logger?.(`Terminated shared manual-login Chrome pid ${pid}`);
124
+ return true;
125
+ }
126
+ catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ logger?.(`Failed to terminate shared manual-login Chrome pid ${pid}: ${message}`);
129
+ return false;
130
+ }
131
+ }
132
+ function isChromeCommandForUserDataDir(command, userDataDir) {
133
+ if (!command)
134
+ return false;
135
+ const lower = command.toLowerCase();
136
+ return ((lower.includes("chrome") || lower.includes("chromium")) &&
137
+ lower.includes("user-data-dir") &&
138
+ command.includes(userDataDir));
139
+ }
140
+ export function isChromeCommandForUserDataDirForTest(command, userDataDir) {
141
+ return isChromeCommandForUserDataDir(command, userDataDir);
142
+ }
72
143
  export function isProcessAlive(pid) {
73
144
  if (!Number.isFinite(pid) || pid <= 0)
74
145
  return false;
@@ -78,7 +149,10 @@ export function isProcessAlive(pid) {
78
149
  }
79
150
  catch (error) {
80
151
  // EPERM means "exists but no permission"; treat as alive.
81
- if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
152
+ if (error &&
153
+ typeof error === "object" &&
154
+ "code" in error &&
155
+ error.code === "EPERM") {
82
156
  return true;
83
157
  }
84
158
  return false;
@@ -91,7 +165,7 @@ function parseProfileRunLock(payload) {
91
165
  const parsed = JSON.parse(payload);
92
166
  if (!Number.isFinite(parsed.pid) || parsed.pid <= 0)
93
167
  return null;
94
- if (!parsed.lockId || typeof parsed.lockId !== 'string')
168
+ if (!parsed.lockId || typeof parsed.lockId !== "string")
95
169
  return null;
96
170
  return parsed;
97
171
  }
@@ -104,7 +178,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
104
178
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
105
179
  return null;
106
180
  }
107
- const pollMs = typeof options.pollMs === 'number' && Number.isFinite(options.pollMs) && options.pollMs > 0
181
+ const pollMs = typeof options.pollMs === "number" && Number.isFinite(options.pollMs) && options.pollMs > 0
108
182
  ? options.pollMs
109
183
  : 1000;
110
184
  const lockPath = path.join(userDataDir, ORACLE_PROFILE_LOCK_FILENAME);
@@ -120,7 +194,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
120
194
  sessionId: options.sessionId,
121
195
  };
122
196
  await mkdir(path.dirname(lockPath), { recursive: true });
123
- await writeFile(lockPath, JSON.stringify(payload), { encoding: 'utf8', flag: 'wx' });
197
+ await writeFile(lockPath, JSON.stringify(payload), { encoding: "utf8", flag: "wx" });
124
198
  options.logger?.(`Acquired Oracle profile lock at ${lockPath}`);
125
199
  return {
126
200
  path: lockPath,
@@ -130,16 +204,16 @@ export async function acquireProfileRunLock(userDataDir, options) {
130
204
  }
131
205
  catch (error) {
132
206
  const code = error.code;
133
- if (code !== 'EEXIST') {
207
+ if (code !== "EEXIST") {
134
208
  throw error;
135
209
  }
136
- let existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
210
+ let existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
137
211
  if (!existing) {
138
212
  // Likely partial write / corruption; re-read once, then delete (user preference: delete unreadable lockfiles).
139
213
  await delay(200);
140
- existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
214
+ existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
141
215
  if (!existing) {
142
- options.logger?.('Oracle profile lock unreadable; deleting lockfile.');
216
+ options.logger?.("Oracle profile lock unreadable; deleting lockfile.");
143
217
  await rm(lockPath, { force: true }).catch(() => undefined);
144
218
  continue;
145
219
  }
@@ -163,7 +237,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
163
237
  }
164
238
  export async function releaseProfileRunLock(lockPath, lockId, logger) {
165
239
  try {
166
- const existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
240
+ const existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
167
241
  if (!existing || existing.lockId !== lockId) {
168
242
  return;
169
243
  }
@@ -174,7 +248,7 @@ export async function releaseProfileRunLock(lockPath, lockId, logger) {
174
248
  // best effort
175
249
  }
176
250
  }
177
- export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attempts = 3, timeoutMs = 3000, }) {
251
+ export async function verifyDevToolsReachable({ port, host = "127.0.0.1", attempts = 3, timeoutMs = 3000, }) {
178
252
  const versionUrl = `http://${host}:${port}/json/version`;
179
253
  for (let attempt = 0; attempt < attempts; attempt++) {
180
254
  try {
@@ -196,12 +270,9 @@ export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attemp
196
270
  return { ok: false, error: message };
197
271
  }
198
272
  }
199
- return { ok: false, error: 'unreachable' };
273
+ return { ok: false, error: "unreachable" };
200
274
  }
201
275
  export async function shouldCleanupManualLoginProfileState(userDataDir, logger, options = {}) {
202
- if (!options.connectionClosedUnexpectedly) {
203
- return true;
204
- }
205
276
  const port = await readDevToolsPort(userDataDir);
206
277
  if (!port) {
207
278
  return true;
@@ -224,8 +295,8 @@ export async function cleanupStaleProfileState(userDataDir, logger, options = {}
224
295
  // ignore cleanup errors
225
296
  }
226
297
  }
227
- const lockRemovalMode = options.lockRemovalMode ?? 'never';
228
- if (lockRemovalMode === 'never') {
298
+ const lockRemovalMode = options.lockRemovalMode ?? "never";
299
+ if (lockRemovalMode === "never") {
229
300
  return;
230
301
  }
231
302
  const pid = await readChromePid(userDataDir);
@@ -239,36 +310,38 @@ export async function cleanupStaleProfileState(userDataDir, logger, options = {}
239
310
  // Extra safety: if Chrome is running with this profile (but with a different PID, e.g. user relaunched
240
311
  // without remote debugging), never delete lock files.
241
312
  if (await isChromeUsingUserDataDir(userDataDir)) {
242
- logger?.('Detected running Chrome using this profile; skipping profile lock cleanup');
313
+ logger?.("Detected running Chrome using this profile; skipping profile lock cleanup");
243
314
  return;
244
315
  }
245
316
  const lockFiles = [
246
- path.join(userDataDir, 'lockfile'),
247
- path.join(userDataDir, 'SingletonLock'),
248
- path.join(userDataDir, 'SingletonSocket'),
249
- path.join(userDataDir, 'SingletonCookie'),
317
+ path.join(userDataDir, "lockfile"),
318
+ path.join(userDataDir, "SingletonLock"),
319
+ path.join(userDataDir, "SingletonSocket"),
320
+ path.join(userDataDir, "SingletonCookie"),
250
321
  ];
251
322
  for (const lock of lockFiles) {
252
323
  await rm(lock, { force: true }).catch(() => undefined);
253
324
  }
254
- logger?.('Cleaned up stale Chrome profile locks');
325
+ logger?.("Cleaned up stale Chrome profile locks");
255
326
  }
256
327
  async function isChromeUsingUserDataDir(userDataDir) {
257
- if (process.platform === 'win32') {
328
+ if (process.platform === "win32") {
258
329
  // On Windows, lockfiles are typically held open and removal should fail anyway; avoid expensive process scans.
259
330
  return false;
260
331
  }
261
332
  try {
262
- const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'command='], { maxBuffer: 10 * 1024 * 1024 });
263
- const lines = String(stdout ?? '').split('\n');
333
+ const { stdout } = await execFileAsync("ps", ["-ax", "-o", "command="], {
334
+ maxBuffer: 10 * 1024 * 1024,
335
+ });
336
+ const lines = String(stdout ?? "").split("\n");
264
337
  const needle = userDataDir;
265
338
  for (const line of lines) {
266
339
  if (!line)
267
340
  continue;
268
341
  const lower = line.toLowerCase();
269
- if (!lower.includes('chrome') && !lower.includes('chromium'))
342
+ if (!lower.includes("chrome") && !lower.includes("chromium"))
270
343
  continue;
271
- if (line.includes(needle) && lower.includes('user-data-dir')) {
344
+ if (line.includes(needle) && lower.includes("user-data-dir")) {
272
345
  return true;
273
346
  }
274
347
  }
@@ -278,3 +351,15 @@ async function isChromeUsingUserDataDir(userDataDir) {
278
351
  }
279
352
  return false;
280
353
  }
354
+ async function readProcessCommand(pid) {
355
+ try {
356
+ const { stdout } = await execFileAsync("ps", ["-p", String(Math.trunc(pid)), "-o", "command="], {
357
+ maxBuffer: 1024 * 1024,
358
+ });
359
+ const command = String(stdout ?? "").trim();
360
+ return command || null;
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
@@ -0,0 +1,366 @@
1
+ import { mkdtemp, mkdir, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { closeTab, connectWithNewTab, hideChromeWindow, launchChrome, registerTerminationHooks, } from "./chromeLifecycle.js";
5
+ import { resolveBrowserConfig } from "./config.js";
6
+ import { syncCookies } from "./cookies.js";
7
+ import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
8
+ import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
9
+ import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
10
+ import { CHATGPT_URL } from "./constants.js";
11
+ import { delay } from "./utils.js";
12
+ import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
13
+ import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
14
+ import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
15
+ export async function runBrowserProjectSources(request) {
16
+ const startedAt = Date.now();
17
+ const logger = ((message) => request.log?.(message));
18
+ const projectUrl = normalizeProjectSourcesUrl(request.chatgptUrl);
19
+ const operation = request.operation;
20
+ const files = request.files ?? [];
21
+ const plannedUploads = buildProjectSourcesUploadPlan(files);
22
+ const warnings = [];
23
+ if (operation === "add" && files.length === 0) {
24
+ throw new Error("Project Sources add requires at least one file.");
25
+ }
26
+ if (request.dryRun) {
27
+ return {
28
+ status: "dry-run",
29
+ operation,
30
+ projectUrl,
31
+ dryRun: true,
32
+ plannedUploads,
33
+ warnings,
34
+ tookMs: Date.now() - startedAt,
35
+ };
36
+ }
37
+ let config = resolveBrowserConfig({
38
+ ...request.config,
39
+ url: projectUrl,
40
+ chatgptUrl: projectUrl,
41
+ });
42
+ if (config.remoteChrome) {
43
+ throw new Error("Project Sources v1 uses local browser automation only. Run it on the signed-in browser host.");
44
+ }
45
+ const manualLogin = Boolean(config.manualLogin);
46
+ const manualProfileDir = config.manualLoginProfileDir
47
+ ? path.resolve(config.manualLoginProfileDir)
48
+ : path.join(os.homedir(), ".oracle", "browser-profile");
49
+ const userDataDir = manualLogin
50
+ ? manualProfileDir
51
+ : await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
52
+ if (manualLogin) {
53
+ await mkdir(userDataDir, { recursive: true });
54
+ logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
55
+ }
56
+ else {
57
+ logger(`Created temporary Chrome profile at ${userDataDir}`);
58
+ }
59
+ let tabLease = null;
60
+ if (manualLogin) {
61
+ tabLease = await acquireBrowserTabLease(userDataDir, {
62
+ maxConcurrentTabs: config.maxConcurrentTabs,
63
+ timeoutMs: config.timeoutMs,
64
+ logger,
65
+ sessionId: "project-sources",
66
+ });
67
+ }
68
+ let chrome = null;
69
+ let reusedChrome = null;
70
+ let client = null;
71
+ let isolatedTargetId = null;
72
+ let removeTerminationHooks = null;
73
+ let removeDialogHandler = null;
74
+ let connectionClosedUnexpectedly = false;
75
+ let completed = false;
76
+ const effectiveKeepBrowser = Boolean(config.keepBrowser);
77
+ try {
78
+ const acquired = manualLogin
79
+ ? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
80
+ : {
81
+ chrome: await launchChrome({ ...config, remoteChrome: null }, userDataDir, logger),
82
+ reusedChrome: null,
83
+ };
84
+ chrome = acquired.chrome;
85
+ reusedChrome = acquired.reusedChrome;
86
+ const chromeHost = chrome.host ?? "127.0.0.1";
87
+ if (tabLease) {
88
+ await tabLease.update({ chromeHost, chromePort: chrome.port });
89
+ }
90
+ removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
91
+ isInFlight: () => !completed,
92
+ preserveUserDataDir: manualLogin,
93
+ });
94
+ const strictTabIsolation = Boolean(manualLogin && reusedChrome);
95
+ const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
96
+ fallbackToDefault: !strictTabIsolation,
97
+ retries: strictTabIsolation ? 3 : 0,
98
+ retryDelayMs: 500,
99
+ });
100
+ client = connection.client;
101
+ isolatedTargetId = connection.targetId ?? null;
102
+ if (tabLease && isolatedTargetId) {
103
+ await tabLease.update({
104
+ chromeHost,
105
+ chromePort: chrome.port,
106
+ chromeTargetId: isolatedTargetId,
107
+ tabUrl: projectUrl,
108
+ });
109
+ }
110
+ const disconnectPromise = new Promise((_, reject) => {
111
+ client?.on("disconnect", () => {
112
+ connectionClosedUnexpectedly = true;
113
+ reject(new Error("Chrome window closed before Project Sources finished."));
114
+ });
115
+ });
116
+ const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
117
+ const { Network, Page, Runtime, Input, DOM } = client;
118
+ if (!config.headless && config.hideWindow) {
119
+ await hideChromeWindow(chrome, logger);
120
+ }
121
+ const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
122
+ if (DOM && typeof DOM.enable === "function") {
123
+ domainEnablers.push(DOM.enable());
124
+ }
125
+ await Promise.all(domainEnablers);
126
+ removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
127
+ if (!manualLogin) {
128
+ await Network.clearBrowserCookies();
129
+ }
130
+ const appliedCookies = await applyProjectSourcesCookies({
131
+ config,
132
+ network: Network,
133
+ manualLogin,
134
+ logger,
135
+ });
136
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
137
+ await raceWithDisconnect(waitForProjectSourcesLogin({
138
+ runtime: Runtime,
139
+ logger,
140
+ appliedCookies,
141
+ manualLogin,
142
+ timeoutMs: config.timeoutMs,
143
+ }));
144
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
145
+ await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
146
+ await raceWithDisconnect(waitForProjectSourcesReady(Runtime, config.inputTimeoutMs, logger));
147
+ const sourcesBefore = await raceWithDisconnect(waitForProjectSourcesListSettled(Runtime, config.inputTimeoutMs, logger));
148
+ let sourcesAfter = sourcesBefore;
149
+ if (operation === "add") {
150
+ sourcesAfter = await raceWithDisconnect(uploadProjectSources({ runtime: Runtime, dom: DOM, input: Input }, files, logger, config.timeoutMs));
151
+ }
152
+ const added = operation === "add" ? diffAddedProjectSources(sourcesBefore, sourcesAfter) : [];
153
+ completed = true;
154
+ return {
155
+ status: "ok",
156
+ operation,
157
+ projectUrl,
158
+ dryRun: false,
159
+ sourcesBefore,
160
+ sourcesAfter,
161
+ plannedUploads,
162
+ added,
163
+ warnings,
164
+ tookMs: Date.now() - startedAt,
165
+ };
166
+ }
167
+ finally {
168
+ removeDialogHandler?.();
169
+ removeTerminationHooks?.();
170
+ const chromeHost = chrome?.host ?? "127.0.0.1";
171
+ try {
172
+ await client?.close();
173
+ }
174
+ catch {
175
+ // ignore close failures
176
+ }
177
+ if (completed && isolatedTargetId && chrome?.port) {
178
+ await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
179
+ }
180
+ let keepBrowserOpen = effectiveKeepBrowser;
181
+ let cleanupProfileLock = null;
182
+ let terminatedRecordedChrome = false;
183
+ if (!keepBrowserOpen && manualLogin && tabLease) {
184
+ const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
185
+ if (cleanupLockTimeoutMs > 0) {
186
+ cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
187
+ timeoutMs: cleanupLockTimeoutMs,
188
+ logger,
189
+ sessionId: "project-sources",
190
+ }).catch(() => null);
191
+ }
192
+ keepBrowserOpen = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id).catch(() => false);
193
+ if (keepBrowserOpen) {
194
+ logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
195
+ }
196
+ else if (reusedChrome && !connectionClosedUnexpectedly) {
197
+ keepBrowserOpen = true;
198
+ logger("[browser] Reused shared Chrome; leaving browser process running.");
199
+ }
200
+ }
201
+ if (tabLease) {
202
+ const handle = tabLease;
203
+ tabLease = null;
204
+ await handle.release().catch(() => undefined);
205
+ }
206
+ if (!keepBrowserOpen && chrome) {
207
+ if (!connectionClosedUnexpectedly) {
208
+ try {
209
+ if (!terminatedRecordedChrome) {
210
+ await chrome.kill();
211
+ }
212
+ }
213
+ catch {
214
+ // ignore kill failures
215
+ }
216
+ }
217
+ if (manualLogin) {
218
+ const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, { connectionClosedUnexpectedly, host: chrome.host ?? "127.0.0.1" });
219
+ if (shouldCleanup) {
220
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
221
+ }
222
+ }
223
+ else {
224
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
225
+ }
226
+ }
227
+ else if (chrome) {
228
+ try {
229
+ chrome.process?.unref();
230
+ }
231
+ catch {
232
+ // best effort
233
+ }
234
+ logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
235
+ }
236
+ if (cleanupProfileLock) {
237
+ await cleanupProfileLock.release().catch(() => undefined);
238
+ }
239
+ }
240
+ }
241
+ async function applyProjectSourcesCookies({ config, network, manualLogin, logger, }) {
242
+ const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
243
+ const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
244
+ if (!cookieSyncEnabled) {
245
+ logger(manualLogin
246
+ ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
247
+ : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
248
+ return 0;
249
+ }
250
+ const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
251
+ allowErrors: config.allowCookieErrors ?? false,
252
+ filterNames: config.cookieNames ?? undefined,
253
+ inlineCookies: config.inlineCookies ?? undefined,
254
+ cookiePath: config.chromeCookiePath ?? undefined,
255
+ waitMs: config.cookieSyncWaitMs ?? 0,
256
+ });
257
+ logger(cookieCount > 0
258
+ ? config.inlineCookies
259
+ ? `Applied ${cookieCount} inline cookies`
260
+ : `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
261
+ : "No Chrome cookies found; continuing without session reuse");
262
+ return cookieCount;
263
+ }
264
+ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
265
+ if (!manualLogin) {
266
+ await ensureLoggedIn(runtime, logger, { appliedCookies });
267
+ return;
268
+ }
269
+ const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
270
+ let lastNotice = 0;
271
+ while (Date.now() < deadline) {
272
+ try {
273
+ await ensureLoggedIn(runtime, logger, { appliedCookies });
274
+ return;
275
+ }
276
+ catch (error) {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ const retryable = message.toLowerCase().includes("login button") ||
279
+ message.toLowerCase().includes("session not detected");
280
+ if (!retryable) {
281
+ throw error;
282
+ }
283
+ const now = Date.now();
284
+ if (now - lastNotice > 5000) {
285
+ logger("Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...");
286
+ lastNotice = now;
287
+ }
288
+ await delay(1000);
289
+ }
290
+ }
291
+ throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
292
+ }
293
+ async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
294
+ const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
295
+ let launchLock = null;
296
+ if (lockTimeoutMs > 0) {
297
+ launchLock = await acquireProfileRunLock(userDataDir, {
298
+ timeoutMs: lockTimeoutMs,
299
+ logger,
300
+ sessionId: "project-sources",
301
+ });
302
+ }
303
+ try {
304
+ const reusedChrome = await maybeReuseProjectSourcesChrome(userDataDir, logger, {
305
+ waitForPortMs: config.reuseChromeWaitMs,
306
+ });
307
+ const chrome = reusedChrome ??
308
+ (await launchChrome({
309
+ ...config,
310
+ remoteChrome: null,
311
+ }, userDataDir, logger));
312
+ if (chrome.port) {
313
+ await writeDevToolsActivePort(userDataDir, chrome.port);
314
+ if (!reusedChrome && chrome.pid) {
315
+ await writeChromePid(userDataDir, chrome.pid);
316
+ }
317
+ }
318
+ return { chrome, reusedChrome };
319
+ }
320
+ finally {
321
+ await launchLock?.release().catch(() => undefined);
322
+ }
323
+ }
324
+ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {}) {
325
+ const waitForPortMs = Math.max(0, options.waitForPortMs ?? 0);
326
+ let port = await readDevToolsPort(userDataDir);
327
+ if (!port && waitForPortMs > 0) {
328
+ const deadline = Date.now() + waitForPortMs;
329
+ logger(`Waiting up to ${Math.round(waitForPortMs / 1000)}s for shared Chrome to appear...`);
330
+ while (!port && Date.now() < deadline) {
331
+ await delay(250);
332
+ port = await readDevToolsPort(userDataDir);
333
+ }
334
+ }
335
+ let pid = await readChromePid(userDataDir);
336
+ if (!port) {
337
+ const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
338
+ if (!discovered) {
339
+ return null;
340
+ }
341
+ const probe = await verifyDevToolsReachable({ port: discovered.port });
342
+ if (!probe.ok) {
343
+ logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${probe.error}); launching new Chrome.`);
344
+ return null;
345
+ }
346
+ await writeDevToolsActivePort(userDataDir, discovered.port);
347
+ await writeChromePid(userDataDir, discovered.pid);
348
+ port = discovered.port;
349
+ pid = discovered.pid;
350
+ logger(`Discovered running Chrome for ${userDataDir}; reusing (DevTools port ${port}, pid ${pid})`);
351
+ return { port, pid, kill: async () => { }, process: undefined };
352
+ }
353
+ const probe = await verifyDevToolsReachable({ port });
354
+ if (!probe.ok) {
355
+ logger(`Recorded Chrome DevTools port ${port} is stale (${probe.error}); launching new Chrome.`);
356
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
357
+ return null;
358
+ }
359
+ logger(`Reusing running Chrome on port ${port} with profile ${userDataDir}`);
360
+ return {
361
+ port,
362
+ pid: pid ?? undefined,
363
+ kill: async () => { },
364
+ process: undefined,
365
+ };
366
+ }