@steipete/oracle 0.11.0 → 0.12.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.
Files changed (49) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/archiveConversation.js +12 -0
  4. package/dist/src/browser/actions/modelSelection.js +61 -18
  5. package/dist/src/browser/actions/navigation.js +5 -3
  6. package/dist/src/browser/actions/promptComposer.js +75 -18
  7. package/dist/src/browser/actions/thinkingTime.js +23 -8
  8. package/dist/src/browser/config.js +1 -7
  9. package/dist/src/browser/constants.js +1 -1
  10. package/dist/src/browser/index.js +65 -48
  11. package/dist/src/browser/manualLoginProfile.js +54 -0
  12. package/dist/src/browser/projectSourcesRunner.js +16 -5
  13. package/dist/src/browser/prompt.js +56 -37
  14. package/dist/src/browser/sessionRunner.js +72 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -18
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +39 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +228 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +5 -2
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +281 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +157 -54
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +5 -1
  49. package/package.json +8 -6
@@ -0,0 +1,242 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ const SECRET_VALUE_FLAGS = new Set([
5
+ "--api-key",
6
+ "--browser-follow-up",
7
+ "--browser-inline-cookies",
8
+ "--browser-inline-cookies-file",
9
+ "--message",
10
+ "--prompt",
11
+ "--remote-token",
12
+ "--token",
13
+ "-p",
14
+ ]);
15
+ const VALUE_FLAGS = new Set([
16
+ "--aspect",
17
+ "--azure-api-version",
18
+ "--azure-deployment",
19
+ "--azure-endpoint",
20
+ "--base-url",
21
+ "--browser-archive",
22
+ "--browser-attachments",
23
+ "--browser-auto-reattach-delay",
24
+ "--browser-auto-reattach-interval",
25
+ "--browser-auto-reattach-timeout",
26
+ "--browser-bundle-format",
27
+ "--browser-cookie-names",
28
+ "--browser-cookie-path",
29
+ "--browser-cookie-wait",
30
+ "--browser-input-timeout",
31
+ "--browser-max-concurrent-tabs",
32
+ "--browser-model-strategy",
33
+ "--browser-port",
34
+ "--browser-profile-lock-timeout",
35
+ "--browser-recheck-delay",
36
+ "--browser-recheck-timeout",
37
+ "--browser-research",
38
+ "--browser-reuse-wait",
39
+ "--browser-tab",
40
+ "--browser-timeout",
41
+ "--browser-url",
42
+ "--chatgpt-url",
43
+ "--engine",
44
+ "--followup",
45
+ "--followup-model",
46
+ "--heartbeat",
47
+ "--http-timeout",
48
+ "--max-file-size-bytes",
49
+ "--model",
50
+ "--models",
51
+ "--output",
52
+ "--partial",
53
+ "--perf-trace-path",
54
+ "--provider",
55
+ "--remote-chrome",
56
+ "--remote-host",
57
+ "--slug",
58
+ "--timeout",
59
+ "--write-output",
60
+ "--youtube",
61
+ "--zombie-timeout",
62
+ "-e",
63
+ "-m",
64
+ "-s",
65
+ ]);
66
+ export function isTraceValueFlag(flag) {
67
+ return SECRET_VALUE_FLAGS.has(flag) || VALUE_FLAGS.has(flag);
68
+ }
69
+ class DisabledPerfTrace {
70
+ mark() { }
71
+ wrapFirstOutput() { }
72
+ flush() { }
73
+ }
74
+ class FilePerfTrace {
75
+ outputPath;
76
+ options;
77
+ events = [];
78
+ wrapped = false;
79
+ firstOutput = false;
80
+ flushed = false;
81
+ constructor(outputPath, options) {
82
+ this.outputPath = outputPath;
83
+ this.options = options;
84
+ }
85
+ mark(name, data) {
86
+ this.events.push({
87
+ name,
88
+ ms: Number(performance.now().toFixed(3)),
89
+ data,
90
+ });
91
+ }
92
+ wrapFirstOutput() {
93
+ if (this.wrapped)
94
+ return;
95
+ this.wrapped = true;
96
+ const wrap = (stream) => {
97
+ const original = stream.write.bind(stream);
98
+ stream.write = ((...args) => {
99
+ if (!this.firstOutput) {
100
+ this.firstOutput = true;
101
+ this.mark("first-output", {
102
+ stream: stream === process.stderr ? "stderr" : "stdout",
103
+ });
104
+ }
105
+ return original(...args);
106
+ });
107
+ };
108
+ wrap(process.stdout);
109
+ wrap(process.stderr);
110
+ }
111
+ flush(exitCode) {
112
+ if (this.flushed)
113
+ return;
114
+ this.flushed = true;
115
+ this.mark("exit", { exitCode: exitCode ?? 0 });
116
+ const payload = {
117
+ version: this.options.version,
118
+ argv: sanitizeTraceArgv(this.options.argv),
119
+ cwd: this.options.cwd ?? process.cwd(),
120
+ pid: process.pid,
121
+ node: process.version,
122
+ timeOrigin: performance.timeOrigin,
123
+ totalMs: Number(performance.now().toFixed(3)),
124
+ events: this.events,
125
+ };
126
+ writeFileSync(this.outputPath, `${JSON.stringify(payload, null, 2)}\n`);
127
+ }
128
+ }
129
+ export function createPerfTrace(options) {
130
+ const envValue = process.env.ORACLE_PERF_TRACE;
131
+ const optionValue = options.value;
132
+ if (!optionValue && !envValue) {
133
+ return new DisabledPerfTrace();
134
+ }
135
+ const rawValue = typeof optionValue === "string" ? optionValue : envValue;
136
+ const outputPath = rawValue && rawValue !== "1" && rawValue !== "true"
137
+ ? path.resolve(options.cwd ?? process.cwd(), rawValue)
138
+ : path.join(options.cwd ?? process.cwd(), `.oracle-perf-${new Date().toISOString().replace(/[:.]/g, "-")}-${process.pid}.json`);
139
+ const trace = new FilePerfTrace(outputPath, options);
140
+ trace.wrapFirstOutput();
141
+ trace.mark("cli-module-ready");
142
+ return trace;
143
+ }
144
+ export function deriveDetachedPerfTraceEnv(value, sessionId) {
145
+ const trimmed = value?.trim();
146
+ if (!trimmed || trimmed === "1" || trimmed.toLowerCase() === "true")
147
+ return value;
148
+ const safeSessionId = sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
149
+ const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
150
+ const lastDot = trimmed.lastIndexOf(".");
151
+ if (lastDot > lastSlash) {
152
+ return `${trimmed.slice(0, lastDot)}.${safeSessionId}${trimmed.slice(lastDot)}`;
153
+ }
154
+ return `${trimmed}.${safeSessionId}.json`;
155
+ }
156
+ export function resolveDetachedPerfTraceEnv(cliValue, envValue, sessionId) {
157
+ if (typeof cliValue === "string") {
158
+ return deriveDetachedPerfTraceEnv(cliValue, sessionId);
159
+ }
160
+ if (cliValue === true) {
161
+ return "1";
162
+ }
163
+ return deriveDetachedPerfTraceEnv(envValue, sessionId);
164
+ }
165
+ export function buildDetachedPerfTraceEnv(env, cliValue, sessionId) {
166
+ const nextEnv = { ...env };
167
+ const traceValue = resolveDetachedPerfTraceEnv(cliValue, env.ORACLE_PERF_TRACE, sessionId);
168
+ if (traceValue) {
169
+ nextEnv.ORACLE_PERF_TRACE = traceValue;
170
+ }
171
+ else {
172
+ delete nextEnv.ORACLE_PERF_TRACE;
173
+ }
174
+ return nextEnv;
175
+ }
176
+ export function sanitizeTraceArgv(argv) {
177
+ const sanitized = [];
178
+ let redactNext = false;
179
+ let valueNext = false;
180
+ let afterDoubleDash = false;
181
+ for (const arg of argv) {
182
+ if (afterDoubleDash) {
183
+ sanitized.push("[redacted-positional]");
184
+ continue;
185
+ }
186
+ if (arg === "--") {
187
+ sanitized.push(arg);
188
+ afterDoubleDash = true;
189
+ continue;
190
+ }
191
+ if (redactNext) {
192
+ sanitized.push("[redacted]");
193
+ redactNext = false;
194
+ continue;
195
+ }
196
+ if (valueNext) {
197
+ sanitized.push(redactPotentialSecret(arg));
198
+ valueNext = false;
199
+ continue;
200
+ }
201
+ const equalsIndex = arg.indexOf("=");
202
+ const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
203
+ if (equalsIndex >= 0 && SECRET_VALUE_FLAGS.has(flag)) {
204
+ sanitized.push(`${flag}=[redacted]`);
205
+ continue;
206
+ }
207
+ if (arg.startsWith("-p") && arg.length > 2) {
208
+ sanitized.push("-p[redacted]");
209
+ continue;
210
+ }
211
+ if (equalsIndex >= 0) {
212
+ sanitized.push(`${flag}=${redactPotentialSecret(arg.slice(equalsIndex + 1))}`);
213
+ continue;
214
+ }
215
+ if (SECRET_VALUE_FLAGS.has(arg)) {
216
+ sanitized.push(arg);
217
+ redactNext = true;
218
+ continue;
219
+ }
220
+ if (VALUE_FLAGS.has(arg)) {
221
+ sanitized.push(arg);
222
+ valueNext = true;
223
+ continue;
224
+ }
225
+ if (!arg.startsWith("-")) {
226
+ sanitized.push("[redacted-positional]");
227
+ continue;
228
+ }
229
+ sanitized.push(arg);
230
+ }
231
+ return sanitized;
232
+ }
233
+ function redactPotentialSecret(value) {
234
+ return value
235
+ .replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [redacted]")
236
+ .replace(/:\/\/([^:/?#\s]+):([^@/?#\s]+)@/g, "://$1:[redacted]@")
237
+ .replace(/([?&](?:access_)?token=)[^&#\s]+/gi, "$1[redacted]")
238
+ .replace(/([?&](?:api[-_]?key|auth|authorization|password|secret)=)[^&#\s]+/gi, "$1[redacted]")
239
+ .replace(/\bsk-(?:ant-|or-)?[A-Za-z0-9_-]{8,}\b/g, "sk-...[redacted]")
240
+ .replace(/\bxai-[A-Za-z0-9_-]{8,}\b/g, "xai-...[redacted]")
241
+ .replace(/\bAIza[0-9A-Za-z_-]{8,}\b/g, "AIza...[redacted]");
242
+ }
@@ -10,6 +10,8 @@ export function shouldRequirePrompt(rawArgs, options) {
10
10
  options.execSession ||
11
11
  options.status ||
12
12
  options.debugHelp ||
13
+ options.route ||
14
+ options.preflight ||
13
15
  firstArg === "status" ||
14
16
  firstArg === "session");
15
17
  const requiresPrompt = options.renderMarkdown || Boolean(options.preview) || Boolean(options.dryRun) || !bypassPrompt;
@@ -0,0 +1,85 @@
1
+ import chalk from "chalk";
2
+ import { DEFAULT_MODEL } from "../oracle/config.js";
3
+ import { resolveApiModel } from "./options.js";
4
+ import { loadUserConfig } from "../config.js";
5
+ import { buildProviderRoutePlan } from "../oracle/providerRoutePlan.js";
6
+ export async function runProviderDoctor(options) {
7
+ if (!options.providers) {
8
+ console.log("Run `oracle doctor --providers` to inspect API provider readiness.");
9
+ return;
10
+ }
11
+ const { config: userConfig } = await loadUserConfig();
12
+ const providerMode = resolveProviderMode(options);
13
+ const azure = resolveAzureOptions(options, userConfig);
14
+ const models = resolveModels(options, userConfig);
15
+ const plans = models.map((model) => buildProviderRoutePlan({
16
+ model,
17
+ providerMode,
18
+ azure,
19
+ baseUrl: options.baseUrl ?? userConfig.apiBaseUrl,
20
+ env: process.env,
21
+ }));
22
+ if (options.json) {
23
+ console.log(JSON.stringify({ providers: plans }, null, 2));
24
+ process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
25
+ return;
26
+ }
27
+ printProviderPlans(plans);
28
+ process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
29
+ }
30
+ export function printProviderPlans(plans, { title = "Provider readiness" } = {}) {
31
+ console.log(chalk.bold(title));
32
+ console.log("");
33
+ for (const plan of plans) {
34
+ const status = plan.ok ? chalk.green("ok") : chalk.red("not ready");
35
+ console.log(`${plan.model}: ${status}`);
36
+ console.log(chalk.dim(` provider: ${plan.providerLabel}`));
37
+ console.log(chalk.dim(` base: ${plan.base || "(none)"}`));
38
+ console.log(chalk.dim(` key: ${plan.keyPreview}`));
39
+ if (plan.isAzureOpenAI || plan.azureDeploymentName) {
40
+ console.log(chalk.dim(` azure deployment: ${plan.azureDeploymentName ?? "none"}`));
41
+ }
42
+ if (plan.azureNote) {
43
+ console.log(chalk.dim(` azure: ${plan.azureNote}`));
44
+ }
45
+ if (plan.error) {
46
+ console.log(chalk.dim(` error: ${plan.error}`));
47
+ }
48
+ console.log("");
49
+ }
50
+ }
51
+ function resolveModels(options, userConfig) {
52
+ const entries = Array.isArray(options.models) && options.models.length > 0
53
+ ? options.models
54
+ : typeof options.models === "string" && options.models.trim().length > 0
55
+ ? options.models
56
+ .split(",")
57
+ .map((entry) => entry.trim())
58
+ .filter(Boolean)
59
+ : [options.model ?? userConfig.model ?? DEFAULT_MODEL];
60
+ return Array.from(new Set(entries.map((entry) => resolveApiModel(entry))));
61
+ }
62
+ function resolveProviderMode(options) {
63
+ const provider = options.provider ?? "auto";
64
+ if (provider === "azure" && options.azure === false) {
65
+ throw new Error("--provider azure cannot be combined with --no-azure.");
66
+ }
67
+ if (options.azure === false) {
68
+ return "openai";
69
+ }
70
+ return provider;
71
+ }
72
+ function resolveAzureOptions(options, userConfig) {
73
+ const endpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
74
+ if (!endpoint) {
75
+ return undefined;
76
+ }
77
+ return {
78
+ endpoint,
79
+ deployment: firstNonEmpty(options.azureDeployment, process.env.AZURE_OPENAI_DEPLOYMENT, userConfig.azure?.deployment),
80
+ apiVersion: firstNonEmpty(options.azureApiVersion, process.env.AZURE_OPENAI_API_VERSION, userConfig.azure?.apiVersion),
81
+ };
82
+ }
83
+ function firstNonEmpty(...values) {
84
+ return values.find((value) => value?.trim());
85
+ }
@@ -5,41 +5,59 @@ import { resolveGeminiModelId } from "../oracle/gemini.js";
5
5
  import { PromptValidationError } from "../oracle/errors.js";
6
6
  import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
7
7
  import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
8
+ import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
8
9
  export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
9
- const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
10
+ const resolvedEngine = resolveEngineWithConfig({
11
+ engine,
12
+ configEngine: userConfig?.engine,
13
+ env,
14
+ });
10
15
  const browserRequested = engine === "browser";
11
16
  const browserConfigured = userConfig?.engine === "browser";
17
+ const envBrowserConfigured = (env.ORACLE_ENGINE ?? "").trim().toLowerCase() === "browser";
12
18
  const requestedModelList = Array.isArray(models) ? models : [];
13
19
  const normalizedRequestedModels = requestedModelList
14
20
  .map((entry) => normalizeModelOption(entry))
15
21
  .filter(Boolean);
16
22
  const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
17
- const inferredModel = resolvedEngine === "browser" && normalizedRequestedModels.length === 0
18
- ? inferModelFromLabel(cliModelArg)
19
- : resolveApiModel(cliModelArg);
20
- // Browser engine maps Pro/legacy aliases to the latest ChatGPT picker targets.
21
- const resolvedModel = resolvedEngine === "browser" ? normalizeChatGptModelForBrowser(inferredModel) : inferredModel;
22
- const isCodex = resolvedModel.startsWith("gpt-5.1-codex");
23
- const isClaude = resolvedModel.startsWith("claude");
24
- const isGrok = resolvedModel.startsWith("grok");
25
- const isGeminiApiOnly = resolvedModel === "gemini-3.1-pro";
23
+ const apiModel = resolveApiModel(cliModelArg);
24
+ const browserModel = normalizeChatGptModelForBrowser(inferModelFromLabel(cliModelArg));
25
+ const isCodex = apiModel.startsWith("gpt-5.1-codex");
26
+ const isClaude = apiModel.startsWith("claude");
27
+ const isGrok = apiModel.startsWith("grok");
28
+ const isGeminiApiOnly = apiModel === "gemini-3.1-pro";
26
29
  const engineWasBrowser = resolvedEngine === "browser";
27
30
  const allModels = normalizedRequestedModels.length > 0
28
31
  ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
29
- : [resolvedModel];
32
+ : [apiModel];
33
+ const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
30
34
  const includesGeminiApiOnly = allModels.some((m) => m === "gemini-3.1-pro");
31
35
  if ((browserRequested || browserConfigured) && includesGeminiApiOnly) {
32
36
  throw new PromptValidationError("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.", { engine: "browser", models: allModels });
33
37
  }
34
38
  const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
35
- const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) && allModels.some((m) => !isBrowserCompatible(m));
39
+ const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) &&
40
+ browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
36
41
  if (hasNonBrowserCompatibleTarget) {
37
42
  throw new PromptValidationError("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.", { engine: "browser", models: allModels });
38
43
  }
39
- const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly);
40
- const fixedEngine = isCodex || isClaude || isGrok || isGeminiApiOnly || normalizedRequestedModels.length > 0
44
+ const azure = resolveAzureOptions(userConfig, env);
45
+ const azureAutoApi = Boolean(azure?.endpoint) &&
46
+ !browserRequested &&
47
+ !browserConfigured &&
48
+ !envBrowserConfigured &&
49
+ allModels.some(isAzureOpenAICandidateModel);
50
+ const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly || azureAutoApi);
51
+ const fixedEngine = isCodex ||
52
+ isClaude ||
53
+ isGrok ||
54
+ isGeminiApiOnly ||
55
+ azureAutoApi ||
56
+ normalizedRequestedModels.length > 0
41
57
  ? "api"
42
58
  : resolvedEngine;
59
+ // Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
60
+ const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
43
61
  const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
44
62
  ? `${prompt.trim()}\n${userConfig.promptSuffix}`
45
63
  : prompt;
@@ -66,11 +84,12 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
66
84
  filesReport: userConfig?.filesReport,
67
85
  background: userConfig?.background,
68
86
  baseUrl,
87
+ azure,
69
88
  effectiveModelId,
70
89
  };
71
90
  return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
72
91
  }
73
- function resolveEngineWithConfig({ engine, configEngine, env, }) {
92
+ function resolveEngineWithConfig({ engine, configEngine, apiProviderRequested, env, }) {
74
93
  if (engine)
75
94
  return engine;
76
95
  const envOverride = (env.ORACLE_ENGINE ?? "").trim().toLowerCase();
@@ -79,7 +98,18 @@ function resolveEngineWithConfig({ engine, configEngine, env, }) {
79
98
  }
80
99
  if (configEngine)
81
100
  return configEngine;
82
- return resolveEngine({ engine: undefined, env });
101
+ return resolveEngine({ engine: undefined, apiProviderRequested, env });
102
+ }
103
+ function resolveAzureOptions(userConfig, env) {
104
+ const endpoint = env.AZURE_OPENAI_ENDPOINT ?? userConfig?.azure?.endpoint;
105
+ if (!endpoint?.trim()) {
106
+ return undefined;
107
+ }
108
+ return {
109
+ endpoint,
110
+ deployment: env.AZURE_OPENAI_DEPLOYMENT ?? userConfig?.azure?.deployment,
111
+ apiVersion: env.AZURE_OPENAI_API_VERSION ?? userConfig?.azure?.apiVersion,
112
+ };
83
113
  }
84
114
  function resolveEffectiveModelId(model) {
85
115
  if (typeof model === "string" && model.startsWith("gemini")) {
@@ -10,6 +10,7 @@ import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportA
10
10
  import { estimateTokenCount } from "../browser/utils.js";
11
11
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
12
12
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
13
+ import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
13
14
  const isTty = () => Boolean(process.stdout.isTTY);
14
15
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
15
16
  export const MAX_RENDER_BYTES = 200_000;
@@ -219,6 +220,8 @@ export async function attachSession(sessionId, options) {
219
220
  browser: {
220
221
  config: metadata.browser?.config,
221
222
  runtime,
223
+ modelSelection: metadata.browser?.modelSelection,
224
+ warnings: metadata.browser?.warnings,
222
225
  },
223
226
  artifacts,
224
227
  response: { status: "completed" },
@@ -266,6 +269,11 @@ export async function attachSession(sessionId, options) {
266
269
  }
267
270
  console.log(`Created: ${metadata.createdAt}`);
268
271
  console.log(`Status: ${metadata.status}`);
272
+ if (metadata.lifecycle) {
273
+ const attached = metadata.lifecycle.attached ? "attached" : "detached";
274
+ console.log(`Execution: ${formatSessionExecutionLabel(metadata)} (${attached})`);
275
+ console.log(`Reattach: ${metadata.lifecycle.reattachCommand}`);
276
+ }
269
277
  if (metadata.models && metadata.models.length > 0) {
270
278
  console.log("Models:");
271
279
  for (const run of metadata.models) {
@@ -278,6 +286,13 @@ export async function attachSession(sessionId, options) {
278
286
  else if (metadata.model) {
279
287
  console.log(`Model: ${metadata.model}`);
280
288
  }
289
+ const browserEvidence = formatBrowserEvidence(metadata);
290
+ if (browserEvidence) {
291
+ console.log("Browser evidence:");
292
+ for (const line of browserEvidence) {
293
+ console.log(dim(`- ${line}`));
294
+ }
295
+ }
281
296
  if (metadata.artifacts && metadata.artifacts.length > 0) {
282
297
  console.log("Artifacts:");
283
298
  for (const artifact of metadata.artifacts) {
@@ -299,7 +314,7 @@ export async function attachSession(sessionId, options) {
299
314
  console.log(dim(`User error: ${userErrorSummary}`));
300
315
  }
301
316
  }
302
- const shouldTrimIntro = initialStatus === "completed" || initialStatus === "error";
317
+ const shouldTrimIntro = initialStatus === "completed" || initialStatus === "partial" || initialStatus === "error";
303
318
  if (options?.renderPrompt !== false) {
304
319
  const prompt = await readStoredPrompt(sessionId);
305
320
  if (prompt) {
@@ -422,7 +437,7 @@ export async function attachSession(sessionId, options) {
422
437
  if (!latest) {
423
438
  break;
424
439
  }
425
- if (latest.status === "completed" || latest.status === "error") {
440
+ if (latest.status === "completed" || latest.status === "partial" || latest.status === "error") {
426
441
  await printNew();
427
442
  flushRemainder();
428
443
  if (!options?.suppressMetadata) {
@@ -430,10 +445,11 @@ export async function attachSession(sessionId, options) {
430
445
  console.log("\nResult:");
431
446
  console.log(`Session failed: ${latest.errorMessage}`);
432
447
  }
433
- if (latest.status === "completed" && latest.usage) {
448
+ if ((latest.status === "completed" || latest.status === "partial") && latest.usage) {
434
449
  const summary = formatCompletionSummary(latest, { includeSlug: true });
435
450
  if (summary) {
436
- console.log(`\n${chalk.green.bold(summary)}`);
451
+ const color = latest.status === "partial" ? chalk.yellow.bold : chalk.green.bold;
452
+ console.log(`\n${color(summary)}`);
437
453
  }
438
454
  else {
439
455
  const usage = latest.usage;
@@ -495,6 +511,25 @@ export function formatUserErrorMetadata(metadata) {
495
511
  }
496
512
  return parts.length > 0 ? parts.join(" | ") : null;
497
513
  }
514
+ export function formatBrowserEvidence(metadata) {
515
+ const browser = metadata.browser;
516
+ if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
517
+ return null;
518
+ }
519
+ const lines = [];
520
+ const evidence = browser.modelSelection;
521
+ if (evidence) {
522
+ const requested = evidence.requestedModel ?? "(none)";
523
+ const resolved = evidence.resolvedLabel ?? "(unavailable)";
524
+ const strategy = evidence.strategy ?? "(default)";
525
+ const verified = evidence.verified ? "yes" : "no";
526
+ lines.push(`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`);
527
+ }
528
+ for (const warning of browser.warnings ?? []) {
529
+ lines.push(`warning ${warning.code}: ${warning.message}`);
530
+ }
531
+ return lines.length > 0 ? lines : null;
532
+ }
498
533
  export function buildReattachLine(metadata) {
499
534
  if (!metadata.id) {
500
535
  return null;
@@ -0,0 +1,38 @@
1
+ export function buildSessionLifecycle({ engine, detached, reattachCommand, }) {
2
+ return {
3
+ engine,
4
+ execution: detached ? "background" : "foreground",
5
+ attached: !detached,
6
+ detached,
7
+ reattachCommand,
8
+ };
9
+ }
10
+ export function formatSessionLifecycleBlock(meta) {
11
+ const lifecycle = meta.lifecycle;
12
+ if (!lifecycle) {
13
+ return [];
14
+ }
15
+ const modelCount = meta.models?.length ?? (meta.model ? 1 : 0);
16
+ const detachValue = lifecycle.detached
17
+ ? lifecycle.execution === "background"
18
+ ? "yes, polling"
19
+ : "yes"
20
+ : "no";
21
+ const lines = [
22
+ `Session: ${meta.id}`,
23
+ `Mode: ${lifecycle.engine} ${lifecycle.execution}`,
24
+ `Models: ${modelCount > 1 ? `${modelCount} parallel` : String(modelCount || 1)}`,
25
+ `Detach: ${detachValue}`,
26
+ `Reattach: ${lifecycle.reattachCommand}`,
27
+ ];
28
+ return lines;
29
+ }
30
+ export function formatSessionExecutionLabel(meta) {
31
+ const lifecycle = meta.lifecycle;
32
+ if (!lifecycle) {
33
+ return meta.mode ?? meta.options?.mode ?? "api";
34
+ }
35
+ const engine = lifecycle.engine === "browser" ? "br" : lifecycle.engine;
36
+ const execution = lifecycle.execution === "background" ? "bg" : "fg";
37
+ return `${engine}/${execution}`;
38
+ }