@steipete/oracle 0.9.0 → 0.11.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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,28 +1,28 @@
1
- import kleur from 'kleur';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
5
- import { runBrowserSessionExecution } from '../browser/sessionRunner.js';
6
- import { renderMarkdownAnsi } from './markdownRenderer.js';
7
- import { formatResponseMetadata, formatTransportMetadata } from './sessionDisplay.js';
8
- import { markErrorLogged } from './errorUtils.js';
9
- import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from './notifier.js';
10
- import { sessionStore } from '../sessionStore.js';
11
- import { wait } from '../sessionManager.js';
12
- import { runMultiModelApiSession } from '../oracle/multiModelRunner.js';
13
- import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from '../oracle/config.js';
14
- import { isKnownModel } from '../oracle/modelResolver.js';
15
- import { resolveModelConfig } from '../oracle/modelResolver.js';
16
- import { buildPrompt, buildRequestBody } from '../oracle/request.js';
17
- import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
18
- import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
19
- import { formatFinishLine } from '../oracle/finishLine.js';
20
- import { sanitizeOscProgress } from './oscUtils.js';
21
- import { readFiles } from '../oracle/files.js';
22
- import { cwd as getCwd } from 'node:process';
23
- import { resumeBrowserSession } from '../browser/reattach.js';
24
- import { estimateTokenCount } from '../browser/utils.js';
25
- import { formatElapsed } from '../oracle/format.js';
1
+ import kleur from "kleur";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from "../oracle.js";
5
+ import { ensureSessionArtifacts, runBrowserSessionExecution, } from "../browser/sessionRunner.js";
6
+ import { renderMarkdownAnsi } from "./markdownRenderer.js";
7
+ import { formatResponseMetadata, formatTransportMetadata } from "./sessionDisplay.js";
8
+ import { markErrorLogged } from "./errorUtils.js";
9
+ import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from "./notifier.js";
10
+ import { sessionStore } from "../sessionStore.js";
11
+ import { wait } from "../sessionManager.js";
12
+ import { runMultiModelApiSession } from "../oracle/multiModelRunner.js";
13
+ import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from "../oracle/config.js";
14
+ import { isKnownModel } from "../oracle/modelResolver.js";
15
+ import { resolveModelConfig } from "../oracle/modelResolver.js";
16
+ import { buildPrompt, buildRequestBody } from "../oracle/request.js";
17
+ import { estimateRequestTokens } from "../oracle/tokenEstimate.js";
18
+ import { formatTokenEstimate, formatTokenValue } from "../oracle/runUtils.js";
19
+ import { formatFinishLine } from "../oracle/finishLine.js";
20
+ import { sanitizeOscProgress } from "./oscUtils.js";
21
+ import { readFiles } from "../oracle/files.js";
22
+ import { cwd as getCwd } from "node:process";
23
+ import { resumeBrowserSession } from "../browser/reattach.js";
24
+ import { estimateTokenCount } from "../browser/utils.js";
25
+ import { formatElapsed } from "../oracle/format.js";
26
26
  const isTty = process.stdout.isTTY;
27
27
  const dim = (text) => (isTty ? kleur.dim(text) : text);
28
28
  export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
@@ -32,7 +32,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
32
32
  return muteStdout ? true : process.stdout.write(chunk);
33
33
  };
