@steipete/oracle 0.17.3 → 0.19.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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +267 -15
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. package/dist/src/browser/windowsCookies.js +0 -219
@@ -6,30 +6,43 @@ import path from "node:path";
6
6
  import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
7
7
  import { appendArtifacts, computeFileSha256, resolveSessionArtifactsDir, resolveUniqueArtifactPath, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
8
8
  import { MAX_REMOTE_ARTIFACT_BYTES, } from "./types.js";
9
+ import { materializeStagedFallbackBundle } from "../browser/prompt.js";
10
+ import { checkRemoteHealth } from "./health.js";
9
11
  import { parseHostPort } from "../bridge/connection.js";
12
+ import { BrowserRunCancelledError } from "../oracle/errors.js";
10
13
  export function createRemoteBrowserExecutor({ host, token }) {
11
14
  // Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
12
15
  return async function remoteBrowserExecutor(options) {
16
+ const callerSignal = options.signal;
17
+ if (callerSignal?.aborted)
18
+ throw new BrowserRunCancelledError("Browser run cancelled before the request was sent.");
19
+ if (callerSignal) {
20
+ const health = await checkRemoteHealth({ host, token, signal: callerSignal });
21
+ if (callerSignal.aborted)
22
+ throw new BrowserRunCancelledError();
23
+ if (health.capabilities?.runCancellation !== true)
24
+ throw new Error("Remote host does not support run cancellation; upgrade the host before using an AbortSignal.");
25
+ }
13
26
  const payload = {
14
27
  prompt: options.prompt,
15
28
  attachments: await serializeAttachments(options.attachments ?? []),
16
- fallbackSubmission: options.fallbackSubmission
17
- ? {
18
- prompt: options.fallbackSubmission.prompt,
19
- attachments: await serializeAttachments(options.fallbackSubmission.attachments ?? []),
20
- }
21
- : undefined,
29
+ fallbackSubmission: await serializeFallback(options.fallbackSubmission, { host, token }),
22
30
  browserConfig: options.config ?? {},
23
31
  options: {
24
32
  heartbeatIntervalMs: options.heartbeatIntervalMs,
25
33
  verbose: options.verbose,
26
34
  sessionId: options.sessionId,
27
35
  followUpPrompts: options.followUpPrompts,
36
+ cancelOnDisconnect: callerSignal ? true : undefined,
28
37
  },
29
38
  };
30
39
  const body = Buffer.from(JSON.stringify(payload));
31
40
  const { hostname, port } = parseHost(host);
32
41
  return new Promise((resolve, reject) => {
42
+ if (callerSignal?.aborted) {
43
+ reject(new Error("Browser run cancelled before the request was sent."));
44
+ return;
45
+ }
33
46
  const transferredFiles = [];
34
47
  const transferFailures = [];
35
48
  const transferPromises = [];
@@ -40,6 +53,7 @@ export function createRemoteBrowserExecutor({ host, token }) {
40
53
  if (settled)
41
54
  return;
42
55
  settled = true;
56
+ callerSignal?.removeEventListener("abort", onCallerAbort);
43
57
  reject(error);
44
58
  };
45
59
  const req = http.request({
@@ -107,17 +121,55 @@ export function createRemoteBrowserExecutor({ host, token }) {
107
121
  return;
108
122
  }
109
123
  settled = true;
124
+ callerSignal?.removeEventListener("abort", onCallerAbort);
110
125
  resolve(mergeTransferredArtifacts(resolved, transferredFiles, transferFailures));
111
126
  })().catch(fail);
112
127
  });
113
128
  res.on("error", fail);
114
129
  });
115
130
  req.on("error", fail);
131
+ // Destroying the request closes the socket, which is how the service learns
132
+ // to abort: its own disconnect handler fires and releases the slot and the
133
+ // browser tab.
134
+ const onCallerAbort = () => {
135
+ req.destroy();
136
+ fail(new BrowserRunCancelledError("Browser run cancelled: the caller aborted."));
137
+ };
138
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
116
139
  req.write(body);
117
140
  req.end();
118
141
  });
119
142
  };
120
143
  }
