@steipete/oracle 1.1.0 → 1.2.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 (58) hide show
  1. package/README.md +29 -4
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/bin/oracle-cli.js +169 -18
  4. package/dist/bin/oracle-mcp.js +6 -0
  5. package/dist/src/browser/actions/modelSelection.js +117 -29
  6. package/dist/src/browser/cookies.js +1 -1
  7. package/dist/src/browser/index.js +2 -1
  8. package/dist/src/browser/prompt.js +6 -5
  9. package/dist/src/browser/sessionRunner.js +4 -2
  10. package/dist/src/cli/dryRun.js +41 -5
  11. package/dist/src/cli/engine.js +7 -0
  12. package/dist/src/cli/help.js +1 -1
  13. package/dist/src/cli/hiddenAliases.js +17 -0
  14. package/dist/src/cli/markdownRenderer.js +79 -0
  15. package/dist/src/cli/notifier.js +223 -0
  16. package/dist/src/cli/promptRequirement.js +3 -0
  17. package/dist/src/cli/runOptions.js +29 -0
  18. package/dist/src/cli/sessionCommand.js +1 -1
  19. package/dist/src/cli/sessionDisplay.js +94 -7
  20. package/dist/src/cli/sessionRunner.js +21 -2
  21. package/dist/src/cli/tui/index.js +436 -0
  22. package/dist/src/config.js +27 -0
  23. package/dist/src/mcp/server.js +36 -0
  24. package/dist/src/mcp/tools/consult.js +158 -0
  25. package/dist/src/mcp/tools/sessionResources.js +64 -0
  26. package/dist/src/mcp/tools/sessions.js +106 -0
  27. package/dist/src/mcp/types.js +17 -0
  28. package/dist/src/mcp/utils.js +24 -0
  29. package/dist/src/oracle/files.js +143 -6
  30. package/dist/src/oracle/run.js +41 -20
  31. package/dist/src/oracle/tokenEstimate.js +34 -0
  32. package/dist/src/sessionManager.js +48 -3
  33. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  34. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  35. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  36. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  39. package/dist/vendor/oracle-notifier/README.md +24 -0
  40. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  41. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  42. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  43. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  44. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  45. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  46. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  47. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  48. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  49. package/package.json +39 -13
  50. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  51. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  52. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  53. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  54. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  55. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  56. package/vendor/oracle-notifier/README.md +24 -0
  57. package/vendor/oracle-notifier/build-notifier.sh +93 -0
  58. package/dist/.DS_Store +0 -0