34
34
  await sessionStore.updateSession(sessionMeta.id, {
35
- status: 'running',
35
+ status: "running",
36
36
  startedAt: new Date().toISOString(),
37
37
  mode,
38
38
  ...(browserConfig ? { browser: { config: browserConfig } } : {}),
@@ -40,13 +40,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
40
40
  const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
41
41
  const modelForStatus = runOptions.model ?? sessionMeta.model;
42
42
  try {
43
- if (mode === 'browser') {
43
+ if (mode === "browser") {
44
44
  if (!browserConfig) {
45
- throw new Error('Missing browser configuration for session.');
45
+ throw new Error("Missing browser configuration for session.");
46
46
  }
47
47
  if (modelForStatus) {
48
48
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
49
- status: 'running',
49
+ status: "running",
50
50
  startedAt: new Date().toISOString(),
51
51
  });
52
52
  }
@@ -54,33 +54,41 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
54
54
  ...browserDeps,
55
55
  persistRuntimeHint: async (runtime) => {
56
56
  await sessionStore.updateSession(sessionMeta.id, {
57
- status: 'running',
57
+ status: "running",
58
58
  browser: { config: browserConfig, runtime },
59
59
  });
60
60
  },
61
61
  };
62
- const result = await runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, runnerDeps);
62
+ const result = await runBrowserSessionExecution({
63
+ runOptions: { ...runOptions, sessionId: runOptions.sessionId ?? sessionMeta.id },
64
+ browserConfig,
65
+ cwd,
66
+ log,
67
+ }, runnerDeps);
63
68
  if (modelForStatus) {
64
69
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
65
- status: 'completed',
70
+ status: "completed",
66
71
  completedAt: new Date().toISOString(),
67
72
  usage: result.usage,
68
73
  });
69
74
  }
70
75
  await sessionStore.updateSession(sessionMeta.id, {
71
- status: 'completed',
76
+ status: "completed",
72
77
  completedAt: new Date().toISOString(),
73
78
  usage: result.usage,
74
79
  elapsedMs: result.elapsedMs,
80
+ errorMessage: undefined,
75
81
  browser: {
76
82
  config: browserConfig,
77
83
  runtime: result.runtime,
84
+ archive: result.archive,
78
85
  },
86
+ artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
79
87
  response: undefined,
80
88
  transport: undefined,
81
89
  error: undefined,
82
90
  });
