@steipete/oracle 0.10.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 (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +78 -13
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +26 -2
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1257 -485
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +40 -0
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +7 -0
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. package/package.json +2 -1
@@ -1,10 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import kleur from "kleur";
3
+ import fs from "node:fs/promises";
3
4
  import { renderMarkdownAnsi } from "./markdownRenderer.js";
4
5
  import { formatFinishLine } from "../oracle/finishLine.js";
5
6
  import { sessionStore, wait } from "../sessionStore.js";
6
7
  import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
7
8
  import { resumeBrowserSession } from "../browser/reattach.js";
9
+ import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
8
10
  import { estimateTokenCount } from "../browser/utils.js";
9
11
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
10
12
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
@@ -29,6 +31,66 @@ function isProcessAlive(pid) {
29
31
  return true;
30
32
  }
31
33
  }
34
+ function formatBytes(bytes) {
35
+ if (bytes < 1024)
36
+ return `${bytes} B`;
37
+ if (bytes < 1024 * 1024)
38
+ return `${(bytes / 1024).toFixed(1)} KB`;
39
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
40
+ }
41
+ function isDeepResearchBrowserSession(metadata) {
42
+ return metadata.mode === "browser" && metadata.browser?.config?.researchMode === "deep";
43
+ }
44
+ function isDeepResearchPlaceholderCapture(metadata, logText) {
45
+ const answer = trimBeforeFirstAnswer(logText)
46
+ .replace(/^Answer:\s*/i, "")
47
+ .toLowerCase()
48
+ .replace(/\s+/g, " ")
49
+ .trim();
50
+ const isToolOnly = answer === "called tool" ||
51
+ answer === "used tool" ||
52
+ answer === "użyto narzędzia" ||
53
+ answer === "narzędzie wywołane";
54
+ const modelUsage = metadata.models?.find((run) => run.model === metadata.model)?.usage;
55
+ const outputTokens = metadata.usage?.outputTokens ?? modelUsage?.outputTokens;
56
+ return isToolOnly && (outputTokens == null || outputTokens <= 8);
57
+ }
58
+ async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
59
+ const body = result.answerMarkdown || result.answerText;
60
+ if (replaceExistingLog) {
61
+ const paths = await sessionStore.getPaths(sessionId);
62
+ await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
63
+ return;
64
+ }
65
+ const logWriter = sessionStore.createLogWriter(sessionId);
66
+ logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
67
+ logWriter.logLine("Answer:");
68
+ logWriter.logLine(body);
69
+ logWriter.stream.end();
70
+ }
71
+ async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
72
+ const body = result.answerMarkdown || result.answerText;
73
+ const conversationUrl = metadata.browser?.runtime?.tabUrl;
74
+ const logger = ((message) => console.log(dim(message)));
75
+ const reportArtifact = isDeepResearchBrowserSession(metadata)
76
+ ? await saveDeepResearchReportArtifact({
77
+ sessionId,
78
+ reportMarkdown: body,
79
+ conversationUrl,
80
+ logger,
81
+ }).catch(() => null)
82
+ : null;
83
+ const prompt = (await readStoredPrompt(sessionId)) ?? metadata.promptPreview ?? "";
84
+ const transcriptArtifact = await saveBrowserTranscriptArtifact({
85
+ sessionId,
86
+ prompt,
87
+ answerMarkdown: body,
88
+ conversationUrl,
89
+ artifacts: appendArtifacts(undefined, [reportArtifact]),
90
+ logger,
91
+ }).catch(() => null);
92
+ return appendArtifacts(metadata.artifacts, [reportArtifact, transcriptArtifact]);
93
+ }
32
94
  const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
33
95
  export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
34
96
  const metas = await sessionStore.listSessions();
@@ -99,12 +161,25 @@ export async function attachSession(sessionId, options) {
99
161
  const runtime = metadata.browser?.runtime;
100
162
  const controllerAlive = isProcessAlive(runtime?.controllerPid);
101
163
  const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
102
- const statusAllowsReattach = metadata.status === "running" || (metadata.status === "error" && hasChromeDisconnect);
103
- const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
104
- const canReattach = statusAllowsReattach &&
164
+ const hasIncompleteCapture = metadata.response?.incompleteReason === "incomplete-capture";
165
+ const statusAllowsReattach = metadata.status === "running" ||
166
+ (metadata.status === "error" && (hasChromeDisconnect || hasIncompleteCapture));
167
+ const hasFallbackSessionInfo = Boolean(runtime?.chromePort ||
168
+ runtime?.chromeBrowserWSEndpoint ||
169
+ runtime?.chromeProfileRoot ||
170
+ runtime?.tabUrl ||
171
+ runtime?.conversationId);
172
+ const deepResearchPlaceholderCapture = isDeepResearchBrowserSession(metadata) &&
173
+ hasFallbackSessionInfo &&
174
+ isDeepResearchPlaceholderCapture(metadata, await sessionStore.readLog(sessionId).catch(() => ""));
175
+ const completedDeepResearchPlaceholder = metadata.status === "completed" && deepResearchPlaceholderCapture;
176
+ const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
105
177
  metadata.mode === "browser" &&
106
178
  hasFallbackSessionInfo &&
107
- (hasChromeDisconnect || (runtime?.controllerPid && !controllerAlive));
179
+ (hasChromeDisconnect ||
180
+ hasIncompleteCapture ||
181
+ completedDeepResearchPlaceholder ||
182
+ (runtime?.controllerPid && !controllerAlive));
108
183
  if (canReattach) {
109
184
  const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : "unknown port";
110
185
  const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : "url=unknown";
@@ -116,11 +191,9 @@ export async function attachSession(sessionId, options) {
116
191
  }
117
192
  }), { verbose: true }), { promptPreview: metadata.promptPreview });
