@steipete/oracle 0.15.0 → 0.15.2

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 (46) hide show
  1. package/dist/bin/oracle-cli.js +14 -6
  2. package/dist/docs-site/bridge.html +17 -1
  3. package/dist/docs-site/browser-mode.html +2 -2
  4. package/dist/docs-site/configuration.html +12 -2
  5. package/dist/docs-site/openai-endpoints.html +12 -0
  6. package/dist/scripts/test-browser.js +13 -2
  7. package/dist/src/browser/actions/assistantResponse.js +81 -50
  8. package/dist/src/browser/actions/attachments.js +31 -5
  9. package/dist/src/browser/actions/deepResearch.js +218 -73
  10. package/dist/src/browser/actions/modelSelection.js +30 -7
  11. package/dist/src/browser/actions/promptComposer.js +75 -19
  12. package/dist/src/browser/actions/thinkingStatus.js +19 -1
  13. package/dist/src/browser/artifacts.js +191 -6
  14. package/dist/src/browser/chatgptFiles.js +529 -98
  15. package/dist/src/browser/chatgptImages.js +3 -4
  16. package/dist/src/browser/chromeLifecycle.js +1 -0
  17. package/dist/src/browser/constants.js +6 -0
  18. package/dist/src/browser/conversationTurns.js +16 -0
  19. package/dist/src/browser/conversationUrlMonitor.js +64 -0
  20. package/dist/src/browser/cookies.js +72 -0
  21. package/dist/src/browser/index.js +103 -94
  22. package/dist/src/browser/projectSourcesRunner.js +3 -2
  23. package/dist/src/browser/reattach.js +27 -11
  24. package/dist/src/browser/reattachHelpers.js +14 -5
  25. package/dist/src/browser/sessionRunner.js +9 -3
  26. package/dist/src/cli/bridge/client.js +4 -1
  27. package/dist/src/cli/bridge/doctor.js +19 -0
  28. package/dist/src/cli/runOptions.js +11 -2
  29. package/dist/src/cli/sessionDisplay.js +6 -1
  30. package/dist/src/cli/sessionRunner.js +28 -10
  31. package/dist/src/config.js +3 -0
  32. package/dist/src/oracle/client.js +2 -0
  33. package/dist/src/oracle/modelResolver.js +85 -0
  34. package/dist/src/oracle/multiModelRunner.js +4 -1
  35. package/dist/src/oracle/oscProgress.js +3 -2
  36. package/dist/src/oracle/run.js +4 -1
  37. package/dist/src/remote/client.js +253 -22
  38. package/dist/src/remote/health.js +27 -0
  39. package/dist/src/remote/server.js +239 -4
  40. package/dist/src/remote/types.js +1 -1
  41. package/dist/src/sessionManager.js +1 -0
  42. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  43. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  44. package/package.json +20 -20
  45. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  46. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,5 +1,5 @@
1
1
  import { formatElapsed } from "../../oracle/format.js";
2
- import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "../constants.js";
2
+ import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
3
3
  const THINKING_STALE_HINT_MS = 10 * 60_000;
4
4
  export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
5
5
  const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
@@ -127,6 +127,7 @@ async function readThinkingStatus(Runtime) {
127
127
  }
128
128
  const SAFE_THINKING_STATUS_MESSAGES = new Set([
129
129
  "active",
130
+ "response streaming",
130
131
  "thinking sidecar active",
131
132
  "thinking sidecar opened",
132
133
  ]);
@@ -157,13 +158,16 @@ function buildThinkingStatusExpression() {
157
158
  '[aria-live="polite"]',
158
159
  ];
159
160
  const keywords = ["pro thinking", "thinking", "reasoning"];
161
+ const stopSelector = STOP_BUTTON_SELECTORS.join(", ");
160
162
  const selectorLiteral = JSON.stringify(selectors);
161
163
  const keywordsLiteral = JSON.stringify(keywords);
164
+ const stopSelectorLiteral = JSON.stringify(stopSelector);
162
165
  return `(async () => {
163
166
  const CONVERSATION_SELECTOR = ${conversationLiteral};
164
167
  const ASSISTANT_SELECTOR = ${assistantLiteral};
165
168
  const selectors = ${selectorLiteral};
166
169
  const keywords = ${keywordsLiteral};
170
+ const stopSelector = ${stopSelectorLiteral};
167
171
  const normalize = (value) =>
168
172
  String(value || '')
169
173
  .normalize('NFD')
@@ -383,6 +387,20 @@ function buildThinkingStatusExpression() {
383
387
  };
384
388
  }
385
389
  }
390
+ // Last-resort liveness fallback: selector drift can hide every thinking
391
+ // indicator while a response is still generating, and returning null here
392
+ // reads as "dead" downstream. The stop/interrupt control is a stable,
393
+ // language-independent signal that generation is active; it lives in the
394
+ // composer, so isComposerAdjacent must not filter it.
395
+ const stopVisible = Array.from(document.querySelectorAll(stopSelector)).some((node) =>
396
+ isVisible(node),
397
+ );
398
+ if (stopVisible) {
399
+ return {
400
+ message: 'response streaming',
401
+ source: 'inline',
402
+ };
403
+ }
386
404
  return null;
387
405
  })()`;