83
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? '', log);
91
+ await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
84
92
  await sendSessionNotification({
85
93
  sessionId: sessionMeta.id,
86
94
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
@@ -95,7 +103,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
95
103
  if (multiModels.length > 1) {
96
104
  const [primaryModel] = multiModels;
97
105
  if (!primaryModel) {
98
- throw new Error('Missing model name for multi-model run.');
106
+ throw new Error("Missing model name for multi-model run.");
99
107
  }
100
108
  const modelConfig = await resolveModelConfig(primaryModel, {
101
109
  baseUrl: runOptions.baseUrl,
@@ -116,30 +124,30 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
116
124
  storeResponse: runOptions.background,
117
125
  });
118
126
  const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
119
- const tokenLabel = formatTokenEstimate(estimatedTokens, (text) => (isTty ? kleur.green(text) : text));
120
- const filesPhrase = files.length === 0 ? 'no files' : `${files.length} files`;
121
- const modelsLabel = multiModels.join(', ');
127
+ const tokenLabel = formatTokenEstimate(estimatedTokens, (text) => isTty ? kleur.green(text) : text);
128
+ const filesPhrase = files.length === 0 ? "no files" : `${files.length} files`;
129
+ const modelsLabel = multiModels.join(", ");
122
130
  log(`Calling ${isTty ? kleur.cyan(modelsLabel) : modelsLabel} — ${tokenLabel} tokens, ${filesPhrase}.`);
123
131
  const multiRunTips = [];
124
132
  if (files.length === 0) {
125
- multiRunTips.push('Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.');
133
+ multiRunTips.push("Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.");
126
134
  }
127
135
  const shortPrompt = (runOptions.prompt?.trim().length ?? 0) < 80;
128
136
  if (shortPrompt) {
129
- multiRunTips.push('Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.');
137
+ multiRunTips.push("Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.");
130
138
  }
131
139
  for (const tip of multiRunTips) {
132
140
  log(dim(tip));
133
141
  }
134
142
  // Surface long-running model expectations up front so users know why a response might lag.
135
- const longRunningModels = multiModels.filter((model) => isKnownModel(model) && MODEL_CONFIGS[model]?.reasoning?.effort === 'high');
143
+ const longRunningModels = multiModels.filter((model) => isKnownModel(model) && MODEL_CONFIGS[model]?.reasoning?.effort === "high");
136
144
  if (longRunningModels.length > 0) {
137
145
  for (const model of longRunningModels) {
138
- log('');
146
+ log("");
139
147
  const headingLabel = `[${model}]`;
140
148
  log(isTty ? kleur.bold(headingLabel) : headingLabel);
141
- log(dim('This model can take up to 60 minutes (usually replies much faster).'));
142
- log(dim('Press Ctrl+C to cancel.'));
149
+ log(dim("This model can take up to 60 minutes (usually replies much faster)."));
150
+ log(dim("Press Ctrl+C to cancel."));
143
151
  }
144
152
  }
145
153
  const shouldStreamInline = !muteStdout && process.stdout.isTTY;
@@ -152,7 +160,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
152
160
  return;
153
161
  printedModels.add(model);
154
162
  const body = stripOscProgress(await sessionStore.readModelLog(sessionMeta.id, model));
155
- log('');
163
+ log("");
156
164
  const fallback = answerFallbacks.get(model);
157
165
  const hasBody = body.length > 0;
158
166
  if (!hasBody && !fallback) {
@@ -162,11 +170,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
162
170
  const headingLabel = `[${model}]`;
163
171
  const heading = shouldStreamInline ? kleur.bold(headingLabel) : headingLabel;
164
172
  log(heading);
165
- const content = hasBody ? body : fallback ?? '';
173
+ const content = hasBody ? body : (fallback ?? "");
166
174
  const printable = shouldRenderMarkdown ? renderMarkdownAnsi(content) : content;
167
175
  writeInline(printable);
168
- if (!printable.endsWith('\n')) {
169
- log('');
176
+ if (!printable.endsWith("\n")) {
177
+ log("");
170
178
  }
171
179
  };
172
180
  const summary = await runMultiModelApiSession({
@@ -192,7 +200,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
192
200
  // If we couldn't stream inline (e.g., non-TTY), print all logs after completion.
193
201
  for (const [index, result] of summary.fulfilled.entries()) {
194
202
  if (index > 0) {
195
- log('');
203
+ log("");
196
204
  }
197
205
  await printModelLog(result.model);
198
206
  }
@@ -216,14 +224,18 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
216
224
  reasoning_tokens: aggregateUsage.reasoningTokens,
217
225
  total_tokens: aggregateUsage.totalTokens,
218
226
  }, idx))
219
- .join('/');
227
+ .join("/");
220
228
  const tokensPart = (() => {
221
- const parts = tokensDisplay.split('/');
229
+ const parts = tokensDisplay.split("/");
222
230
  if (parts.length !== 4)
223
231
  return tokensDisplay;
224
232
  return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
225
233
  })();
226
- const statusColor = summary.rejected.length === 0 ? kleur.green : summary.fulfilled.length > 0 ? kleur.yellow : kleur.red;
234
+ const statusColor = summary.rejected.length === 0
235
+ ? kleur.green
236
+ : summary.fulfilled.length > 0
237
+ ? kleur.yellow
238
+ : kleur.red;
227
239
  const overallText = `${summary.fulfilled.length}/${multiModels.length} models`;
228
240
  const { line1 } = formatFinishLine({
229
241
  elapsedMs: summary.elapsedMs,
@@ -234,10 +246,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
234
246
  log(statusColor(line1));
235
247
  const hasFailure = summary.rejected.length > 0;
236
248
  await sessionStore.updateSession(sessionMeta.id, {
237
- status: hasFailure ? 'error' : 'completed',
249
+ status: hasFailure ? "error" : "completed",
238
250
  completedAt: new Date().toISOString(),
239
251
  usage: aggregateUsage,
240
252
  elapsedMs: summary.elapsedMs,
253
+ errorMessage: undefined,
241
254
  response: undefined,
242
255
  transport: undefined,
243
256
  error: undefined,
@@ -261,7 +274,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
261
274
  }
262
275
  }
263
276
  if (savedOutputs.length > 0) {
264
- log(dim('Saved outputs:'));
277
+ log(dim("Saved outputs:"));
265
278
  for (const item of savedOutputs) {
266
279
  log(dim(`- ${item.model} -> ${item.path}`));
267
280
  }
@@ -278,7 +291,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
278
291
  : runOptions;
279
292
  if (modelForStatus && singleModelOverride == null) {
280
293
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
281
- status: 'running',
294
+ status: "running",
282
295
  startedAt: new Date().toISOString(),
283
296
  });
284
297
  }
@@ -288,21 +301,22 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
288
301
  write,
289
302
  allowStdout: !muteStdout,
290
303
  });
291
- if (result.mode !== 'live') {
292
- throw new Error('Unexpected preview result while running a session.');
304
+ if (result.mode !== "live") {
305
+ throw new Error("Unexpected preview result while running a session.");
293
306
  }
294
307
  await sessionStore.updateSession(sessionMeta.id, {
295
- status: 'completed',
308
+ status: "completed",
296
309
  completedAt: new Date().toISOString(),
297
310
  usage: result.usage,
298
311
  elapsedMs: result.elapsedMs,
312
+ errorMessage: undefined,
299
313
  response: extractResponseMetadata(result.response),
300
314
  transport: undefined,
301
315
  error: undefined,
302
316
  });
303
317
  if (modelForStatus && singleModelOverride == null) {
304
318
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
305
- status: 'completed',
319
+ status: "completed",
306
320
  completedAt: new Date().toISOString(),
307
321
  usage: result.usage,
308
322
  });
@@ -323,49 +337,65 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
323
337
  log(`ERROR: ${message}`);
324
338
  markErrorLogged(error);
325
339
  const userError = asOracleUserError(error);
326
- const connectionLost = userError?.category === 'browser-automation' && userError.details?.stage === 'connection-lost';
327
- const assistantTimeout = userError?.category === 'browser-automation' && userError.details?.stage === 'assistant-timeout';
328
- const cloudflareChallenge = userError?.category === 'browser-automation' &&
329
- userError.details?.stage === 'cloudflare-challenge';
330
- if (connectionLost && mode === 'browser') {
331
- const runtime = userError.details?.runtime;
332
- log(dim('Chrome disconnected before completion; keeping session running for reattach.'));
340
+ const connectionLost = userError?.category === "browser-automation" &&
341
+ userError.details?.stage === "connection-lost";
342
+ const assistantTimeout = userError?.category === "browser-automation" &&
343
+ userError.details?.stage === "assistant-timeout";
344
+ const cloudflareChallenge = userError?.category === "browser-automation" &&
345
+ userError.details?.stage === "cloudflare-challenge";
346
+ if (connectionLost && mode === "browser") {
347
+ const runtime = userError.details
348
+ ?.runtime;
349
+ log(dim("Chrome disconnected before completion; keeping session running for reattach."));
333
350
  if (modelForStatus) {
334
351
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
335
- status: 'running',
352
+ status: "running",
336
353
  completedAt: undefined,
337
354
  });
338
355
  }
339
356
  await sessionStore.updateSession(sessionMeta.id, {
340
- status: 'running',
357
+ status: "running",
341
358
  errorMessage: message,
342
359
  mode,
343
360
  browser: {
344
361
  config: browserConfig,
345
362
  runtime: runtime ?? sessionMeta.browser?.runtime,
346
363
  },
347
- response: { status: 'running', incompleteReason: 'chrome-disconnected' },
364
+ response: { status: "running", incompleteReason: "chrome-disconnected" },
348
365
  });
349
366
  return;
350
367
  }
351
- if (assistantTimeout && mode === 'browser') {
352
- const runtime = userError.details?.runtime;
353
- log(dim('Assistant response timed out; keeping session running for reattach.'));
368
+ if (assistantTimeout && mode === "browser") {
369
+ const runtime = userError.details
370
+ ?.runtime;
371
+ log(dim("Assistant response timed out; marking capture incomplete for reattach."));
354
372
  if (modelForStatus) {
355
373
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
356
- status: 'running',
357
- completedAt: undefined,
374
+ status: "error",
375
+ completedAt: new Date().toISOString(),
376
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
377
+ error: {
378
+ category: userError.category,
379
+ message: userError.message,
380
+ details: userError.details,
381
+ },
358
382
  });
359
383
  }
360
384
  await sessionStore.updateSession(sessionMeta.id, {
361
- status: 'running',
385
+ status: "error",
386
+ completedAt: new Date().toISOString(),
362
387
  errorMessage: message,
363
388
  mode,
364
389
  browser: {
365
390
  config: browserConfig,
366
391
  runtime: runtime ?? sessionMeta.browser?.runtime,
367
392
  },
368
- response: { status: 'running', incompleteReason: 'assistant-timeout' },
393
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
394
+ error: {
395
+ category: userError.category,
396
+ message: userError.message,
397
+ details: userError.details,
398
+ },
369
399
  });
370
400
  const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
371
401
  if (autoReattachIntervalMs > 0) {
@@ -386,9 +416,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
386
416
  log(dim(`Reattach later with: oracle session ${sessionMeta.id}`));
387
417
  return;
388
418
  }
389
- if (cloudflareChallenge && mode === 'browser') {
419
+ if (cloudflareChallenge && mode === "browser") {
390
420
  const details = userError.details;
391
- log(dim('Cloudflare challenge detected; browser left running so you can complete the check.'));
421
+ log(dim("Cloudflare challenge detected; browser left running so you can complete the check."));
392
422
  if (details?.reuseProfileHint) {
393
423
  log(dim(`Reuse this browser profile with: ${details.reuseProfileHint}`));
394
424
  }
@@ -406,9 +436,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
406
436
  if (transportLine) {
407
437
  log(dim(`Transport: ${transportLine}`));
408
438
  }
409
- const browserRuntime = mode === 'browser' ? userError?.details?.runtime : undefined;
439
+ const browserRuntime = mode === "browser"
440
+ ? userError?.details?.runtime
441
+ : undefined;
410
442
  await sessionStore.updateSession(sessionMeta.id, {
411
- status: 'error',
443
+ status: "error",
412
444
  completedAt: new Date().toISOString(),
413
445
  errorMessage: message,
414
446
  mode,
@@ -430,13 +462,24 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
430
462
  });
431
463
  if (modelForStatus) {
432
464
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
433
- status: 'error',
465
+ status: "error",
434
466
  completedAt: new Date().toISOString(),
435
467
  });
436
468
  }
437
469
  throw error;
438
470
  }
