@steipete/oracle 0.8.6 → 0.10.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 (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  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/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,12 +1,13 @@
1
- import chalk from 'chalk';
2
- import kleur from 'kleur';
3
- import { renderMarkdownAnsi } from './markdownRenderer.js';
4
- import { formatFinishLine } from '../oracle/finishLine.js';
5
- import { sessionStore, wait } from '../sessionStore.js';
6
- import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
7
- import { resumeBrowserSession } from '../browser/reattach.js';
8
- import { estimateTokenCount } from '../browser/utils.js';
9
- import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost } from './sessionTable.js';
1
+ import chalk from "chalk";
2
+ import kleur from "kleur";
3
+ import { renderMarkdownAnsi } from "./markdownRenderer.js";
4
+ import { formatFinishLine } from "../oracle/finishLine.js";
5
+ import { sessionStore, wait } from "../sessionStore.js";
6
+ import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
7
+ import { resumeBrowserSession } from "../browser/reattach.js";
8
+ import { estimateTokenCount } from "../browser/utils.js";
9
+ import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
10
+ import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
10
11
  const isTty = () => Boolean(process.stdout.isTTY);
11
12
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
12
13
  export const MAX_RENDER_BYTES = 200_000;
@@ -19,10 +20,10 @@ function isProcessAlive(pid) {
19
20
  }