388
406
  }
@@ -1,8 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
1
3
  import fs from "node:fs/promises";
2
4
  import path from "node:path";
3
5
  import { getOracleHomeDir } from "../oracleHome.js";
4
6
  import { isDeepResearchIncompleteText } from "./deepResearchResult.js";
5
7
  const ARTIFACTS_DIRNAME = "artifacts";
8
+ const ZIP_EOCD_SIGNATURE = 0x06054b50;
9
+ const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
10
+ const ZIP_EMPTY_ARCHIVE_LENGTH = 22;
11
+ const ZIP_MAX_EOCD_COMMENT_BYTES = 65_535;
6
12
  function sanitizePathSegment(value, fallback) {
7
13
  const sanitized = value
8
14
  .toLowerCase()
@@ -12,6 +18,29 @@ function sanitizePathSegment(value, fallback) {
12
18
  .slice(0, 80);
13
19
  return sanitized || fallback;
14
20
  }
21
+ export function sanitizeArtifactFilename(value, fallback = "artifact.bin") {
22
+ const normalized = String(value ?? "")
23
+ .replace(/\0/g, "")
24
+ .replace(/\\/g, "/");
25
+ const basename = path.basename(normalized).replace(/\.crdownload$/i, "");
26
+ const fallbackName = path.basename(fallback.replace(/\\/g, "/")) || "artifact.bin";
27
+ const sanitized = sanitizePathSegment(basename, sanitizePathSegment(fallbackName, "artifact.bin"));
28
+ return sanitized === "." || sanitized === ".."
29
+ ? sanitizePathSegment(fallbackName, "artifact.bin")
30
+ : sanitized;
31
+ }
32
+ export function sanitizeArtifactMimeType(value) {
33
+ const mime = String(value ?? "")
34
+ .split(";", 1)[0]
35
+ ?.trim()
36
+ .toLowerCase();
37
+ if (!mime ||
38
+ mime.length > 127 ||
39
+ !/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(mime)) {
40
+ return undefined;
41
+ }
42
+ return mime;
43
+ }
15
44
  function normalizeSessionId(sessionId) {
16
45
  return sanitizePathSegment(path.basename(sessionId), "session");
17
46
  }
@@ -27,7 +56,7 @@ async function pathExists(targetPath) {
27
56
  return false;
28
57
  }
29
58
  }