144
+ async function serializeFallback(fallback, remote) {
145
+ if (!fallback)
146
+ return undefined;
147
+ if (fallback.pendingBundle) {
148
+ const health = await checkRemoteHealth(remote);
149
+ if (!health.ok || health.capabilities?.deferredFallbackBundling !== true) {
150
+ // Older hosts ignore bundle metadata, so send a ready-to-upload fallback.
151
+ const prepared = await materializeStagedFallbackBundle({
152
+ composerText: fallback.prompt,
153
+ attachments: fallback.attachments,
154
+ ...fallback.pendingBundle,
155
+ });
156
+ try {
157
+ return {
158
+ prompt: prepared.composerText,
159
+ attachments: await serializeAttachments(prepared.attachments),
160
+ };
161
+ }
162
+ finally {
163
+ await rm(path.dirname(prepared.bundled.bundlePath), { recursive: true, force: true });
164
+ }
165
+ }
166
+ }
167
+ return {
168
+ prompt: fallback.prompt,
169
+ attachments: await serializeAttachments(fallback.attachments),
170
+ bundle: fallback.pendingBundle,
171
+ };
172
+ }
121
173
  async function serializeAttachments(attachments) {
122
174
  const serialized = [];
123
175
  for (const attachment of attachments) {
@@ -173,12 +225,17 @@ function handleEvent(params) {
173
225
  token: params.token,
174
226
  descriptor: event.artifact,
175
227
  sessionId: params.options.sessionId,
228
+ signal: params.options.signal,
176
229
  log: params.options.log,
177
230
  })
178
231
  .then((artifact) => {
179
232
  params.onArtifact(artifact);
180
233
  })
181
234
  .catch((error) => {
235
+ if (params.options.signal?.aborted) {
236
+ params.onError(new BrowserRunCancelledError());
237
+ return;
238
+ }
182
239
  const message = error instanceof Error ? error.message : String(error);
183
240
  const fallback = `Oracle captured the browser text response, but bridge artifact transfer failed for ${displayFilename}. Open the ChatGPT browser on the bridge host, download the ZIP/file shown in the current response, and copy it to a cloud-readable path. Reason: ${message}`;
184
241
  params.options.log?.(`[browser] ${fallback}`);
@@ -192,6 +249,7 @@ function handleEvent(params) {
192
249
  return null;
193
250
  }
194
251
  async function transferRemoteArtifact(params) {
252
+ params.signal?.throwIfAborted();
195
253
  validateRemoteArtifactDescriptor(params.descriptor);
196
254
  const sessionId = params.sessionId ?? params.descriptor.runId;
197
255
  const artifactsDir = resolveSessionArtifactsDir(sessionId);
@@ -208,6 +266,7 @@ async function transferRemoteArtifact(params) {
208
266
  token: params.token,
209
267
  targetPath: partPath,
210
268
  descriptor: params.descriptor,
269
+ signal: params.signal,
211
270
  }).catch(async (error) => {
212
271
  await rm(partPath, { force: true }).catch(() => undefined);
213
272
  throw error;
@@ -231,6 +290,10 @@ async function transferRemoteArtifact(params) {
231
290
  await rm(partPath, { force: true }).catch(() => undefined);
232
291
  throw new Error(`${validation.type} validation failed: ${validation.error ?? "invalid"}`);
233
292
  }
293
+ if (params.signal?.aborted) {
294
+ await rm(partPath, { force: true });
295
+ throw new BrowserRunCancelledError();
296
+ }
234
297
  await rename(partPath, finalPath);
235
298
  params.log?.(`[browser] Transferred artifact to ${finalPath}`);
236
299
  const publishedFilename = path.basename(finalPath);
@@ -257,6 +320,7 @@ async function downloadArtifactToFile(params) {
257
320
  port: params.port,
258
321
  path: params.path,
259
322
  method: "GET",
323
+ signal: params.signal,
260
324
  headers: params.token ? { authorization: `Bearer ${params.token}` } : undefined,
261
325
  }, (res) => {
262
326
  if (res.statusCode !== 200) {
@@ -30,7 +30,7 @@ export async function checkTcpConnection(host, timeoutMs = 2000) {
30
30
  socket.once("timeout", onTimeout);
31
31
  });
32
32
  }
33
- export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
33
+ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, signal, }) {
34
34
  const { hostname, port } = parseHostPort(host);
35
35
  const headers = { accept: "application/json" };
36
36
  if (token) {
@@ -43,6 +43,7 @@ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
43
43
  path: "/health",
44
44
  headers,
45
45
  timeoutMs,
46
+ signal,
46
47
  });
47
48
  if (response.statusCode === 200 && typeof response.json === "object" && response.json) {
48
49
  const ok = response.json.ok === true;
@@ -90,6 +91,8 @@ function parseCapabilities(value) {
90
91
  return undefined;
91
92
  }
92
93
  return {
94
+ ...(raw.deferredFallbackBundling === true ? { deferredFallbackBundling: true } : {}),
95
+ ...(raw.runCancellation === true ? { runCancellation: true } : {}),
93
96
  artifactTransfer: true,
94
97
  artifactProtocolVersion,
95
98
  maxArtifactBytes: Math.min(maxArtifactBytes, MAX_REMOTE_ARTIFACT_BYTES),
@@ -105,7 +108,7 @@ function extractErrorMessage(json, bodyText) {
105
108
  const trimmed = bodyText.trim();
106
109
  return trimmed.length ? trimmed : null;
107
110
  }
108
- async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
111
+ async function requestJson({ hostname, port, path, headers, timeoutMs, signal, }) {
109
112
  return await new Promise((resolve, reject) => {
110
113
  const req = http.request({
111
114
  hostname,
@@ -113,6 +116,7 @@ async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
113
116
  path,
114
117
  method: "GET",
115
118
  headers,
119
+ signal,
116
120
  }, (res) => {
117
121
  res.setEncoding("utf8");
118
122
  let body = "";
@@ -0,0 +1,83 @@
1
+ export class RunQueueFullError extends Error {
2
+ constructor() {
3
+ super("The browser run queue is full.");
4
+ this.name = "RunQueueFullError";
5
+ }
6
+ }
7
+ export class RunSlots {
8
+ maxConcurrent;
9
+ maxQueued;
10
+ active = 0;
11
+ waiting = [];
12
+ constructor(maxConcurrent, maxQueued) {
13
+ this.maxConcurrent = maxConcurrent;
14
+ this.maxQueued = maxQueued;
15
+ if (!Number.isSafeInteger(maxConcurrent) ||
16
+ maxConcurrent < 1 ||
17
+ !Number.isSafeInteger(maxQueued) ||
18
+ maxQueued < 0) {
19
+ throw new Error("Run capacity must be a positive integer and queue capacity a nonnegative integer.");
20
+ }
21
+ }
22
+ get activeCount() {
23
+ return this.active;
24
+ }
25
+ get queuedCount() {
26
+ return this.waiting.length;
27
+ }
28
+ get capacity() {
29
+ return this.maxConcurrent;
30
+ }
31
+ get queueCapacity() {
32
+ return this.maxQueued;
33
+ }
34
+ get isSaturated() {
35
+ return this.active >= this.maxConcurrent && this.waiting.length >= this.maxQueued;
36
+ }
37
+ positionFor() {
38
+ return this.active < this.maxConcurrent ? 0 : this.waiting.length + 1;
39
+ }
40
+ // Reservation is synchronous, including the queue bound; request-body reads cannot race it.
41
+ acquire(signal) {
42
+ if (signal?.aborted)
43
+ return Promise.reject(new Error("cancelled before a slot was available"));
44
+ if (this.active < this.maxConcurrent) {
45
+ this.active++;
46
+ return Promise.resolve(this.makeRelease());
47
+ }
48
+ if (this.waiting.length >= this.maxQueued)
49
+ return Promise.reject(new RunQueueFullError());
50
+ return new Promise((resolve, reject) => {
51
+ const onAbort = () => {
52
+ const index = this.waiting.indexOf(entry);
53
+ if (index < 0)
54
+ return;
55
+ this.waiting.splice(index, 1);
56
+ entry.cleanup();
57
+ reject(new Error("cancelled while waiting for a slot"));
58
+ };
59
+ const entry = {
60
+ resolve,
61
+ reject,
62
+ cleanup: () => signal?.removeEventListener("abort", onAbort),
63
+ };
64
+ this.waiting.push(entry);
65
+ signal?.addEventListener("abort", onAbort, { once: true });
66
+ });
67
+ }
68
+ makeRelease() {
69
+ let released = false;
70
+ return () => {
71
+ if (released)
72
+ return;
73
+ released = true;
74
+ const next = this.waiting.shift();
75
+ if (next) {
76
+ next.cleanup();
77
+ next.resolve(this.makeRelease());
78
+ }
79
+ else
80
+ this.active--;
81
+ };
82
+ }
83
+ }