20
21
  catch (error) {
21
22
  const code = error instanceof Error ? error.code : undefined;
22
- if (code === 'ESRCH' || code === 'EINVAL') {
23
+ if (code === "ESRCH" || code === "EINVAL") {
23
24
  return false;
24
25
  }
25
- if (code === 'EPERM') {
26
+ if (code === "EPERM") {
26
27
  return true;
27
28
  }
28
29
  return true;
@@ -31,9 +32,16 @@ function isProcessAlive(pid) {
31
32
  const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
32
33
  export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
33
34
  const metas = await sessionStore.listSessions();
34
- const { entries, truncated, total } = sessionStore.filterSessions(metas, { hours, includeAll, limit });
35
- const filteredEntries = modelFilter ? entries.filter((entry) => matchesModel(entry, modelFilter)) : entries;
35
+ const { entries, truncated, total } = sessionStore.filterSessions(metas, {
36
+ hours,
37
+ includeAll,
38
+ limit,
39
+ });
40
+ const filteredEntries = modelFilter
41
+ ? entries.filter((entry) => matchesModel(entry, modelFilter))
42
+ : entries;
36
43
  const richTty = process.stdout.isTTY && chalk.level > 0;
44
+ const responseOwners = buildResponseOwnerIndex(metas);
37
45
  if (!filteredEntries.length) {
38
46
  console.log(CLEANUP_TIP);
39
47
  if (showExamples) {
@@ -41,10 +49,17 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
41
49
  }
42
50
  return;
43
51
  }
44
- console.log(chalk.bold('Recent Sessions'));
52
+ console.log(chalk.bold("Recent Sessions"));
45
53
  console.log(formatSessionTableHeader(richTty));
46
- for (const entry of filteredEntries) {
47
- console.log(formatSessionTableRow(entry, { rich: richTty }));
54
+ const treeRows = buildStatusTreeRows(filteredEntries, responseOwners);
55
+ for (const row of treeRows) {
56
+ const line = formatSessionTableRow(row.entry, { rich: richTty, displaySlug: row.displaySlug });
57
+ const detachedParent = row.detachedParentLabel != null
58
+ ? richTty
59
+ ? chalk.gray(` <- ${row.detachedParentLabel}`)
60
+ : ` <- ${row.detachedParentLabel}`
61
+ : "";
62
+ console.log(`${line}${detachedParent}`);
48
63
  }
49
64
  if (truncated) {
50
65
  const sessionsDir = sessionStore.sessionsDir();
@@ -61,7 +76,7 @@ export async function attachSession(sessionId, options) {
61
76
  process.exitCode = 1;
62
77
  return;
63
78
  }
64
- if (metadata.mode === 'browser' && metadata.status === 'running' && !metadata.browser?.runtime) {
79
+ if (metadata.mode === "browser" && metadata.status === "running" && !metadata.browser?.runtime) {
65
80
  await wait(250);
66
81
  const refreshed = await sessionStore.readSession(sessionId);
67
82
  if (refreshed) {
@@ -83,16 +98,16 @@ export async function attachSession(sessionId, options) {
83
98
  const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
84
99
  const runtime = metadata.browser?.runtime;
85
100
  const controllerAlive = isProcessAlive(runtime?.controllerPid);
86
- const hasChromeDisconnect = metadata.response?.incompleteReason === 'chrome-disconnected';
87
- const statusAllowsReattach = metadata.status === 'running' || (metadata.status === 'error' && hasChromeDisconnect);
101
+ const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
102
+ const statusAllowsReattach = metadata.status === "running" || (metadata.status === "error" && hasChromeDisconnect);
88
103
  const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
89
104
  const canReattach = statusAllowsReattach &&
90
- metadata.mode === 'browser' &&
105
+ metadata.mode === "browser" &&
91
106
  hasFallbackSessionInfo &&
92
107
  (hasChromeDisconnect || (runtime?.controllerPid && !controllerAlive));
93
108
  if (canReattach) {
94
- const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
95
- const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
109
+ const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : "unknown port";
110
+ const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : "url=unknown";
96
111
  console.log(chalk.yellow(`Attempting to reattach to the existing Chrome session (${portInfo}, ${urlInfo})...`));
97
112
  try {
98
113
  const result = await resumeBrowserSession(runtime, metadata.browser?.config, Object.assign(((message) => {
@@ -102,13 +117,13 @@ export async function attachSession(sessionId, options) {
102
117
  }), { verbose: true }), { promptPreview: metadata.promptPreview });
103
118
  const outputTokens = estimateTokenCount(result.answerMarkdown);
104
119
  const logWriter = sessionStore.createLogWriter(sessionId);
105
- logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
106
- logWriter.logLine('Answer:');
120
+ logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
121
+ logWriter.logLine("Answer:");
107
122
  logWriter.logLine(result.answerMarkdown || result.answerText);
108
123
  logWriter.stream.end();
109
124
  if (metadata.model) {
110
125
  await sessionStore.updateModelRun(metadata.id, metadata.model, {
111
- status: 'completed',
126
+ status: "completed",
112
127
  usage: {
113
128
  inputTokens: 0,
114
129
  outputTokens,
@@ -119,7 +134,7 @@ export async function attachSession(sessionId, options) {
119
134
  });
120
135
  }
121
136
  await sessionStore.updateSession(sessionId, {
122
- status: 'completed',
137
+ status: "completed",
123
138
  completedAt: new Date().toISOString(),
124
139
  usage: {
125
140
  inputTokens: 0,
@@ -131,11 +146,11 @@ export async function attachSession(sessionId, options) {
131
146
  config: metadata.browser?.config,
132
147
  runtime,
133
148
  },
134
- response: { status: 'completed' },
149
+ response: { status: "completed" },
135
150
  error: undefined,
136
151
  transport: undefined,
137
152
  });
138
- console.log(chalk.green('Reattach succeeded; session marked completed.'));
153
+ console.log(chalk.green("Reattach succeeded; session marked completed."));
139
154
  metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
140
155
  }
141
156
  catch (error) {
@@ -148,14 +163,18 @@ export async function attachSession(sessionId, options) {
148
163
  if (reattachLine) {
149
164
  console.log(chalk.blue(reattachLine));
150
165
  }
166
+ const chainLine = await buildSessionChainLine(metadata);
167
+ if (chainLine) {
168
+ console.log(dim(`Chain: ${chainLine}`));
169
+ }
151
170
  console.log(`Created: ${metadata.createdAt}`);
152
171
  console.log(`Status: ${metadata.status}`);
153
172
  if (metadata.models && metadata.models.length > 0) {
154
- console.log('Models:');
173
+ console.log("Models:");
155
174
  for (const run of metadata.models) {
156
175
  const usage = run.usage
157
176
  ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
158
- : '';
177
+ : "";
159
178
  console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
160
179
  }
161
180
  }
@@ -175,19 +194,19 @@ export async function attachSession(sessionId, options) {
175
194
  console.log(dim(`User error: ${userErrorSummary}`));
176
195
  }
177
196
  }
178
- const shouldTrimIntro = initialStatus === 'completed' || initialStatus === 'error';
197
+ const shouldTrimIntro = initialStatus === "completed" || initialStatus === "error";
179
198
  if (options?.renderPrompt !== false) {
180
199
  const prompt = await readStoredPrompt(sessionId);
181
200
  if (prompt) {
182
- console.log(chalk.bold('Prompt:'));
201
+ console.log(chalk.bold("Prompt:"));
183
202
  console.log(renderMarkdownAnsi(prompt));
184
- console.log(dim('---'));
203
+ console.log(dim("---"));
185
204
  }
186
205
  }
187
206
  if (shouldTrimIntro) {
188
207
  const fullLog = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
189
208
  const trimmed = trimBeforeFirstAnswer(fullLog);
190
- const size = Buffer.byteLength(trimmed, 'utf8');
209
+ const size = Buffer.byteLength(trimmed, "utf8");
191
210
  const canRender = wantsRender && isTty() && size <= MAX_RENDER_BYTES;
192
211
  if (wantsRender && size > MAX_RENDER_BYTES) {
193
212
  const msg = `Render skipped (log too large: ${size} bytes > ${MAX_RENDER_BYTES}). Showing raw text.`;
@@ -197,7 +216,7 @@ export async function attachSession(sessionId, options) {
197
216
  }
198
217
  }
199
218
  else if (wantsRender && !isTty()) {
200
- const msg = 'Render requested but stdout is not a TTY; showing raw text.';
219
+ const msg = "Render requested but stdout is not a TTY; showing raw text.";
201
220
  console.log(dim(msg));
202
221
  if (isVerbose) {
203
222
  console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
@@ -219,13 +238,20 @@ export async function attachSession(sessionId, options) {
219
238
  return;
220
239
  }
221
240
  if (wantsRender) {
222
- console.log(dim('Render will apply after completion; streaming raw text meanwhile...'));
241
+ console.log(dim("Render will apply after completion; streaming raw text meanwhile..."));
223
242
  if (isVerbose) {
224
243
  console.log(dim(`Verbose: streaming phase renderMarkdown=true tty=${isTty()}`));
225
244
  }
226
245
  }
227
246
  const liveRenderState = wantsRender && isTty()
228
- ? { pending: '', inFence: false, inTable: false, renderedBytes: 0, fallback: false, noticedFallback: false }
247
+ ? {
248
+ pending: "",
249
+ inFence: false,
250
+ inTable: false,
251
+ renderedBytes: 0,
252
+ fallback: false,
253
+ noticedFallback: false,
254
+ }
229
255
  : null;
230
256
  let lastLength = 0;
231
257
  const renderLiveChunk = (chunk) => {
@@ -241,7 +267,7 @@ export async function attachSession(sessionId, options) {
241
267
  const { chunks, remainder } = extractRenderableChunks(liveRenderState.pending, liveRenderState);
242
268
  liveRenderState.pending = remainder;
243
269
  for (const candidate of chunks) {
244
- const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, 'utf8');
270
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, "utf8");
245
271
  if (projected > MAX_RENDER_BYTES) {
246
272
  if (!liveRenderState.noticedFallback) {
247
273
  console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
@@ -249,11 +275,11 @@ export async function attachSession(sessionId, options) {
249
275
  }
250
276
  liveRenderState.fallback = true;
251
277
  process.stdout.write(candidate + liveRenderState.pending);
252
- liveRenderState.pending = '';
278
+ liveRenderState.pending = "";
253
279
  return;
254
280
  }
255
281
  process.stdout.write(renderMarkdownAnsi(candidate));
256
- liveRenderState.renderedBytes += Buffer.byteLength(candidate, 'utf8');
282
+ liveRenderState.renderedBytes += Buffer.byteLength(candidate, "utf8");
257
283
  }
258
284
  };
259
285
  const flushRemainder = () => {
@@ -264,8 +290,8 @@ export async function attachSession(sessionId, options) {
264
290
  return;
265
291
  }
266
292
  const text = liveRenderState.pending;
267
- liveRenderState.pending = '';
268
- const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, 'utf8');
293
+ liveRenderState.pending = "";
294
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, "utf8");
269
295
  if (projected > MAX_RENDER_BYTES) {
270
296
  if (!liveRenderState.noticedFallback) {
271
297
  console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
@@ -291,15 +317,15 @@ export async function attachSession(sessionId, options) {
291
317
  if (!latest) {
292
318
  break;
293
319
  }
294
- if (latest.status === 'completed' || latest.status === 'error') {
320
+ if (latest.status === "completed" || latest.status === "error") {
295
321
  await printNew();
296
322
  flushRemainder();
297
323
  if (!options?.suppressMetadata) {
298
- if (latest.status === 'error' && latest.errorMessage) {
299
- console.log('\nResult:');
324
+ if (latest.status === "error" && latest.errorMessage) {
325
+ console.log("\nResult:");
300
326
  console.log(`Session failed: ${latest.errorMessage}`);
301
327
  }
302
- if (latest.status === 'completed' && latest.usage) {
328
+ if (latest.status === "completed" && latest.usage) {
303
329
  const summary = formatCompletionSummary(latest, { includeSlug: true });
304
330
  if (summary) {
305
331
  console.log(`\n${chalk.green.bold(summary)}`);
@@ -333,19 +359,19 @@ export function formatResponseMetadata(metadata) {
333
359
  if (metadata.incompleteReason) {
334
360
  parts.push(`incomplete=${metadata.incompleteReason}`);
335
361
  }
336
- return parts.length > 0 ? parts.join(' | ') : null;
362
+ return parts.length > 0 ? parts.join(" | ") : null;
337
363
  }
338
364
  export function formatTransportMetadata(metadata) {
339
365
  if (!metadata?.reason) {
340
366
  return null;
341
367
  }
342
368
  const reasonLabels = {
343
- 'client-timeout': 'client timeout (deadline exceeded)',
344
- 'connection-lost': 'connection lost before completion',
345
- 'client-abort': 'request aborted locally',
346
- unknown: 'unknown transport failure',
369
+ "client-timeout": "client timeout (deadline exceeded)",
370
+ "connection-lost": "connection lost before completion",
371
+ "client-abort": "request aborted locally",
372
+ unknown: "unknown transport failure",
347
373
  };
348
- const label = reasonLabels[metadata.reason] ?? 'transport error';
374
+ const label = reasonLabels[metadata.reason] ?? "transport error";
349
375
  return `${metadata.reason} — ${label}`;
350
376
  }
351
377
  export function formatUserErrorMetadata(metadata) {
@@ -362,7 +388,7 @@ export function formatUserErrorMetadata(metadata) {
362
388
  if (metadata.details && Object.keys(metadata.details).length > 0) {
363
389
  parts.push(`details=${JSON.stringify(metadata.details)}`);
364
390
  }
365
- return parts.length > 0 ? parts.join(' | ') : null;
391
+ return parts.length > 0 ? parts.join(" | ") : null;
366
392
  }
367
393
  export function buildReattachLine(metadata) {
368
394
  if (!metadata.id) {
@@ -376,13 +402,13 @@ export function buildReattachLine(metadata) {
376
402
  if (!elapsedLabel) {
377
403
  return null;
378
404
  }
379
- if (metadata.status === 'running') {
405
+ if (metadata.status === "running") {
380
406
  return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
381
407
  }
382
408
  return null;
383
409
  }
384
410
  export function trimBeforeFirstAnswer(logText) {
385
- const marker = 'Answer:';
411
+ const marker = "Answer:";
386
412
  const index = logText.indexOf(marker);
387
413
  if (index === -1) {
388
414
  return logText;
@@ -414,7 +440,7 @@ function formatRelativeDuration(referenceIso) {
414
440
  if (remainingMinutes > 0) {
415
441
  parts.push(`${remainingMinutes}m`);
416
442
  }
417
- return parts.join(' ');
443
+ return parts.join(" ");
418
444
  }
419
445
  const days = Math.floor(hours / 24);
420
446
  const remainingHours = hours % 24;
@@ -425,17 +451,17 @@ function formatRelativeDuration(referenceIso) {
425
451
  if (remainingMinutes > 0 && days === 0) {
426
452
  parts.push(`${remainingMinutes}m`);
427
453
  }
428
- return parts.join(' ');
454
+ return parts.join(" ");
429
455
  }
430
456
  function printStatusExamples() {
431
- console.log('');
432
- console.log(chalk.bold('Usage Examples'));
433
- console.log(`${chalk.bold(' oracle status --hours 72 --limit 50')}`);
434
- console.log(dim(' Show 72h of history capped at 50 entries.'));
435
- console.log(`${chalk.bold(' oracle status --clear --hours 168')}`);
436
- console.log(dim(' Delete sessions older than 7 days (use --all to wipe everything).'));
437
- console.log(`${chalk.bold(' oracle session <session-id>')}`);
438
- console.log(dim(' Attach to a specific running/completed session to stream its output.'));
457
+ console.log("");
458
+ console.log(chalk.bold("Usage Examples"));
459
+ console.log(`${chalk.bold(" oracle status --hours 72 --limit 50")}`);
460
+ console.log(dim(" Show 72h of history capped at 50 entries."));
461
+ console.log(`${chalk.bold(" oracle status --clear --hours 168")}`);
462
+ console.log(dim(" Delete sessions older than 7 days (use --all to wipe everything)."));
463
+ console.log(`${chalk.bold(" oracle session <session-id>")}`);
464
+ console.log(dim(" Attach to a specific running/completed session to stream its output."));
439
465
  console.log(dim(CLEANUP_TIP));
440
466
  }
441
467
  function matchesModel(entry, filter) {
@@ -443,9 +469,86 @@ function matchesModel(entry, filter) {
443
469
  if (!normalized) {
444
470
  return true;
445
471
  }
446
- const models = entry.models?.map((model) => model.model.toLowerCase()) ?? (entry.model ? [entry.model.toLowerCase()] : []);
472
+ const models = entry.models?.map((model) => model.model.toLowerCase()) ??
473
+ (entry.model ? [entry.model.toLowerCase()] : []);
447
474
  return models.includes(normalized);
448
475
  }
476
+ function buildStatusTreeRows(entries, responseOwners) {
477
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
478
+ const orderIndex = new Map(entries.map((entry, index) => [entry.id, index]));
479
+ const lineageById = new Map();
480
+ const childMap = new Map();
481
+ for (const entry of entries) {
482
+ const lineage = resolveSessionLineage(entry, responseOwners);
483
+ lineageById.set(entry.id, lineage);
484
+ const parentId = lineage?.parentSessionId;
485
+ if (parentId && parentId !== entry.id && entryById.has(parentId)) {
486
+ const siblings = childMap.get(parentId) ?? [];
487
+ siblings.push(entry);
488
+ childMap.set(parentId, siblings);
489
+ }
490
+ }
491
+ for (const siblings of childMap.values()) {
492
+ siblings.sort((a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0));
493
+ }
494
+ const rows = [];
495
+ const visited = new Set();
496
+ const walkChild = (entry, ancestorHasMore, isLast) => {
497
+ if (visited.has(entry.id)) {
498
+ return;
499
+ }
500
+ visited.add(entry.id);
501
+ const children = childMap.get(entry.id) ?? [];
502
+ const nodeBranch = isLast ? "└─ " : "├─ ";
503
+ const prefix = `${ancestorHasMore.map((hasMore) => (hasMore ? "│ " : " ")).join("")}${nodeBranch}`;
504
+ rows.push({ entry, displaySlug: `${prefix}${entry.id}` });
505
+ children.forEach((child, index) => {
506
+ walkChild(child, [...ancestorHasMore, !isLast], index === children.length - 1);
507
+ });
508
+ };
509
+ const walkRoot = (entry) => {
510
+ if (visited.has(entry.id)) {
511
+ return;
512
+ }
513
+ visited.add(entry.id);
514
+ const lineage = lineageById.get(entry.id);
515
+ const hiddenParent = lineage?.parentSessionId && !entryById.has(lineage.parentSessionId)
516
+ ? `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)})`
517
+ : undefined;
518
+ const children = childMap.get(entry.id) ?? [];
519
+ rows.push({ entry, displaySlug: entry.id, detachedParentLabel: hiddenParent });
520
+ children.forEach((child, index) => {
521
+ walkChild(child, [], index === children.length - 1);
522
+ });
523
+ };
524
+ const roots = entries.filter((entry) => {
525
+ const parentId = lineageById.get(entry.id)?.parentSessionId;
526
+ return !(parentId && parentId !== entry.id && entryById.has(parentId));
527
+ });
528
+ roots.forEach((entry) => {
529
+ walkRoot(entry);
530
+ });
531
+ entries.forEach((entry) => {
532
+ walkRoot(entry);
533
+ });
534
+ return rows;
535
+ }
536
+ async function buildSessionChainLine(metadata) {
537
+ const lineageWithoutLookup = resolveSessionLineage(metadata);
538
+ if (!lineageWithoutLookup) {
539
+ return `root -> ${metadata.id}`;
540
+ }
541
+ if (lineageWithoutLookup.parentSessionId) {
542
+ return `${lineageWithoutLookup.parentSessionId} (${abbreviateResponseId(lineageWithoutLookup.parentResponseId)}) -> ${metadata.id}`;
543
+ }
544
+ const sessions = await sessionStore.listSessions().catch(() => []);
545
+ const responseOwners = buildResponseOwnerIndex(sessions);
546
+ const lineage = resolveSessionLineage(metadata, responseOwners) ?? lineageWithoutLookup;
547
+ if (lineage.parentSessionId) {
548
+ return `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)}) -> ${metadata.id}`;
549
+ }
550
+ return `${abbreviateResponseId(lineage.parentResponseId)} -> ${metadata.id}`;
551
+ }
449
552
  async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
450
553
  const normalizedFilter = modelFilter?.trim().toLowerCase();
451
554
  const freshMetadata = (await sessionStore.readSession(sessionId)) ?? fallbackMeta;
@@ -460,12 +563,12 @@ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
460
563
  ? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
461
564
  : models;
462
565
  if (candidates.length === 0) {
463
- return '';
566
+ return "";
464
567
  }
465
568
  const sections = [];
466
569
  let hasContent = false;
467
570
  for (const model of candidates) {
468
- const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? '';
571
+ const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? "";
469
572
  if (body.trim().length > 0) {
470
573
  hasContent = true;
471
574
  }
@@ -475,18 +578,18 @@ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
475
578
  // Fallback for runs that recorded output only in the session log (e.g., browser runs without per-model logs).
476
579
  return await sessionStore.readLog(sessionId);
477
580
  }
478
- return sections.join('\n\n');
581
+ return sections.join("\n\n");
479
582
  }
480
583
  function extractRenderableChunks(text, state) {
481
584
  const chunks = [];
482
- let buffer = '';
585
+ let buffer = "";
483
586
  const lines = text.split(/(\n)/);
484
587
  for (let i = 0; i < lines.length; i += 1) {
485
588
  const segment = lines[i];
486
- if (segment === '\n') {
589
+ if (segment === "\n") {
487
590
  buffer += segment;
488
591
  // Detect code fences
489
- const prev = lines[i - 1] ?? '';
592
+ const prev = lines[i - 1] ?? "";
490
593
  const fenceMatch = prev.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
491
594
  if (!state.inFence && fenceMatch) {
492
595
  state.inFence = true;
@@ -498,17 +601,17 @@ function extractRenderableChunks(text, state) {
498
601
  }
499
602
  const trimmed = prev.trim();
500
603
  if (!state.inFence) {
501
- if (!state.inTable && trimmed.startsWith('|') && trimmed.includes('|')) {
604
+ if (!state.inTable && trimmed.startsWith("|") && trimmed.includes("|")) {
502
605
  state.inTable = true;
503
606
  }
504
- if (state.inTable && trimmed === '') {
607
+ if (state.inTable && trimmed === "") {
505
608
  state.inTable = false;
506
609
  }
507
610
  }
508
- const safeBreak = !state.inFence && !state.inTable && trimmed === '';
611
+ const safeBreak = !state.inFence && !state.inTable && trimmed === "";
509
612
  if (safeBreak) {
510
613
  chunks.push(buffer);
511
- buffer = '';
614
+ buffer = "";
512
615
  }
513
616
  continue;
514
617
  }
@@ -520,7 +623,7 @@ export function formatCompletionSummary(metadata, options = {}) {
520
623
  if (!metadata.usage || metadata.elapsedMs == null) {
521
624
  return null;
522
625
  }
523
- const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
626
+ const modeLabel = metadata.mode === "browser" ? `${metadata.model ?? "n/a"}[browser]` : (metadata.model ?? "n/a");
524
627
  const usage = metadata.usage;
525
628
  const cost = resolveSessionCost(metadata);
526
629
  const tokensDisplay = [
@@ -535,9 +638,9 @@ export function formatCompletionSummary(metadata, options = {}) {
535
638
  reasoning_tokens: usage.reasoningTokens,
536
639
  total_tokens: usage.totalTokens,
537
640
  }, index))
538
- .join('/');
641
+ .join("/");
539
642
  const tokensPart = (() => {
540
- const parts = tokensDisplay.split('/');
643
+ const parts = tokensDisplay.split("/");
541
644
  if (parts.length !== 4)
542
645
  return tokensDisplay;
543
646
  return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
@@ -0,0 +1,60 @@
1
+ function readResponseId(record) {
2
+ if (!record)
3
+ return null;
4
+ const candidate = typeof record.responseId === "string"
5
+ ? record.responseId
6
+ : typeof record.id === "string"
7
+ ? record.id
8
+ : null;
9
+ if (!candidate || !candidate.startsWith("resp_")) {
10
+ return null;
11
+ }
12
+ return candidate;
13
+ }
14
+ export function collectSessionResponseIds(meta) {
15
+ const ids = new Set();
16
+ const rootResponse = readResponseId(meta.response);
17
+ if (rootResponse) {
18
+ ids.add(rootResponse);
19
+ }
20
+ const runs = Array.isArray(meta.models) ? meta.models : [];
21
+ for (const run of runs) {
22
+ const runResponse = readResponseId(run.response);
23
+ if (runResponse) {
24
+ ids.add(runResponse);
25
+ }
26
+ }
27
+ return [...ids];
28
+ }
29
+ export function buildResponseOwnerIndex(sessions) {
30
+ const byResponse = new Map();
31
+ for (const session of sessions) {
32
+ for (const responseId of collectSessionResponseIds(session)) {
33
+ if (!byResponse.has(responseId)) {
34
+ byResponse.set(responseId, session.id);
35
+ }
36
+ }
37
+ }
38
+ return byResponse;
39
+ }
40
+ export function resolveSessionLineage(meta, responseOwners) {
41
+ const previous = meta.options?.previousResponseId?.trim();
42
+ if (!previous) {
43
+ return null;
44
+ }
45
+ let parentSessionId = meta.options?.followupSessionId?.trim();
46
+ if (!parentSessionId && responseOwners) {
47
+ parentSessionId = responseOwners.get(previous);
48
+ }
49
+ if (parentSessionId === meta.id) {
50
+ parentSessionId = undefined;
51
+ }
52
+ return { parentResponseId: previous, parentSessionId };
53
+ }
54
+ export function abbreviateResponseId(responseId, max = 18) {
55
+ if (responseId.length <= max) {
56
+ return responseId;
57
+ }
58
+ const head = Math.max(8, max - 7);
59
+ return `${responseId.slice(0, head)}...${responseId.slice(-4)}`;
60
+ }