118
193
  const outputTokens = estimateTokenCount(result.answerMarkdown);
119
- const logWriter = sessionStore.createLogWriter(sessionId);
120
- logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
121
- logWriter.logLine("Answer:");
122
- logWriter.logLine(result.answerMarkdown || result.answerText);
123
- logWriter.stream.end();
194
+ const artifacts = await saveReattachBrowserArtifacts(sessionId, metadata, result);
195
+ await writeReattachAnswer(sessionId, result, completedDeepResearchPlaceholder ||
196
+ (hasIncompleteCapture && deepResearchPlaceholderCapture));
124
197
  if (metadata.model) {
125
198
  await sessionStore.updateModelRun(metadata.id, metadata.model, {
126
199
  status: "completed",
@@ -142,10 +215,12 @@ export async function attachSession(sessionId, options) {
142
215
  reasoningTokens: 0,
143
216
  totalTokens: outputTokens,
144
217
  },
218
+ errorMessage: undefined,
145
219
  browser: {
146
220
  config: metadata.browser?.config,
147
221
  runtime,
148
222
  },
223
+ artifacts,
149
224
  response: { status: "completed" },
150
225
  error: undefined,
151
226
  transport: undefined,
@@ -156,6 +231,28 @@ export async function attachSession(sessionId, options) {
156
231
  catch (error) {
157
232
  const message = error instanceof Error ? error.message : String(error);
158
233
  console.log(chalk.red(`Reattach failed: ${message}`));
234
+ if (completedDeepResearchPlaceholder) {
235
+ if (metadata.model) {
236
+ await sessionStore.updateModelRun(metadata.id, metadata.model, {
237
+ status: "error",
238
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
239
+ error: {
240
+ category: "browser-automation",
241
+ message: `Deep Research capture incomplete: ${message}`,
242
+ },
243
+ });
244
+ }
245
+ await sessionStore.updateSession(sessionId, {
246
+ status: "error",
247
+ errorMessage: `Deep Research capture incomplete: ${message}`,
248
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
249
+ error: {
250
+ category: "browser-automation",
251
+ message: `Deep Research capture incomplete: ${message}`,
252
+ },
253
+ });
254
+ metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
255
+ }
159
256
  }
160
257
  }
161
258
  if (!options?.suppressMetadata) {
@@ -181,6 +278,14 @@ export async function attachSession(sessionId, options) {
181
278
  else if (metadata.model) {
182
279
  console.log(`Model: ${metadata.model}`);
183
280
  }
281
+ if (metadata.artifacts && metadata.artifacts.length > 0) {
282
+ console.log("Artifacts:");
283
+ for (const artifact of metadata.artifacts) {
284
+ const label = artifact.label ?? artifact.kind;
285
+ const size = artifact.sizeBytes ? ` (${formatBytes(artifact.sizeBytes)})` : "";
286
+ console.log(`- ${chalk.cyan(label)} — ${artifact.path}${size}`);
287
+ }
288
+ }
184
289
  const responseSummary = formatResponseMetadata(metadata.response);
185
290
  if (responseSummary) {
186
291
  console.log(dim(`Response: ${responseSummary}`));
@@ -413,6 +518,13 @@ export function trimBeforeFirstAnswer(logText) {
413
518
  if (index === -1) {
414
519
  return logText;
415
520
  }
521
+ const fromFirstAnswer = logText.slice(index);
522
+ if (/^Answer:\s*(called tool|used tool|użyto narzędzia|narzędzie wywołane)\s*\n\[reattach\]/i.test(fromFirstAnswer)) {
523
+ const laterIndex = logText.lastIndexOf(marker);
524
+ if (laterIndex > index) {
525
+ return logText.slice(laterIndex);
526
+ }
527
+ }
416
528
  return logText.slice(index);
417
529
  }
418
530
  function formatRelativeDuration(referenceIso) {
@@ -2,7 +2,7 @@ import kleur from "kleur";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from "../oracle.js";
5
- import { runBrowserSessionExecution, } from "../browser/sessionRunner.js";
5
+ import { ensureSessionArtifacts, runBrowserSessionExecution, } from "../browser/sessionRunner.js";
6
6
  import { renderMarkdownAnsi } from "./markdownRenderer.js";
7
7
  import { formatResponseMetadata, formatTransportMetadata } from "./sessionDisplay.js";
8
8
  import { markErrorLogged } from "./errorUtils.js";
@@ -59,7 +59,12 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
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
70
  status: "completed",
@@ -72,10 +77,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
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,
@@ -242,6 +250,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
242
250
  completedAt: new Date().toISOString(),
243
251
  usage: aggregateUsage,
244
252
  elapsedMs: summary.elapsedMs,
253
+ errorMessage: undefined,
245
254
  response: undefined,
246
255
  transport: undefined,
247
256
  error: undefined,
@@ -300,6 +309,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
300
309
  completedAt: new Date().toISOString(),
301
310
  usage: result.usage,
302
311
  elapsedMs: result.elapsedMs,
312
+ errorMessage: undefined,
303
313
  response: extractResponseMetadata(result.response),
304
314
  transport: undefined,
305
315
  error: undefined,
@@ -358,22 +368,34 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
358
368
  if (assistantTimeout && mode === "browser") {
359
369
  const runtime = userError.details
360
370
  ?.runtime;
361
- log(dim("Assistant response timed out; keeping session running for reattach."));
371
+ log(dim("Assistant response timed out; marking capture incomplete for reattach."));
362
372
  if (modelForStatus) {
363
373
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
364
- status: "running",
365
- 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
+ },
366
382
  });
367
383
  }
368
384
  await sessionStore.updateSession(sessionMeta.id, {
369
- status: "running",
385
+ status: "error",
386
+ completedAt: new Date().toISOString(),
370
387
  errorMessage: message,
371
388
  mode,
372
389
  browser: {
373
390
  config: browserConfig,
374
391
  runtime: runtime ?? sessionMeta.browser?.runtime,
375
392
  },
376
- 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
+ },
377
399
  });
378
400
  const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
379
401
  if (autoReattachIntervalMs > 0) {
@@ -447,6 +469,17 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
447
469
  throw error;
448
470
  }
449
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
+ }
450
483
  function formatError(error) {
451
484
  return error instanceof Error ? error.message : String(error);
452
485
  }
@@ -538,6 +571,15 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
538
571
  });
539
572
  const answerText = result.answerMarkdown || result.answerText || "";
540
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
+ });
541
583
  const logWriter = sessionStore.createLogWriter(sessionMeta.id);
