@steipete/oracle 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/bin/oracle-cli.js +16 -6
  2. package/dist/bin/oracle.js +569 -0
  3. package/dist/docs-site/.nojekyll +0 -0
  4. package/dist/docs-site/CNAME +1 -0
  5. package/dist/docs-site/RELEASING.html +410 -0
  6. package/dist/docs-site/agents.html +374 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +416 -0
  9. package/dist/docs-site/browser-mode.html +594 -0
  10. package/dist/docs-site/chromium-forks.html +347 -0
  11. package/dist/docs-site/cli-reference.html +346 -0
  12. package/dist/docs-site/configuration.html +462 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +383 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +43 -0
  21. package/dist/docs-site/manual-tests.html +596 -0
  22. package/dist/docs-site/mcp.html +391 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +399 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +532 -0
  30. package/dist/docs-site/sessions.html +388 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +320 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +323 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/src/browser/actions/assistantResponse.js +40 -31
  39. package/dist/src/browser/actions/attachments.js +28 -1
  40. package/dist/src/browser/actions/deepResearch.js +344 -128
  41. package/dist/src/browser/actions/modelSelection.js +75 -9
  42. package/dist/src/browser/actions/promptComposer.js +71 -14
  43. package/dist/src/browser/actions/thinkingStatus.js +19 -1
  44. package/dist/src/browser/actions/thinkingTime.js +65 -20
  45. package/dist/src/browser/artifacts.js +193 -14
  46. package/dist/src/browser/chatgptFiles.js +674 -91
  47. package/dist/src/browser/chromeCookies.js +312 -0
  48. package/dist/src/browser/chromeLifecycle.js +35 -4
  49. package/dist/src/browser/constants.js +5 -0
  50. package/dist/src/browser/deepResearchResult.js +23 -0
  51. package/dist/src/browser/index.js +112 -41
  52. package/dist/src/browser/keytarShim.js +56 -0
  53. package/dist/src/browser/profileCopy.js +93 -0
  54. package/dist/src/browser/sessionRunner.js +9 -3
  55. package/dist/src/browser/windowsCookies.js +219 -0
  56. package/dist/src/cli/bridge/client.js +4 -1
  57. package/dist/src/cli/bridge/doctor.js +19 -0
  58. package/dist/src/cli/browserConfig.js +17 -1
  59. package/dist/src/cli/runOptions.js +11 -2
  60. package/dist/src/cli/sessionDisplay.js +6 -1
  61. package/dist/src/cli/sessionRunner.js +41 -17
  62. package/dist/src/config.js +3 -0
  63. package/dist/src/oracle/client.js +2 -0
  64. package/dist/src/oracle/modelResolver.js +85 -0
  65. package/dist/src/oracle/multiModelRunner.js +4 -1
  66. package/dist/src/oracle/run.js +4 -1
  67. package/dist/src/remote/client.js +253 -22
  68. package/dist/src/remote/health.js +27 -0
  69. package/dist/src/remote/server.js +239 -4
  70. package/dist/src/remote/types.js +1 -1
  71. package/dist/src/sessionManager.js +1 -0
  72. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  74. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  75. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  76. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  77. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  78. package/package.json +45 -43
  79. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  83. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  84. package/vendor/oracle-notifier/README.md +26 -0
  85. package/vendor/oracle-notifier/build-notifier.sh +0 -0
@@ -1,7 +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";
6
+ import { isDeepResearchIncompleteText } from "./deepResearchResult.js";
4
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;
5
12
  function sanitizePathSegment(value, fallback) {
6
13
  const sanitized = value
7
14
  .toLowerCase()
@@ -11,6 +18,29 @@ function sanitizePathSegment(value, fallback) {
11
18
  .slice(0, 80);
12
19
  return sanitized || fallback;
13
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
+ }
14
44
  function normalizeSessionId(sessionId) {
15
45
  return sanitizePathSegment(path.basename(sessionId), "session");
16
46
  }
@@ -26,7 +56,7 @@ async function pathExists(targetPath) {
26
56
  return false;
27
57
  }
28
58
  }
