@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +954 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/bin/oracle.js +683 -0
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/chrome/browser-tools.js +295 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/src/browser/actions/assistantResponse.js +555 -0
- package/dist/src/browser/actions/attachments.js +82 -0
- package/dist/src/browser/actions/modelSelection.js +300 -0
- package/dist/src/browser/actions/navigation.js +175 -0
- package/dist/src/browser/actions/promptComposer.js +167 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
- package/dist/src/browser/chromeCookies.js +274 -0
- package/dist/src/browser/chromeLifecycle.js +107 -0
- package/dist/src/browser/config.js +49 -0
- package/dist/src/browser/constants.js +42 -0
- package/dist/src/browser/cookies.js +130 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +541 -0
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/prompt.js +82 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/sessionRunner.js +96 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +112 -0
- package/dist/src/browser/windowsCookies.js +218 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/browserConfig.js +193 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +103 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +25 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +300 -0
- package/dist/src/cli/options.js +193 -0
- package/dist/src/cli/oscUtils.js +20 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +62 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +540 -0
- package/dist/src/cli/sessionRunner.js +419 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +520 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +27 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +221 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +96 -0
- package/dist/src/mcp/types.js +18 -0
- package/dist/src/mcp/utils.js +27 -0
- package/dist/src/oracle/background.js +134 -0
- package/dist/src/oracle/claude.js +95 -0
- package/dist/src/oracle/client.js +87 -0
- package/dist/src/oracle/config.js +92 -0
- package/dist/src/oracle/errors.js +104 -0
- package/dist/src/oracle/files.js +371 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +185 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/multiModelRunner.js +164 -0
- package/dist/src/oracle/oscProgress.js +66 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +49 -0
- package/dist/src/oracle/run.js +492 -0
- package/dist/src/oracle/runUtils.js +27 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/remote/client.js +128 -0
- package/dist/src/remote/server.js +294 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +462 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +102 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
|
|
5
|
+
import { runBrowserSessionExecution } from '../browser/sessionRunner.js';
|
|
6
|
+
import { renderMarkdownAnsi } from './markdownRenderer.js';
|
|
7
|
+
import { formatResponseMetadata, formatTransportMetadata } from './sessionDisplay.js';
|
|
8
|
+
import { markErrorLogged } from './errorUtils.js';
|
|
9
|
+
import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from './notifier.js';
|
|
10
|
+
import { sessionStore } from '../sessionStore.js';
|
|
11
|
+
import { runMultiModelApiSession } from '../oracle/multiModelRunner.js';
|
|
12
|
+
import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from '../oracle/config.js';
|
|
13
|
+
import { buildPrompt, buildRequestBody } from '../oracle/request.js';
|
|
14
|
+
import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
|
|
15
|
+
import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
|
|
16
|
+
import { formatElapsed } from '../oracle/format.js';
|
|
17
|
+
import { sanitizeOscProgress } from './oscUtils.js';
|
|
18
|
+
import { readFiles } from '../oracle/files.js';
|
|
19
|
+
import { formatUSD } from '../oracle/format.js';
|
|
20
|
+
import { SESSIONS_DIR } from '../sessionManager.js';
|
|
21
|
+
import { cwd as getCwd } from 'node:process';
|
|
22
|
+
const isTty = process.stdout.isTTY;
|
|
23
|
+
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
24
|
+
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
25
|
+
const writeInline = (chunk) => {
|
|
26
|
+
// Keep session logs intact while still echoing inline output to the user.
|
|
27
|
+
write(chunk);
|
|
28
|
+
return muteStdout ? true : process.stdout.write(chunk);
|
|
29
|
+
};
|
|
30
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
31
|
+
status: 'running',
|
|
32
|
+
startedAt: new Date().toISOString(),
|
|
33
|
+
mode,
|
|
34
|
+
...(browserConfig ? { browser: { config: browserConfig } } : {}),
|
|
35
|
+
});
|
|
36
|
+
const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
|
|
37
|
+
const modelForStatus = runOptions.model ?? sessionMeta.model;
|
|
38
|
+
try {
|
|
39
|
+
if (mode === 'browser') {
|
|
40
|
+
if (runOptions.model.startsWith('gemini')) {
|
|
41
|
+
throw new Error('Gemini models are not available in browser mode. Re-run with --engine api.');
|
|
42
|
+
}
|
|
43
|
+
if (!browserConfig) {
|
|
44
|
+
throw new Error('Missing browser configuration for session.');
|
|
45
|
+
}
|
|
46
|
+
if (modelForStatus) {
|
|
47
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
48
|
+
status: 'running',
|
|
49
|
+
startedAt: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const result = await runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, browserDeps);
|
|
53
|
+
if (modelForStatus) {
|
|
54
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
55
|
+
status: 'completed',
|
|
56
|
+
completedAt: new Date().toISOString(),
|
|
57
|
+
usage: result.usage,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
61
|
+
status: 'completed',
|
|
62
|
+
completedAt: new Date().toISOString(),
|
|
63
|
+
usage: result.usage,
|
|
64
|
+
elapsedMs: result.elapsedMs,
|
|
65
|
+
browser: {
|
|
66
|
+
config: browserConfig,
|
|
67
|
+
runtime: result.runtime,
|
|
68
|
+
},
|
|
69
|
+
response: undefined,
|
|
70
|
+
transport: undefined,
|
|
71
|
+
error: undefined,
|
|
72
|
+
});
|
|
73
|
+
await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? '', log);
|
|
74
|
+
await sendSessionNotification({
|
|
75
|
+
sessionId: sessionMeta.id,
|
|
76
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
77
|
+
mode,
|
|
78
|
+
model: sessionMeta.model,
|
|
79
|
+
usage: result.usage,
|
|
80
|
+
characters: result.answerText?.length,
|
|
81
|
+
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const multiModels = Array.isArray(runOptions.models) ? runOptions.models.filter(Boolean) : [];
|
|
85
|
+
if (multiModels.length > 1) {
|
|
86
|
+
const [primaryModel] = multiModels;
|
|
87
|
+
if (!primaryModel) {
|
|
88
|
+
throw new Error('Missing model name for multi-model run.');
|
|
89
|
+
}
|
|
90
|
+
const modelConfig = MODEL_CONFIGS[primaryModel];
|
|
91
|
+
if (!modelConfig) {
|
|
92
|
+
throw new Error(`Unsupported model "${primaryModel}".`);
|
|
93
|
+
}
|
|
94
|
+
const files = await readFiles(runOptions.file ?? [], { cwd });
|
|
95
|
+
const promptWithFiles = buildPrompt(runOptions.prompt, files, cwd);
|
|
96
|
+
const requestBody = buildRequestBody({
|
|
97
|
+
modelConfig,
|
|
98
|
+
systemPrompt: runOptions.system ?? DEFAULT_SYSTEM_PROMPT,
|
|
99
|
+
userPrompt: promptWithFiles,
|
|
100
|
+
searchEnabled: runOptions.search !== false,
|
|
101
|
+
maxOutputTokens: runOptions.maxOutput,
|
|
102
|
+
background: runOptions.background,
|
|
103
|
+
storeResponse: runOptions.background,
|
|
104
|
+
});
|
|
105
|
+
const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
|
|
106
|
+
const tokenLabel = formatTokenEstimate(estimatedTokens, (text) => (isTty ? kleur.green(text) : text));
|
|
107
|
+
const filesPhrase = files.length === 0 ? 'no files' : `${files.length} files`;
|
|
108
|
+
const modelsLabel = multiModels.join(', ');
|
|
109
|
+
log(`Calling ${isTty ? kleur.cyan(modelsLabel) : modelsLabel} — ${tokenLabel} tokens, ${filesPhrase}.`);
|
|
110
|
+
const multiRunTips = [];
|
|
111
|
+
if (files.length === 0) {
|
|
112
|
+
multiRunTips.push('Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.');
|
|
113
|
+
}
|
|
114
|
+
const shortPrompt = (runOptions.prompt?.trim().length ?? 0) < 80;
|
|
115
|
+
if (shortPrompt) {
|
|
116
|
+
multiRunTips.push('Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.');
|
|
117
|
+
}
|
|
118
|
+
for (const tip of multiRunTips) {
|
|
119
|
+
log(dim(tip));
|
|
120
|
+
}
|
|
121
|
+
// Surface long-running model expectations up front so users know why a response might lag.
|
|
122
|
+
const longRunningModels = multiModels.filter((model) => MODEL_CONFIGS[model]?.reasoning?.effort === 'high');
|
|
123
|
+
if (longRunningModels.length > 0) {
|
|
124
|
+
for (const model of longRunningModels) {
|
|
125
|
+
log('');
|
|
126
|
+
const headingLabel = `[${model}]`;
|
|
127
|
+
log(isTty ? kleur.bold(headingLabel) : headingLabel);
|
|
128
|
+
log(dim('This model can take up to 60 minutes (usually replies much faster).'));
|
|
129
|
+
log(dim('Press Ctrl+C to cancel.'));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const shouldStreamInline = !muteStdout && process.stdout.isTTY;
|
|
133
|
+
const shouldRenderMarkdown = shouldStreamInline && runOptions.renderPlain !== true;
|
|
134
|
+
const printedModels = new Set();
|
|
135
|
+
const answerFallbacks = new Map();
|
|
136
|
+
const stripOscProgress = (text) => sanitizeOscProgress(text, shouldStreamInline);
|
|
137
|
+
const printModelLog = async (model) => {
|
|
138
|
+
if (printedModels.has(model))
|
|
139
|
+
return;
|
|
140
|
+
printedModels.add(model);
|
|
141
|
+
const body = stripOscProgress(await sessionStore.readModelLog(sessionMeta.id, model));
|
|
142
|
+
log('');
|
|
143
|
+
const fallback = answerFallbacks.get(model);
|
|
144
|
+
const hasBody = body.length > 0;
|
|
145
|
+
if (!hasBody && !fallback) {
|
|
146
|
+
log(dim(`${model}: (no output recorded)`));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const headingLabel = `[${model}]`;
|
|
150
|
+
const heading = shouldStreamInline ? kleur.bold(headingLabel) : headingLabel;
|
|
151
|
+
log(heading);
|
|
152
|
+
const content = hasBody ? body : fallback ?? '';
|
|
153
|
+
const printable = shouldRenderMarkdown ? renderMarkdownAnsi(content) : content;
|
|
154
|
+
writeInline(printable);
|
|
155
|
+
if (!printable.endsWith('\n')) {
|
|
156
|
+
log('');
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const summary = await runMultiModelApiSession({
|
|
160
|
+
sessionMeta,
|
|
161
|
+
runOptions,
|
|
162
|
+
models: multiModels,
|
|
163
|
+
cwd,
|
|
164
|
+
version,
|
|
165
|
+
onModelDone: shouldStreamInline
|
|
166
|
+
? async (result) => {
|
|
167
|
+
if (result.answerText) {
|
|
168
|
+
answerFallbacks.set(result.model, result.answerText);
|
|
169
|
+
}
|
|
170
|
+
await printModelLog(result.model);
|
|
171
|
+
}
|
|
172
|
+
: undefined,
|
|
173
|
+
}, {
|
|
174
|
+
runOracleImpl: muteStdout
|
|
175
|
+
? (opts, deps) => runOracle(opts, { ...deps, allowStdout: false })
|
|
176
|
+
: undefined,
|
|
177
|
+
});
|
|
178
|
+
if (!shouldStreamInline) {
|
|
179
|
+
// If we couldn't stream inline (e.g., non-TTY), print all logs after completion.
|
|
180
|
+
for (const [index, result] of summary.fulfilled.entries()) {
|
|
181
|
+
if (index > 0) {
|
|
182
|
+
log('');
|
|
183
|
+
}
|
|
184
|
+
await printModelLog(result.model);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const aggregateUsage = summary.fulfilled.reduce((acc, entry) => ({
|
|
188
|
+
inputTokens: acc.inputTokens + entry.usage.inputTokens,
|
|
189
|
+
outputTokens: acc.outputTokens + entry.usage.outputTokens,
|
|
190
|
+
reasoningTokens: acc.reasoningTokens + entry.usage.reasoningTokens,
|
|
191
|
+
totalTokens: acc.totalTokens + entry.usage.totalTokens,
|
|
192
|
+
cost: (acc.cost ?? 0) + (entry.usage.cost ?? 0),
|
|
193
|
+
}), { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, totalTokens: 0, cost: 0 });
|
|
194
|
+
const tokensDisplay = [
|
|
195
|
+
aggregateUsage.inputTokens,
|
|
196
|
+
aggregateUsage.outputTokens,
|
|
197
|
+
aggregateUsage.reasoningTokens,
|
|
198
|
+
aggregateUsage.totalTokens,
|
|
199
|
+
]
|
|
200
|
+
.map((v, idx) => formatTokenValue(v, {
|
|
201
|
+
input_tokens: aggregateUsage.inputTokens,
|
|
202
|
+
output_tokens: aggregateUsage.outputTokens,
|
|
203
|
+
reasoning_tokens: aggregateUsage.reasoningTokens,
|
|
204
|
+
total_tokens: aggregateUsage.totalTokens,
|
|
205
|
+
}, idx))
|
|
206
|
+
.join('/');
|
|
207
|
+
const costLabel = aggregateUsage.cost != null ? formatUSD(aggregateUsage.cost) : 'cost=N/A';
|
|
208
|
+
const statusColor = summary.rejected.length === 0 ? kleur.green : summary.fulfilled.length > 0 ? kleur.yellow : kleur.red;
|
|
209
|
+
const overallText = `${summary.fulfilled.length}/${multiModels.length} models`;
|
|
210
|
+
log(statusColor(`Finished in ${formatElapsed(summary.elapsedMs)} (${overallText} | ${costLabel} | tok(i/o/r/t)=${tokensDisplay})`));
|
|
211
|
+
const hasFailure = summary.rejected.length > 0;
|
|
212
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
213
|
+
status: hasFailure ? 'error' : 'completed',
|
|
214
|
+
completedAt: new Date().toISOString(),
|
|
215
|
+
usage: aggregateUsage,
|
|
216
|
+
elapsedMs: summary.elapsedMs,
|
|
217
|
+
response: undefined,
|
|
218
|
+
transport: undefined,
|
|
219
|
+
error: undefined,
|
|
220
|
+
});
|
|
221
|
+
const totalCharacters = summary.fulfilled.reduce((sum, entry) => sum + entry.answerText.length, 0);
|
|
222
|
+
await sendSessionNotification({
|
|
223
|
+
sessionId: sessionMeta.id,
|
|
224
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
225
|
+
mode,
|
|
226
|
+
model: `${multiModels.length} models`,
|
|
227
|
+
usage: aggregateUsage,
|
|
228
|
+
characters: totalCharacters,
|
|
229
|
+
}, notificationSettings, log);
|
|
230
|
+
if (runOptions.writeOutputPath) {
|
|
231
|
+
const savedOutputs = [];
|
|
232
|
+
for (const entry of summary.fulfilled) {
|
|
233
|
+
const modelOutputPath = deriveModelOutputPath(runOptions.writeOutputPath, entry.model);
|
|
234
|
+
const savedPath = await writeAssistantOutput(modelOutputPath, entry.answerText, log);
|
|
235
|
+
if (savedPath) {
|
|
236
|
+
savedOutputs.push({ model: entry.model, path: savedPath });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (savedOutputs.length > 0) {
|
|
240
|
+
log(dim('Saved outputs:'));
|
|
241
|
+
for (const item of savedOutputs) {
|
|
242
|
+
log(dim(`- ${item.model} -> ${item.path}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (hasFailure) {
|
|
247
|
+
throw summary.rejected[0].reason;
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const singleModelOverride = multiModels.length === 1 ? multiModels[0] : undefined;
|
|
252
|
+
const apiRunOptions = singleModelOverride
|
|
253
|
+
? { ...runOptions, model: singleModelOverride, models: undefined }
|
|
254
|
+
: runOptions;
|
|
255
|
+
if (modelForStatus && singleModelOverride == null) {
|
|
256
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
257
|
+
status: 'running',
|
|
258
|
+
startedAt: new Date().toISOString(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const result = await runOracle(apiRunOptions, {
|
|
262
|
+
cwd,
|
|
263
|
+
log,
|
|
264
|
+
write,
|
|
265
|
+
allowStdout: !muteStdout,
|
|
266
|
+
});
|
|
267
|
+
if (result.mode !== 'live') {
|
|
268
|
+
throw new Error('Unexpected preview result while running a session.');
|
|
269
|
+
}
|
|
270
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
271
|
+
status: 'completed',
|
|
272
|
+
completedAt: new Date().toISOString(),
|
|
273
|
+
usage: result.usage,
|
|
274
|
+
elapsedMs: result.elapsedMs,
|
|
275
|
+
response: extractResponseMetadata(result.response),
|
|
276
|
+
transport: undefined,
|
|
277
|
+
error: undefined,
|
|
278
|
+
});
|
|
279
|
+
if (modelForStatus && singleModelOverride == null) {
|
|
280
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
281
|
+
status: 'completed',
|
|
282
|
+
completedAt: new Date().toISOString(),
|
|
283
|
+
usage: result.usage,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const answerText = extractTextOutput(result.response);
|
|
287
|
+
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
288
|
+
await sendSessionNotification({
|
|
289
|
+
sessionId: sessionMeta.id,
|
|
290
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
291
|
+
mode,
|
|
292
|
+
model: sessionMeta.model ?? runOptions.model,
|
|
293
|
+
usage: result.usage,
|
|
294
|
+
characters: answerText.length,
|
|
295
|
+
}, notificationSettings, log, answerText.slice(0, 140));
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const message = formatError(error);
|
|
299
|
+
log(`ERROR: ${message}`);
|
|
300
|
+
markErrorLogged(error);
|
|
301
|
+
const userError = asOracleUserError(error);
|
|
302
|
+
if (userError) {
|
|
303
|
+
log(dim(`User error (${userError.category}): ${userError.message}`));
|
|
304
|
+
}
|
|
305
|
+
const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
|
|
306
|
+
const metadataLine = formatResponseMetadata(responseMetadata);
|
|
307
|
+
if (metadataLine) {
|
|
308
|
+
log(dim(`Response metadata: ${metadataLine}`));
|
|
309
|
+
}
|
|
310
|
+
const transportMetadata = error instanceof OracleTransportError ? { reason: error.reason } : undefined;
|
|
311
|
+
const transportLine = formatTransportMetadata(transportMetadata);
|
|
312
|
+
if (transportLine) {
|
|
313
|
+
log(dim(`Transport: ${transportLine}`));
|
|
314
|
+
}
|
|
315
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
316
|
+
status: 'error',
|
|
317
|
+
completedAt: new Date().toISOString(),
|
|
318
|
+
errorMessage: message,
|
|
319
|
+
mode,
|
|
320
|
+
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
321
|
+
response: responseMetadata,
|
|
322
|
+
transport: transportMetadata,
|
|
323
|
+
error: userError
|
|
324
|
+
? {
|
|
325
|
+
category: userError.category,
|
|
326
|
+
message: userError.message,
|
|
327
|
+
details: userError.details,
|
|
328
|
+
}
|
|
329
|
+
: undefined,
|
|
330
|
+
});
|
|
331
|
+
if (mode === 'browser') {
|
|
332
|
+
log(dim('Next steps (browser fallback):')); // guides users when automation breaks
|
|
333
|
+
log(dim('- Rerun with --engine api to bypass Chrome entirely.'));
|
|
334
|
+
log(dim('- Or rerun with --engine api --render-markdown [--file …] to generate a single markdown bundle you can paste into ChatGPT manually (add --browser-bundle-files if you still want attachments).'));
|
|
335
|
+
}
|
|
336
|
+
if (modelForStatus) {
|
|
337
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
338
|
+
status: 'error',
|
|
339
|
+
completedAt: new Date().toISOString(),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function formatError(error) {
|
|
346
|
+
return error instanceof Error ? error.message : String(error);
|
|
347
|
+
}
|
|
348
|
+
async function writeAssistantOutput(targetPath, content, log) {
|
|
349
|
+
if (!targetPath)
|
|
350
|
+
return;
|
|
351
|
+
if (!content || content.trim().length === 0) {
|
|
352
|
+
log(dim('write-output skipped: no assistant content to save.'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
356
|
+
const normalizedSessionsDir = path.resolve(SESSIONS_DIR);
|
|
357
|
+
if (normalizedTarget === normalizedSessionsDir ||
|
|
358
|
+
normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
359
|
+
log(dim(`write-output skipped: refusing to write inside session storage (${normalizedSessionsDir}).`));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
|
|
364
|
+
const payload = content.endsWith('\n') ? content : `${content}\n`;
|
|
365
|
+
await fs.writeFile(normalizedTarget, payload, 'utf8');
|
|
366
|
+
log(dim(`Saved assistant output to ${normalizedTarget}`));
|
|
367
|
+
return normalizedTarget;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
371
|
+
if (isPermissionError(error)) {
|
|
372
|
+
const fallbackPath = buildFallbackPath(normalizedTarget);
|
|
373
|
+
if (fallbackPath) {
|
|
374
|
+
try {
|
|
375
|
+
await fs.mkdir(path.dirname(fallbackPath), { recursive: true });
|
|
376
|
+
const payload = content.endsWith('\n') ? content : `${content}\n`;
|
|
377
|
+
await fs.writeFile(fallbackPath, payload, 'utf8');
|
|
378
|
+
log(dim(`write-output fallback to ${fallbackPath} (original failed: ${reason})`));
|
|
379
|
+
return fallbackPath;
|
|
380
|
+
}
|
|
381
|
+
catch (innerError) {
|
|
382
|
+
const innerReason = innerError instanceof Error ? innerError.message : String(innerError);
|
|
383
|
+
log(dim(`write-output failed (${reason}); fallback failed (${innerReason}); session completed anyway.`));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
export function deriveModelOutputPath(basePath, model) {
|
|
392
|
+
if (!basePath)
|
|
393
|
+
return undefined;
|
|
394
|
+
const ext = path.extname(basePath);
|
|
395
|
+
const stem = path.basename(basePath, ext);
|
|
396
|
+
const dir = path.dirname(basePath);
|
|
397
|
+
const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
|
|
398
|
+
return path.join(dir, suffix);
|
|
399
|
+
}
|
|
400
|
+
function isPermissionError(error) {
|
|
401
|
+
if (!(error instanceof Error))
|
|
402
|
+
return false;
|
|
403
|
+
const code = error.code;
|
|
404
|
+
return code === 'EACCES' || code === 'EPERM';
|
|
405
|
+
}
|
|
406
|
+
function buildFallbackPath(original) {
|
|
407
|
+
const ext = path.extname(original);
|
|
408
|
+
const stem = path.basename(original, ext);
|
|
409
|
+
const dir = getCwd();
|
|
410
|
+
const candidate = ext ? `${stem}.fallback${ext}` : `${stem}.fallback`;
|
|
411
|
+
const fallback = path.join(dir, candidate);
|
|
412
|
+
const normalizedSessionsDir = path.resolve(SESSIONS_DIR);
|
|
413
|
+
const normalizedFallback = path.resolve(fallback);
|
|
414
|
+
if (normalizedFallback === normalizedSessionsDir ||
|
|
415
|
+
normalizedFallback.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
return fallback;
|
|
419
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const TAGLINES = [
|
|
3
|
+
'Whispering your tokens to the silicon sage.',
|
|
4
|
+
'Turning scattered files into one sharp question.',
|
|
5
|
+
'One slug to gather them all.',
|
|
6
|
+
'Token thrift, oracle lift.',
|
|
7
|
+
'Globs to gospel, minus the incense.',
|
|
8
|
+
'Your repo, neatly bottled, gently shaken.',
|
|
9
|
+
'Clarity, with a hint of smoke.',
|
|
10
|
+
'Questions in, clarity out.',
|
|
11
|
+
'Globs become guidance.',
|
|
12
|
+
'Token-aware, omen-ready.',
|
|
13
|
+
'Globs go in; citations and costs come out.',
|
|
14
|
+
'Keeps 196k tokens feeling roomy, not risky.',
|
|
15
|
+
'Remembers your paths, forgets your past runs.',
|
|
16
|
+
'A TUI when you want it, a one-liner when you do not.',
|
|
17
|
+
'Less ceremony, more certainty.',
|
|
18
|
+
'Guidance without the guesswork.',
|
|
19
|
+
'One prompt fanned out, no echoes wasted.',
|
|
20
|
+
'Detached runs, tethered results.',
|
|
21
|
+
'Calm CLI, loud answers.',
|
|
22
|
+
'Single scroll, many seers.',
|
|
23
|
+
'Background magic with foreground receipts.',
|
|
24
|
+
'Paths aligned, models attuned.',
|
|
25
|
+
'Light spell, heavy insight.',
|
|
26
|
+
'Signal first, sorcery second.',
|
|
27
|
+
'One command, several seers; results stay grounded.',
|
|
28
|
+
'Context braided, answers sharpened.',
|
|
29
|
+
'Short incantation, long provenance.',
|
|
30
|
+
'Attach, cast, reattach later.',
|
|
31
|
+
'Spell once, cite always.',
|
|
32
|
+
'Edge cases foretold, receipts attached.',
|
|
33
|
+
'Silent run, loud receipts.',
|
|
34
|
+
'Detours gone; clarity walks in.',
|
|
35
|
+
'Tokens tallied, omens tallied.',
|
|
36
|
+
'Calm prompt, converged truths.',
|
|
37
|
+
'Single spell, multiple verdicts.',
|
|
38
|
+
'Prompt once, harvest many omens.',
|
|
39
|
+
'Light on ceremony, heavy on receipts.',
|
|
40
|
+
'From globs to guidance in one breath.',
|
|
41
|
+
'Quiet prompt, thunderous answers.',
|
|
42
|
+
'Balanced mystique, measurable results.',
|
|
43
|
+
'Debugger by day, oracle by night.',
|
|
44
|
+
"Your code's confessional booth.",
|
|
45
|
+
'Edge cases fear this inbox.',
|
|
46
|
+
'Slop in, sharp answers out.',
|
|
47
|
+
"Your AI coworker's quality control.",
|
|
48
|
+
"Because vibes aren't a deliverable.",
|
|
49
|
+
'When the other agents shrug, the oracle ships.',
|
|
50
|
+
'Hallucinations checked at the door.',
|
|
51
|
+
'Context police for overeager LLMs.',
|
|
52
|
+
'Turns prompt spaghetti into ship-ready sauce.',
|
|
53
|
+
'Lint for large language models.',
|
|
54
|
+
"Slaps wrists before they hit 'ship'.",
|
|
55
|
+
"Because 'let the model figure it out' is not QA.",
|
|
56
|
+
"Fine, I'll write the test for the AI too.",
|
|
57
|
+
'We bring receipts; they bring excuses.',
|
|
58
|
+
'Less swagger, more citations.',
|
|
59
|
+
'LLM babysitter with a shipping agenda.',
|
|
60
|
+
'Ships facts, not vibes.',
|
|
61
|
+
'Context sanitizer for reckless prompts.',
|
|
62
|
+
'AI babysitter with merge rights.',
|
|
63
|
+
'Stops the hallucination before it hits prod.',
|
|
64
|
+
'Slop filter set to aggressive.',
|
|
65
|
+
'We debug the debugger.',
|
|
66
|
+
'Model said maybe; oracle says ship/no.',
|
|
67
|
+
'Less lorem, more logic.',
|
|
68
|
+
"Your prompt's adult supervision.",
|
|
69
|
+
'Cleanup crew for AI messes.',
|
|
70
|
+
'AI wrote it? Oracle babysits it.',
|
|
71
|
+
'Turning maybe into mergeable.',
|
|
72
|
+
'The AI said vibes; we said tests.',
|
|
73
|
+
'Cleanup crew for model-made messes—now with citations.',
|
|
74
|
+
'Less hallucination, more escalation.',
|
|
75
|
+
"Your AI's ghostwriter, but with citations.",
|
|
76
|
+
'Where prompt soup becomes production code.',
|
|
77
|
+
'From shruggy agents to shippable PRs.',
|
|
78
|
+
'Token mop for agent spillover.',
|
|
79
|
+
'We QA the AI so you can ship the code.',
|
|
80
|
+
'Less improv, more implementation.',
|
|
81
|
+
'Ships facts faster than agents make excuses.',
|
|
82
|
+
'From prompt chaos to PR-ready prose.',
|
|
83
|
+
"Your AI's hot take, fact-checked.",
|
|
84
|
+
'Cleanup crew for LLM loose ends.',
|
|
85
|
+
'We babysit the bot; you ship the build.',
|
|
86
|
+
'Prompt drama in; release notes out.',
|
|
87
|
+
'AI confidence filtered through reality.',
|
|
88
|
+
"From 'it told me so' to 'tests say so'.",
|
|
89
|
+
"We refactor the model's hubris before it hits prod.",
|
|
90
|
+
'Prompt chaos triaged, answers discharged.',
|
|
91
|
+
'Oracle babysits; you merge.',
|
|
92
|
+
'Vibes quarantined; facts admitted.',
|
|
93
|
+
'The cleanup crew for speculative stack traces.',
|
|
94
|
+
'Ship-ready answers, minus the AI improv.',
|
|
95
|
+
"We pre-empt the hallucination so you don't triage it at 2am.",
|
|
96
|
+
'AI confidence monitored, citations required.',
|
|
97
|
+
'Ship logs, not lore.',
|
|
98
|
+
'Hallucinations flagged, reality shipped.',
|
|
99
|
+
'We lint the lore so you can ship the code.',
|
|
100
|
+
'Hallucination hotline: we answer, not the pager.',
|
|
101
|
+
'Less mystique, more mergeability.',
|
|
102
|
+
'Slop filter set past 11.',
|
|
103
|
+
'Bottled prompt chaos, filtered answers.',
|
|
104
|
+
"Your AI's swagger, audited.",
|
|
105
|
+
'New year, same oracle: resolutions shipped, not wished.',
|
|
106
|
+
'Lunar New Year sweep: clear caches, invite good deploys.',
|
|
107
|
+
'Eid Mubarak: feast on clarity, fast from hallucinations.',
|
|
108
|
+
'Diwali: lights on, incident lights off.',
|
|
109
|
+
'Holi colors on dashboards, not in logs.',
|
|
110
|
+
"Workers' Day: let oracle haul the heavy context.",
|
|
111
|
+
'Earth Day: trim carbon, trim token waste.',
|
|
112
|
+
'Halloween: ship treats, not trick exceptions.',
|
|
113
|
+
'Independence Day: sparkles in the sky, not in the error console.',
|
|
114
|
+
'Christmas: all is calm, all is shipped.',
|
|
115
|
+
'Nowruz reset: sweep caches, welcome clean deploys.',
|
|
116
|
+
'Hanukkah lights, zero prod fires.',
|
|
117
|
+
'Ramadan focus: fast from scope creep, feast on clarity.',
|
|
118
|
+
'Pride Month: more color on the streets, less red in CI.',
|
|
119
|
+
'Thanksgiving: grateful for green builds, no turkey outages.',
|
|
120
|
+
'Solstice deploy: longest day, shortest incident list.',
|
|
121
|
+
];
|
|
122
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
123
|
+
function utcParts(date) {
|
|
124
|
+
return {
|
|
125
|
+
year: date.getUTCFullYear(),
|
|
126
|
+
month: date.getUTCMonth(),
|
|
127
|
+
day: date.getUTCDate(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const onMonthDay = (month, day) => (date) => {
|
|
131
|
+
const parts = utcParts(date);
|
|
132
|
+
return parts.month === month && parts.day === day;
|
|
133
|
+
};
|
|
134
|
+
const onSpecificDates = (dates, durationDays = 1) => (date) => {
|
|
135
|
+
const parts = utcParts(date);
|
|
136
|
+
return dates.some(([year, month, day]) => {
|
|
137
|
+
if (parts.year !== year)
|
|
138
|
+
return false;
|
|
139
|
+
const start = Date.UTC(year, month, day);
|
|
140
|
+
const current = Date.UTC(parts.year, parts.month, parts.day);
|
|
141
|
+
return current >= start && current < start + durationDays * DAY_MS;
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const inYearWindow = (windows) => (date) => {
|
|
145
|
+
const parts = utcParts(date);
|
|
146
|
+
const window = windows.find((entry) => entry.year === parts.year);
|
|
147
|
+
if (!window)
|
|
148
|
+
return false;
|
|
149
|
+
const start = Date.UTC(window.year, window.month, window.day);
|
|
150
|
+
const current = Date.UTC(parts.year, parts.month, parts.day);
|
|
151
|
+
return current >= start && current < start + window.duration * DAY_MS;
|
|
152
|
+
};
|
|
153
|
+
const isFourthThursdayOfNovember = (date) => {
|
|
154
|
+
const parts = utcParts(date);
|
|
155
|
+
if (parts.month !== 10)
|
|
156
|
+
return false; // November
|
|
157
|
+
const firstDay = new Date(Date.UTC(parts.year, 10, 1)).getUTCDay();
|
|
158
|
+
const offsetToThursday = (4 - firstDay + 7) % 7; // 4 = Thursday
|
|
159
|
+
const fourthThursday = 1 + offsetToThursday + 21; // 1st + offset + 3 weeks
|
|
160
|
+
return parts.day === fourthThursday;
|
|
161
|
+
};
|
|
162
|
+
const HOLIDAY_RULES = new Map([
|
|
163
|
+
['New year, same oracle: resolutions shipped, not wished.', onMonthDay(0, 1)],
|
|
164
|
+
[
|
|
165
|
+
'Lunar New Year sweep: clear caches, invite good deploys.',
|
|
166
|
+
onSpecificDates([
|
|
167
|
+
[2025, 0, 29],
|
|
168
|
+
[2026, 1, 17],
|
|
169
|
+
[2027, 1, 6],
|
|
170
|
+
], 1),
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
'Eid Mubarak: feast on clarity, fast from hallucinations.',
|
|
174
|
+
onSpecificDates([
|
|
175
|
+
[2025, 2, 31],
|
|
176
|
+
[2026, 2, 20],
|
|
177
|
+
[2027, 2, 10],
|
|
178
|
+
], 1),
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
'Diwali: lights on, incident lights off.',
|
|
182
|
+
onSpecificDates([
|
|
183
|
+
[2025, 9, 20],
|
|
184
|
+
[2026, 10, 8],
|
|
185
|
+
[2027, 9, 29],
|
|
186
|
+
], 1),
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
'Holi colors on dashboards, not in logs.',
|
|
190
|
+
onSpecificDates([
|
|
191
|
+
[2025, 2, 14],
|
|
192
|
+
[2026, 2, 3],
|
|
193
|
+
[2027, 2, 23],
|
|
194
|
+
], 1),
|
|
195
|
+
],
|
|
196
|
+
["Workers' Day: let oracle haul the heavy context.", onMonthDay(4, 1)],
|
|
197
|
+
['Earth Day: trim carbon, trim token waste.', onMonthDay(3, 22)],
|
|
198
|
+
['Halloween: ship treats, not trick exceptions.', onMonthDay(9, 31)],
|
|
199
|
+
[
|
|
200
|
+
'Independence Day: sparkles in the sky, not in the error console.',
|
|
201
|
+
onMonthDay(6, 4),
|
|
202
|
+
],
|
|
203
|
+
['Christmas: all is calm, all is shipped.', onMonthDay(11, 25)],
|
|
204
|
+
['Nowruz reset: sweep caches, welcome clean deploys.', onMonthDay(2, 20)],
|
|
205
|
+
[
|
|
206
|
+
'Hanukkah lights, zero prod fires.',
|
|
207
|
+
inYearWindow([
|
|
208
|
+
{ year: 2025, month: 11, day: 14, duration: 8 },
|
|
209
|
+
{ year: 2026, month: 11, day: 4, duration: 8 },
|
|
210
|
+
{ year: 2027, month: 10, day: 24, duration: 8 },
|
|
211
|
+
]),
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
'Ramadan focus: fast from scope creep, feast on clarity.',
|
|
215
|
+
inYearWindow([
|
|
216
|
+
{ year: 2025, month: 1, day: 28, duration: 30 },
|
|
217
|
+
{ year: 2026, month: 1, day: 17, duration: 30 },
|
|
218
|
+
{ year: 2027, month: 1, day: 7, duration: 30 },
|
|
219
|
+
]),
|
|
220
|
+
],
|
|
221
|
+
['Pride Month: more color on the streets, less red in CI.', (date) => utcParts(date).month === 5],
|
|
222
|
+
['Thanksgiving: grateful for green builds, no turkey outages.', isFourthThursdayOfNovember],
|
|
223
|
+
['Solstice deploy: longest day, shortest incident list.', onMonthDay(5, 21)],
|
|
224
|
+
]);
|
|
225
|
+
function isTaglineActive(tagline, date) {
|
|
226
|
+
const rule = HOLIDAY_RULES.get(tagline);
|
|
227
|
+
if (!rule)
|
|
228
|
+
return true;
|
|
229
|
+
return rule(date);
|
|
230
|
+
}
|
|
231
|
+
export function activeTaglines(options = {}) {
|
|
232
|
+
const today = options.now ? options.now() : new Date();
|
|
233
|
+
const filtered = TAGLINES.filter((tagline) => isTaglineActive(tagline, today));
|
|
234
|
+
return filtered.length > 0 ? filtered : TAGLINES;
|
|
235
|
+
}
|
|
236
|
+
export function pickTagline(options = {}) {
|
|
237
|
+
const env = options.env ?? process.env;
|
|
238
|
+
const override = env?.ORACLE_TAGLINE_INDEX;
|
|
239
|
+
if (override !== undefined) {
|
|
240
|
+
const parsed = Number.parseInt(override, 10);
|
|
241
|
+
if (!Number.isNaN(parsed) && parsed >= 0) {
|
|
242
|
+
return TAGLINES[parsed % TAGLINES.length];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const pool = activeTaglines(options);
|
|
246
|
+
const rand = options.random ?? Math.random;
|
|
247
|
+
const index = Math.floor(rand() * pool.length) % pool.length;
|
|
248
|
+
return pool[index];
|
|
249
|
+
}
|
|
250
|
+
export function formatIntroLine(version, options = {}) {
|
|
251
|
+
const tagline = pickTagline(options);
|
|
252
|
+
const rich = options.richTty ?? true;
|
|
253
|
+
if (rich && chalk.level > 0) {
|
|
254
|
+
return `${chalk.bold('🧿 oracle')} ${version} — ${tagline}`;
|
|
255
|
+
}
|
|
256
|
+
return `🧿 oracle ${version} — ${tagline}`;
|
|
257
|
+
}
|
|
258
|
+
export { TAGLINES };
|