@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.
- package/dist/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +81 -50
- package/dist/src/browser/actions/attachments.js +31 -5
- package/dist/src/browser/actions/deepResearch.js +218 -73
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +75 -19
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +529 -98
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +1 -0
- package/dist/src/browser/constants.js +6 -0
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +103 -94
- package/dist/src/browser/projectSourcesRunner.js +3 -2
- package/dist/src/browser/reattach.js +27 -11
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +20 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- 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
|
|
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 =
|
|
58
|
-
const targetPath = await
|
|
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 =
|
|
77
|
-
const targetPath = await
|
|
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
|
-
|
|
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
|
};
|