439
471
  }
472
+ function mergeArtifacts(existing, additions) {
473
+ const merged = new Map();
474
+ for (const artifact of existing ?? []) {
475
+ merged.set(`${artifact.kind}:${artifact.path}`, artifact);
476
+ }
477
+ for (const artifact of additions ?? []) {
478
+ merged.set(`${artifact.kind}:${artifact.path}`, artifact);
479
+ }
480
+ const values = Array.from(merged.values());
481
+ return values.length > 0 ? values : undefined;
482
+ }
440
483
  function formatError(error) {
441
484
  return error instanceof Error ? error.message : String(error);
442
485
  }
@@ -444,7 +487,7 @@ async function writeAssistantOutput(targetPath, content, log) {
444
487
  if (!targetPath)
445
488
  return;
446
489
  if (!content || content.trim().length === 0) {
447
- log(dim('write-output skipped: no assistant content to save.'));
490
+ log(dim("write-output skipped: no assistant content to save."));
448
491
  return;
449
492
  }
450
493
  const normalizedTarget = path.resolve(targetPath);
@@ -456,8 +499,8 @@ async function writeAssistantOutput(targetPath, content, log) {
456
499
  }
457
500
  try {
458
501
  await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
459
- const payload = content.endsWith('\n') ? content : `${content}\n`;
460
- await fs.writeFile(normalizedTarget, payload, 'utf8');
502
+ const payload = content.endsWith("\n") ? content : `${content}\n`;
503
+ await fs.writeFile(normalizedTarget, payload, "utf8");
461
504
  log(dim(`Saved assistant output to ${normalizedTarget}`));
462
505
  return normalizedTarget;
463
506
  }
@@ -468,8 +511,8 @@ async function writeAssistantOutput(targetPath, content, log) {
468
511
  if (fallbackPath) {
469
512
  try {
470
513
  await fs.mkdir(path.dirname(fallbackPath), { recursive: true });
471
- const payload = content.endsWith('\n') ? content : `${content}\n`;
472
- await fs.writeFile(fallbackPath, payload, 'utf8');
514
+ const payload = content.endsWith("\n") ? content : `${content}\n`;
515
+ await fs.writeFile(fallbackPath, payload, "utf8");
473
516
  log(dim(`write-output fallback to ${fallbackPath} (original failed: ${reason})`));
474
517
  return fallbackPath;
475
518
  }
@@ -485,7 +528,7 @@ async function writeAssistantOutput(targetPath, content, log) {
485
528
  }
486
529
  async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, runOptions, modelForStatus, notificationSettings, log, }) {
487
530
  if (!runtime || !browserConfig) {
488
- log(dim('Auto-reattach disabled: missing runtime or browser config.'));
531
+ log(dim("Auto-reattach disabled: missing runtime or browser config."));
489
532
  return false;
490
533
  }
491
534
  const delayMs = Math.max(0, browserConfig.autoReattachDelayMs ?? 0);
@@ -526,16 +569,25 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
526
569
  const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
527
570
  promptPreview: sessionMeta.promptPreview,
528
571
  });