@@ -0,0 +1,223 @@
1
+ import notifier from 'toasted-notifier';
2
+ import { spawn } from 'node:child_process';
3
+ import { formatUSD, formatNumber } from '../oracle/format.js';
4
+ import { MODEL_CONFIGS } from '../oracle/config.js';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { createRequire } from 'node:module';
8
+ import { fileURLToPath } from 'node:url';
9
+ const ORACLE_EMOJI = '🧿';
10
+ export function resolveNotificationSettings({ cliNotify, cliNotifySound, env, config, }) {
11
+ const defaultEnabled = !(bool(env.CI) || bool(env.SSH_CONNECTION) || muteByConfig(env, config));
12
+ const envNotify = parseToggle(env.ORACLE_NOTIFY);
13
+ const envSound = parseToggle(env.ORACLE_NOTIFY_SOUND);
14
+ const enabled = cliNotify ?? envNotify ?? config?.enabled ?? defaultEnabled;
15
+ const sound = cliNotifySound ?? envSound ?? config?.sound ?? false;
16
+ return { enabled, sound };
17
+ }
18
+ export function deriveNotificationSettingsFromMetadata(metadata, env, config) {
19
+ if (metadata?.notifications) {
20
+ return metadata.notifications;
21
+ }
22
+ return resolveNotificationSettings({ cliNotify: undefined, cliNotifySound: undefined, env, config });
23
+ }
24
+ export async function sendSessionNotification(payload, settings, log, answerPreview) {
25
+ if (!settings.enabled || isTestEnv(process.env)) {
26
+ return;
27
+ }
28
+ const title = `Oracle${ORACLE_EMOJI} finished`;
29
+ const message = buildMessage(payload, sanitizePreview(answerPreview));
30
+ try {
31
+ if (await tryMacNativeNotifier(title, message, settings)) {
32
+ return;
33
+ }
34
+ // Fallback to toasted-notifier (cross-platform). macAppIconOption() is only honored on macOS.
35
+ await notifier.notify({
36
+ title,
37
+ message,
38
+ sound: settings.sound,
39
+ });
40
+ }
41
+ catch (error) {
42
+ if (isMacExecError(error)) {
43
+ const repaired = await repairMacNotifier(log);
44
+ if (repaired) {
45
+ try {
46
+ await notifier.notify({ title, message, sound: settings.sound, ...(macAppIconOption()) });
47
+ return;
48
+ }
49
+ catch (retryError) {
50
+ const reason = retryError instanceof Error ? retryError.message : String(retryError);
51
+ log(`(notify skipped after retry: ${reason})`);
52
+ return;
53
+ }
54
+ }
55
+ }
56
+ const reason = error instanceof Error ? error.message : String(error);
57
+ log(`(notify skipped: ${reason})`);
58
+ }
59
+ }
60
+ function buildMessage(payload, answerPreview) {
61
+ const parts = [];
62
+ const sessionLabel = payload.sessionName || payload.sessionId;
63
+ parts.push(sessionLabel);
64
+ // Show cost only for API runs.
65
+ if (payload.mode === 'api') {
66
+ const cost = payload.costUsd ?? inferCost(payload);
67
+ if (cost !== undefined) {
68
+ // Round to $0.00 for a concise toast.
69
+ parts.push(formatUSD(Number(cost.toFixed(2))));
70
+ }
71
+ }
72
+ if (payload.characters != null) {
73
+ parts.push(`${formatNumber(payload.characters)} chars`);
74
+ }
75
+ if (answerPreview) {
76
+ parts.push(answerPreview);
77
+ }
78
+ return parts.join(' · ');
79
+ }
80
+ function sanitizePreview(preview) {
81
+ if (!preview)
82
+ return undefined;
83
+ let text = preview;
84
+ // Strip code fences and inline code markers.
85
+ text = text.replace(/```[\s\S]*?```/g, ' ');
86
+ text = text.replace(/`([^`]+)`/g, '$1');
87
+ // Convert markdown links and images to their visible text.
88
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1');
89
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
90
+ // Drop bold/italic markers.
91
+ text = text.replace(/(\*\*|__|\*|_)/g, '');
92
+ // Remove headings / list markers / blockquotes.
93
+ text = text.replace(/^\s*#+\s*/gm, '');
94
+ text = text.replace(/^\s*[-*+]\s+/gm, '');
95
+ text = text.replace(/^\s*>\s+/gm, '');
96
+ // Collapse whitespace and trim.
97
+ text = text.replace(/\s+/g, ' ').trim();
98
+ // Limit length to keep notifications short.
99
+ const max = 200;
100
+ if (text.length > max) {
101
+ text = `${text.slice(0, max - 1)}…`;
102
+ }
103
+ return text;
104
+ }
105
+ // Exposed for unit tests only.
106
+ export const testHelpers = { sanitizePreview };
107
+ function inferCost(payload) {
108
+ const model = payload.model;
109
+ const usage = payload.usage;
110
+ if (!model || !usage)
111
+ return undefined;
112
+ const config = MODEL_CONFIGS[model];
113
+ if (!config)
114
+ return undefined;
115
+ return (usage.inputTokens * config.pricing.inputPerToken +
116
+ usage.outputTokens * config.pricing.outputPerToken);
117
+ }
118
+ function parseToggle(value) {
119
+ if (value == null)
120
+ return undefined;
121
+ const normalized = value.trim().toLowerCase();
122
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
123
+ return true;
124
+ if (['0', 'false', 'no', 'off'].includes(normalized))
125
+ return false;
126
+ return undefined;
127
+ }
128
+ function bool(value) {
129
+ return Boolean(value && String(value).length > 0);
130
+ }
131
+ function isMacExecError(error) {
132
+ return Boolean(process.platform === 'darwin' &&
133
+ error &&
134
+ typeof error === 'object' &&
135
+ 'code' in error &&
136
+ error.code === 'EACCES');
137
+ }
138
+ async function repairMacNotifier(log) {
139
+ const binPath = macNotifierPath();
140
+ if (!binPath)
141
+ return false;
142
+ try {
143
+ await fs.chmod(binPath, 0o755);
144
+ return true;
145
+ }
146
+ catch (chmodError) {
147
+ const reason = chmodError instanceof Error ? chmodError.message : String(chmodError);
148
+ log(`(notify repair failed: ${reason} — try: xattr -dr com.apple.quarantine "${path.dirname(binPath)}")`);
149
+ return false;
150
+ }
151
+ }
152
+ function macNotifierPath() {
153
+ if (process.platform !== 'darwin')
154
+ return null;
155
+ try {
156
+ const req = createRequire(import.meta.url);
157
+ const modPath = req.resolve('toasted-notifier');
158
+ const base = path.dirname(modPath);
159
+ return path.join(base, 'vendor', 'mac.noindex', 'terminal-notifier.app', 'Contents', 'MacOS', 'terminal-notifier');
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function macAppIconOption() {
166
+ if (process.platform !== 'darwin')
167
+ return {};
168
+ const iconPaths = [
169
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../assets-oracle-icon.png'),
170
+ path.resolve(process.cwd(), 'assets-oracle-icon.png'),
171
+ ];
172
+ for (const candidate of iconPaths) {
173
+ if (candidate && fsExistsSync(candidate)) {
174
+ return { appIcon: candidate };
175
+ }
176
+ }
177
+ return {};
178
+ }
179
+ function fsExistsSync(target) {
180
+ try {
181
+ return Boolean(require('node:fs').statSync(target));
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ }
187
+ async function tryMacNativeNotifier(title, message, settings) {
188
+ const binary = macNativeNotifierPath();
189
+ if (!binary)
190
+ return false;
191
+ return new Promise((resolve) => {
192
+ const child = spawn(binary, [title, message, settings.sound ? 'Glass' : ''], {
193
+ stdio: 'ignore',
194
+ });
195
+ child.on('error', () => resolve(false));
196
+ child.on('exit', (code) => resolve(code === 0));
197
+ });
198
+ }
199
+ function macNativeNotifierPath() {
200
+ if (process.platform !== 'darwin')
201
+ return null;
202
+ const candidates = [
203
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
204
+ path.resolve(process.cwd(), 'vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
205
+ ];
206
+ for (const candidate of candidates) {
207
+ if (fsExistsSync(candidate)) {
208
+ return candidate;
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ function muteByConfig(env, config) {
214
+ if (!config?.muteIn)
215
+ return false;
216
+ return ((config.muteIn.includes('CI') && bool(env.CI)) ||
217
+ (config.muteIn.includes('SSH') && bool(env.SSH_CONNECTION)));
218
+ }
219
+ function isTestEnv(env) {
220
+ return (env.ORACLE_DISABLE_NOTIFICATIONS === '1' ||
221
+ env.NODE_ENV === 'test' ||
222
+ Boolean(env.VITEST || env.VITEST_WORKER_ID || env.JEST_WORKER_ID));
223
+ }
@@ -2,6 +2,9 @@
2
2
  * Determine whether the CLI should enforce a prompt requirement based on raw args and options.
3
3
  */
4
4
  export function shouldRequirePrompt(rawArgs, options) {
5
+ if (rawArgs.length === 0) {
6
+ return !options.prompt;
7
+ }
5
8
  const firstArg = rawArgs[0];
6
9
  const bypassPrompt = Boolean(options.session ||
7
10
  options.execSession ||
@@ -0,0 +1,29 @@
1
+ import { resolveEngine } from './engine.js';
2
+ import { normalizeModelOption, inferModelFromLabel, resolveApiModel } from './options.js';
3
+ export function resolveRunOptionsFromConfig({ prompt, files = [], model, engine, userConfig, env = process.env, }) {
4
+ const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
5
+ const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || 'gpt-5-pro';
6
+ const resolvedModel = resolvedEngine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
7
+ const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
8
+ ? `${prompt.trim()}\n${userConfig.promptSuffix}`
9
+ : prompt;
10
+ const search = userConfig?.search !== 'off';
11
+ const heartbeatIntervalMs = userConfig?.heartbeatSeconds !== undefined ? userConfig.heartbeatSeconds * 1000 : 30_000;
12
+ const runOptions = {
13
+ prompt: promptWithSuffix,
14
+ model: resolvedModel,
15
+ file: files ?? [],
16
+ search,
17
+ heartbeatIntervalMs,
18
+ filesReport: userConfig?.filesReport,
19
+ background: userConfig?.background,
20
+ };
21
+ return { runOptions, resolvedEngine };
22
+ }
23
+ function resolveEngineWithConfig({ engine, configEngine, env, }) {
24
+ if (engine)
25
+ return engine;
26
+ if (configEngine)
27
+ return configEngine;
28
+ return resolveEngine({ engine: undefined, env });
29
+ }
@@ -77,7 +77,7 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
77
77
  console.log(`Ignoring flags on session attach: ${ignoredFlags.join(', ')}`);
78
78
  }
79
79
  const renderMarkdown = Boolean(sessionOptions.render || sessionOptions.renderMarkdown || autoRender);
80
- await deps.attachSession(sessionId, { renderMarkdown });
80
+ await deps.attachSession(sessionId, { renderMarkdown, renderPrompt: !sessionOptions.hidePrompt });
81
81
  }
82
82
  export function formatSessionCleanupMessage(result, scope) {
83
83
  const deletedLabel = `${result.deleted} ${result.deleted === 1 ? 'session' : 'sessions'}`;
@@ -1,10 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import kleur from 'kleur';
3
- import { filterSessionsByRange, listSessionsMetadata, readSessionLog, readSessionMetadata, SESSIONS_DIR, wait, } from '../sessionManager.js';
3
+ import { filterSessionsByRange, listSessionsMetadata, readSessionLog, readSessionMetadata, readSessionRequest, SESSIONS_DIR, wait, } from '../sessionManager.js';
4
4
  import { renderMarkdownAnsi } from './markdownRenderer.js';
5
+ import { formatElapsed, formatUSD } from '../oracle/format.js';
6
+ import { MODEL_CONFIGS } from '../oracle.js';
5
7
  const isTty = () => Boolean(process.stdout.isTTY);
6
8
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
7
- const MAX_RENDER_BYTES = 200_000;
9
+ export const MAX_RENDER_BYTES = 200_000;
8
10
  const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
9
11
  export async function showStatus({ hours, includeAll, limit, showExamples = false }) {
10
12
  const metas = await listSessionsMetadata();
@@ -18,12 +20,17 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
18
20
  return;
19
21
  }
20
22
  console.log(chalk.bold('Recent Sessions'));
23
+ console.log(chalk.dim('Timestamp Chars Cost Status Model ID'));
21
24
  for (const entry of entries) {
22
25
  const statusRaw = (entry.status || 'unknown').padEnd(9);
23
26
  const status = richTty ? colorStatus(entry.status ?? 'unknown', statusRaw) : statusRaw;
24
27
  const model = (entry.model || 'n/a').padEnd(9);
25
- const created = entry.createdAt.replace('T', ' ').replace('Z', '');
26
- console.log(`${created} | ${status} | ${model} | ${entry.id}`);
28
+ const created = formatTimestamp(entry.createdAt);
29
+ const chars = entry.options?.prompt?.length ?? entry.promptPreview?.length ?? 0;
30
+ const charLabel = chars > 0 ? String(chars).padStart(5) : ' -';
31
+ const costValue = resolveCost(entry);
32
+ const costLabel = costValue != null ? formatCostTable(costValue) : ' -';
33
+ console.log(`${created} | ${charLabel} | ${costLabel} | ${status} | ${model} | ${entry.id}`);
27
34
  }
28
35
  if (truncated) {
29
36
  console.log(chalk.yellow(`Showing ${entries.length} of ${total} sessions from the requested range. Run "oracle session --clear" or delete entries in ${SESSIONS_DIR} to free space, or rerun with --status-limit/--status-all.`));
@@ -76,6 +83,14 @@ export async function attachSession(sessionId, options) {
76
83
  }
77
84
  }
78
85
  const shouldTrimIntro = initialStatus === 'completed' || initialStatus === 'error';
86
+ if (options?.renderPrompt !== false) {
87
+ const prompt = await readStoredPrompt(sessionId);
88
+ if (prompt) {
89
+ console.log(chalk.bold('Prompt:'));
90
+ console.log(renderMarkdownAnsi(prompt));
91
+ console.log(dim('---'));
92
+ }
93
+ }
79
94
  if (shouldTrimIntro) {
80
95
  const fullLog = await readSessionLog(sessionId);
81
96
  const trimmed = trimBeforeFirstAnswer(fullLog);
@@ -104,6 +119,10 @@ export async function attachSession(sessionId, options) {
104
119
  else {
105
120
  process.stdout.write(trimmed);
106
121
  }
122
+ const summary = formatCompletionSummary(metadata, { includeSlug: true });
123
+ if (summary) {
124
+ console.log(`\n${chalk.green.bold(summary)}`);
125
+ }
107
126
  return;
108
127
  }
109
128
  if (wantsRender) {
@@ -187,9 +206,15 @@ export async function attachSession(sessionId, options) {
187
206
  console.log('\nResult:');
188
207
  console.log(`Session failed: ${latest.errorMessage}`);
189
208
  }
190
- if (latest.usage && initialStatus === 'running') {
191
- const usage = latest.usage;
192
- console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
209
+ if (latest.status === 'completed' && latest.usage) {
210
+ const summary = formatCompletionSummary(latest, { includeSlug: true });
211
+ if (summary) {
212
+ console.log(`\n${chalk.green.bold(summary)}`);
213
+ }
214
+ else {
215
+ const usage = latest.usage;
216
+ console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
217
+ }
193
218
  }
194
219
  }
195
220
  break;
@@ -359,3 +384,65 @@ function extractRenderableChunks(text, state) {
359
384
  }
360
385
  return { chunks, remainder: buffer };
361
386
  }
387
+ function formatTimestamp(iso) {
388
+ const date = new Date(iso);
389
+ const locale = 'en-US';
390
+ const opts = {
391
+ year: 'numeric',
392
+ month: '2-digit',
393
+ day: '2-digit',
394
+ hour: 'numeric',
395
+ minute: '2-digit',
396
+ second: undefined,
397
+ hour12: true,
398
+ };
399
+ const formatted = date.toLocaleString(locale, opts);
400
+ return formatted.replace(/(, )(\d:)/, '$1 $2');
401
+ }
402
+ export function formatCompletionSummary(metadata, options = {}) {
403
+ if (!metadata.usage || metadata.elapsedMs == null) {
404
+ return null;
405
+ }
406
+ const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
407
+ const usage = metadata.usage;
408
+ const cost = metadata.mode === 'browser' ? null : resolveCost(metadata);
409
+ const costPart = cost != null ? ` | ${formatUSD(cost)}` : '';
410
+ const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
411
+ const filesCount = metadata.options?.file?.length ?? 0;
412
+ const filesPart = filesCount > 0 ? ` | files=${filesCount}` : '';
413
+ const slugPart = options.includeSlug ? ` | slug=${metadata.id}` : '';
414
+ return `Finished in ${formatElapsed(metadata.elapsedMs)} (${modeLabel}${costPart} | tok(i/o/r/t)=${tokensDisplay}${filesPart}${slugPart})`;
415
+ }
416
+ function resolveCost(metadata) {
417
+ if (metadata.mode === 'browser') {
418
+ return null;
419
+ }
420
+ if (metadata.usage?.cost != null) {
421
+ return metadata.usage.cost;
422
+ }
423
+ if (!metadata.model || !metadata.usage) {
424
+ return null;
425
+ }
426
+ const pricing = MODEL_CONFIGS[metadata.model]?.pricing;
427
+ if (!pricing) {
428
+ return null;
429
+ }
430
+ const input = metadata.usage.inputTokens ?? 0;
431
+ const output = metadata.usage.outputTokens ?? 0;
432
+ const cost = input * pricing.inputPerToken + output * pricing.outputPerToken;
433
+ return cost > 0 ? cost : null;
434
+ }
435
+ function formatCostTable(cost) {
436
+ return `$${cost.toFixed(3)}`.padStart(7);
437
+ }
438
+ async function readStoredPrompt(sessionId) {
439
+ const request = await readSessionRequest(sessionId);
440
+ if (request?.prompt && request.prompt.trim().length > 0) {
441
+ return request.prompt;
442
+ }
443
+ const meta = await readSessionMetadata(sessionId);
444
+ if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
445
+ return meta.options.prompt;
446
+ }
447
+ return null;
448
+ }
@@ -1,18 +1,20 @@
1
1
  import kleur from 'kleur';
2
2
  import { updateSessionMetadata } from '../sessionManager.js';
3
- import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, } from '../oracle.js';
3
+ import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
4
4
  import { runBrowserSessionExecution } from '../browser/sessionRunner.js';
5
5
  import { formatResponseMetadata, formatTransportMetadata } from './sessionDisplay.js';
6
6
  import { markErrorLogged } from './errorUtils.js';
7
+ import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from './notifier.js';
7
8
  const isTty = process.stdout.isTTY;
8
9
  const dim = (text) => (isTty ? kleur.dim(text) : text);
9
- export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, }) {
10
+ export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, }) {
10
11
  await updateSessionMetadata(sessionMeta.id, {
11
12
  status: 'running',
12
13
  startedAt: new Date().toISOString(),
13
14
  mode,
14
15
  ...(browserConfig ? { browser: { config: browserConfig } } : {}),
15
16
  });
17
+ const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
16
18
  try {
17
19
  if (mode === 'browser') {
18
20
  if (!browserConfig) {
@@ -32,6 +34,14 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
32
34
  transport: undefined,
33
35
  error: undefined,
34
36
  });
37
+ await sendSessionNotification({
38
+ sessionId: sessionMeta.id,
39
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
40
+ mode,
41
+ model: sessionMeta.model,
42
+ usage: result.usage,
43
+ characters: result.answerText?.length,
44
+ }, notificationSettings, log, result.answerText?.slice(0, 140));
35
45
  return;
36
46
  }
37
47
  const result = await runOracle(runOptions, {
@@ -51,6 +61,15 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
51
61
  transport: undefined,
52
62
  error: undefined,
53
63
  });
64
+ const answerText = extractTextOutput(result.response);
65
+ await sendSessionNotification({
66
+ sessionId: sessionMeta.id,
67
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
68
+ mode,
69
+ model: sessionMeta.model ?? runOptions.model,
70
+ usage: result.usage,
71
+ characters: answerText.length,
72
+ }, notificationSettings, log, answerText.slice(0, 140));
54
73
  }
55
74
  catch (error) {
56
75
  const message = formatError(error);