542
584
  logWriter.logLine(`[auto-reattach] captured assistant response on attempt ${attempt}`);
543
585
  logWriter.logLine("Answer:");
@@ -564,10 +606,12 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
564
606
  reasoningTokens: 0,
565
607
  totalTokens: outputTokens,
566
608
  },
609
+ errorMessage: undefined,
567
610
  browser: {
568
611
  config: browserConfig,
569
612
  runtime,
570
613
  },
614
+ artifacts: mergeArtifacts(sessionMeta.artifacts, artifacts),
571
615
  response: { status: "completed" },
572
616
  error: undefined,
573
617
  transport: undefined,
@@ -0,0 +1,19 @@
1
+ const CHATGPT_PRO_HEAVY_MODEL = "gpt-5.5-pro";
2
+ const CHATGPT_PRO_HEAVY_THINKING_TIME = "extended";
3
+ export function applyConsultPreset(input) {
4
+ if (!input.preset) {
5
+ return input;
6
+ }
7
+ if (input.preset === "chatgpt-pro-heavy") {
8
+ if (input.models && input.models.length > 0) {
9
+ throw new Error('MCP consult preset "chatgpt-pro-heavy" cannot be combined with models.');
10
+ }
11
+ return {
12
+ ...input,
13
+ engine: input.engine ?? "browser",
14
+ model: input.model ?? CHATGPT_PRO_HEAVY_MODEL,
15
+ browserThinkingTime: input.browserThinkingTime ?? CHATGPT_PRO_HEAVY_THINKING_TIME,
16
+ };
17
+ }
18
+ return input;
19
+ }
@@ -6,6 +6,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { getCliVersion } from "../version.js";
8
8
  import { registerConsultTool } from "./tools/consult.js";
9
+ import { registerProjectSourcesTool } from "./tools/projectSources.js";
9
10
  import { registerSessionsTool } from "./tools/sessions.js";
10
11
  import { registerSessionResources } from "./tools/sessionResources.js";
11
12
  export async function startMcpServer() {
@@ -18,6 +19,7 @@ export async function startMcpServer() {
18
19
  },
19
20
  });
20
21
  registerConsultTool(server);
22
+ registerProjectSourcesTool(server);
21
23
  registerSessionsTool(server);
22
24
  registerSessionResources(server);
23
25
  const transport = new StdioServerTransport();