29
- async function resolveUniquePath(basePath) {
59
+ export async function resolveUniqueArtifactPath(basePath) {
30
60
  const ext = path.extname(basePath);
31
61
  const stem = ext ? path.basename(basePath, ext) : path.basename(basePath);
32
62
  const dir = path.dirname(basePath);
@@ -46,6 +76,138 @@ async function readSizeBytes(targetPath) {
46
76
  return undefined;
47
77
  }
48
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
+ }
49
211
  export async function writeTextBrowserArtifact(params) {
50
212
  const text = params.contents.trim();
51
213
  if (!params.sessionId || text.length === 0) {
@@ -53,8 +215,8 @@ export async function writeTextBrowserArtifact(params) {
53
215
  }
54
216
  const dir = resolveSessionArtifactsDir(params.sessionId);
55
217
  await fs.mkdir(dir, { recursive: true });
56
- const filename = sanitizePathSegment(params.filename, "artifact.md");
57
- 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));
58
220
  await fs.writeFile(targetPath, `${text}\n`, "utf8");
59
221
  params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
60
222
  return {
@@ -64,6 +226,10 @@ export async function writeTextBrowserArtifact(params) {
64
226
  mimeType: params.mimeType ?? "text/markdown",
65
227
  sizeBytes: await readSizeBytes(targetPath),
66
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" },
67
233
  };
68
234
  }
69
235
  export async function writeBinaryBrowserArtifact(params) {
@@ -72,10 +238,18 @@ export async function writeBinaryBrowserArtifact(params) {
72
238
  }
73
239
  const dir = resolveSessionArtifactsDir(params.sessionId);
74
240
  await fs.mkdir(dir, { recursive: true });
75
- const filename = sanitizePathSegment(params.filename, "artifact.bin");
76
- 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));
77
243
  await fs.writeFile(targetPath, params.contents);
244
+ const validation = validateArtifactBuffer({
245
+ filename,
246
+ mimeType: params.mimeType,
247
+ contents: params.contents,
248
+ });
78
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
+ }
79
253
  return {
80
254
  kind: params.kind,
81
255
  path: targetPath,
@@ -83,18 +257,15 @@ export async function writeBinaryBrowserArtifact(params) {
83
257
  mimeType: params.mimeType,
84
258
  sizeBytes: params.contents.length,
85
259
  sourceUrl: params.sourceUrl,
260
+ sha256: computeBufferSha256(params.contents),
261
+ validation,
262
+ transfer: { status: "not-needed" },
263
+ origin: { mode: "local" },
86
264
  };
87
265
  }
88
- function isToolOnlyPlaceholder(text) {
89
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
90
- return (normalized === "called tool" ||
91
- normalized === "used tool" ||
92
- normalized === "użyto narzędzia" ||
93
- normalized === "narzędzie wywołane");
94
- }
95
266
  export async function saveDeepResearchReportArtifact(params) {
96
267
  const report = params.reportMarkdown.trim();
97
- if (report.length < 40 || isToolOnlyPlaceholder(report)) {
268
+ if (report.length < 40 || isDeepResearchIncompleteText(report)) {
98
269
  return null;
99
270
  }
100
271
  return writeTextBrowserArtifact({
@@ -120,7 +291,14 @@ export async function saveBrowserTranscriptArtifact(params) {
120
291
  "",
121
292
  ...params.artifacts.map((artifact) => {
122
293
  const label = artifact.label ?? artifact.kind;
123
- 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}`;
124
302
  }),
125
303
  ]
126
304
  : [];
@@ -165,5 +343,6 @@ export function appendArtifacts(existing, additions) {
165
343
  }
166
344
  export const __test__ = {
167
345
  normalizeSessionId,
346
+ sanitizeArtifactFilename,
168
347
  sanitizePathSegment,
169
348
  };