@steipete/oracle 0.18.0 → 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.
- package/README.md +1 -1
- package/dist/bin/oracle-cli.js +65 -63
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +119 -16
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +13 -0
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +444 -261
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +10 -3
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +3 -1
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +84 -22
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +345 -71
- package/dist/src/sessionManager.js +36 -1
- 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 +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -2,9 +2,10 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
|
-
import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT,
|
|
5
|
+
import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
|
|
6
6
|
import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
|
|
7
7
|
import { resolveOutputPath } from "./writeOutputPath.js";
|
|
8
|
+
import { persistBrowserHarvest } from "./harvestIntegrity.js";
|
|
8
9
|
const LIVE_POLL_MS = 2000;
|
|
9
10
|
const DEFAULT_STALL_THRESHOLD_MS = 60_000;
|
|
10
11
|
function isRecoverableMissingTabError(message) {
|
|
@@ -85,35 +86,17 @@ function resolveSessionTabRef(meta) {
|
|
|
85
86
|
export function resolveSessionTabRefForTest(meta) {
|
|
86
87
|
return resolveSessionTabRef(meta);
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
const hash = createHash("sha1")
|
|
90
|
-
.update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
|
|
91
|
-
.digest("hex");
|
|
92
|
-
const browser = {
|
|
93
|
-
...(meta.browser ?? {}),
|
|
94
|
-
harvest: {
|
|
95
|
-
targetId: harvested.targetId,
|
|
96
|
-
url: harvested.url,
|
|
97
|
-
conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
|
|
98
|
-
harvestedAt: new Date().toISOString(),
|
|
99
|
-
assistantHash: hash,
|
|
100
|
-
state: harvested.state,
|
|
101
|
-
stopExists: harvested.stopExists,
|
|
102
|
-
sendExists: harvested.sendExists,
|
|
103
|
-
assistantCount: harvested.assistantCount,
|
|
104
|
-
currentModelLabel: harvested.currentModelLabel,
|
|
105
|
-
lastAssistantSnippet: harvested.lastAssistantSnippet,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
await sessionStore.updateSession(sessionId, { browser });
|
|
109
|
-
}
|
|
110
|
-
function printHarvestSummary(sessionId, harvested) {
|
|
89
|
+
function printHarvestSummary(sessionId, harvested, integrity) {
|
|
111
90
|
console.log(chalk.bold(`Session: ${sessionId}`));
|
|
112
91
|
console.log(`Target: ${harvested.targetId}`);
|
|
113
92
|
console.log(`State: ${formatBrowserTabState(harvested)}`);
|
|
114
93
|
console.log(`Model: ${harvested.currentModelLabel || "(unknown)"}`);
|
|
115
94
|
console.log(`URL: ${harvested.url}`);
|
|
116
95
|
console.log(`Assistant turns: ${harvested.assistantCount}`);
|
|
96
|
+
console.log(`Capture identity: ${integrity.status}${integrity.explicitTarget ? " (explicit target)" : ""}`);
|
|
97
|
+
if (integrity.status === "mismatch") {
|
|
98
|
+
console.log(chalk.yellow("Explicit harvest target differs from the saved capture; original artifacts are unchanged."));
|
|
99
|
+
}
|
|
117
100
|
console.log(`Signals: stop=${harvested.stopExists ? "yes" : "no"} send=${harvested.sendExists ? "yes" : "no"}`);
|
|
118
101
|
if (harvested.lastUserSnippet) {
|
|
119
102
|
console.log(`Last user: ${harvested.lastUserSnippet}`);
|
|
@@ -207,8 +190,8 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
207
190
|
stallWindowMs: options.stallWindowMs,
|
|
208
191
|
});
|
|
209
192
|
}
|
|
210
|
-
await
|
|
211
|
-
printHarvestSummary(sessionId, harvested);
|
|
193
|
+
const integrity = await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
|
|
194
|
+
printHarvestSummary(sessionId, harvested, integrity);
|
|
212
195
|
const output = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
|
|
213
196
|
if (options.writeOutputPath) {
|
|
214
197
|
await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
|
|
@@ -287,8 +270,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
|
|
|
287
270
|
const statusLine = `[${new Date().toISOString()}] state=${harvested.state} stop=${harvested.stopExists ? "yes" : "no"} ` +
|
|
288
271
|
`send=${harvested.sendExists ? "yes" : "no"} model=${harvested.currentModelLabel || "(unknown)"} ` +
|
|
289
272
|
`snippet=${snippet(harvested.lastAssistantSnippet || fullText, 160)}`;
|
|
273
|
+
await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
|
|
290
274
|
console.log(statusLine);
|
|
291
|
-
await persistHarvest(sessionId, meta, harvested);
|
|
292
275
|
}
|
|
293
276
|
const derivedState = harvested.stopExists
|
|
294
277
|
? Date.now() - unchangedSince >= stallThresholdMs
|
|
@@ -304,8 +287,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
|
|
|
304
287
|
...harvested,
|
|
305
288
|
state: derivedState,
|
|
306
289
|
};
|
|
307
|
-
await
|
|
308
|
-
printHarvestSummary(sessionId, finalHarvest);
|
|
290
|
+
const integrity = await persistBrowserHarvest(sessionId, finalHarvest, Boolean(options.browserTabRef));
|
|
291
|
+
printHarvestSummary(sessionId, finalHarvest, integrity);
|
|
309
292
|
const output = finalHarvest.lastAssistantMarkdown ?? finalHarvest.lastAssistantText ?? "";
|
|
310
293
|
if (options.writeOutputPath) {
|
|
311
294
|
await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export function resolveOracleCliEntrypoint(moduleUrl = import.meta.url) {
|
|
4
|
+
const extension = fileURLToPath(moduleUrl).endsWith(".ts") ? "ts" : "js";
|
|
5
|
+
return fileURLToPath(new URL(`../../bin/oracle-cli.${extension}`, moduleUrl));
|
|
6
|
+
}
|
|
7
|
+
export function buildDetachedSessionSpawnSpec({ sessionId, cliEntrypoint = resolveOracleCliEntrypoint(), env = process.env, nodeExecutable = process.execPath, }) {
|
|
8
|
+
return {
|
|
9
|
+
command: nodeExecutable,
|
|
10
|
+
args: ["--", cliEntrypoint, "--exec-session", sessionId],
|
|
11
|
+
options: {
|
|
12
|
+
detached: true,
|
|
13
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
14
|
+
env: {
|
|
15
|
+
...env,
|
|
16
|
+
ORACLE_DETACHED_START_GATE: "1",
|
|
17
|
+
},
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function launchDetachedSession({ sessionId, cliEntrypoint, env, nodeExecutable, prepare, spawnProcess = spawn, }) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let child;
|
|
25
|
+
try {
|
|
26
|
+
const spec = buildDetachedSessionSpawnSpec({
|
|
27
|
+
sessionId,
|
|
28
|
+
cliEntrypoint,
|
|
29
|
+
env,
|
|
30
|
+
nodeExecutable,
|
|
31
|
+
});
|
|
32
|
+
child = spawnProcess(spec.command, spec.args, spec.options);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
reject(error);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
child.once("error", reject);
|
|
39
|
+
child.once("spawn", async () => {
|
|
40
|
+
if (child.pid === undefined) {
|
|
41
|
+
child.kill();
|
|
42
|
+
reject(new Error("Detached session worker started without a process ID."));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await prepare(child.pid);
|
|
47
|
+
if (!child.stdin) {
|
|
48
|
+
throw new Error("Detached session worker started without a writable start gate.");
|
|
49
|
+
}
|
|
50
|
+
const startGate = child.stdin;
|
|
51
|
+
await new Promise((resolveGate, rejectGate) => {
|
|
52
|
+
startGate.once("error", rejectGate);
|
|
53
|
+
startGate.end("ready\n", (error) => {
|
|
54
|
+
if (error)
|
|
55
|
+
rejectGate(error);
|
|
56
|
+
else
|
|
57
|
+
resolveGate();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
child.unref();
|
|
61
|
+
resolve(child.pid);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
child.kill();
|
|
65
|
+
reject(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from "../oracle.js";
|
|
3
3
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
4
|
-
import { assembleBrowserPrompt } from "../browser/prompt.js";
|
|
4
|
+
import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, } from "../browser/prompt.js";
|
|
5
5
|
import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
|
|
6
6
|
import { buildCookiePlan } from "../browser/policies.js";
|
|
7
7
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "../browser/controlPlan.js";
|
|
@@ -45,19 +45,24 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig,
|
|
|
45
45
|
validateBrowserFollowUps(runOptions, browserConfig);
|
|
46
46
|
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
47
47
|
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
try {
|
|
49
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
50
|
+
const displayModel = formatBrowserModelTarget({
|
|
51
|
+
model: runOptions.model,
|
|
52
|
+
desiredModel: browserConfig?.desiredModel,
|
|
53
|
+
modelStrategy: browserConfig?.modelStrategy,
|
|
54
|
+
});
|
|
55
|
+
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
56
|
+
log(chalk.cyan(headerLine));
|
|
57
|
+
logBrowserControlPlan(browserConfig, log, "dry-run");
|
|
58
|
+
logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
|
|
59
|
+
logBrowserCookieStrategy(browserConfig, log, "dry-run");
|
|
60
|
+
logBrowserArchivePolicy(browserConfig, log, "dry-run");
|
|
61
|
+
logBrowserFileSummary(artifacts, log, "dry-run");
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await cleanupGeneratedBrowserBundles(artifacts);
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
function logBrowserControlPlan(browserConfig, log, label) {
|
|
63
68
|
const plan = describeBrowserControlPlan(browserConfig);
|
|
@@ -100,42 +105,47 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
|
|
|
100
105
|
validateBrowserFollowUps(runOptions, browserConfig);
|
|
101
106
|
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
102
107
|
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
desiredModel: browserConfig?.desiredModel,
|
|
107
|
-
modelStrategy: browserConfig?.modelStrategy,
|
|
108
|
-
});
|
|
109
|
-
const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
110
|
-
log(chalk.cyan(headerLine));
|
|
111
|
-
logBrowserControlPlan(browserConfig, log, "preview");
|
|
112
|
-
logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
|
|
113
|
-
logBrowserCookieStrategy(browserConfig, log, "preview");
|
|
114
|
-
logBrowserFileSummary(artifacts, log, "preview");
|
|
115
|
-
if (previewMode === "json" || previewMode === "full") {
|
|
116
|
-
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
117
|
-
path: attachment.path,
|
|
118
|
-
displayPath: attachment.displayPath,
|
|
119
|
-
sizeBytes: attachment.sizeBytes,
|
|
120
|
-
}));
|
|
121
|
-
const previewPayload = {
|
|
108
|
+
try {
|
|
109
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
110
|
+
const displayModel = formatBrowserModelTarget({
|
|
122
111
|
model: runOptions.model,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
log
|
|
132
|
-
|
|
133
|
-
|
|
112
|
+
desiredModel: browserConfig?.desiredModel,
|
|
113
|
+
modelStrategy: browserConfig?.modelStrategy,
|
|
114
|
+
});
|
|
115
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
116
|
+
log(chalk.cyan(headerLine));
|
|
117
|
+
logBrowserControlPlan(browserConfig, log, "preview");
|
|
118
|
+
logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
|
|
119
|
+
logBrowserCookieStrategy(browserConfig, log, "preview");
|
|
120
|
+
logBrowserFileSummary(artifacts, log, "preview");
|
|
121
|
+
if (previewMode === "json" || previewMode === "full") {
|
|
122
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
123
|
+
path: attachment.path,
|
|
124
|
+
displayPath: attachment.displayPath,
|
|
125
|
+
sizeBytes: attachment.sizeBytes,
|
|
126
|
+
}));
|
|
127
|
+
const previewPayload = {
|
|
128
|
+
model: runOptions.model,
|
|
129
|
+
engine: "browser",
|
|
130
|
+
composerText: artifacts.composerText,
|
|
131
|
+
attachments: attachmentSummary,
|
|
132
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
133
|
+
bundled: artifacts.bundled,
|
|
134
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
135
|
+
browserFollowUps: runOptions.browserFollowUps ?? [],
|
|
136
|
+
};
|
|
137
|
+
log("");
|
|
138
|
+
log(chalk.bold("Preview JSON"));
|
|
139
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
140
|
+
}
|
|
141
|
+
if (previewMode === "full") {
|
|
142
|
+
log("");
|
|
143
|
+
log(chalk.bold("Composer Text"));
|
|
144
|
+
log(artifacts.composerText || chalk.dim("(empty prompt)"));
|
|
145
|
+
}
|
|
134
146
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
log(chalk.bold("Composer Text"));
|
|
138
|
-
log(artifacts.composerText || chalk.dim("(empty prompt)"));
|
|
147
|
+
finally {
|
|
148
|
+
await cleanupGeneratedBrowserBundles(artifacts);
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
function logBrowserFollowUpSummary(followUps, log, label) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { sessionStore } from "../sessionStore.js";
|
|
6
|
+
import { extractConversationIdFromUrl } from "../browser/liveTabs.js";
|
|
7
|
+
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
8
|
+
const INTEGRITY_WARNING = "browser-harvest-integrity";
|
|
9
|
+
const INTEGRITY_MESSAGE = "Harvest or saved conversation identities conflict. Original artifacts were preserved; inspect browser.harvest.integrity before using the answer.";
|
|
10
|
+
async function readTranscriptConversation(filename) {
|
|
11
|
+
let file;
|
|
12
|
+
try {
|
|
13
|
+
const stat = await fs.lstat(filename);
|
|
14
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
15
|
+
return undefined;
|
|
16
|
+
file = await fs.open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
17
|
+
if (!(await file.stat()).isFile())
|
|
18
|
+
return undefined;
|
|
19
|
+
const buffer = Buffer.alloc(2_048);
|
|
20
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
21
|
+
const match = buffer
|
|
22
|
+
.subarray(0, bytesRead)
|
|
23
|
+
.toString("utf8")
|
|
24
|
+
.match(/^# Oracle Browser Transcript\r?\n\r?\nConversation: ([^\r\n]+)\r?\n/);
|
|
25
|
+
return match ? extractConversationIdFromUrl(match[1]) : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await file?.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function persistBrowserHarvest(sessionId, harvested, explicitTarget = false) {
|
|
35
|
+
// Harvesting can take time; compare against the latest saved capture, not the initial read.
|
|
36
|
+
const meta = await sessionStore.readSession(sessionId);
|
|
37
|
+
if (!meta)
|
|
38
|
+
throw new Error(`No session found with ID ${sessionId}.`);
|
|
39
|
+
const captured = [];
|
|
40
|
+
const unverifiedSources = [];
|
|
41
|
+
const add = (source, id) => {
|
|
42
|
+
if (id)
|
|
43
|
+
captured.push({ source, conversationId: id });
|
|
44
|
+
};
|
|
45
|
+
const addUrl = (source, url, expectsConversation = true) => {
|
|
46
|
+
if (!url)
|
|
47
|
+
return;
|
|
48
|
+
const id = extractConversationIdFromUrl(url);
|
|
49
|
+
add(source, id);
|
|
50
|
+
if (!id && expectsConversation)
|
|
51
|
+
unverifiedSources.push(source);
|
|
52
|
+
};
|
|
53
|
+
add("runtime", meta.browser?.runtime?.conversationId);
|
|
54
|
+
addUrl("runtime-url", meta.browser?.runtime?.tabUrl);
|
|
55
|
+
addUrl("archive", meta.browser?.archive?.conversationUrl);
|
|
56
|
+
for (const [index, artifact] of (meta.artifacts ?? []).entries()) {
|
|
57
|
+
addUrl(`artifact:${index}`, artifact.sourceUrl, artifact.kind === "transcript" || artifact.kind === "deep-research-report");
|
|
58
|
+
}
|
|
59
|
+
const artifactDir = path.join((await sessionStore.getPaths(sessionId)).dir, "artifacts");
|
|
60
|
+
const directoryStat = await fs.lstat(artifactDir).catch(() => null);
|
|
61
|
+
if (directoryStat?.isDirectory() && !directoryStat.isSymbolicLink()) {
|
|
62
|
+
const transcripts = new Map([[path.join(artifactDir, "transcript.md"), false]]);
|
|
63
|
+
for (const artifact of meta.artifacts ?? []) {
|
|
64
|
+
if (artifact.kind !== "transcript")
|
|
65
|
+
continue;
|
|
66
|
+
const candidate = path.isAbsolute(artifact.path)
|
|
67
|
+
? artifact.path
|
|
68
|
+
: path.resolve(artifactDir, "..", artifact.path);
|
|
69
|
+
// Stored paths must remain direct children of this session's artifact directory.
|
|
70
|
+
if (path.dirname(candidate) === artifactDir)
|
|
71
|
+
transcripts.set(candidate, true);
|
|
72
|
+
else
|
|
73
|
+
unverifiedSources.push("external-transcript-path");
|
|
74
|
+
}
|
|
75
|
+
for (const [filename, expected] of transcripts) {
|
|
76
|
+
const source = `transcript:${path.basename(filename)}`;
|
|
77
|
+
const id = await readTranscriptConversation(filename);
|
|
78
|
+
add(source, id);
|
|
79
|
+
if (!id && (expected || (await fs.lstat(filename).catch(() => null)))) {
|
|
80
|
+
unverifiedSources.push(source);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (directoryStat || meta.artifacts?.some((artifact) => artifact.kind === "transcript")) {
|
|
85
|
+
unverifiedSources.push("artifact-directory");
|
|
86
|
+
}
|
|
87
|
+
const observedConversationId = harvested.conversationId ?? extractConversationIdFromUrl(harvested.url);
|
|
88
|
+
const capturedIds = new Set(captured.map((entry) => entry.conversationId));
|
|
89
|
+
const previousIntegrity = meta.browser?.harvest?.integrity;
|
|
90
|
+
const mismatch = capturedIds.size > 1 ||
|
|
91
|
+
(observedConversationId
|
|
92
|
+
? captured.some((entry) => entry.conversationId !== observedConversationId)
|
|
93
|
+
: previousIntegrity?.status === "mismatch");
|
|
94
|
+
const integrity = {
|
|
95
|
+
status: mismatch
|
|
96
|
+
? "mismatch"
|
|
97
|
+
: observedConversationId && captured.length && unverifiedSources.length === 0
|
|
98
|
+
? "matched"
|
|
99
|
+
: "unverified",
|
|
100
|
+
observedConversationId,
|
|
101
|
+
captured,
|
|
102
|
+
unverifiedSources,
|
|
103
|
+
explicitTarget,
|
|
104
|
+
previousHarvestConversationId: meta.browser?.harvest?.conversationId ?? previousIntegrity?.previousHarvestConversationId,
|
|
105
|
+
};
|
|
106
|
+
const warnings = (meta.browser?.warnings ?? []).filter((warning) => warning.code !== INTEGRITY_WARNING);
|
|
107
|
+
if (mismatch) {
|
|
108
|
+
warnings.push({ code: INTEGRITY_WARNING, severity: "warning", message: INTEGRITY_MESSAGE });
|
|
109
|
+
}
|
|
110
|
+
await sessionStore.updateSession(sessionId, {
|
|
111
|
+
browser: {
|
|
112
|
+
...meta.browser,
|
|
113
|
+
warnings,
|
|
114
|
+
harvest: {
|
|
115
|
+
targetId: harvested.targetId,
|
|
116
|
+
url: harvested.url,
|
|
117
|
+
conversationId: observedConversationId,
|
|
118
|
+
harvestedAt: new Date().toISOString(),
|
|
119
|
+
assistantHash: createHash("sha1")
|
|
120
|
+
.update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
|
|
121
|
+
.digest("hex"),
|
|
122
|
+
state: harvested.state,
|
|
123
|
+
stopExists: harvested.stopExists,
|
|
124
|
+
sendExists: harvested.sendExists,
|
|
125
|
+
assistantCount: harvested.assistantCount,
|
|
126
|
+
currentModelLabel: harvested.currentModelLabel,
|
|
127
|
+
lastAssistantSnippet: harvested.lastAssistantSnippet,
|
|
128
|
+
integrity,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (mismatch && !explicitTarget) {
|
|
133
|
+
throw new BrowserAutomationError(INTEGRITY_MESSAGE, {
|
|
134
|
+
stage: "harvest-integrity",
|
|
135
|
+
code: "conversation-identity-mismatch",
|
|
136
|
+
integrity,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return integrity;
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveBrowserApprovalWait } from "../browser/config.js";
|
|
2
|
+
import { parseRemoteChromeTarget } from "./browserConfig.js";
|
|
3
|
+
import { applyBrowserDefaultsFromConfig } from "./browserDefaults.js";
|
|
4
|
+
export function buildServeBrowserConfig(options, config) {
|
|
5
|
+
const flags = {
|
|
6
|
+
browserAttachRunning: options.browserAttachRunning,
|
|
7
|
+
remoteChrome: options.remoteChrome,
|
|
8
|
+
browserApprovalWait: options.browserApprovalWait ??
|
|
9
|
+
(process.env.ORACLE_BROWSER_APPROVAL_WAIT?.trim() || undefined),
|
|
10
|
+
};
|
|
11
|
+
const browser = config.browser;
|
|
12
|
+
applyBrowserDefaultsFromConfig(flags, {
|
|
13
|
+
browser: {
|
|
14
|
+
attachRunning: browser?.attachRunning,
|
|
15
|
+
remoteChrome: browser?.remoteChrome,
|
|
16
|
+
approvalWaitMs: browser?.approvalWaitMs,
|
|
17
|
+
},
|
|
18
|
+
}, (key) => (flags[key] === undefined ? undefined : "cli"));
|
|
19
|
+
return {
|
|
20
|
+
attachRunning: flags.browserAttachRunning ?? false,
|
|
21
|
+
remoteChrome: flags.remoteChrome ? parseRemoteChromeTarget(flags.remoteChrome) : null,
|
|
22
|
+
approvalWaitMs: resolveBrowserApprovalWait(flags.browserApprovalWait),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -12,7 +12,7 @@ import { estimateTokenCount } from "../browser/utils.js";
|
|
|
12
12
|
import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
|
|
13
13
|
import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
|
|
14
14
|
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
|
|
15
|
-
import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
|
|
15
|
+
import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
|
|
16
16
|
const isTty = () => Boolean(process.stdout.isTTY);
|
|
17
17
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
18
18
|
export const MAX_RENDER_BYTES = 200_000;
|
|
@@ -255,6 +255,7 @@ export async function attachSession(sessionId, options) {
|
|
|
255
255
|
config: metadata.browser?.config,
|
|
256
256
|
runtime,
|
|
257
257
|
modelSelection: metadata.browser?.modelSelection,
|
|
258
|
+
thinkingSelection: metadata.browser?.thinkingSelection,
|
|
258
259
|
warnings: metadata.browser?.warnings,
|
|
259
260
|
},
|
|
260
261
|
artifacts,
|
|
@@ -603,7 +604,9 @@ export function formatUserErrorMetadata(metadata) {
|
|
|
603
604
|
}
|
|
604
605
|
export function formatBrowserEvidence(metadata) {
|
|
605
606
|
const browser = metadata.browser;
|
|
606
|
-
if (!browser?.modelSelection &&
|
|
607
|
+
if (!browser?.modelSelection &&
|
|
608
|
+
!browser?.thinkingSelection &&
|
|
609
|
+
(!browser?.warnings || browser.warnings.length === 0)) {
|
|
607
610
|
return null;
|
|
608
611
|
}
|
|
609
612
|
const lines = [];
|
|
@@ -611,6 +614,10 @@ export function formatBrowserEvidence(metadata) {
|
|
|
611
614
|
if (evidence) {
|
|
612
615
|
lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
|
|
613
616
|
}
|
|
617
|
+
const thinkingEvidence = browser.thinkingSelection;
|
|
618
|
+
if (thinkingEvidence) {
|
|
619
|
+
lines.push(`effort ${formatBrowserThinkingSelectionEvidence(thinkingEvidence)}`);
|
|
620
|
+
}
|
|
614
621
|
for (const warning of browser.warnings ?? []) {
|
|
615
622
|
lines.push(`warning ${warning.code}: ${warning.message}`);
|
|
616
623
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
|
|
@@ -23,6 +24,7 @@ import { cwd as getCwd } from "node:process";
|
|
|
23
24
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
24
25
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
25
26
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
27
|
+
import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
|
|
26
28
|
import { formatElapsed } from "../oracle/format.js";
|
|
27
29
|
import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
|
|
28
30
|
const isTty = process.stdout.isTTY;
|
|
@@ -78,7 +80,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
78
80
|
cwd,
|
|
79
81
|
log,
|
|
80
82
|
}, runnerDeps);
|
|
81
|
-
await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
83
|
+
const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
84
|
+
const outputArtifacts = await copyBrowserOutputArtifacts({
|
|
85
|
+
outputPath: writtenOutputPath,
|
|
86
|
+
savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
|
|
87
|
+
log,
|
|
88
|
+
});
|
|
89
|
+
const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
|
|
82
90
|
await sendSessionNotification({
|
|
83
91
|
sessionId: sessionMeta.id,
|
|
84
92
|
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
@@ -105,9 +113,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
105
113
|
runtime: result.runtime,
|
|
106
114
|
archive: result.archive,
|
|
107
115
|
modelSelection: result.modelSelection,
|
|
108
|
-
|
|
116
|
+
thinkingSelection: result.thinkingSelection,
|
|
117
|
+
warnings: browserWarnings.length > 0 ? browserWarnings : undefined,
|
|
109
118
|
},
|
|
110
|
-
artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
|
|
119
|
+
artifacts: mergeArtifacts(sessionMeta.artifacts, mergeArtifacts(result.artifacts, outputArtifacts.artifacts)),
|
|
111
120
|
response: undefined,
|
|
112
121
|
transport: undefined,
|
|
113
122
|
error: undefined,
|
|
@@ -848,6 +857,81 @@ function resolveSessionPath(sessionDir, targetPath) {
|
|
|
848
857
|
}
|
|
849
858
|
return path.join(sessionDir, targetPath);
|
|
850
859
|
}
|
|
860
|
+
async function copyBrowserOutputArtifacts(params) {
|
|
861
|
+
const artifacts = [];
|
|
862
|
+
const warnings = [];
|
|
863
|
+
if (!params.outputPath || !params.savedFiles?.length) {
|
|
864
|
+
return { artifacts, warnings };
|
|
865
|
+
}
|
|
866
|
+
const outputDir = path.dirname(params.outputPath);
|
|
867
|
+
for (const savedFile of params.savedFiles) {
|
|
868
|
+
const filename = sanitizeArtifactFilename(savedFile.filename ?? path.basename(savedFile.path), "artifact.bin");
|
|
869
|
+
let copied;
|
|
870
|
+
try {
|
|
871
|
+
copied = await copyFileWithoutOverwrite(savedFile.path, path.join(outputDir, filename));
|
|
872
|
+
const [stat, sha256] = await Promise.all([fs.stat(copied), computeFileSha256(copied)]);
|
|
873
|
+
if (savedFile.sizeBytes != null && stat.size !== savedFile.sizeBytes) {
|
|
874
|
+
throw new Error(`size mismatch (expected ${savedFile.sizeBytes}, received ${stat.size})`);
|
|
875
|
+
}
|
|
876
|
+
if (savedFile.sha256 && sha256 !== savedFile.sha256) {
|
|
877
|
+
throw new Error(`sha256 mismatch (expected ${savedFile.sha256}, received ${sha256})`);
|
|
878
|
+
}
|
|
879
|
+
const artifact = {
|
|
880
|
+
kind: "file",
|
|
881
|
+
path: copied,
|
|
882
|
+
label: `${savedFile.label ?? filename} (write-output)`,
|
|
883
|
+
mimeType: savedFile.mimeType,
|
|
884
|
+
sizeBytes: stat.size,
|
|
885
|
+
sourceUrl: savedFile.sourceUrl,
|
|
886
|
+
sha256,
|
|
887
|
+
validation: savedFile.validation,
|
|
888
|
+
transfer: { status: "not-needed" },
|
|
889
|
+
origin: { mode: "local" },
|
|
890
|
+
};
|
|
891
|
+
artifacts.push(artifact);
|
|
892
|
+
params.log(dim(`[browser] Saved write-output file artifact to ${copied} sha256=${sha256}`));
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
if (copied) {
|
|
896
|
+
await fs.unlink(copied).catch(() => undefined);
|
|
897
|
+
}
|
|
898
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
899
|
+
const message = `Failed to copy saved file artifact ${savedFile.path} beside ${params.outputPath}: ${reason}`;
|
|
900
|
+
params.log(dim(`[browser] ${message}`));
|
|
901
|
+
warnings.push({
|
|
902
|
+
code: "browser-output-artifact-copy-failed",
|
|
903
|
+
severity: "warning",
|
|
904
|
+
message,
|
|
905
|
+
details: {
|
|
906
|
+
sourcePath: savedFile.path,
|
|
907
|
+
outputPath: params.outputPath,
|
|
908
|
+
sha256: savedFile.sha256,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return { artifacts, warnings };
|
|
914
|
+
}
|
|
915
|
+
async function copyFileWithoutOverwrite(sourcePath, baseTargetPath) {
|
|
916
|
+
const ext = path.extname(baseTargetPath);
|
|
917
|
+
const stem = ext ? path.basename(baseTargetPath, ext) : path.basename(baseTargetPath);
|
|
918
|
+
const dir = path.dirname(baseTargetPath);
|
|
919
|
+
let suffix = 1;
|
|
920
|
+
for (;;) {
|
|
921
|
+
const targetPath = suffix === 1 ? baseTargetPath : path.join(dir, `${stem}-${suffix}${ext}`);
|
|
922
|
+
try {
|
|
923
|
+
await fs.copyFile(sourcePath, targetPath, fsConstants.COPYFILE_EXCL);
|
|
924
|
+
return targetPath;
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
if (isErrorCode(error, "EEXIST")) {
|
|
928
|
+
suffix += 1;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
851
935
|
async function writeAssistantOutput(targetPath, content, log) {
|
|
852
936
|
if (!targetPath)
|
|
853
937
|
return;
|
|
@@ -1057,11 +1141,11 @@ export function deriveModelOutputPath(basePath, model) {
|
|
|
1057
1141
|
const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
|
|
1058
1142
|
return path.join(dir, suffix);
|
|
1059
1143
|
}
|
|
1144
|
+
function isErrorCode(error, expected) {
|
|
1145
|
+
return error instanceof Error && error.code === expected;
|
|
1146
|
+
}
|
|
1060
1147
|
function isPermissionError(error) {
|
|
1061
|
-
|
|
1062
|
-
return false;
|
|
1063
|
-
const code = error.code;
|
|
1064
|
-
return code === "EACCES" || code === "EPERM";
|
|
1148
|
+
return isErrorCode(error, "EACCES") || isErrorCode(error, "EPERM");
|
|
1065
1149
|
}
|
|
1066
1150
|
function buildFallbackPath(original) {
|
|
1067
1151
|
const ext = path.extname(original);
|