@indykish/oracle 0.9.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1252 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +125 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1378 -0
  14. package/dist/scripts/test-browser.js +103 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1067 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
  20. package/dist/src/browser/actions/attachments.js +1910 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +485 -0
  23. package/dist/src/browser/actions/navigation.js +445 -0
  24. package/dist/src/browser/actions/promptComposer.js +485 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +344 -0
  28. package/dist/src/browser/config.js +103 -0
  29. package/dist/src/browser/constants.js +71 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1741 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +43 -0
  37. package/dist/src/browser/profileState.js +280 -0
  38. package/dist/src/browser/prompt.js +152 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/reattach.js +186 -0
  41. package/dist/src/browser/reattachHelpers.js +382 -0
  42. package/dist/src/browser/sessionRunner.js +119 -0
  43. package/dist/src/browser/types.js +1 -0
  44. package/dist/src/browser/utils.js +122 -0
  45. package/dist/src/browserMode.js +1 -0
  46. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  47. package/dist/src/cli/bridge/client.js +73 -0
  48. package/dist/src/cli/bridge/codexConfig.js +43 -0
  49. package/dist/src/cli/bridge/doctor.js +107 -0
  50. package/dist/src/cli/bridge/host.js +259 -0
  51. package/dist/src/cli/browserConfig.js +278 -0
  52. package/dist/src/cli/browserDefaults.js +81 -0
  53. package/dist/src/cli/bundleWarnings.js +9 -0
  54. package/dist/src/cli/clipboard.js +10 -0
  55. package/dist/src/cli/detach.js +11 -0
  56. package/dist/src/cli/dryRun.js +105 -0
  57. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  58. package/dist/src/cli/engine.js +41 -0
  59. package/dist/src/cli/errorUtils.js +9 -0
  60. package/dist/src/cli/format.js +13 -0
  61. package/dist/src/cli/help.js +77 -0
  62. package/dist/src/cli/hiddenAliases.js +22 -0
  63. package/dist/src/cli/markdownBundle.js +17 -0
  64. package/dist/src/cli/markdownRenderer.js +97 -0
  65. package/dist/src/cli/notifier.js +306 -0
  66. package/dist/src/cli/options.js +281 -0
  67. package/dist/src/cli/oscUtils.js +2 -0
  68. package/dist/src/cli/promptRequirement.js +17 -0
  69. package/dist/src/cli/renderFlags.js +9 -0
  70. package/dist/src/cli/renderOutput.js +26 -0
  71. package/dist/src/cli/rootAlias.js +30 -0
  72. package/dist/src/cli/runOptions.js +78 -0
  73. package/dist/src/cli/sessionCommand.js +111 -0
  74. package/dist/src/cli/sessionDisplay.js +567 -0
  75. package/dist/src/cli/sessionRunner.js +602 -0
  76. package/dist/src/cli/sessionTable.js +92 -0
  77. package/dist/src/cli/tagline.js +258 -0
  78. package/dist/src/cli/tui/index.js +486 -0
  79. package/dist/src/cli/writeOutputPath.js +21 -0
  80. package/dist/src/config.js +26 -0
  81. package/dist/src/gemini-web/client.js +328 -0
  82. package/dist/src/gemini-web/executor.js +285 -0
  83. package/dist/src/gemini-web/index.js +1 -0
  84. package/dist/src/gemini-web/types.js +1 -0
  85. package/dist/src/heartbeat.js +43 -0
  86. package/dist/src/mcp/server.js +40 -0
  87. package/dist/src/mcp/tools/consult.js +290 -0
  88. package/dist/src/mcp/tools/sessionResources.js +75 -0
  89. package/dist/src/mcp/tools/sessions.js +105 -0
  90. package/dist/src/mcp/types.js +22 -0
  91. package/dist/src/mcp/utils.js +37 -0
  92. package/dist/src/oracle/background.js +141 -0
  93. package/dist/src/oracle/claude.js +101 -0
  94. package/dist/src/oracle/client.js +197 -0
  95. package/dist/src/oracle/config.js +227 -0
  96. package/dist/src/oracle/errors.js +132 -0
  97. package/dist/src/oracle/files.js +378 -0
  98. package/dist/src/oracle/finishLine.js +32 -0
  99. package/dist/src/oracle/format.js +30 -0
  100. package/dist/src/oracle/fsAdapter.js +10 -0
  101. package/dist/src/oracle/gemini.js +195 -0
  102. package/dist/src/oracle/logging.js +36 -0
  103. package/dist/src/oracle/markdown.js +46 -0
  104. package/dist/src/oracle/modelResolver.js +183 -0
  105. package/dist/src/oracle/multiModelRunner.js +153 -0
  106. package/dist/src/oracle/oscProgress.js +24 -0
  107. package/dist/src/oracle/promptAssembly.js +13 -0
  108. package/dist/src/oracle/request.js +50 -0
  109. package/dist/src/oracle/run.js +596 -0
  110. package/dist/src/oracle/runUtils.js +31 -0
  111. package/dist/src/oracle/tokenEstimate.js +37 -0
  112. package/dist/src/oracle/tokenStats.js +39 -0
  113. package/dist/src/oracle/tokenStringifier.js +24 -0
  114. package/dist/src/oracle/types.js +1 -0
  115. package/dist/src/oracle.js +12 -0
  116. package/dist/src/oracleHome.js +13 -0
  117. package/dist/src/remote/client.js +129 -0
  118. package/dist/src/remote/health.js +113 -0
  119. package/dist/src/remote/remoteServiceConfig.js +31 -0
  120. package/dist/src/remote/server.js +533 -0
  121. package/dist/src/remote/types.js +1 -0
  122. package/dist/src/sessionManager.js +637 -0
  123. package/dist/src/sessionStore.js +56 -0
  124. package/dist/src/version.js +39 -0
  125. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  126. package/dist/vendor/oracle-notifier/README.md +24 -0
  127. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  128. package/package.json +115 -0
  129. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  130. package/vendor/oracle-notifier/README.md +24 -0
  131. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,567 @@
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
+ const isTty = () => Boolean(process.stdout.isTTY);
11
+ const dim = (text) => (isTty() ? kleur.dim(text) : text);
12
+ export const MAX_RENDER_BYTES = 200_000;
13
+ function isProcessAlive(pid) {
14
+ if (!pid)
15
+ return false;
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ }
20
+ catch (error) {
21
+ const code = error instanceof Error ? error.code : undefined;
22
+ if (code === 'ESRCH' || code === 'EINVAL') {
23
+ return false;
24
+ }
25
+ if (code === 'EPERM') {
26
+ return true;
27
+ }
28
+ return true;
29
+ }
30
+ }
31
+ const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
32
+ export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
33
+ 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;
36
+ const richTty = process.stdout.isTTY && chalk.level > 0;
37
+ if (!filteredEntries.length) {
38
+ console.log(CLEANUP_TIP);
39
+ if (showExamples) {
40
+ printStatusExamples();
41
+ }
42
+ return;
43
+ }
44
+ console.log(chalk.bold('Recent Sessions'));
45
+ console.log(formatSessionTableHeader(richTty));
46
+ for (const entry of filteredEntries) {
47
+ console.log(formatSessionTableRow(entry, { rich: richTty }));
48
+ }
49
+ if (truncated) {
50
+ const sessionsDir = sessionStore.sessionsDir();
51
+ console.log(chalk.yellow(`Showing ${entries.length} of ${total} sessions from the requested range. Run "oracle session --clear" or delete entries in ${sessionsDir} to free space, or rerun with --status-limit/--status-all.`));
52
+ }
53
+ if (showExamples) {
54
+ printStatusExamples();
55
+ }
56
+ }
57
+ export async function attachSession(sessionId, options) {
58
+ let metadata = await sessionStore.readSession(sessionId);
59
+ if (!metadata) {
60
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ if (metadata.mode === 'browser' && metadata.status === 'running' && !metadata.browser?.runtime) {
65
+ await wait(250);
66
+ const refreshed = await sessionStore.readSession(sessionId);
67
+ if (refreshed) {
68
+ metadata = refreshed;
69
+ }
70
+ }
71
+ const normalizedModelFilter = options?.model?.trim().toLowerCase();
72
+ if (normalizedModelFilter) {
73
+ const availableModels = metadata.models?.map((model) => model.model.toLowerCase()) ??
74
+ (metadata.model ? [metadata.model.toLowerCase()] : []);
75
+ if (!availableModels.includes(normalizedModelFilter)) {
76
+ console.error(chalk.red(`Model "${options?.model}" not found in session ${sessionId}.`));
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ }
81
+ const initialStatus = metadata.status;
82
+ const wantsRender = Boolean(options?.renderMarkdown);
83
+ const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
84
+ const runtime = metadata.browser?.runtime;
85
+ const controllerAlive = isProcessAlive(runtime?.controllerPid);
86
+ const hasChromeDisconnect = metadata.response?.incompleteReason === 'chrome-disconnected';
87
+ const statusAllowsReattach = metadata.status === 'running' || (metadata.status === 'error' && hasChromeDisconnect);
88
+ const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
89
+ const canReattach = statusAllowsReattach &&
90
+ metadata.mode === 'browser' &&
91
+ hasFallbackSessionInfo &&
92
+ (hasChromeDisconnect || (runtime?.controllerPid && !controllerAlive));
93
+ if (canReattach) {
94
+ const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
95
+ const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
96
+ console.log(chalk.yellow(`Attempting to reattach to the existing Chrome session (${portInfo}, ${urlInfo})...`));
97
+ try {
98
+ const result = await resumeBrowserSession(runtime, metadata.browser?.config, Object.assign(((message) => {
99
+ if (message) {
100
+ console.log(dim(message));
101
+ }
102
+ }), { verbose: true }), { promptPreview: metadata.promptPreview });
103
+ const outputTokens = estimateTokenCount(result.answerMarkdown);
104
+ const logWriter = sessionStore.createLogWriter(sessionId);
105
+ logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
106
+ logWriter.logLine('Answer:');
107
+ logWriter.logLine(result.answerMarkdown || result.answerText);
108
+ logWriter.stream.end();
109
+ if (metadata.model) {
110
+ await sessionStore.updateModelRun(metadata.id, metadata.model, {
111
+ status: 'completed',
112
+ usage: {
113
+ inputTokens: 0,
114
+ outputTokens,
115
+ reasoningTokens: 0,
116
+ totalTokens: outputTokens,
117
+ },
118
+ completedAt: new Date().toISOString(),
119
+ });
120
+ }
121
+ await sessionStore.updateSession(sessionId, {
122
+ status: 'completed',
123
+ completedAt: new Date().toISOString(),
124
+ usage: {
125
+ inputTokens: 0,
126
+ outputTokens,
127
+ reasoningTokens: 0,
128
+ totalTokens: outputTokens,
129
+ },
130
+ browser: {
131
+ config: metadata.browser?.config,
132
+ runtime,
133
+ },
134
+ response: { status: 'completed' },
135
+ error: undefined,
136
+ transport: undefined,
137
+ });
138
+ console.log(chalk.green('Reattach succeeded; session marked completed.'));
139
+ metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ console.log(chalk.red(`Reattach failed: ${message}`));
144
+ }
145
+ }
146
+ if (!options?.suppressMetadata) {
147
+ const reattachLine = buildReattachLine(metadata);
148
+ if (reattachLine) {
149
+ console.log(chalk.blue(reattachLine));
150
+ }
151
+ console.log(`Created: ${metadata.createdAt}`);
152
+ console.log(`Status: ${metadata.status}`);
153
+ if (metadata.models && metadata.models.length > 0) {
154
+ console.log('Models:');
155
+ for (const run of metadata.models) {
156
+ const usage = run.usage
157
+ ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
158
+ : '';
159
+ console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
160
+ }
161
+ }
162
+ else if (metadata.model) {
163
+ console.log(`Model: ${metadata.model}`);
164
+ }
165
+ const responseSummary = formatResponseMetadata(metadata.response);
166
+ if (responseSummary) {
167
+ console.log(dim(`Response: ${responseSummary}`));
168
+ }
169
+ const transportSummary = formatTransportMetadata(metadata.transport);
170
+ if (transportSummary) {
171
+ console.log(dim(`Transport: ${transportSummary}`));
172
+ }
173
+ const userErrorSummary = formatUserErrorMetadata(metadata.error);
174
+ if (userErrorSummary) {
175
+ console.log(dim(`User error: ${userErrorSummary}`));
176
+ }
177
+ }
178
+ const shouldTrimIntro = initialStatus === 'completed' || initialStatus === 'error';
179
+ if (options?.renderPrompt !== false) {
180
+ const prompt = await readStoredPrompt(sessionId);
181
+ if (prompt) {
182
+ console.log(chalk.bold('Prompt:'));
183
+ console.log(renderMarkdownAnsi(prompt));
184
+ console.log(dim('---'));
185
+ }
186
+ }
187
+ if (shouldTrimIntro) {
188
+ const fullLog = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
189
+ const trimmed = trimBeforeFirstAnswer(fullLog);
190
+ const size = Buffer.byteLength(trimmed, 'utf8');
191
+ const canRender = wantsRender && isTty() && size <= MAX_RENDER_BYTES;
192
+ if (wantsRender && size > MAX_RENDER_BYTES) {
193
+ const msg = `Render skipped (log too large: ${size} bytes > ${MAX_RENDER_BYTES}). Showing raw text.`;
194
+ console.log(dim(msg));
195
+ if (isVerbose) {
196
+ console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
197
+ }
198
+ }
199
+ else if (wantsRender && !isTty()) {
200
+ const msg = 'Render requested but stdout is not a TTY; showing raw text.';
201
+ console.log(dim(msg));
202
+ if (isVerbose) {
203
+ console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
204
+ }
205
+ }
206
+ if (canRender) {
207
+ if (isVerbose) {
208
+ console.log(dim(`Verbose: rendering markdown (size=${size}, tty=${isTty()})`));
209
+ }
210
+ process.stdout.write(renderMarkdownAnsi(trimmed));
211
+ }
212
+ else {
213
+ process.stdout.write(trimmed);
214
+ }
215
+ const summary = formatCompletionSummary(metadata, { includeSlug: true });
216
+ if (summary) {
217
+ console.log(`\n${chalk.green.bold(summary)}`);
218
+ }
219
+ return;
220
+ }
221
+ if (wantsRender) {
222
+ console.log(dim('Render will apply after completion; streaming raw text meanwhile...'));
223
+ if (isVerbose) {
224
+ console.log(dim(`Verbose: streaming phase renderMarkdown=true tty=${isTty()}`));
225
+ }
226
+ }
227
+ const liveRenderState = wantsRender && isTty()
228
+ ? { pending: '', inFence: false, inTable: false, renderedBytes: 0, fallback: false, noticedFallback: false }
229
+ : null;
230
+ let lastLength = 0;
231
+ const renderLiveChunk = (chunk) => {
232
+ if (!liveRenderState || chunk.length === 0) {
233
+ process.stdout.write(chunk);
234
+ return;
235
+ }
236
+ if (liveRenderState.fallback) {
237
+ process.stdout.write(chunk);
238
+ return;
239
+ }
240
+ liveRenderState.pending += chunk;
241
+ const { chunks, remainder } = extractRenderableChunks(liveRenderState.pending, liveRenderState);
242
+ liveRenderState.pending = remainder;
243
+ for (const candidate of chunks) {
244
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, 'utf8');
245
+ if (projected > MAX_RENDER_BYTES) {
246
+ if (!liveRenderState.noticedFallback) {
247
+ console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
248
+ liveRenderState.noticedFallback = true;
249
+ }
250
+ liveRenderState.fallback = true;
251
+ process.stdout.write(candidate + liveRenderState.pending);
252
+ liveRenderState.pending = '';
253
+ return;
254
+ }
255
+ process.stdout.write(renderMarkdownAnsi(candidate));
256
+ liveRenderState.renderedBytes += Buffer.byteLength(candidate, 'utf8');
257
+ }
258
+ };
259
+ const flushRemainder = () => {
260
+ if (!liveRenderState || liveRenderState.fallback) {
261
+ return;
262
+ }
263
+ if (liveRenderState.pending.length === 0) {
264
+ return;
265
+ }
266
+ const text = liveRenderState.pending;
267
+ liveRenderState.pending = '';
268
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, 'utf8');
269
+ if (projected > MAX_RENDER_BYTES) {
270
+ if (!liveRenderState.noticedFallback) {
271
+ console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
272
+ }
273
+ process.stdout.write(text);
274
+ liveRenderState.fallback = true;
275
+ return;
276
+ }
277
+ process.stdout.write(renderMarkdownAnsi(text));
278
+ };
279
+ const printNew = async () => {
280
+ const text = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
281
+ const nextChunk = text.slice(lastLength);
282
+ if (nextChunk.length > 0) {
283
+ renderLiveChunk(nextChunk);
284
+ lastLength = text.length;
285
+ }
286
+ };
287
+ await printNew();
288
+ // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate infinite poll
289
+ while (true) {
290
+ const latest = await sessionStore.readSession(sessionId);
291
+ if (!latest) {
292
+ break;
293
+ }
294
+ if (latest.status === 'completed' || latest.status === 'error') {
295
+ await printNew();
296
+ flushRemainder();
297
+ if (!options?.suppressMetadata) {
298
+ if (latest.status === 'error' && latest.errorMessage) {
299
+ console.log('\nResult:');
300
+ console.log(`Session failed: ${latest.errorMessage}`);
301
+ }
302
+ if (latest.status === 'completed' && latest.usage) {
303
+ const summary = formatCompletionSummary(latest, { includeSlug: true });
304
+ if (summary) {
305
+ console.log(`\n${chalk.green.bold(summary)}`);
306
+ }
307
+ else {
308
+ const usage = latest.usage;
309
+ console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
310
+ }
311
+ }
312
+ }
313
+ break;
314
+ }
315
+ await wait(1000);
316
+ await printNew();
317
+ }
318
+ }
319
+ export function formatResponseMetadata(metadata) {
320
+ if (!metadata) {
321
+ return null;
322
+ }
323
+ const parts = [];
324
+ if (metadata.responseId) {
325
+ parts.push(`response=${metadata.responseId}`);
326
+ }
327
+ if (metadata.requestId) {
328
+ parts.push(`request=${metadata.requestId}`);
329
+ }
330
+ if (metadata.status) {
331
+ parts.push(`status=${metadata.status}`);
332
+ }
333
+ if (metadata.incompleteReason) {
334
+ parts.push(`incomplete=${metadata.incompleteReason}`);
335
+ }
336
+ return parts.length > 0 ? parts.join(' | ') : null;
337
+ }
338
+ export function formatTransportMetadata(metadata) {
339
+ if (!metadata?.reason) {
340
+ return null;
341
+ }
342
+ 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',
347
+ };
348
+ const label = reasonLabels[metadata.reason] ?? 'transport error';
349
+ return `${metadata.reason} — ${label}`;
350
+ }
351
+ export function formatUserErrorMetadata(metadata) {
352
+ if (!metadata) {
353
+ return null;
354
+ }
355
+ const parts = [];
356
+ if (metadata.category) {
357
+ parts.push(metadata.category);
358
+ }
359
+ if (metadata.message) {
360
+ parts.push(`message=${metadata.message}`);
361
+ }
362
+ if (metadata.details && Object.keys(metadata.details).length > 0) {
363
+ parts.push(`details=${JSON.stringify(metadata.details)}`);
364
+ }
365
+ return parts.length > 0 ? parts.join(' | ') : null;
366
+ }
367
+ export function buildReattachLine(metadata) {
368
+ if (!metadata.id) {
369
+ return null;
370
+ }
371
+ const referenceTime = metadata.startedAt ?? metadata.createdAt;
372
+ if (!referenceTime) {
373
+ return null;
374
+ }
375
+ const elapsedLabel = formatRelativeDuration(referenceTime);
376
+ if (!elapsedLabel) {
377
+ return null;
378
+ }
379
+ if (metadata.status === 'running') {
380
+ return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
381
+ }
382
+ return null;
383
+ }
384
+ export function trimBeforeFirstAnswer(logText) {
385
+ const marker = 'Answer:';
386
+ const index = logText.indexOf(marker);
387
+ if (index === -1) {
388
+ return logText;
389
+ }
390
+ return logText.slice(index);
391
+ }
392
+ function formatRelativeDuration(referenceIso) {
393
+ const timestamp = Date.parse(referenceIso);
394
+ if (Number.isNaN(timestamp)) {
395
+ return null;
396
+ }
397
+ const diffMs = Date.now() - timestamp;
398
+ if (diffMs < 0) {
399
+ return null;
400
+ }
401
+ const seconds = Math.max(1, Math.round(diffMs / 1000));
402
+ if (seconds < 60) {
403
+ return `${seconds}s`;
404
+ }
405
+ const minutes = Math.floor(seconds / 60);
406
+ const remainingSeconds = seconds % 60;
407
+ if (minutes < 60) {
408
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
409
+ }
410
+ const hours = Math.floor(minutes / 60);
411
+ const remainingMinutes = minutes % 60;
412
+ if (hours < 24) {
413
+ const parts = [`${hours}h`];
414
+ if (remainingMinutes > 0) {
415
+ parts.push(`${remainingMinutes}m`);
416
+ }
417
+ return parts.join(' ');
418
+ }
419
+ const days = Math.floor(hours / 24);
420
+ const remainingHours = hours % 24;
421
+ const parts = [`${days}d`];
422
+ if (remainingHours > 0) {
423
+ parts.push(`${remainingHours}h`);
424
+ }
425
+ if (remainingMinutes > 0 && days === 0) {
426
+ parts.push(`${remainingMinutes}m`);
427
+ }
428
+ return parts.join(' ');
429
+ }
430
+ 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.'));
439
+ console.log(dim(CLEANUP_TIP));
440
+ }
441
+ function matchesModel(entry, filter) {
442
+ const normalized = filter.trim().toLowerCase();
443
+ if (!normalized) {
444
+ return true;
445
+ }
446
+ const models = entry.models?.map((model) => model.model.toLowerCase()) ?? (entry.model ? [entry.model.toLowerCase()] : []);
447
+ return models.includes(normalized);
448
+ }
449
+ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
450
+ const normalizedFilter = modelFilter?.trim().toLowerCase();
451
+ const freshMetadata = (await sessionStore.readSession(sessionId)) ?? fallbackMeta;
452
+ const models = freshMetadata.models ?? fallbackMeta.models ?? [];
453
+ if (models.length === 0) {
454
+ if (normalizedFilter) {
455
+ return await sessionStore.readModelLog(sessionId, modelFilter);
456
+ }
457
+ return await sessionStore.readLog(sessionId);
458
+ }
459
+ const candidates = normalizedFilter
460
+ ? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
461
+ : models;
462
+ if (candidates.length === 0) {
463
+ return '';
464
+ }
465
+ const sections = [];
466
+ let hasContent = false;
467
+ for (const model of candidates) {
468
+ const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? '';
469
+ if (body.trim().length > 0) {
470
+ hasContent = true;
471
+ }
472
+ sections.push(`=== ${model.model} ===\n${body}`.trimEnd());
473
+ }
474
+ if (!hasContent) {
475
+ // Fallback for runs that recorded output only in the session log (e.g., browser runs without per-model logs).
476
+ return await sessionStore.readLog(sessionId);
477
+ }
478
+ return sections.join('\n\n');
479
+ }
480
+ function extractRenderableChunks(text, state) {
481
+ const chunks = [];
482
+ let buffer = '';
483
+ const lines = text.split(/(\n)/);
484
+ for (let i = 0; i < lines.length; i += 1) {
485
+ const segment = lines[i];
486
+ if (segment === '\n') {
487
+ buffer += segment;
488
+ // Detect code fences
489
+ const prev = lines[i - 1] ?? '';
490
+ const fenceMatch = prev.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
491
+ if (!state.inFence && fenceMatch) {
492
+ state.inFence = true;
493
+ state.fenceDelimiter = fenceMatch[2];
494
+ }
495
+ else if (state.inFence && state.fenceDelimiter && prev.startsWith(state.fenceDelimiter)) {
496
+ state.inFence = false;
497
+ state.fenceDelimiter = undefined;
498
+ }
499
+ const trimmed = prev.trim();
500
+ if (!state.inFence) {
501
+ if (!state.inTable && trimmed.startsWith('|') && trimmed.includes('|')) {
502
+ state.inTable = true;
503
+ }
504
+ if (state.inTable && trimmed === '') {
505
+ state.inTable = false;
506
+ }
507
+ }
508
+ const safeBreak = !state.inFence && !state.inTable && trimmed === '';
509
+ if (safeBreak) {
510
+ chunks.push(buffer);
511
+ buffer = '';
512
+ }
513
+ continue;
514
+ }
515
+ buffer += segment;
516
+ }
517
+ return { chunks, remainder: buffer };
518
+ }
519
+ export function formatCompletionSummary(metadata, options = {}) {
520
+ if (!metadata.usage || metadata.elapsedMs == null) {
521
+ return null;
522
+ }
523
+ const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
524
+ const usage = metadata.usage;
525
+ const cost = resolveSessionCost(metadata);
526
+ const tokensDisplay = [
527
+ usage.inputTokens ?? 0,
528
+ usage.outputTokens ?? 0,
529
+ usage.reasoningTokens ?? 0,
530
+ usage.totalTokens ?? 0,
531
+ ]
532
+ .map((value, index) => formatTokenValue(value, {
533
+ input_tokens: usage.inputTokens,
534
+ output_tokens: usage.outputTokens,
535
+ reasoning_tokens: usage.reasoningTokens,
536
+ total_tokens: usage.totalTokens,
537
+ }, index))
538
+ .join('/');
539
+ const tokensPart = (() => {
540
+ const parts = tokensDisplay.split('/');
541
+ if (parts.length !== 4)
542
+ return tokensDisplay;
543
+ return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
544
+ })();
545
+ const filesCount = metadata.options?.file?.length ?? 0;
546
+ const filesPart = filesCount > 0 ? `files=${filesCount}` : null;
547
+ const slugPart = options.includeSlug ? `slug=${metadata.id}` : null;
548
+ const { line1, line2 } = formatFinishLine({
549
+ elapsedMs: metadata.elapsedMs,
550
+ model: modeLabel,
551
+ costUsd: cost ?? null,
552
+ tokensPart,
553
+ detailParts: [filesPart, slugPart],
554
+ });
555
+ return line2 ? `${line1} | ${line2}` : line1;
556
+ }
557
+ async function readStoredPrompt(sessionId) {
558
+ const request = await sessionStore.readRequest(sessionId);
559
+ if (request?.prompt && request.prompt.trim().length > 0) {
560
+ return request.prompt;
561
+ }
562
+ const meta = await sessionStore.readSession(sessionId);
563
+ if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
564
+ return meta.options.prompt;
565
+ }
566
+ return null;
567
+ }