@steipete/oracle 0.4.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,111 @@
1
+ import chalk from 'chalk';
2
+ import { usesDefaultStatusFilters } from './options.js';
3
+ import { attachSession, showStatus } from './sessionDisplay.js';
4
+ import { sessionStore } from '../sessionStore.js';
5
+ const defaultDependencies = {
6
+ showStatus,
7
+ attachSession,
8
+ usesDefaultStatusFilters,
9
+ deleteSessionsOlderThan: (options) => sessionStore.deleteOlderThan(options),
10
+ getSessionPaths: (sessionId) => sessionStore.getPaths(sessionId),
11
+ };
12
+ const SESSION_OPTION_KEYS = new Set(['hours', 'limit', 'all', 'clear', 'clean', 'render', 'renderMarkdown', 'path', 'model']);
13
+ export async function handleSessionCommand(sessionId, command, deps = defaultDependencies) {
14
+ const sessionOptions = command.opts();
15
+ if (sessionOptions.verboseRender) {
16
+ process.env.ORACLE_VERBOSE_RENDER = '1';
17
+ }
18
+ const renderSource = command.getOptionValueSource?.('render');
19
+ const renderMarkdownSource = command.getOptionValueSource?.('renderMarkdown');
20
+ const renderExplicit = renderSource === 'cli' || renderMarkdownSource === 'cli';
21
+ const autoRender = !renderExplicit && process.stdout.isTTY;
22
+ const pathRequested = Boolean(sessionOptions.path);
23
+ const clearRequested = Boolean(sessionOptions.clear || sessionOptions.clean);
24
+ if (clearRequested) {
25
+ if (sessionId) {
26
+ console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ const hours = sessionOptions.hours;
31
+ const includeAll = sessionOptions.all;
32
+ const result = await deps.deleteSessionsOlderThan({ hours, includeAll });
33
+ const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
34
+ console.log(formatSessionCleanupMessage(result, scope));
35
+ return;
36
+ }
37
+ if (sessionId === 'clear' || sessionId === 'clean') {
38
+ console.error('Session cleanup now uses --clear. Run "oracle session --clear --hours <n>" instead.');
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ if (pathRequested) {
43
+ if (!sessionId) {
44
+ console.error('The --path flag requires a session ID.');
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ try {
49
+ const paths = await deps.getSessionPaths(sessionId);
50
+ const richTty = Boolean(process.stdout.isTTY && chalk.level > 0);
51
+ const label = (text) => (richTty ? chalk.cyan(text) : text);
52
+ const value = (text) => (richTty ? chalk.dim(text) : text);
53
+ console.log(`${label('Session dir:')} ${value(paths.dir)}`);
54
+ console.log(`${label('Metadata:')} ${value(paths.metadata)}`);
55
+ console.log(`${label('Request:')} ${value(paths.request)}`);
56
+ console.log(`${label('Log:')} ${value(paths.log)}`);
57
+ }
58
+ catch (error) {
59
+ console.error(error instanceof Error ? error.message : String(error));
60
+ process.exitCode = 1;
61
+ }
62
+ return;
63
+ }
64
+ if (!sessionId) {
65
+ const showExamples = deps.usesDefaultStatusFilters(command);
66
+ await deps.showStatus({
67
+ hours: sessionOptions.all ? Infinity : sessionOptions.hours,
68
+ includeAll: sessionOptions.all,
69
+ limit: sessionOptions.limit,
70
+ showExamples,
71
+ modelFilter: sessionOptions.model,
72
+ });
73
+ return;
74
+ }
75
+ // Surface any root-level flags that were provided but are ignored when attaching to a session.
76
+ const ignoredFlags = listIgnoredFlags(command);
77
+ if (ignoredFlags.length > 0) {
78
+ console.log(`Ignoring flags on session attach: ${ignoredFlags.join(', ')}`);
79
+ }
80
+ const renderMarkdown = Boolean(sessionOptions.render || sessionOptions.renderMarkdown || autoRender);
81
+ await deps.attachSession(sessionId, {
82
+ renderMarkdown,
83
+ renderPrompt: !sessionOptions.hidePrompt,
84
+ model: sessionOptions.model,
85
+ });
86
+ }
87
+ export function formatSessionCleanupMessage(result, scope) {
88
+ const deletedLabel = `${result.deleted} ${result.deleted === 1 ? 'session' : 'sessions'}`;
89
+ const remainingLabel = `${result.remaining} ${result.remaining === 1 ? 'session' : 'sessions'} remain`;
90
+ const hint = 'Run "oracle session --clear --all" to delete everything.';
91
+ return `Deleted ${deletedLabel} (${scope}). ${remainingLabel}.\n${hint}`;
92
+ }
93
+ function listIgnoredFlags(command) {
94
+ const opts = command.optsWithGlobals();
95
+ const ignored = [];
96
+ for (const key of Object.keys(opts)) {
97
+ if (SESSION_OPTION_KEYS.has(key)) {
98
+ continue;
99
+ }
100
+ const source = command.getOptionValueSource?.(key);
101
+ if (source !== 'cli' && source !== 'env') {
102
+ continue;
103
+ }
104
+ const value = opts[key];
105
+ if (value === undefined || value === false || value === null) {
106
+ continue;
107
+ }
108
+ ignored.push(key);
109
+ }
110
+ return ignored;
111
+ }
@@ -0,0 +1,540 @@
1
+ import chalk from 'chalk';
2
+ import kleur from 'kleur';
3
+ import { renderMarkdownAnsi } from './markdownRenderer.js';
4
+ import { formatElapsed, formatUSD } from '../oracle/format.js';
5
+ import { MODEL_CONFIGS } from '../oracle.js';
6
+ import { sessionStore, wait } from '../sessionStore.js';
7
+ const isTty = () => Boolean(process.stdout.isTTY);
8
+ const dim = (text) => (isTty() ? kleur.dim(text) : text);
9
+ export const MAX_RENDER_BYTES = 200_000;
10
+ const MODEL_COLUMN_WIDTH = 18;
11
+ const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
12
+ export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
13
+ const metas = await sessionStore.listSessions();
14
+ const { entries, truncated, total } = sessionStore.filterSessions(metas, { hours, includeAll, limit });
15
+ const filteredEntries = modelFilter ? entries.filter((entry) => matchesModel(entry, modelFilter)) : entries;
16
+ const richTty = process.stdout.isTTY && chalk.level > 0;
17
+ if (!filteredEntries.length) {
18
+ console.log(CLEANUP_TIP);
19
+ if (showExamples) {
20
+ printStatusExamples();
21
+ }
22
+ return;
23
+ }
24
+ console.log(chalk.bold('Recent Sessions'));
25
+ console.log(chalk.dim('Timestamp Chars Cost Status Models ID'));
26
+ for (const entry of filteredEntries) {
27
+ const statusRaw = (entry.status || 'unknown').padEnd(9);
28
+ const status = richTty ? colorStatus(entry.status ?? 'unknown', statusRaw) : statusRaw;
29
+ const modelColumn = formatModelColumn(entry, MODEL_COLUMN_WIDTH, richTty);
30
+ const created = formatTimestamp(entry.createdAt);
31
+ const chars = entry.options?.prompt?.length ?? entry.promptPreview?.length ?? 0;
32
+ const charLabel = chars > 0 ? String(chars).padStart(5) : ' -';
33
+ const costValue = resolveCost(entry);
34
+ const costLabel = costValue != null ? formatCostTable(costValue) : ' -';
35
+ console.log(`${created} | ${charLabel} | ${costLabel} | ${status} | ${modelColumn} | ${entry.id}`);
36
+ }
37
+ if (truncated) {
38
+ const sessionsDir = sessionStore.sessionsDir();
39
+ 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.`));
40
+ }
41
+ if (showExamples) {
42
+ printStatusExamples();
43
+ }
44
+ }
45
+ function colorStatus(status, padded) {
46
+ switch (status) {
47
+ case 'completed':
48
+ return chalk.green(padded);
49
+ case 'error':
50
+ return chalk.red(padded);
51
+ case 'running':
52
+ return chalk.yellow(padded);
53
+ default:
54
+ return padded;
55
+ }
56
+ }
57
+ export async function attachSession(sessionId, options) {
58
+ const 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
+ const normalizedModelFilter = options?.model?.trim().toLowerCase();
65
+ if (normalizedModelFilter) {
66
+ const availableModels = metadata.models?.map((model) => model.model.toLowerCase()) ??
67
+ (metadata.model ? [metadata.model.toLowerCase()] : []);
68
+ if (!availableModels.includes(normalizedModelFilter)) {
69
+ console.error(chalk.red(`Model "${options?.model}" not found in session ${sessionId}.`));
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ }
74
+ const initialStatus = metadata.status;
75
+ const wantsRender = Boolean(options?.renderMarkdown);
76
+ const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
77
+ if (!options?.suppressMetadata) {
78
+ const reattachLine = buildReattachLine(metadata);
79
+ if (reattachLine) {
80
+ console.log(chalk.blue(reattachLine));
81
+ }
82
+ console.log(`Created: ${metadata.createdAt}`);
83
+ console.log(`Status: ${metadata.status}`);
84
+ if (metadata.models && metadata.models.length > 0) {
85
+ console.log('Models:');
86
+ for (const run of metadata.models) {
87
+ const usage = run.usage
88
+ ? ` tok=${run.usage.outputTokens?.toLocaleString() ?? 0}/${run.usage.totalTokens?.toLocaleString() ?? 0}`
89
+ : '';
90
+ console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
91
+ }
92
+ }
93
+ else if (metadata.model) {
94
+ console.log(`Model: ${metadata.model}`);
95
+ }
96
+ const responseSummary = formatResponseMetadata(metadata.response);
97
+ if (responseSummary) {
98
+ console.log(dim(`Response: ${responseSummary}`));
99
+ }
100
+ const transportSummary = formatTransportMetadata(metadata.transport);
101
+ if (transportSummary) {
102
+ console.log(dim(`Transport: ${transportSummary}`));
103
+ }
104
+ const userErrorSummary = formatUserErrorMetadata(metadata.error);
105
+ if (userErrorSummary) {
106
+ console.log(dim(`User error: ${userErrorSummary}`));
107
+ }
108
+ }
109
+ const shouldTrimIntro = initialStatus === 'completed' || initialStatus === 'error';
110
+ if (options?.renderPrompt !== false) {
111
+ const prompt = await readStoredPrompt(sessionId);
112
+ if (prompt) {
113
+ console.log(chalk.bold('Prompt:'));
114
+ console.log(renderMarkdownAnsi(prompt));
115
+ console.log(dim('---'));
116
+ }
117
+ }
118
+ if (shouldTrimIntro) {
119
+ const fullLog = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
120
+ const trimmed = trimBeforeFirstAnswer(fullLog);
121
+ const size = Buffer.byteLength(trimmed, 'utf8');
122
+ const canRender = wantsRender && isTty() && size <= MAX_RENDER_BYTES;
123
+ if (wantsRender && size > MAX_RENDER_BYTES) {
124
+ const msg = `Render skipped (log too large: ${size} bytes > ${MAX_RENDER_BYTES}). Showing raw text.`;
125
+ console.log(dim(msg));
126
+ if (isVerbose) {
127
+ console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
128
+ }
129
+ }
130
+ else if (wantsRender && !isTty()) {
131
+ const msg = 'Render requested but stdout is not a TTY; showing raw text.';
132
+ console.log(dim(msg));
133
+ if (isVerbose) {
134
+ console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
135
+ }
136
+ }
137
+ if (canRender) {
138
+ if (isVerbose) {
139
+ console.log(dim(`Verbose: rendering markdown (size=${size}, tty=${isTty()})`));
140
+ }
141
+ process.stdout.write(renderMarkdownAnsi(trimmed));
142
+ }
143
+ else {
144
+ process.stdout.write(trimmed);
145
+ }
146
+ const summary = formatCompletionSummary(metadata, { includeSlug: true });
147
+ if (summary) {
148
+ console.log(`\n${chalk.green.bold(summary)}`);
149
+ }
150
+ return;
151
+ }
152
+ if (wantsRender) {
153
+ console.log(dim('Render will apply after completion; streaming raw text meanwhile...'));
154
+ if (isVerbose) {
155
+ console.log(dim(`Verbose: streaming phase renderMarkdown=true tty=${isTty()}`));
156
+ }
157
+ }
158
+ const liveRenderState = wantsRender && isTty()
159
+ ? { pending: '', inFence: false, inTable: false, renderedBytes: 0, fallback: false, noticedFallback: false }
160
+ : null;
161
+ let lastLength = 0;
162
+ const renderLiveChunk = (chunk) => {
163
+ if (!liveRenderState || chunk.length === 0) {
164
+ process.stdout.write(chunk);
165
+ return;
166
+ }
167
+ if (liveRenderState.fallback) {
168
+ process.stdout.write(chunk);
169
+ return;
170
+ }
171
+ liveRenderState.pending += chunk;
172
+ const { chunks, remainder } = extractRenderableChunks(liveRenderState.pending, liveRenderState);
173
+ liveRenderState.pending = remainder;
174
+ for (const candidate of chunks) {
175
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, 'utf8');
176
+ if (projected > MAX_RENDER_BYTES) {
177
+ if (!liveRenderState.noticedFallback) {
178
+ console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
179
+ liveRenderState.noticedFallback = true;
180
+ }
181
+ liveRenderState.fallback = true;
182
+ process.stdout.write(candidate + liveRenderState.pending);
183
+ liveRenderState.pending = '';
184
+ return;
185
+ }
186
+ process.stdout.write(renderMarkdownAnsi(candidate));
187
+ liveRenderState.renderedBytes += Buffer.byteLength(candidate, 'utf8');
188
+ }
189
+ };
190
+ const flushRemainder = () => {
191
+ if (!liveRenderState || liveRenderState.fallback) {
192
+ return;
193
+ }
194
+ if (liveRenderState.pending.length === 0) {
195
+ return;
196
+ }
197
+ const text = liveRenderState.pending;
198
+ liveRenderState.pending = '';
199
+ const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, 'utf8');
200
+ if (projected > MAX_RENDER_BYTES) {
201
+ if (!liveRenderState.noticedFallback) {
202
+ console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
203
+ }
204
+ process.stdout.write(text);
205
+ liveRenderState.fallback = true;
206
+ return;
207
+ }
208
+ process.stdout.write(renderMarkdownAnsi(text));
209
+ };
210
+ const printNew = async () => {
211
+ const text = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
212
+ const nextChunk = text.slice(lastLength);
213
+ if (nextChunk.length > 0) {
214
+ renderLiveChunk(nextChunk);
215
+ lastLength = text.length;
216
+ }
217
+ };
218
+ await printNew();
219
+ // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate infinite poll
220
+ while (true) {
221
+ const latest = await sessionStore.readSession(sessionId);
222
+ if (!latest) {
223
+ break;
224
+ }
225
+ if (latest.status === 'completed' || latest.status === 'error') {
226
+ await printNew();
227
+ flushRemainder();
228
+ if (!options?.suppressMetadata) {
229
+ if (latest.status === 'error' && latest.errorMessage) {
230
+ console.log('\nResult:');
231
+ console.log(`Session failed: ${latest.errorMessage}`);
232
+ }
233
+ if (latest.status === 'completed' && latest.usage) {
234
+ const summary = formatCompletionSummary(latest, { includeSlug: true });
235
+ if (summary) {
236
+ console.log(`\n${chalk.green.bold(summary)}`);
237
+ }
238
+ else {
239
+ const usage = latest.usage;
240
+ console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
241
+ }
242
+ }
243
+ }
244
+ break;
245
+ }
246
+ await wait(1000);
247
+ await printNew();
248
+ }
249
+ }
250
+ export function formatResponseMetadata(metadata) {
251
+ if (!metadata) {
252
+ return null;
253
+ }
254
+ const parts = [];
255
+ if (metadata.responseId) {
256
+ parts.push(`response=${metadata.responseId}`);
257
+ }
258
+ if (metadata.requestId) {
259
+ parts.push(`request=${metadata.requestId}`);
260
+ }
261
+ if (metadata.status) {
262
+ parts.push(`status=${metadata.status}`);
263
+ }
264
+ if (metadata.incompleteReason) {
265
+ parts.push(`incomplete=${metadata.incompleteReason}`);
266
+ }
267
+ return parts.length > 0 ? parts.join(' | ') : null;
268
+ }
269
+ export function formatTransportMetadata(metadata) {
270
+ if (!metadata?.reason) {
271
+ return null;
272
+ }
273
+ const reasonLabels = {
274
+ 'client-timeout': 'client timeout (deadline exceeded)',
275
+ 'connection-lost': 'connection lost before completion',
276
+ 'client-abort': 'request aborted locally',
277
+ unknown: 'unknown transport failure',
278
+ };
279
+ const label = reasonLabels[metadata.reason] ?? 'transport error';
280
+ return `${metadata.reason} — ${label}`;
281
+ }
282
+ export function formatUserErrorMetadata(metadata) {
283
+ if (!metadata) {
284
+ return null;
285
+ }
286
+ const parts = [];
287
+ if (metadata.category) {
288
+ parts.push(metadata.category);
289
+ }
290
+ if (metadata.message) {
291
+ parts.push(`message=${metadata.message}`);
292
+ }
293
+ if (metadata.details && Object.keys(metadata.details).length > 0) {
294
+ parts.push(`details=${JSON.stringify(metadata.details)}`);
295
+ }
296
+ return parts.length > 0 ? parts.join(' | ') : null;
297
+ }
298
+ export function buildReattachLine(metadata) {
299
+ if (!metadata.id) {
300
+ return null;
301
+ }
302
+ const referenceTime = metadata.startedAt ?? metadata.createdAt;
303
+ if (!referenceTime) {
304
+ return null;
305
+ }
306
+ const elapsedLabel = formatRelativeDuration(referenceTime);
307
+ if (!elapsedLabel) {
308
+ return null;
309
+ }
310
+ if (metadata.status === 'running') {
311
+ return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
312
+ }
313
+ return null;
314
+ }
315
+ export function trimBeforeFirstAnswer(logText) {
316
+ const marker = 'Answer:';
317
+ const index = logText.indexOf(marker);
318
+ if (index === -1) {
319
+ return logText;
320
+ }
321
+ return logText.slice(index);
322
+ }
323
+ function formatRelativeDuration(referenceIso) {
324
+ const timestamp = Date.parse(referenceIso);
325
+ if (Number.isNaN(timestamp)) {
326
+ return null;
327
+ }
328
+ const diffMs = Date.now() - timestamp;
329
+ if (diffMs < 0) {
330
+ return null;
331
+ }
332
+ const seconds = Math.max(1, Math.round(diffMs / 1000));
333
+ if (seconds < 60) {
334
+ return `${seconds}s`;
335
+ }
336
+ const minutes = Math.floor(seconds / 60);
337
+ const remainingSeconds = seconds % 60;
338
+ if (minutes < 60) {
339
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
340
+ }
341
+ const hours = Math.floor(minutes / 60);
342
+ const remainingMinutes = minutes % 60;
343
+ if (hours < 24) {
344
+ const parts = [`${hours}h`];
345
+ if (remainingMinutes > 0) {
346
+ parts.push(`${remainingMinutes}m`);
347
+ }
348
+ return parts.join(' ');
349
+ }
350
+ const days = Math.floor(hours / 24);
351
+ const remainingHours = hours % 24;
352
+ const parts = [`${days}d`];
353
+ if (remainingHours > 0) {
354
+ parts.push(`${remainingHours}h`);
355
+ }
356
+ if (remainingMinutes > 0 && days === 0) {
357
+ parts.push(`${remainingMinutes}m`);
358
+ }
359
+ return parts.join(' ');
360
+ }
361
+ function printStatusExamples() {
362
+ console.log('');
363
+ console.log(chalk.bold('Usage Examples'));
364
+ console.log(`${chalk.bold(' oracle status --hours 72 --limit 50')}`);
365
+ console.log(dim(' Show 72h of history capped at 50 entries.'));
366
+ console.log(`${chalk.bold(' oracle status --clear --hours 168')}`);
367
+ console.log(dim(' Delete sessions older than 7 days (use --all to wipe everything).'));
368
+ console.log(`${chalk.bold(' oracle session <session-id>')}`);
369
+ console.log(dim(' Attach to a specific running/completed session to stream its output.'));
370
+ console.log(dim(CLEANUP_TIP));
371
+ }
372
+ function matchesModel(entry, filter) {
373
+ const normalized = filter.trim().toLowerCase();
374
+ if (!normalized) {
375
+ return true;
376
+ }
377
+ const models = entry.models?.map((model) => model.model.toLowerCase()) ?? (entry.model ? [entry.model.toLowerCase()] : []);
378
+ return models.includes(normalized);
379
+ }
380
+ function formatModelColumn(entry, width, richTty) {
381
+ const models = entry.models && entry.models.length > 0
382
+ ? entry.models
383
+ : entry.model
384
+ ? [{ model: entry.model, status: entry.status }]
385
+ : [];
386
+ if (models.length === 0) {
387
+ return 'n/a'.padEnd(width);
388
+ }
389
+ const badges = models.map((model) => formatModelBadge(model, richTty));
390
+ const text = badges.join(' ');
391
+ if (text.length > width) {
392
+ return `${text.slice(0, width - 1)}…`;
393
+ }
394
+ return text.padEnd(width);
395
+ }
396
+ function formatModelBadge(model, richTty) {
397
+ const glyph = statusGlyph(model.status);
398
+ const text = `${model.model}${glyph}`;
399
+ return richTty ? chalk.cyan(text) : text;
400
+ }
401
+ function statusGlyph(status) {
402
+ switch (status) {
403
+ case 'completed':
404
+ return '✓';
405
+ case 'running':
406
+ return '⌛';
407
+ case 'pending':
408
+ return '…';
409
+ case 'error':
410
+ return '✖';
411
+ case 'cancelled':
412
+ return '⦻';
413
+ default:
414
+ return '?';
415
+ }
416
+ }
417
+ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
418
+ const normalizedFilter = modelFilter?.trim().toLowerCase();
419
+ const freshMetadata = (await sessionStore.readSession(sessionId)) ?? fallbackMeta;
420
+ const models = freshMetadata.models ?? fallbackMeta.models ?? [];
421
+ if (models.length === 0) {
422
+ if (normalizedFilter) {
423
+ return await sessionStore.readModelLog(sessionId, modelFilter);
424
+ }
425
+ return await sessionStore.readLog(sessionId);
426
+ }
427
+ const candidates = normalizedFilter != null
428
+ ? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
429
+ : models;
430
+ if (candidates.length === 0) {
431
+ return '';
432
+ }
433
+ const sections = [];
434
+ for (const model of candidates) {
435
+ const body = await sessionStore.readModelLog(sessionId, model.model);
436
+ sections.push(`=== ${model.model} ===\n${body}`.trimEnd());
437
+ }
438
+ return sections.join('\n\n');
439
+ }
440
+ function extractRenderableChunks(text, state) {
441
+ const chunks = [];
442
+ let buffer = '';
443
+ const lines = text.split(/(\n)/);
444
+ for (let i = 0; i < lines.length; i += 1) {
445
+ const segment = lines[i];
446
+ if (segment === '\n') {
447
+ buffer += segment;
448
+ // Detect code fences
449
+ const prev = lines[i - 1] ?? '';
450
+ const fenceMatch = prev.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
451
+ if (!state.inFence && fenceMatch) {
452
+ state.inFence = true;
453
+ state.fenceDelimiter = fenceMatch[2];
454
+ }
455
+ else if (state.inFence && state.fenceDelimiter && prev.startsWith(state.fenceDelimiter)) {
456
+ state.inFence = false;
457
+ state.fenceDelimiter = undefined;
458
+ }
459
+ const trimmed = prev.trim();
460
+ if (!state.inFence) {
461
+ if (!state.inTable && trimmed.startsWith('|') && trimmed.includes('|')) {
462
+ state.inTable = true;
463
+ }
464
+ if (state.inTable && trimmed === '') {
465
+ state.inTable = false;
466
+ }
467
+ }
468
+ const safeBreak = !state.inFence && !state.inTable && trimmed === '';
469
+ if (safeBreak) {
470
+ chunks.push(buffer);
471
+ buffer = '';
472
+ }
473
+ continue;
474
+ }
475
+ buffer += segment;
476
+ }
477
+ return { chunks, remainder: buffer };
478
+ }
479
+ function formatTimestamp(iso) {
480
+ const date = new Date(iso);
481
+ const locale = 'en-US';
482
+ const opts = {
483
+ year: 'numeric',
484
+ month: '2-digit',
485
+ day: '2-digit',
486
+ hour: 'numeric',
487
+ minute: '2-digit',
488
+ second: undefined,
489
+ hour12: true,
490
+ };
491
+ const formatted = date.toLocaleString(locale, opts);
492
+ return formatted.replace(/(, )(\d:)/, '$1 $2');
493
+ }
494
+ export function formatCompletionSummary(metadata, options = {}) {
495
+ if (!metadata.usage || metadata.elapsedMs == null) {
496
+ return null;
497
+ }
498
+ const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
499
+ const usage = metadata.usage;
500
+ const cost = metadata.mode === 'browser' ? null : resolveCost(metadata);
501
+ const costPart = cost != null ? ` | ${formatUSD(cost)}` : '';
502
+ const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
503
+ const filesCount = metadata.options?.file?.length ?? 0;
504
+ const filesPart = filesCount > 0 ? ` | files=${filesCount}` : '';
505
+ const slugPart = options.includeSlug ? ` | slug=${metadata.id}` : '';
506
+ return `Finished in ${formatElapsed(metadata.elapsedMs)} (${modeLabel}${costPart} | tok(i/o/r/t)=${tokensDisplay}${filesPart}${slugPart})`;
507
+ }
508
+ function resolveCost(metadata) {
509
+ if (metadata.mode === 'browser') {
510
+ return null;
511
+ }
512
+ if (metadata.usage?.cost != null) {
513
+ return metadata.usage.cost;
514
+ }
515
+ if (!metadata.model || !metadata.usage) {
516
+ return null;
517
+ }
518
+ const pricing = MODEL_CONFIGS[metadata.model]?.pricing;
519
+ if (!pricing) {
520
+ return null;
521
+ }
522
+ const input = metadata.usage.inputTokens ?? 0;
523
+ const output = metadata.usage.outputTokens ?? 0;
524
+ const cost = input * pricing.inputPerToken + output * pricing.outputPerToken;
525
+ return cost > 0 ? cost : null;
526
+ }
527
+ function formatCostTable(cost) {
528
+ return `$${cost.toFixed(3)}`.padStart(7);
529
+ }
530
+ async function readStoredPrompt(sessionId) {
531
+ const request = await sessionStore.readRequest(sessionId);
532
+ if (request?.prompt && request.prompt.trim().length > 0) {
533
+ return request.prompt;
534
+ }
535
+ const meta = await sessionStore.readSession(sessionId);
536
+ if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
537
+ return meta.options.prompt;
538
+ }
539
+ return null;
540
+ }