@steipete/oracle 0.16.0 → 0.17.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 +72 -337
- package/dist/bin/oracle-cli.js +184 -55
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +410 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +389 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +324 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/scripts/test-browser.js +1 -25
- package/dist/src/browser/actions/modelSelection.js +36 -34
- package/dist/src/browser/actions/navigation.js +8 -3
- package/dist/src/browser/actions/thinkingTime.js +39 -3
- package/dist/src/browser/cdpLiveness.js +78 -0
- package/dist/src/browser/chromeLifecycle.js +85 -39
- package/dist/src/browser/conversationUrl.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +2 -4
- package/dist/src/browser/cookies.js +6 -4
- package/dist/src/browser/index.js +181 -50
- package/dist/src/browser/liveTabs.js +2 -2
- package/dist/src/browser/modelDisplay.js +67 -0
- package/dist/src/browser/reattach.js +14 -1
- package/dist/src/browser/reattachHelpers.js +3 -5
- package/dist/src/browser/reattachability.js +2 -1
- package/dist/src/browser/recoverConversation.js +2 -1
- package/dist/src/browser/sessionRunner.js +9 -10
- package/dist/src/browser/tabLeaseRegistry.js +9 -3
- package/dist/src/browser/wslHost.js +50 -0
- package/dist/src/cli/browserConfig.js +31 -11
- package/dist/src/cli/browserDefaults.js +4 -1
- package/dist/src/cli/detach.js +21 -4
- package/dist/src/cli/dryRun.js +13 -2
- package/dist/src/cli/engine.js +2 -2
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/sessionDisplay.js +60 -8
- package/dist/src/cli/sessionLifecycle.js +2 -1
- package/dist/src/cli/sessionRunner.js +159 -62
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/cli/tui/index.js +12 -4
- package/dist/src/duration.js +3 -0
- package/dist/src/gemini-web/client.js +19 -1
- package/dist/src/oracle/modelResolver.js +8 -1
- package/dist/src/oracle/request.js +9 -2
- package/dist/src/oracle/run.js +43 -3
- package/dist/src/remote/server.js +8 -0
- package/dist/src/sessionManager.js +41 -12
- 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 +16 -16
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -78,6 +78,15 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
78
78
|
cwd,
|
|
79
79
|
log,
|
|
80
80
|
}, runnerDeps);
|
|
81
|
+
await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
82
|
+
await sendSessionNotification({
|
|
83
|
+
sessionId: sessionMeta.id,
|
|
84
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
85
|
+
mode,
|
|
86
|
+
model: sessionMeta.model,
|
|
87
|
+
usage: result.usage,
|
|
88
|
+
characters: result.answerText?.length,
|
|
89
|
+
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
81
90
|
if (modelForStatus) {
|
|
82
91
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
83
92
|
status: "completed",
|
|
@@ -103,15 +112,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
103
112
|
transport: undefined,
|
|
104
113
|
error: undefined,
|
|
105
114
|
});
|
|
106
|
-
await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
107
|
-
await sendSessionNotification({
|
|
108
|
-
sessionId: sessionMeta.id,
|
|
109
|
-
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
110
|
-
mode,
|
|
111
|
-
model: sessionMeta.model,
|
|
112
|
-
usage: result.usage,
|
|
113
|
-
characters: result.answerText?.length,
|
|
114
|
-
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
const multiModels = Array.isArray(runOptions.models) ? runOptions.models.filter(Boolean) : [];
|
|
@@ -362,16 +362,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
362
362
|
if (result.mode !== "live") {
|
|
363
363
|
throw new Error("Unexpected preview result while running a session.");
|
|
364
364
|
}
|
|
365
|
-
await sessionStore.updateSession(sessionMeta.id, {
|
|
366
|
-
status: "completed",
|
|
367
|
-
completedAt: new Date().toISOString(),
|
|
368
|
-
usage: result.usage,
|
|
369
|
-
elapsedMs: result.elapsedMs,
|
|
370
|
-
errorMessage: undefined,
|
|
371
|
-
response: extractResponseMetadata(result.response),
|
|
372
|
-
transport: undefined,
|
|
373
|
-
error: undefined,
|
|
374
|
-
});
|
|
375
365
|
if (modelForStatus && singleModelOverride == null) {
|
|
376
366
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
377
367
|
status: "completed",
|
|
@@ -389,6 +379,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
389
379
|
usage: result.usage,
|
|
390
380
|
characters: answerText.length,
|
|
391
381
|
}, notificationSettings, log, answerText.slice(0, 140));
|
|
382
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
383
|
+
status: "completed",
|
|
384
|
+
completedAt: new Date().toISOString(),
|
|
385
|
+
usage: result.usage,
|
|
386
|
+
elapsedMs: result.elapsedMs,
|
|
387
|
+
errorMessage: undefined,
|
|
388
|
+
response: extractResponseMetadata(result.response),
|
|
389
|
+
transport: undefined,
|
|
390
|
+
error: undefined,
|
|
391
|
+
});
|
|
392
392
|
}
|
|
393
393
|
catch (error) {
|
|
394
394
|
const message = formatError(error);
|
|
@@ -469,22 +469,101 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
469
469
|
response: { status: "running", incompleteReason: "chrome-disconnected" },
|
|
470
470
|
});
|
|
471
471
|
logBrowserReattachGuidance(recoverableRuntime);
|
|
472
|
+
// Only auto-reattach when liveness classified the target as still alive.
|
|
473
|
+
// Closed-Chrome disconnects stay running + guidance but must not enter a
|
|
474
|
+
// futile resume loop (fail closed on availability).
|
|
475
|
+
const recoverableDisconnect = userError.details
|
|
476
|
+
?.recoverableDisconnect === true;
|
|
477
|
+
if (!recoverableDisconnect) {
|
|
478
|
+
log(dim("Skipping auto-reattach: disconnect classified as non-recoverable."));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Connection-lost should attempt the same recovery path as assistant-timeout.
|
|
482
|
+
// When auto-reattach interval is unset, still try a single resume so a live
|
|
483
|
+
// Chrome/target can be harvested instead of leaving the session permanently running.
|
|
484
|
+
const configuredIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
485
|
+
const connectionLostIntervalMs = configuredIntervalMs > 0
|
|
486
|
+
? configuredIntervalMs
|
|
487
|
+
: Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
|
|
488
|
+
const success = await autoReattachUntilComplete({
|
|
489
|
+
sessionMeta,
|
|
490
|
+
runtime: recoverableRuntime ?? undefined,
|
|
491
|
+
browserConfig: {
|
|
492
|
+
...browserConfig,
|
|
493
|
+
autoReattachIntervalMs: connectionLostIntervalMs,
|
|
494
|
+
autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
|
|
495
|
+
autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
|
|
496
|
+
},
|
|
497
|
+
browserMetadata: currentBrowser,
|
|
498
|
+
runOptions,
|
|
499
|
+
modelForStatus,
|
|
500
|
+
notificationSettings,
|
|
501
|
+
log,
|
|
502
|
+
maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
|
|
503
|
+
});
|
|
504
|
+
if (success) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
472
507
|
return;
|
|
473
508
|
}
|
|
474
509
|
if (assistantTimeout && mode === "browser" && browserCanReattach) {
|
|
475
510
|
const runtime = userError.details
|
|
476
511
|
?.runtime;
|
|
477
512
|
log(dim("Assistant response timed out; marking capture incomplete for reattach."));
|
|
513
|
+
const timeoutResponse = {
|
|
514
|
+
status: "incomplete",
|
|
515
|
+
incompleteReason: "incomplete-capture",
|
|
516
|
+
};
|
|
517
|
+
const timeoutError = {
|
|
518
|
+
category: userError.category,
|
|
519
|
+
message: userError.message,
|
|
520
|
+
details: userError.details,
|
|
521
|
+
};
|
|
522
|
+
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
523
|
+
const autoRuntime = runtime ?? currentBrowser?.runtime;
|
|
524
|
+
const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
|
|
525
|
+
if (willAutoReattach) {
|
|
526
|
+
if (modelForStatus) {
|
|
527
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
528
|
+
status: "running",
|
|
529
|
+
completedAt: undefined,
|
|
530
|
+
response: timeoutResponse,
|
|
531
|
+
error: timeoutError,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
535
|
+
status: "running",
|
|
536
|
+
completedAt: undefined,
|
|
537
|
+
errorMessage: message,
|
|
538
|
+
mode,
|
|
539
|
+
browser: {
|
|
540
|
+
...currentBrowser,
|
|
541
|
+
config: browserConfig,
|
|
542
|
+
runtime: autoRuntime,
|
|
543
|
+
},
|
|
544
|
+
response: timeoutResponse,
|
|
545
|
+
error: timeoutError,
|
|
546
|
+
});
|
|
547
|
+
const success = await autoReattachUntilComplete({
|
|
548
|
+
sessionMeta,
|
|
549
|
+
runtime: autoRuntime,
|
|
550
|
+
browserConfig,
|
|
551
|
+
browserMetadata: currentBrowser,
|
|
552
|
+
runOptions,
|
|
553
|
+
modelForStatus,
|
|
554
|
+
notificationSettings,
|
|
555
|
+
log,
|
|
556
|
+
});
|
|
557
|
+
if (success) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
478
561
|
if (modelForStatus) {
|
|
479
562
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
480
563
|
status: "error",
|
|
481
564
|
completedAt: new Date().toISOString(),
|
|
482
|
-
response:
|
|
483
|
-
error:
|
|
484
|
-
category: userError.category,
|
|
485
|
-
message: userError.message,
|
|
486
|
-
details: userError.details,
|
|
487
|
-
},
|
|
565
|
+
response: timeoutResponse,
|
|
566
|
+
error: timeoutError,
|
|
488
567
|
});
|
|
489
568
|
}
|
|
490
569
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
@@ -497,30 +576,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
497
576
|
config: browserConfig,
|
|
498
577
|
runtime: runtime ?? currentBrowser?.runtime,
|
|
499
578
|
},
|
|
500
|
-
response:
|
|
501
|
-
error:
|
|
502
|
-
category: userError.category,
|
|
503
|
-
message: userError.message,
|
|
504
|
-
details: userError.details,
|
|
505
|
-
},
|
|
579
|
+
response: timeoutResponse,
|
|
580
|
+
error: timeoutError,
|
|
506
581
|
});
|
|
507
|
-
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
508
|
-
if (autoReattachIntervalMs > 0) {
|
|
509
|
-
const autoRuntime = runtime ?? currentBrowser?.runtime;
|
|
510
|
-
const success = await autoReattachUntilComplete({
|
|
511
|
-
sessionMeta,
|
|
512
|
-
runtime: autoRuntime ?? undefined,
|
|
513
|
-
browserConfig,
|
|
514
|
-
browserMetadata: currentBrowser,
|
|
515
|
-
runOptions,
|
|
516
|
-
modelForStatus,
|
|
517
|
-
notificationSettings,
|
|
518
|
-
log,
|
|
519
|
-
});
|
|
520
|
-
if (success) {
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
582
|
logBrowserReattachGuidance(runtime ?? currentBrowser?.runtime);
|
|
525
583
|
return;
|
|
526
584
|
}
|
|
@@ -833,7 +891,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
833
891
|
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
834
892
|
}
|
|
835
893
|
}
|
|
836
|
-
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, }) {
|
|
894
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
|
|
837
895
|
if (!runtime || !browserConfig) {
|
|
838
896
|
log(dim("Auto-reattach disabled: missing runtime or browser config."));
|
|
839
897
|
return false;
|
|
@@ -848,11 +906,19 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
848
906
|
120_000;
|
|
849
907
|
const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
|
|
850
908
|
const maxDeadline = Date.now() + maxTotalMs;
|
|
909
|
+
const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
|
|
910
|
+
? Math.floor(maxAttempts)
|
|
911
|
+
: Number.POSITIVE_INFINITY;
|
|
851
912
|
if (delayMs > 0) {
|
|
852
913
|
log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
|
|
853
914
|
await wait(delayMs);
|
|
854
915
|
}
|
|
855
|
-
|
|
916
|
+
if (Number.isFinite(attemptLimit)) {
|
|
917
|
+
log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
log(dim(`Auto-reattach will stop after ${formatElapsed(maxTotalMs)} if no answer is captured.`));
|
|
921
|
+
}
|
|
856
922
|
const logger = ((message) => {
|
|
857
923
|
if (message) {
|
|
858
924
|
log(dim(message));
|
|
@@ -868,6 +934,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
868
934
|
}
|
|
869
935
|
attempt += 1;
|
|
870
936
|
log(dim(`Auto-reattach attempt ${attempt}...`));
|
|
937
|
+
let captureSucceeded = false;
|
|
871
938
|
try {
|
|
872
939
|
const reattachConfig = {
|
|
873
940
|
...browserConfig,
|
|
@@ -876,6 +943,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
876
943
|
const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
877
944
|
promptPreview: sessionMeta.promptPreview,
|
|
878
945
|
});
|
|
946
|
+
captureSucceeded = true;
|
|
879
947
|
const answerText = result.answerMarkdown || result.answerText || "";
|
|
880
948
|
const outputTokens = estimateTokenCount(answerText);
|
|
881
949
|
const artifacts = await ensureSessionArtifacts({
|
|
@@ -904,6 +972,18 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
904
972
|
},
|
|
905
973
|
});
|
|
906
974
|
}
|
|
975
|
+
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
976
|
+
await sendSessionNotification({
|
|
977
|
+
sessionId: sessionMeta.id,
|
|
978
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
979
|
+
mode: sessionMeta.mode ?? "browser",
|
|
980
|
+
model: sessionMeta.model ?? runOptions.model,
|
|
981
|
+
usage: {
|
|
982
|
+
inputTokens: 0,
|
|
983
|
+
outputTokens,
|
|
984
|
+
},
|
|
985
|
+
characters: answerText.length,
|
|
986
|
+
}, notificationSettings, log, answerText.slice(0, 140));
|
|
907
987
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
908
988
|
status: "completed",
|
|
909
989
|
completedAt: new Date().toISOString(),
|
|
@@ -924,25 +1004,42 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
924
1004
|
error: undefined,
|
|
925
1005
|
transport: undefined,
|
|
926
1006
|
});
|
|
927
|
-
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
928
|
-
await sendSessionNotification({
|
|
929
|
-
sessionId: sessionMeta.id,
|
|
930
|
-
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
931
|
-
mode: sessionMeta.mode ?? "browser",
|
|
932
|
-
model: sessionMeta.model ?? runOptions.model,
|
|
933
|
-
usage: {
|
|
934
|
-
inputTokens: 0,
|
|
935
|
-
outputTokens,
|
|
936
|
-
},
|
|
937
|
-
characters: answerText.length,
|
|
938
|
-
}, notificationSettings, log, answerText.slice(0, 140));
|
|
939
1007
|
log(kleur.green("Auto-reattach succeeded; session marked completed."));
|
|
940
1008
|
return true;
|
|
941
1009
|
}
|
|
942
1010
|
catch (error) {
|
|
1011
|
+
if (captureSucceeded) {
|
|
1012
|
+
const message = formatError(error);
|
|
1013
|
+
if (modelForStatus) {
|
|
1014
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
1015
|
+
status: "error",
|
|
1016
|
+
completedAt: new Date().toISOString(),
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
1020
|
+
status: "error",
|
|
1021
|
+
completedAt: new Date().toISOString(),
|
|
1022
|
+
errorMessage: message,
|
|
1023
|
+
browser: {
|
|
1024
|
+
...browserMetadata,
|
|
1025
|
+
config: browserConfig,
|
|
1026
|
+
runtime,
|
|
1027
|
+
},
|
|
1028
|
+
response: { status: "error", incompleteReason: "incomplete-capture" },
|
|
1029
|
+
error: {
|
|
1030
|
+
category: "internal",
|
|
1031
|
+
message,
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
943
1036
|
const message = error instanceof Error ? error.message : String(error);
|
|
944
1037
|
log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
|
|
945
1038
|
}
|
|
1039
|
+
if (attempt >= attemptLimit) {
|
|
1040
|
+
log(dim(`Auto-reattach stopped after ${attempt} attempt(s) without capturing an answer.`));
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
946
1043
|
const remainingAfterAttemptMs = maxDeadline - Date.now();
|
|
947
1044
|
if (remainingAfterAttemptMs <= 0) {
|
|
948
1045
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
@@ -3,6 +3,7 @@ import kleur from "kleur";
|
|
|
3
3
|
import { MODEL_CONFIGS } from "../oracle.js";
|
|
4
4
|
import { estimateUsdCost } from "tokentally";
|
|
5
5
|
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
|
|
6
|
+
import { resolveSessionBrowserModelDisplayName } from "../browser/modelDisplay.js";
|
|
6
7
|
const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
7
8
|
const dim = (text, rich) => (rich ? kleur.dim(text) : text);
|
|
8
9
|
export const STATUS_PAD = 9;
|
|
@@ -18,7 +19,10 @@ export function formatSessionTableHeader(rich) {
|
|
|
18
19
|
export function formatSessionTableRow(meta, options) {
|
|
19
20
|
const rich = isRich(options?.rich);
|
|
20
21
|
const status = colorStatus(meta.status ?? "unknown", rich);
|
|
21
|
-
const
|
|
22
|
+
const displayModel = (meta.mode ?? meta.options?.mode) === "browser"
|
|
23
|
+
? resolveSessionBrowserModelDisplayName(meta)
|
|
24
|
+
: (meta.model ?? "n/a");
|
|
25
|
+
const modelLabel = displayModel.padEnd(MODEL_PAD);
|
|
22
26
|
const model = rich ? chalk.white(modelLabel) : modelLabel;
|
|
23
27
|
const modeLabel = formatSessionExecutionLabel(meta).padEnd(MODE_PAD);
|
|
24
28
|
const mode = rich ? chalk.gray(modeLabel) : modeLabel;
|
|
@@ -15,6 +15,7 @@ import { resolveNotificationSettings } from "../notifier.js";
|
|
|
15
15
|
import { loadUserConfig } from "../../config.js";
|
|
16
16
|
import { resolveConfiguredMaxFileSizeBytes } from "../fileSize.js";
|
|
17
17
|
import { formatTokenCount } from "../../oracle/runUtils.js";
|
|
18
|
+
import { formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../../browser/modelDisplay.js";
|
|
18
19
|
const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
19
20
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
20
21
|
const RECENT_WINDOW_HOURS = 24;
|
|
@@ -157,7 +158,7 @@ async function showSessionDetail(sessionId) {
|
|
|
157
158
|
console.clear();
|
|
158
159
|
printSessionHeader(meta);
|
|
159
160
|
if (meta.models && meta.models.length > 0) {
|
|
160
|
-
printModelSummaries(meta
|
|
161
|
+
printModelSummaries(meta);
|
|
161
162
|
}
|
|
162
163
|
const prompt = await readStoredPrompt(sessionId);
|
|
163
164
|
if (prompt) {
|
|
@@ -250,7 +251,10 @@ function printSessionHeader(meta) {
|
|
|
250
251
|
console.log(`${chalk.white("Status:")} ${meta.status}`);
|
|
251
252
|
console.log(`${chalk.white("Created:")} ${meta.createdAt}`);
|
|
252
253
|
if (meta.model) {
|
|
253
|
-
|
|
254
|
+
const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
|
|
255
|
+
? resolveSessionBrowserModelDisplayName(meta)
|
|
256
|
+
: meta.model;
|
|
257
|
+
console.log(`${chalk.white("Model:")} ${modelLabel}`);
|
|
254
258
|
}
|
|
255
259
|
const mode = meta.mode ?? meta.options?.mode;
|
|
256
260
|
if (mode) {
|
|
@@ -260,7 +264,8 @@ function printSessionHeader(meta) {
|
|
|
260
264
|
console.log(chalk.red(`Error: ${meta.errorMessage}`));
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
|
-
function printModelSummaries(
|
|
267
|
+
function printModelSummaries(meta) {
|
|
268
|
+
const models = meta.models ?? [];
|
|
264
269
|
if (models.length === 0) {
|
|
265
270
|
return;
|
|
266
271
|
}
|
|
@@ -269,7 +274,10 @@ function printModelSummaries(models) {
|
|
|
269
274
|
const usage = run.usage
|
|
270
275
|
? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
|
|
271
276
|
: "";
|
|
272
|
-
|
|
277
|
+
const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
|
|
278
|
+
? formatSessionBrowserModelWithRequestedKey(meta, run.model)
|
|
279
|
+
: run.model;
|
|
280
|
+
console.log(` - ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
|
|
273
281
|
}
|
|
274
282
|
console.log("");
|
|
275
283
|
}
|
package/dist/src/duration.js
CHANGED
|
@@ -21,6 +21,9 @@ export function parseDuration(input, fallback) {
|
|
|
21
21
|
let lastIndex = 0;
|
|
22
22
|
let match = multiDuration.exec(normalized);
|
|
23
23
|
while (match !== null) {
|
|
24
|
+
if (match.index !== lastIndex) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
24
27
|
total += convertUnit(Number(match[1]), match[2]);
|
|
25
28
|
lastIndex = multiDuration.lastIndex;
|
|
26
29
|
match = multiDuration.exec(normalized);
|
|
@@ -12,11 +12,29 @@ const GEMINI_UPLOAD_MIME_TYPES = {
|
|
|
12
12
|
".gif": "image/gif",
|
|
13
13
|
".jpeg": "image/jpeg",
|
|
14
14
|
".jpg": "image/jpeg",
|
|
15
|
+
".mov": "video/quicktime",
|
|
16
|
+
".mp4": "video/mp4",
|
|
15
17
|
".pdf": "application/pdf",
|
|
16
18
|
".png": "image/png",
|
|
17
19
|
".svg": "image/svg+xml",
|
|
20
|
+
".webm": "video/webm",
|
|
18
21
|
".webp": "image/webp",
|
|
19
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the MIME type Gemini should be told an upload carries.
|
|
25
|
+
*
|
|
26
|
+
* Gemini silently discards uploads it cannot type: the run still reports the file as
|
|
27
|
+
* attached, but the model never receives it and answers as though nothing was sent.
|
|
28
|
+
* Anything falling back to `application/octet-stream` is therefore invisible to the model.
|
|
29
|
+
*
|
|
30
|
+
* Only formats confirmed to work against the Gemini web upload endpoint are listed. The
|
|
31
|
+
* endpoint also gates on the file extension, not just the declared type — an `.m4v` byte
|
|
32
|
+
* for byte identical to a working `.mp4`, and declared `video/mp4`, is still dropped — so
|
|
33
|
+
* entries here cannot be extrapolated from what the Gemini API documents.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveGeminiUploadMimeType(filePath) {
|
|
36
|
+
return (GEMINI_UPLOAD_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream");
|
|
37
|
+
}
|
|
20
38
|
function getNestedValue(value, pathParts, fallback) {
|
|
21
39
|
let current = value;
|
|
22
40
|
for (const part of pathParts) {
|
|
@@ -125,7 +143,7 @@ async function uploadGeminiFile(filePath, signal) {
|
|
|
125
143
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
126
144
|
const data = await readFile(absPath);
|
|
127
145
|
const fileName = path.basename(absPath);
|
|
128
|
-
const mimeType =
|
|
146
|
+
const mimeType = resolveGeminiUploadMimeType(absPath);
|
|
129
147
|
const form = new FormData();
|
|
130
148
|
form.append("file", new Blob([data], { type: mimeType }), fileName);
|
|
131
149
|
const res = await fetch(GEMINI_UPLOAD_URL, {
|
|
@@ -192,7 +192,14 @@ async function resolveBaseModelConfig(model, options = {}) {
|
|
|
192
192
|
export function isProModel(model) {
|
|
193
193
|
return isKnownModel(model) && PRO_MODELS.has(model);
|
|
194
194
|
}
|
|
195
|
-
const VALID_REASONING_EFFORTS = [
|
|
195
|
+
const VALID_REASONING_EFFORTS = [
|
|
196
|
+
"none",
|
|
197
|
+
"low",
|
|
198
|
+
"medium",
|
|
199
|
+
"high",
|
|
200
|
+
"xhigh",
|
|
201
|
+
"max",
|
|
202
|
+
];
|
|
196
203
|
function isRecord(value) {
|
|
197
204
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
198
205
|
}
|
|
@@ -11,8 +11,15 @@ export function buildPrompt(basePrompt, files, cwd = process.cwd()) {
|
|
|
11
11
|
const sectionText = formatFileSections(sections, { includeFileIndex: true });
|
|
12
12
|
return `${basePrompt.trim()}\n\n${sectionText}`;
|
|
13
13
|
}
|
|
14
|
-
export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, previousResponseId, }) {
|
|
14
|
+
export function buildRequestBody({ modelConfig, reasoningEffort, reasoningMode, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, previousResponseId, }) {
|
|
15
15
|
const searchToolType = modelConfig.searchToolType ?? "web_search_preview";
|
|
16
|
+
const reasoning = modelConfig.reasoning || reasoningEffort || reasoningMode
|
|
17
|
+
? {
|
|
18
|
+
...(modelConfig.reasoning ?? {}),
|
|
19
|
+
...(reasoningEffort ? { effort: reasoningEffort } : {}),
|
|
20
|
+
...(reasoningMode ? { mode: reasoningMode } : {}),
|
|
21
|
+
}
|
|
22
|
+
: undefined;
|
|
16
23
|
return {
|
|
17
24
|
model: modelConfig.apiModel ?? modelConfig.model,
|
|
18
25
|
previous_response_id: previousResponseId ? previousResponseId : undefined,
|
|
@@ -29,7 +36,7 @@ export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, search
|
|
|
29
36
|
},
|
|
30
37
|
],
|
|
31
38
|
tools: searchEnabled ? [{ type: searchToolType }] : undefined,
|
|
32
|
-
reasoning
|
|
39
|
+
reasoning,
|
|
33
40
|
max_output_tokens: maxOutputTokens,
|
|
34
41
|
background: background ? true : undefined,
|
|
35
42
|
store: storeResponse ? true : undefined,
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -33,6 +33,9 @@ const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
|
|
|
33
33
|
// Default timeout for non-pro API runs (fast models) — give them up to 120s.
|
|
34
34
|
const DEFAULT_TIMEOUT_NON_PRO_MS = 120_000;
|
|
35
35
|
const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
|
|
36
|
+
const GPT_5_6_API_MODELS = new Set(["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
37
|
+
const REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
38
|
+
const REASONING_MODES = new Set(["standard", "pro"]);
|
|
36
39
|
const defaultWait = (ms) => new Promise((resolve) => {
|
|
37
40
|
setTimeout(resolve, ms);
|
|
38
41
|
});
|
|
@@ -69,6 +72,33 @@ function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
|
|
|
69
72
|
return "XAI_API_KEY";
|
|
70
73
|
return optionsApiKey ? "apiKey option" : route.keySource;
|
|
71
74
|
}
|
|
75
|
+
function validateReasoningOptions(options, route) {
|
|
76
|
+
const { reasoningEffort, reasoningMode } = options;
|
|
77
|
+
if (!reasoningEffort && !reasoningMode)
|
|
78
|
+
return;
|
|
79
|
+
if (reasoningEffort && !REASONING_EFFORTS.has(reasoningEffort)) {
|
|
80
|
+
throw new PromptValidationError(`Invalid reasoning effort "${reasoningEffort}". Expected none, low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort });
|
|
81
|
+
}
|
|
82
|
+
if (reasoningMode && !REASONING_MODES.has(reasoningMode)) {
|
|
83
|
+
throw new PromptValidationError(`Invalid reasoning mode "${reasoningMode}". Expected standard or pro.`, { model: options.model, reasoningMode });
|
|
84
|
+
}
|
|
85
|
+
if (!GPT_5_6_API_MODELS.has(options.model)) {
|
|
86
|
+
const option = reasoningMode
|
|
87
|
+
? `Reasoning mode "${reasoningMode}"`
|
|
88
|
+
: `Reasoning effort "${reasoningEffort}"`;
|
|
89
|
+
const guidance = reasoningMode
|
|
90
|
+
? `Use --model gpt-5.6-sol --reasoning-mode ${reasoningMode}.`
|
|
91
|
+
: `Use --model gpt-5.6-sol --reasoning-effort ${reasoningEffort}.`;
|
|
92
|
+
throw new PromptValidationError(`${option} is available only for GPT-5.6 API models. ${guidance}`, { model: options.model, reasoningEffort, reasoningMode });
|
|
93
|
+
}
|
|
94
|
+
if (reasoningMode &&
|
|
95
|
+
!route.isAzureOpenAI &&
|
|
96
|
+
(route.openRouterFallback ||
|
|
97
|
+
isOpenRouterBaseUrl(route.baseUrl) ||
|
|
98
|
+
isCustomBaseUrl(route.baseUrl))) {
|
|
99
|
+
throw new PromptValidationError("--reasoning-mode requires the OpenAI or Azure OpenAI Responses API; OpenRouter and custom --base-url routes use the Chat Completions adapter.", { model: options.model, reasoningMode });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
72
102
|
export async function runOracle(options, deps = {}) {
|
|
73
103
|
const { apiKey: optionsApiKey = options.apiKey, cwd = process.cwd(), fs: fsModule = createFsAdapter(fs), log = console.log, write: sinkWrite = (_text) => true, allowStdout = true, stdoutWrite: stdoutWriteDep, now = () => performance.now(), clientFactory = createDefaultClientFactory(), client, wait = defaultWait, } = deps;
|
|
74
104
|
const stdoutWrite = allowStdout
|
|
@@ -100,6 +130,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
100
130
|
const { isAzureOpenAI, azureDeploymentName } = route;
|
|
101
131
|
const baseUrl = route.baseUrl;
|
|
102
132
|
const openRouterFallback = route.openRouterFallback;
|
|
133
|
+
validateReasoningOptions(options, route);
|
|
103
134
|
const logVerbose = (message) => {
|
|
104
135
|
if (options.verbose) {
|
|
105
136
|
log(dim(`[verbose] ${message}`));
|
|
@@ -125,7 +156,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
125
156
|
const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? "10", 10);
|
|
126
157
|
const promptLength = options.prompt?.trim().length ?? 0;
|
|
127
158
|
// Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
|
|
128
|
-
const isProTierModel = isProModel(options.model);
|
|
159
|
+
const isProTierModel = isProModel(options.model) || options.reasoningMode === "pro";
|
|
129
160
|
if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
|
|
130
161
|
throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
|
|
131
162
|
}
|
|
@@ -205,6 +236,8 @@ export async function runOracle(options, deps = {}) {
|
|
|
205
236
|
}
|
|
206
237
|
const requestBody = buildRequestBody({
|
|
207
238
|
modelConfig,
|
|
239
|
+
reasoningEffort: options.reasoningEffort,
|
|
240
|
+
reasoningMode: options.reasoningMode,
|
|
208
241
|
systemPrompt,
|
|
209
242
|
userPrompt: promptWithFiles,
|
|
210
243
|
searchEnabled,
|
|
@@ -249,6 +282,12 @@ export async function runOracle(options, deps = {}) {
|
|
|
249
282
|
if (options.background && !supportsBackground) {
|
|
250
283
|
log(dim("Background runs are not supported for this model; streaming in foreground instead."));
|
|
251
284
|
}
|
|
285
|
+
if (options.reasoningMode) {
|
|
286
|
+
log(dim(`Reasoning mode: ${options.reasoningMode}`));
|
|
287
|
+
}
|
|
288
|
+
if (options.reasoningEffort) {
|
|
289
|
+
log(dim(`Reasoning effort: ${options.reasoningEffort}`));
|
|
290
|
+
}
|
|
252
291
|
if (!options.suppressTips) {
|
|
253
292
|
if (pendingNoFilesTip) {
|
|
254
293
|
log(dim(pendingNoFilesTip));
|
|
@@ -547,8 +586,9 @@ export async function runOracle(options, deps = {}) {
|
|
|
547
586
|
},
|
|
548
587
|
})?.totalUsd
|
|
549
588
|
: undefined;
|
|
550
|
-
const effortLabel = modelConfig.reasoning?.effort;
|
|
551
|
-
const
|
|
589
|
+
const effortLabel = options.reasoningEffort ?? modelConfig.reasoning?.effort;
|
|
590
|
+
const reasoningLabel = [options.reasoningMode, effortLabel].filter(Boolean).join("/");
|
|
591
|
+
const modelLabel = reasoningLabel ? `${modelConfig.model}[${reasoningLabel}]` : modelConfig.model;
|
|
552
592
|
const sessionIdContainsModel = typeof options.sessionId === "string" &&
|
|
553
593
|
options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
|
|
554
594
|
const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
|
|
@@ -189,6 +189,9 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
automationLogger.verbose = Boolean(payload.options.verbose);
|
|
192
|
+
// Preserve an explicit request to leave the completed conversation tab
|
|
193
|
+
// open before the service forces `keepBrowser` for process lifetime.
|
|
194
|
+
const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
|
|
192
195
|
// Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
|
|
193
196
|
if (payload.browserConfig) {
|
|
194
197
|
payload.browserConfig.inlineCookies = null;
|
|
@@ -212,6 +215,11 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
212
215
|
attachments,
|
|
213
216
|
fallbackSubmission,
|
|
214
217
|
config: payload.browserConfig,
|
|
218
|
+
// `keepBrowser` above preserves the authenticated shared Chrome
|
|
219
|
+
// process. This separate service policy closes only a successfully
|
|
220
|
+
// captured tab owned by this run, preventing one renderer leak per
|
|
221
|
+
// request while incomplete/reattachable tabs remain untouched.
|
|
222
|
+
closeOwnedTabOnComplete: Boolean(options.manualLoginDefault && !clientRequestedKeepBrowser),
|
|
215
223
|
log: automationLogger,
|
|
216
224
|
heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
|
|
217
225
|
verbose: payload.options.verbose,
|