30
- async function resolveUniquePath(basePath) {
59
+ export async function resolveUniqueArtifactPath(basePath) {
31
60
  const ext = path.extname(basePath);
32
61
  const stem = ext ? path.basename(basePath, ext) : path.basename(basePath);
33
62
  const dir = path.dirname(basePath);
@@ -47,6 +76,138 @@ async function readSizeBytes(targetPath) {
47
76
  return undefined;
48
77
  }
49
78
  }
79
+ export function computeBufferSha256(contents) {
80
+ return createHash("sha256").update(contents).digest("hex");
81
+ }
82
+ export async function computeFileSha256(targetPath) {
83
+ const hash = createHash("sha256");
84
+ await new Promise((resolve, reject) => {
85
+ const stream = createReadStream(targetPath);
86
+ stream.on("data", (chunk) => hash.update(chunk));
87
+ stream.on("error", reject);
88
+ stream.on("end", () => resolve());
89
+ });
90
+ return hash.digest("hex");
91
+ }
92
+ export function isZipArtifact(filename, mimeType) {
93
+ const ext = path.extname(String(filename ?? "")).toLowerCase();
94
+ const mime = String(mimeType ?? "").toLowerCase();
95
+ return (ext === ".zip" ||
96
+ mime === "application/zip" ||
97
+ mime === "application/x-zip-compressed" ||
98
+ mime.endsWith("+zip"));
99
+ }
100
+ export function validateZipBuffer(contents) {
101
+ if (contents.length < ZIP_EMPTY_ARCHIVE_LENGTH) {
102
+ return { type: "zip", ok: false, error: "zip-too-small" };
103
+ }
104
+ const firstSignature = contents.readUInt32LE(0);
105
+ if (firstSignature !== ZIP_LOCAL_FILE_HEADER_SIGNATURE && firstSignature !== ZIP_EOCD_SIGNATURE) {
106
+ return { type: "zip", ok: false, error: "zip-magic-mismatch" };
107
+ }
108
+ const searchStart = Math.max(0, contents.length - ZIP_EMPTY_ARCHIVE_LENGTH - ZIP_MAX_EOCD_COMMENT_BYTES);
109
+ let eocdOffset = -1;
110
+ for (let offset = contents.length - ZIP_EMPTY_ARCHIVE_LENGTH; offset >= searchStart; offset -= 1) {
111
+ if (contents.readUInt32LE(offset) === ZIP_EOCD_SIGNATURE) {
112
+ eocdOffset = offset;
113
+ break;
114
+ }
115
+ }
116
+ if (eocdOffset < 0) {
117
+ return { type: "zip", ok: false, error: "zip-central-directory-missing" };
118
+ }
119
+ const commentLength = contents.readUInt16LE(eocdOffset + 20);
120
+ if (eocdOffset + ZIP_EMPTY_ARCHIVE_LENGTH + commentLength !== contents.length) {
121
+ return { type: "zip", ok: false, error: "zip-eocd-size-mismatch" };
122
+ }
123
+ const centralDirectorySize = contents.readUInt32LE(eocdOffset + 12);
124
+ const centralDirectoryOffset = contents.readUInt32LE(eocdOffset + 16);
125
+ if (centralDirectoryOffset + centralDirectorySize > eocdOffset) {
126
+ return { type: "zip", ok: false, error: "zip-central-directory-out-of-range" };
127
+ }
128
+ return { type: "zip", ok: true };
129
+ }
130
+ async function readFileWindow(handle, length, position) {
131
+ const buffer = Buffer.alloc(length);
132
+ let offset = 0;
133
+ while (offset < length) {
134
+ const result = await handle.read(buffer, offset, length - offset, position + offset);
135
+ if (result.bytesRead === 0) {
136
+ return null;
137
+ }
138
+ offset += result.bytesRead;
139
+ }
140
+ return buffer;
141
+ }
142
+ export async function validateZipFile(targetPath) {
143
+ const handle = await fs.open(targetPath, "r");
144
+ try {
145
+ const fileStat = await handle.stat();
146
+ if (fileStat.size < ZIP_EMPTY_ARCHIVE_LENGTH) {
147
+ return { type: "zip", ok: false, error: "zip-too-small" };
148
+ }
149
+ const first = await readFileWindow(handle, 4, 0);
150
+ if (!first) {
151
+ return { type: "zip", ok: false, error: "zip-too-small" };
152
+ }
153
+ const firstSignature = first.readUInt32LE(0);
154
+ if (firstSignature !== ZIP_LOCAL_FILE_HEADER_SIGNATURE &&
155
+ firstSignature !== ZIP_EOCD_SIGNATURE) {
156
+ return { type: "zip", ok: false, error: "zip-magic-mismatch" };
157
+ }
158
+ const tailLength = Math.min(fileStat.size, ZIP_EMPTY_ARCHIVE_LENGTH + ZIP_MAX_EOCD_COMMENT_BYTES);
159
+ const tailStart = fileStat.size - tailLength;
160
+ const tail = await readFileWindow(handle, tailLength, tailStart);
161
+ if (!tail) {
162
+ return { type: "zip", ok: false, error: "zip-central-directory-missing" };
163
+ }
164
+ let relativeEocdOffset = -1;
165
+ for (let offset = tail.length - ZIP_EMPTY_ARCHIVE_LENGTH; offset >= 0; offset -= 1) {
166
+ if (tail.readUInt32LE(offset) === ZIP_EOCD_SIGNATURE) {
167
+ relativeEocdOffset = offset;
168
+ break;
169
+ }
170
+ }
171
+ if (relativeEocdOffset < 0) {
172
+ return { type: "zip", ok: false, error: "zip-central-directory-missing" };
173
+ }
174
+ const eocdOffset = tailStart + relativeEocdOffset;
175
+ const commentLength = tail.readUInt16LE(relativeEocdOffset + 20);
176
+ if (eocdOffset + ZIP_EMPTY_ARCHIVE_LENGTH + commentLength !== fileStat.size) {
177
+ return { type: "zip", ok: false, error: "zip-eocd-size-mismatch" };
178
+ }
179
+ const centralDirectorySize = tail.readUInt32LE(relativeEocdOffset + 12);
180
+ const centralDirectoryOffset = tail.readUInt32LE(relativeEocdOffset + 16);
181
+ if (centralDirectoryOffset + centralDirectorySize > eocdOffset) {
182
+ return { type: "zip", ok: false, error: "zip-central-directory-out-of-range" };
183
+ }
184
+ return { type: "zip", ok: true };
185
+ }
186
+ finally {
187
+ await handle.close();
188
+ }
189
+ }
190
+ export function validateArtifactBuffer(params) {
191
+ if (isZipArtifact(params.filename, params.mimeType)) {
192
+ return validateZipBuffer(params.contents);
193
+ }
194
+ return {
195
+ type: "generic",
196
+ ok: params.contents.length > 0,
197
+ error: params.contents.length > 0 ? undefined : "empty-file",
198
+ };
199
+ }
200
+ export async function validateArtifactFile(params) {
201
+ if (isZipArtifact(params.filename ?? path.basename(params.path), params.mimeType)) {
202
+ return validateZipFile(params.path);
203
+ }
204
+ const size = await readSizeBytes(params.path);
205
+ return {
206
+ type: "generic",
207
+ ok: Boolean(size && size > 0),
208
+ error: size && size > 0 ? undefined : "empty-file",
209
+ };
210
+ }
50
211
  export async function writeTextBrowserArtifact(params) {
51
212
  const text = params.contents.trim();
52
213
  if (!params.sessionId || text.length === 0) {
@@ -54,8 +215,8 @@ export async function writeTextBrowserArtifact(params) {
54
215
  }
55
216
  const dir = resolveSessionArtifactsDir(params.sessionId);
56
217
  await fs.mkdir(dir, { recursive: true });
57
- const filename = sanitizePathSegment(params.filename, "artifact.md");
58
- const targetPath = await resolveUniquePath(path.join(dir, filename));
218
+ const filename = sanitizeArtifactFilename(params.filename, "artifact.md");
219
+ const targetPath = await resolveUniqueArtifactPath(path.join(dir, filename));
59
220
  await fs.writeFile(targetPath, `${text}\n`, "utf8");
60
221
  params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
61
222
  return {
@@ -65,6 +226,10 @@ export async function writeTextBrowserArtifact(params) {
65
226
  mimeType: params.mimeType ?? "text/markdown",
66
227
  sizeBytes: await readSizeBytes(targetPath),
67
228
  sourceUrl: params.sourceUrl,
229
+ sha256: computeBufferSha256(Buffer.from(`${text}\n`, "utf8")),
230
+ validation: { type: "generic", ok: true },
231
+ transfer: { status: "not-needed" },
232
+ origin: { mode: "local" },
68
233
  };
69
234
  }
70
235
  export async function writeBinaryBrowserArtifact(params) {
@@ -73,10 +238,18 @@ export async function writeBinaryBrowserArtifact(params) {
73
238
  }
74
239
  const dir = resolveSessionArtifactsDir(params.sessionId);
75
240
  await fs.mkdir(dir, { recursive: true });
76
- const filename = sanitizePathSegment(params.filename, "artifact.bin");
77
- const targetPath = await resolveUniquePath(path.join(dir, filename));
241
+ const filename = sanitizeArtifactFilename(params.filename, "artifact.bin");
242
+ const targetPath = await resolveUniqueArtifactPath(path.join(dir, filename));
78
243
  await fs.writeFile(targetPath, params.contents);
244
+ const validation = validateArtifactBuffer({
245
+ filename,
246
+ mimeType: params.mimeType,
247
+ contents: params.contents,
248
+ });
79
249
  params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
250
+ if (validation.type === "zip" && !validation.ok) {
251
+ params.logger?.(`[browser] ZIP validation failed for ${filename}: ${validation.error ?? "invalid"}`);
252
+ }
80
253
  return {
81
254
  kind: params.kind,
82
255
  path: targetPath,
@@ -84,6 +257,10 @@ export async function writeBinaryBrowserArtifact(params) {
84
257
  mimeType: params.mimeType,
85
258
  sizeBytes: params.contents.length,
86
259
  sourceUrl: params.sourceUrl,
260
+ sha256: computeBufferSha256(params.contents),
261
+ validation,
262
+ transfer: { status: "not-needed" },
263
+ origin: { mode: "local" },
87
264
  };
88
265
  }
89
266
  export async function saveDeepResearchReportArtifact(params) {
@@ -114,7 +291,14 @@ export async function saveBrowserTranscriptArtifact(params) {
114
291
  "",
115
292
  ...params.artifacts.map((artifact) => {
116
293
  const label = artifact.label ?? artifact.kind;
117
- return `- ${label}: ${artifact.path}`;
294
+ const hash = artifact.sha256 ? ` sha256=${artifact.sha256}` : "";
295
+ const transfer = artifact.transfer?.status
296
+ ? ` transfer=${artifact.transfer.status}`
297
+ : "";
298
+ const validation = artifact.validation
299
+ ? ` validation=${artifact.validation.ok ? "ok" : (artifact.validation.error ?? "failed")}`
300
+ : "";
301
+ return `- ${label}: ${artifact.path}${hash}${transfer}${validation}`;
118
302
  }),
119
303
  ]
120
304
  : [];
@@ -159,5 +343,6 @@ export function appendArtifacts(existing, additions) {
159
343
  }
160
344
  export const __test__ = {
161
345
  normalizeSessionId,
346
+ sanitizeArtifactFilename,
162
347
  sanitizePathSegment,
163
348
  };