529
- const answerText = result.answerMarkdown || result.answerText || '';
572
+ const answerText = result.answerMarkdown || result.answerText || "";
530
573
  const outputTokens = estimateTokenCount(answerText);
574
+ const artifacts = await ensureSessionArtifacts({
575
+ sessionId: sessionMeta.id,
576
+ prompt: runOptions.prompt,
577
+ answerMarkdown: answerText,
578
+ conversationUrl: runtime.tabUrl,
579
+ browserConfig,
580
+ existingArtifacts: sessionMeta.artifacts,
581
+ logger,
582
+ });
531
583
  const logWriter = sessionStore.createLogWriter(sessionMeta.id);
532
584
  logWriter.logLine(`[auto-reattach] captured assistant response on attempt ${attempt}`);
533
- logWriter.logLine('Answer:');
585
+ logWriter.logLine("Answer:");
534
586
  logWriter.logLine(answerText);
535
587
  logWriter.stream.end();
536
588
  if (modelForStatus) {
537
589
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
538
- status: 'completed',
590
+ status: "completed",
539
591
  completedAt: new Date().toISOString(),
540
592
  usage: {
541
593
  inputTokens: 0,
@@ -546,7 +598,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
546
598
  });
547
599
  }
548
600
  await sessionStore.updateSession(sessionMeta.id, {
549
- status: 'completed',
601
+ status: "completed",
550
602
  completedAt: new Date().toISOString(),
551
603
  usage: {
552
604
  inputTokens: 0,
@@ -554,11 +606,13 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
554
606
  reasoningTokens: 0,
555
607
  totalTokens: outputTokens,
556
608
  },
609
+ errorMessage: undefined,
557
610
  browser: {
558
611
  config: browserConfig,
559
612
  runtime,
560
613
  },
561
- response: { status: 'completed' },
614
+ artifacts: mergeArtifacts(sessionMeta.artifacts, artifacts),
615
+ response: { status: "completed" },
562
616
  error: undefined,
563
617
  transport: undefined,
564
618
  });
@@ -566,7 +620,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
566
620
  await sendSessionNotification({
567
621
  sessionId: sessionMeta.id,
568
622
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
569
- mode: sessionMeta.mode ?? 'browser',
623
+ mode: sessionMeta.mode ?? "browser",
570
624
  model: sessionMeta.model ?? runOptions.model,
571
625
  usage: {
572
626
  inputTokens: 0,
@@ -574,7 +628,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
574
628
  },
575
629
  characters: answerText.length,
576
630
  }, notificationSettings, log, answerText.slice(0, 140));
577
- log(kleur.green('Auto-reattach succeeded; session marked completed.'));
631
+ log(kleur.green("Auto-reattach succeeded; session marked completed."));
578
632
  return true;
579
633
  }
580
634
  catch (error) {
@@ -602,7 +656,7 @@ function isPermissionError(error) {
602
656
  if (!(error instanceof Error))
603
657
  return false;
604
658
  const code = error.code;
605
- return code === 'EACCES' || code === 'EPERM';
659
+ return code === "EACCES" || code === "EPERM";
606
660
  }
607
661
  function buildFallbackPath(original) {
608
662
  const ext = path.extname(original);
@@ -1,7 +1,7 @@
1
- import chalk from 'chalk';
2
- import kleur from 'kleur';
3
- import { MODEL_CONFIGS } from '../oracle.js';
4
- import { estimateUsdCost } from 'tokentally';
1
+ import chalk from "chalk";
2
+ import kleur from "kleur";
3
+ import { MODEL_CONFIGS } from "../oracle.js";
4
+ import { estimateUsdCost } from "tokentally";
5
5
  const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
6
6
  const dim = (text, rich) => (rich ? kleur.dim(text) : text);
7
7
  export const STATUS_PAD = 9;
@@ -11,23 +11,23 @@ export const TIMESTAMP_PAD = 19;
11
11
  export const CHARS_PAD = 5;
12
12
  export const COST_PAD = 7;
13
13
  export function formatSessionTableHeader(rich) {
14
- const header = `${'Status'.padEnd(STATUS_PAD)} ${'Model'.padEnd(MODEL_PAD)} ${'Mode'.padEnd(MODE_PAD)} ${'Timestamp'.padEnd(TIMESTAMP_PAD)} ${'Chars'.padStart(CHARS_PAD)} ${'Cost'.padStart(COST_PAD)} Slug`;
14
+ const header = `${"Status".padEnd(STATUS_PAD)} ${"Model".padEnd(MODEL_PAD)} ${"Mode".padEnd(MODE_PAD)} ${"Timestamp".padEnd(TIMESTAMP_PAD)} ${"Chars".padStart(CHARS_PAD)} ${"Cost".padStart(COST_PAD)} Slug`;
15
15
  return dim(header, isRich(rich));
16
16
  }
17
17
  export function formatSessionTableRow(meta, options) {
18
18
  const rich = isRich(options?.rich);
19
- const status = colorStatus(meta.status ?? 'unknown', rich);
20
- const modelLabel = (meta.model ?? 'n/a').padEnd(MODEL_PAD);
19
+ const status = colorStatus(meta.status ?? "unknown", rich);
20
+ const modelLabel = (meta.model ?? "n/a").padEnd(MODEL_PAD);
21
21
  const model = rich ? chalk.white(modelLabel) : modelLabel;
22
- const modeLabel = (meta.mode ?? meta.options?.mode ?? 'api').padEnd(MODE_PAD);
22
+ const modeLabel = (meta.mode ?? meta.options?.mode ?? "api").padEnd(MODE_PAD);
23
23
  const mode = rich ? chalk.gray(modeLabel) : modeLabel;
24
24
  const timestampLabel = formatTimestampAligned(meta.createdAt).padEnd(TIMESTAMP_PAD);
25
25
  const timestamp = rich ? chalk.gray(timestampLabel) : timestampLabel;
26
26
  const charsValue = meta.options?.prompt?.length ?? meta.promptPreview?.length ?? 0;
27
- const charsRaw = charsValue > 0 ? String(charsValue).padStart(CHARS_PAD) : `${''.padStart(CHARS_PAD - 1)}-`;
27
+ const charsRaw = charsValue > 0 ? String(charsValue).padStart(CHARS_PAD) : `${"".padStart(CHARS_PAD - 1)}-`;
28
28
  const chars = rich ? chalk.gray(charsRaw) : charsRaw;
29
29
  const costValue = resolveSessionCost(meta);
30
- const costRaw = costValue != null ? formatCostTable(costValue) : `${''.padStart(COST_PAD - 1)}-`;
30
+ const costRaw = costValue != null ? formatCostTable(costValue) : `${"".padStart(COST_PAD - 1)}-`;
31
31
  const cost = rich ? chalk.gray(costRaw) : costRaw;
32
32
  const slugValue = options?.displaySlug ?? meta.id;
33
33
  const slug = rich ? chalk.cyan(slugValue) : slugValue;
@@ -35,7 +35,7 @@ export function formatSessionTableRow(meta, options) {
35
35
  }
36
36
  export function resolveSessionCost(meta) {
37
37
  const mode = meta.mode ?? meta.options?.mode;
38
- if (mode === 'browser') {
38
+ if (mode === "browser") {
39
39
  return null;
40
40
  }
41
41
  if (meta.usage?.cost != null) {
@@ -52,25 +52,28 @@ export function resolveSessionCost(meta) {
52
52
  const output = meta.usage.outputTokens ?? 0;
53
53
  const cost = estimateUsdCost({
54
54
  usage: { inputTokens: input, outputTokens: output },
55
- pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
55
+ pricing: {
56
+ inputUsdPerToken: pricing.inputPerToken,
57
+ outputUsdPerToken: pricing.outputPerToken,
58
+ },
56
59
  })?.totalUsd ?? 0;
57
60
  return cost > 0 ? cost : null;
58
61
  }
59
62
  export function formatTimestampAligned(iso) {
60
63
  const date = new Date(iso);
61
- const locale = 'en-US';
64
+ const locale = "en-US";
62
65
  const opts = {
63
- year: 'numeric',
64
- month: '2-digit',
65
- day: '2-digit',
66
- hour: 'numeric',
67
- minute: '2-digit',
66
+ year: "numeric",
67
+ month: "2-digit",
68
+ day: "2-digit",
69
+ hour: "numeric",
70
+ minute: "2-digit",
68
71
  second: undefined,
69
72
  hour12: true,
70
73
  };
71
74
  let formatted = date.toLocaleString(locale, opts);
72
- formatted = formatted.replace(', ', ' ');
73
- return formatted.replace(/(\s)(\d:)/, '$1 $2');
75
+ formatted = formatted.replace(", ", " ");
76
+ return formatted.replace(/(\s)(\d:)/, "$1 $2");
74
77
  }
75
78
  function formatCostTable(cost) {
76
79
  return `$${cost.toFixed(3)}`.padStart(COST_PAD);
@@ -81,11 +84,11 @@ function colorStatus(status, rich) {
81
84
  return padded;
82
85
  }
83
86
  switch (status) {
84
- case 'completed':
87
+ case "completed":
85
88
  return chalk.green(padded);
86
- case 'error':
89
+ case "error":
87
90
  return chalk.red(padded);
88
- case 'running':
91
+ case "running":
89
92
  return chalk.yellow(padded);
90
93
  default:
91
94
  return padded;