@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,492 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, PRO_MODELS, TOKENIZER_OPTIONS } from './config.js';
|
|
8
|
+
import { readFiles } from './files.js';
|
|
9
|
+
import { buildPrompt, buildRequestBody } from './request.js';
|
|
10
|
+
import { estimateRequestTokens } from './tokenEstimate.js';
|
|
11
|
+
import { formatElapsed, formatUSD } from './format.js';
|
|
12
|
+
import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
|
|
13
|
+
import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
|
|
14
|
+
import { createDefaultClientFactory } from './client.js';
|
|
15
|
+
import { formatBaseUrlForLog, maskApiKey } from './logging.js';
|
|
16
|
+
import { startHeartbeat } from '../heartbeat.js';
|
|
17
|
+
import { startOscProgress } from './oscProgress.js';
|
|
18
|
+
import { createFsAdapter } from './fsAdapter.js';
|
|
19
|
+
import { resolveGeminiModelId } from './gemini.js';
|
|
20
|
+
import { resolveClaudeModelId } from './claude.js';
|
|
21
|
+
import { renderMarkdownAnsi } from '../cli/markdownRenderer.js';
|
|
22
|
+
import { executeBackgroundResponse } from './background.js';
|
|
23
|
+
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from './runUtils.js';
|
|
24
|
+
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
25
|
+
const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
|
|
26
|
+
// Default timeout for non-pro API runs (fast models) — give them up to 120s.
|
|
27
|
+
const DEFAULT_TIMEOUT_NON_PRO_MS = 120_000;
|
|
28
|
+
const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
|
|
29
|
+
const defaultWait = (ms) => new Promise((resolve) => {
|
|
30
|
+
setTimeout(resolve, ms);
|
|
31
|
+
});
|
|
32
|
+
export async function runOracle(options, deps = {}) {
|
|
33
|
+
const { apiKey: optionsApiKey = options.apiKey, cwd = process.cwd(), fs: fsModule = createFsAdapter(fs), log = console.log, write: sinkWrite = (_text) => true, allowStdout = true, stdoutWrite: stdoutWriteDep, now = () => performance.now(), clientFactory = createDefaultClientFactory(), client, wait = defaultWait, } = deps;
|
|
34
|
+
const stdoutWrite = allowStdout
|
|
35
|
+
? stdoutWriteDep ?? process.stdout.write.bind(process.stdout)
|
|
36
|
+
: () => true;
|
|
37
|
+
const isTty = allowStdout && isStdoutTty;
|
|
38
|
+
const baseUrl = options.baseUrl?.trim() || process.env.OPENAI_BASE_URL?.trim();
|
|
39
|
+
const logVerbose = (message) => {
|
|
40
|
+
if (options.verbose) {
|
|
41
|
+
log(dim(`[verbose] ${message}`));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const previewMode = resolvePreviewMode(options.previewMode ?? options.preview);
|
|
45
|
+
const isPreview = Boolean(previewMode);
|
|
46
|
+
const isAzureOpenAI = Boolean(options.azure?.endpoint);
|
|
47
|
+
const getApiKeyForModel = (model) => {
|
|
48
|
+
if (model.startsWith('gpt')) {
|
|
49
|
+
if (optionsApiKey)
|
|
50
|
+
return optionsApiKey;
|
|
51
|
+
if (isAzureOpenAI) {
|
|
52
|
+
return process.env.AZURE_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
53
|
+
}
|
|
54
|
+
return process.env.OPENAI_API_KEY;
|
|
55
|
+
}
|
|
56
|
+
if (model.startsWith('gemini')) {
|
|
57
|
+
return optionsApiKey ?? process.env.GEMINI_API_KEY;
|
|
58
|
+
}
|
|
59
|
+
if (model.startsWith('claude')) {
|
|
60
|
+
return optionsApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
const envVar = options.model.startsWith('gpt')
|
|
65
|
+
? isAzureOpenAI
|
|
66
|
+
? 'AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)'
|
|
67
|
+
: 'OPENAI_API_KEY'
|
|
68
|
+
: options.model.startsWith('gemini')
|
|
69
|
+
? 'GEMINI_API_KEY'
|
|
70
|
+
: 'ANTHROPIC_API_KEY';
|
|
71
|
+
const apiKey = getApiKeyForModel(options.model);
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
throw new PromptValidationError(`Missing ${envVar}. Set it via the environment or a .env file.`, {
|
|
74
|
+
env: envVar,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? '10', 10);
|
|
78
|
+
const promptLength = options.prompt?.trim().length ?? 0;
|
|
79
|
+
// Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
|
|
80
|
+
const isProTierModel = PRO_MODELS.has(options.model);
|
|
81
|
+
if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
|
|
82
|
+
throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
|
|
83
|
+
}
|
|
84
|
+
const modelConfig = MODEL_CONFIGS[options.model];
|
|
85
|
+
if (!modelConfig) {
|
|
86
|
+
throw new PromptValidationError(`Unsupported model "${options.model}". Choose one of: ${Object.keys(MODEL_CONFIGS).join(', ')}`, { model: options.model });
|
|
87
|
+
}
|
|
88
|
+
const isLongRunningModel = isProTierModel;
|
|
89
|
+
const useBackground = options.background ?? isLongRunningModel;
|
|
90
|
+
const inputTokenBudget = options.maxInput ?? modelConfig.inputLimit;
|
|
91
|
+
const files = await readFiles(options.file ?? [], { cwd, fsModule });
|
|
92
|
+
const searchEnabled = options.search !== false;
|
|
93
|
+
logVerbose(`cwd: ${cwd}`);
|
|
94
|
+
let pendingNoFilesTip = null;
|
|
95
|
+
let pendingShortPromptTip = null;
|
|
96
|
+
if (files.length > 0) {
|
|
97
|
+
const displayPaths = files
|
|
98
|
+
.map((file) => path.relative(cwd, file.path) || file.path)
|
|
99
|
+
.slice(0, 10)
|
|
100
|
+
.join(', ');
|
|
101
|
+
const extra = files.length > 10 ? ` (+${files.length - 10} more)` : '';
|
|
102
|
+
logVerbose(`Attached files (${files.length}): ${displayPaths}${extra}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
logVerbose('No files attached.');
|
|
106
|
+
if (!isPreview) {
|
|
107
|
+
pendingNoFilesTip =
|
|
108
|
+
'Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const shortPrompt = (options.prompt?.trim().length ?? 0) < 80;
|
|
112
|
+
if (!isPreview && shortPrompt) {
|
|
113
|
+
pendingShortPromptTip =
|
|
114
|
+
'Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.';
|
|
115
|
+
}
|
|
116
|
+
const fileTokenInfo = getFileTokenStats(files, {
|
|
117
|
+
cwd,
|
|
118
|
+
tokenizer: modelConfig.tokenizer,
|
|
119
|
+
tokenizerOptions: TOKENIZER_OPTIONS,
|
|
120
|
+
inputTokenBudget,
|
|
121
|
+
});
|
|
122
|
+
const totalFileTokens = fileTokenInfo.totalTokens;
|
|
123
|
+
logVerbose(`Attached files use ${totalFileTokens.toLocaleString()} tokens`);
|
|
124
|
+
const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
125
|
+
const promptWithFiles = buildPrompt(options.prompt, files, cwd);
|
|
126
|
+
const fileCount = files.length;
|
|
127
|
+
const richTty = allowStdout && process.stdout.isTTY && chalk.level > 0;
|
|
128
|
+
const renderPlain = Boolean(options.renderPlain);
|
|
129
|
+
const timeoutSeconds = options.timeoutSeconds === undefined || options.timeoutSeconds === 'auto'
|
|
130
|
+
? isLongRunningModel
|
|
131
|
+
? DEFAULT_TIMEOUT_PRO_MS / 1000
|
|
132
|
+
: DEFAULT_TIMEOUT_NON_PRO_MS / 1000
|
|
133
|
+
: options.timeoutSeconds;
|
|
134
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
135
|
+
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
136
|
+
const effectiveModelId = options.effectiveModelId ??
|
|
137
|
+
(options.model.startsWith('gemini')
|
|
138
|
+
? resolveGeminiModelId(options.model)
|
|
139
|
+
: modelConfig.apiModel ?? modelConfig.model);
|
|
140
|
+
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
141
|
+
const requestBody = buildRequestBody({
|
|
142
|
+
modelConfig,
|
|
143
|
+
systemPrompt,
|
|
144
|
+
userPrompt: promptWithFiles,
|
|
145
|
+
searchEnabled,
|
|
146
|
+
maxOutputTokens: options.maxOutput,
|
|
147
|
+
background: useBackground,
|
|
148
|
+
storeResponse: useBackground,
|
|
149
|
+
});
|
|
150
|
+
const estimatedInputTokens = estimateRequestTokens(requestBody, modelConfig);
|
|
151
|
+
const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
|
|
152
|
+
const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
|
|
153
|
+
const filesPhrase = fileCount === 0 ? 'no files' : `${fileLabel} files`;
|
|
154
|
+
const headerLine = `Calling ${headerModelLabel} — ${tokenLabel} tokens, ${filesPhrase}.`;
|
|
155
|
+
const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
|
|
156
|
+
if (!isPreview) {
|
|
157
|
+
if (!options.suppressHeader) {
|
|
158
|
+
log(headerLine);
|
|
159
|
+
}
|
|
160
|
+
const maskedKey = maskApiKey(apiKey);
|
|
161
|
+
if (maskedKey && options.verbose) {
|
|
162
|
+
const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (resolved: ${effectiveModelId})` : '';
|
|
163
|
+
log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
|
|
164
|
+
}
|
|
165
|
+
if (baseUrl) {
|
|
166
|
+
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
167
|
+
}
|
|
168
|
+
if (!options.suppressTips) {
|
|
169
|
+
if (pendingNoFilesTip) {
|
|
170
|
+
log(dim(pendingNoFilesTip));
|
|
171
|
+
}
|
|
172
|
+
if (pendingShortPromptTip) {
|
|
173
|
+
log(dim(pendingShortPromptTip));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (isLongRunningModel) {
|
|
177
|
+
log(dim('This model can take up to 60 minutes (usually replies much faster).'));
|
|
178
|
+
}
|
|
179
|
+
if (options.verbose || isLongRunningModel) {
|
|
180
|
+
log(dim('Press Ctrl+C to cancel.'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (shouldReportFiles) {
|
|
184
|
+
printFileTokenStats(fileTokenInfo, { inputTokenBudget, log });
|
|
185
|
+
}
|
|
186
|
+
if (estimatedInputTokens > inputTokenBudget) {
|
|
187
|
+
throw new PromptValidationError(`Input too large (${estimatedInputTokens.toLocaleString()} tokens). Limit is ${inputTokenBudget.toLocaleString()} tokens.`, { estimatedInputTokens, inputTokenBudget });
|
|
188
|
+
}
|
|
189
|
+
logVerbose(`Estimated tokens (request body): ${estimatedInputTokens.toLocaleString()}`);
|
|
190
|
+
if (isPreview && previewMode) {
|
|
191
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
192
|
+
log('Request JSON');
|
|
193
|
+
log(JSON.stringify(requestBody, null, 2));
|
|
194
|
+
log('');
|
|
195
|
+
}
|
|
196
|
+
if (previewMode === 'full') {
|
|
197
|
+
log('Assembled Prompt');
|
|
198
|
+
log(promptWithFiles);
|
|
199
|
+
log('');
|
|
200
|
+
}
|
|
201
|
+
log(`Estimated input tokens: ${estimatedInputTokens.toLocaleString()} / ${inputTokenBudget.toLocaleString()} (model: ${modelConfig.model})`);
|
|
202
|
+
return {
|
|
203
|
+
mode: 'preview',
|
|
204
|
+
previewMode,
|
|
205
|
+
requestBody,
|
|
206
|
+
estimatedInputTokens,
|
|
207
|
+
inputTokenBudget,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const apiEndpoint = modelConfig.model.startsWith('gemini')
|
|
211
|
+
? undefined
|
|
212
|
+
: modelConfig.model.startsWith('claude')
|
|
213
|
+
? process.env.ANTHROPIC_BASE_URL ?? baseUrl
|
|
214
|
+
: baseUrl;
|
|
215
|
+
const clientInstance = client ??
|
|
216
|
+
clientFactory(apiKey, {
|
|
217
|
+
baseUrl: apiEndpoint,
|
|
218
|
+
azure: options.azure,
|
|
219
|
+
model: options.model,
|
|
220
|
+
resolvedModelId: modelConfig.model.startsWith('claude')
|
|
221
|
+
? resolveClaudeModelId(effectiveModelId)
|
|
222
|
+
: modelConfig.model.startsWith('gemini')
|
|
223
|
+
? resolveGeminiModelId(effectiveModelId)
|
|
224
|
+
: effectiveModelId,
|
|
225
|
+
});
|
|
226
|
+
logVerbose('Dispatching request to API...');
|
|
227
|
+
if (options.verbose) {
|
|
228
|
+
log(''); // ensure verbose section is separated from Answer stream
|
|
229
|
+
}
|
|
230
|
+
const stopOscProgress = startOscProgress({
|
|
231
|
+
label: useBackground ? 'Waiting for API (background)' : 'Waiting for API',
|
|
232
|
+
targetMs: useBackground ? timeoutMs : Math.min(timeoutMs, 10 * 60_000),
|
|
233
|
+
indeterminate: true,
|
|
234
|
+
write: sinkWrite,
|
|
235
|
+
});
|
|
236
|
+
const runStart = now();
|
|
237
|
+
let response = null;
|
|
238
|
+
let elapsedMs = 0;
|
|
239
|
+
let sawTextDelta = false;
|
|
240
|
+
// Buffer streamed text so we can re-render markdown once the stream ends (TTY + non-plain mode).
|
|
241
|
+
const streamedChunks = [];
|
|
242
|
+
let answerHeaderPrinted = false;
|
|
243
|
+
const allowAnswerHeader = options.suppressAnswerHeader !== true;
|
|
244
|
+
const timeoutExceeded = () => now() - runStart >= timeoutMs;
|
|
245
|
+
const throwIfTimedOut = () => {
|
|
246
|
+
if (timeoutExceeded()) {
|
|
247
|
+
throw new OracleTransportError('client-timeout', `Timed out waiting for API response after ${formatElapsed(timeoutMs)}.`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const ensureAnswerHeader = () => {
|
|
251
|
+
if (options.silent || answerHeaderPrinted)
|
|
252
|
+
return;
|
|
253
|
+
// Always add a separating newline for readability; optionally include the label depending on caller needs.
|
|
254
|
+
log('');
|
|
255
|
+
if (allowAnswerHeader) {
|
|
256
|
+
log(chalk.bold('Answer:'));
|
|
257
|
+
}
|
|
258
|
+
answerHeaderPrinted = true;
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
if (useBackground) {
|
|
262
|
+
response = await executeBackgroundResponse({
|
|
263
|
+
client: clientInstance,
|
|
264
|
+
requestBody,
|
|
265
|
+
log,
|
|
266
|
+
wait,
|
|
267
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
268
|
+
now,
|
|
269
|
+
maxWaitMs: timeoutMs,
|
|
270
|
+
});
|
|
271
|
+
elapsedMs = now() - runStart;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const stream = await clientInstance.responses.stream(requestBody);
|
|
275
|
+
let heartbeatActive = false;
|
|
276
|
+
let stopHeartbeat = null;
|
|
277
|
+
const stopHeartbeatNow = () => {
|
|
278
|
+
if (!heartbeatActive) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
heartbeatActive = false;
|
|
282
|
+
stopHeartbeat?.();
|
|
283
|
+
stopHeartbeat = null;
|
|
284
|
+
};
|
|
285
|
+
if (options.heartbeatIntervalMs && options.heartbeatIntervalMs > 0) {
|
|
286
|
+
heartbeatActive = true;
|
|
287
|
+
stopHeartbeat = startHeartbeat({
|
|
288
|
+
intervalMs: options.heartbeatIntervalMs,
|
|
289
|
+
log: (message) => log(message),
|
|
290
|
+
isActive: () => heartbeatActive,
|
|
291
|
+
makeMessage: (elapsedMs) => {
|
|
292
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
293
|
+
const remainingMs = Math.max(timeoutMs - elapsedMs, 0);
|
|
294
|
+
const remainingLabel = remainingMs >= 60_000
|
|
295
|
+
? `${Math.ceil(remainingMs / 60_000)} min`
|
|
296
|
+
: `${Math.max(1, Math.ceil(remainingMs / 1000))}s`;
|
|
297
|
+
return `API connection active — ${elapsedText} elapsed. Timeout in ~${remainingLabel} if no response.`;
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
for await (const event of stream) {
|
|
303
|
+
throwIfTimedOut();
|
|
304
|
+
const isTextDelta = event.type === 'chunk' || event.type === 'response.output_text.delta';
|
|
305
|
+
if (isTextDelta) {
|
|
306
|
+
stopOscProgress();
|
|
307
|
+
stopHeartbeatNow();
|
|
308
|
+
sawTextDelta = true;
|
|
309
|
+
ensureAnswerHeader();
|
|
310
|
+
if (!options.silent && typeof event.delta === 'string') {
|
|
311
|
+
// Always keep the log/bookkeeping sink up to date.
|
|
312
|
+
sinkWrite(event.delta);
|
|
313
|
+
if (renderPlain) {
|
|
314
|
+
// Plain mode: stream directly to stdout regardless of write sink.
|
|
315
|
+
stdoutWrite(event.delta);
|
|
316
|
+
}
|
|
317
|
+
else if (isTty) {
|
|
318
|
+
// Buffer for end-of-stream markdown rendering on TTY.
|
|
319
|
+
streamedChunks.push(event.delta);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Non-TTY streams should still surface output; fall back to raw stdout.
|
|
323
|
+
stdoutWrite(event.delta);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throwIfTimedOut();
|
|
329
|
+
}
|
|
330
|
+
catch (streamError) {
|
|
331
|
+
// stream.abort() is not available on the interface
|
|
332
|
+
stopHeartbeatNow();
|
|
333
|
+
const transportError = toTransportError(streamError);
|
|
334
|
+
log(chalk.yellow(describeTransportError(transportError, timeoutMs)));
|
|
335
|
+
throw transportError;
|
|
336
|
+
}
|
|
337
|
+
response = await stream.finalResponse();
|
|
338
|
+
throwIfTimedOut();
|
|
339
|
+
stopHeartbeatNow();
|
|
340
|
+
elapsedMs = now() - runStart;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
stopOscProgress();
|
|
345
|
+
}
|
|
346
|
+
if (!response) {
|
|
347
|
+
throw new Error('API did not return a response.');
|
|
348
|
+
}
|
|
349
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: we only add spacing when any streamed text was printed
|
|
350
|
+
if (sawTextDelta && !options.silent) {
|
|
351
|
+
const fullStreamedText = streamedChunks.join('');
|
|
352
|
+
const shouldRenderAfterStream = isTty && !renderPlain && fullStreamedText.length > 0;
|
|
353
|
+
if (shouldRenderAfterStream) {
|
|
354
|
+
const rendered = renderMarkdownAnsi(fullStreamedText);
|
|
355
|
+
stdoutWrite(rendered);
|
|
356
|
+
if (!rendered.endsWith('\n')) {
|
|
357
|
+
stdoutWrite('\n');
|
|
358
|
+
}
|
|
359
|
+
log('');
|
|
360
|
+
}
|
|
361
|
+
else if (renderPlain) {
|
|
362
|
+
// Plain streaming already wrote chunks; ensure clean separation.
|
|
363
|
+
stdoutWrite('\n');
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
log('');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
logVerbose(`Response status: ${response.status ?? 'completed'}`);
|
|
370
|
+
if (response.status && response.status !== 'completed') {
|
|
371
|
+
// API can reply `in_progress` even after the stream closes; give it a brief grace poll.
|
|
372
|
+
if (response.id && response.status === 'in_progress') {
|
|
373
|
+
const polishingStart = now();
|
|
374
|
+
const pollIntervalMs = 2_000;
|
|
375
|
+
const maxWaitMs = 60_000;
|
|
376
|
+
log(chalk.dim('Response still in_progress; polling until completion...'));
|
|
377
|
+
// Short polling loop — we don't want to hang forever, just catch late finalization.
|
|
378
|
+
while (now() - polishingStart < maxWaitMs) {
|
|
379
|
+
await wait(pollIntervalMs);
|
|
380
|
+
const refreshed = await clientInstance.responses.retrieve(response.id);
|
|
381
|
+
if (refreshed.status === 'completed') {
|
|
382
|
+
response = refreshed;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (response.status !== 'completed') {
|
|
388
|
+
const detail = response.error?.message || response.incomplete_details?.reason || response.status;
|
|
389
|
+
log(chalk.yellow(`API ended the run early (status=${response.status}${response.incomplete_details?.reason ? `, reason=${response.incomplete_details.reason}` : ''}).`));
|
|
390
|
+
throw new OracleResponseError(`Response did not complete: ${detail}`, response);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const answerText = extractTextOutput(response);
|
|
394
|
+
if (!options.silent) {
|
|
395
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: flips true when streaming events arrive
|
|
396
|
+
if (sawTextDelta) {
|
|
397
|
+
// Already handled above (rendered or streamed); avoid double-printing.
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
ensureAnswerHeader();
|
|
401
|
+
// Render markdown to ANSI in rich TTYs unless the caller opts out with --render-plain.
|
|
402
|
+
const printable = answerText
|
|
403
|
+
? renderPlain || !richTty
|
|
404
|
+
? answerText
|
|
405
|
+
: renderMarkdownAnsi(answerText)
|
|
406
|
+
: chalk.dim('(no text output)');
|
|
407
|
+
sinkWrite(printable);
|
|
408
|
+
if (!printable.endsWith('\n')) {
|
|
409
|
+
sinkWrite('\n');
|
|
410
|
+
}
|
|
411
|
+
stdoutWrite(printable);
|
|
412
|
+
if (!printable.endsWith('\n')) {
|
|
413
|
+
stdoutWrite('\n');
|
|
414
|
+
}
|
|
415
|
+
log('');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const usage = response.usage ?? {};
|
|
419
|
+
const inputTokens = usage.input_tokens ?? estimatedInputTokens;
|
|
420
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
421
|
+
const reasoningTokens = usage.reasoning_tokens ?? 0;
|
|
422
|
+
const totalTokens = usage.total_tokens ?? inputTokens + outputTokens + reasoningTokens;
|
|
423
|
+
const pricing = modelConfig.pricing ?? undefined;
|
|
424
|
+
const cost = pricing
|
|
425
|
+
? inputTokens * pricing.inputPerToken + outputTokens * pricing.outputPerToken
|
|
426
|
+
: undefined;
|
|
427
|
+
const elapsedDisplay = formatElapsed(elapsedMs);
|
|
428
|
+
const statsParts = [];
|
|
429
|
+
const effortLabel = modelConfig.reasoning?.effort;
|
|
430
|
+
const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
|
|
431
|
+
const sessionIdContainsModel = typeof options.sessionId === 'string' && options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
|
|
432
|
+
// Avoid duplicating the model name in the prefix (session id) and the stats bundle; keep a single source of truth.
|
|
433
|
+
if (!sessionIdContainsModel) {
|
|
434
|
+
statsParts.push(modelLabel);
|
|
435
|
+
}
|
|
436
|
+
if (cost != null) {
|
|
437
|
+
statsParts.push(formatUSD(cost));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
statsParts.push('cost=N/A');
|
|
441
|
+
}
|
|
442
|
+
const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
|
|
443
|
+
.map((value, index) => formatTokenValue(value, usage, index))
|
|
444
|
+
.join('/');
|
|
445
|
+
const tokensLabel = options.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
446
|
+
statsParts.push(`${tokensLabel}=${tokensDisplay}`);
|
|
447
|
+
if (options.verbose) {
|
|
448
|
+
// Only surface request-vs-response deltas when verbose is explicitly requested to keep default stats compact.
|
|
449
|
+
const actualInput = usage.input_tokens;
|
|
450
|
+
if (actualInput !== undefined) {
|
|
451
|
+
const delta = actualInput - estimatedInputTokens;
|
|
452
|
+
const deltaText = delta === 0 ? '' : delta > 0 ? ` (+${delta.toLocaleString()})` : ` (${delta.toLocaleString()})`;
|
|
453
|
+
statsParts.push(`est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!searchEnabled) {
|
|
457
|
+
statsParts.push('search=off');
|
|
458
|
+
}
|
|
459
|
+
if (files.length > 0) {
|
|
460
|
+
statsParts.push(`files=${files.length}`);
|
|
461
|
+
}
|
|
462
|
+
const sessionPrefix = options.sessionId ? `${options.sessionId} ` : '';
|
|
463
|
+
log(chalk.blue(`Finished ${sessionPrefix}in ${elapsedDisplay} (${statsParts.join(' | ')})`));
|
|
464
|
+
return {
|
|
465
|
+
mode: 'live',
|
|
466
|
+
response,
|
|
467
|
+
usage: { inputTokens, outputTokens, reasoningTokens, totalTokens, ...(cost != null ? { cost } : {}) },
|
|
468
|
+
elapsedMs,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
export function extractTextOutput(response) {
|
|
472
|
+
if (Array.isArray(response.output_text) && response.output_text.length > 0) {
|
|
473
|
+
return response.output_text.join('\n');
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(response.output)) {
|
|
476
|
+
const segments = [];
|
|
477
|
+
for (const item of response.output) {
|
|
478
|
+
if (Array.isArray(item.content)) {
|
|
479
|
+
for (const chunk of item.content) {
|
|
480
|
+
if (chunk && (chunk.type === 'output_text' || chunk.type === 'text') && chunk.text) {
|
|
481
|
+
segments.push(chunk.text);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else if (typeof item.text === 'string') {
|
|
486
|
+
segments.push(item.text);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return segments.join('\n');
|
|
490
|
+
}
|
|
491
|
+
return '';
|
|
492
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function resolvePreviewMode(value) {
|
|
2
|
+
const allowed = new Set(['summary', 'json', 'full']);
|
|
3
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
4
|
+
return allowed.has(value) ? value : 'summary';
|
|
5
|
+
}
|
|
6
|
+
if (value) {
|
|
7
|
+
return 'summary';
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function formatTokenEstimate(value, format = (text) => text) {
|
|
12
|
+
if (value >= 1000) {
|
|
13
|
+
const abbreviated = Math.floor(value / 100) / 10; // 4,252 -> 4.2
|
|
14
|
+
const text = `${abbreviated.toFixed(1).replace(/\.0$/, '')}k`;
|
|
15
|
+
return format(text);
|
|
16
|
+
}
|
|
17
|
+
const text = value.toLocaleString();
|
|
18
|
+
return format(text);
|
|
19
|
+
}
|
|
20
|
+
export function formatTokenValue(value, usage, index) {
|
|
21
|
+
const estimatedFlag = (index === 0 && usage?.input_tokens == null) ||
|
|
22
|
+
(index === 1 && usage?.output_tokens == null) ||
|
|
23
|
+
(index === 2 && usage?.reasoning_tokens == null) ||
|
|
24
|
+
(index === 3 && usage?.total_tokens == null);
|
|
25
|
+
const text = value.toLocaleString();
|
|
26
|
+
return estimatedFlag ? `${text}*` : text;
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TOKENIZER_OPTIONS } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Estimate input tokens from the full request body instead of just system/user text.
|
|
4
|
+
* This is a conservative approximation: we tokenize the key textual fields and add a fixed buffer
|
|
5
|
+
* to cover structural JSON overhead and server-side wrappers (tools/reasoning/background/store).
|
|
6
|
+
*/
|
|
7
|
+
export function estimateRequestTokens(requestBody, modelConfig, bufferTokens = 200) {
|
|
8
|
+
const SEARCH_RESULT_BUFFER_TOKENS = 4000;
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (requestBody.instructions) {
|
|
11
|
+
parts.push(requestBody.instructions);
|
|
12
|
+
}
|
|
13
|
+
for (const turn of requestBody.input ?? []) {
|
|
14
|
+
for (const content of turn.content ?? []) {
|
|
15
|
+
if (typeof content.text === 'string') {
|
|
16
|
+
parts.push(content.text);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (requestBody.tools && requestBody.tools.length > 0) {
|
|
21
|
+
parts.push(JSON.stringify(requestBody.tools));
|
|
22
|
+
}
|
|
23
|
+
if (requestBody.reasoning) {
|
|
24
|
+
parts.push(JSON.stringify(requestBody.reasoning));
|
|
25
|
+
}
|
|
26
|
+
if (requestBody.background) {
|
|
27
|
+
parts.push('background:true');
|
|
28
|
+
}
|
|
29
|
+
if (requestBody.store) {
|
|
30
|
+
parts.push('store:true');
|
|
31
|
+
}
|
|
32
|
+
const concatenated = parts.join('\n');
|
|
33
|
+
const baseEstimate = modelConfig.tokenizer(concatenated, TOKENIZER_OPTIONS);
|
|
34
|
+
const hasWebSearch = requestBody.tools?.some((tool) => tool?.type === 'web_search_preview');
|
|
35
|
+
const searchBuffer = hasWebSearch ? SEARCH_RESULT_BUFFER_TOKENS : 0;
|
|
36
|
+
return baseEstimate + bufferTokens + searchBuffer;
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createFileSections } from './files.js';
|
|
3
|
+
export function getFileTokenStats(files, { cwd = process.cwd(), tokenizer, tokenizerOptions, inputTokenBudget, }) {
|
|
4
|
+
if (!files.length) {
|
|
5
|
+
return { stats: [], totalTokens: 0 };
|
|
6
|
+
}
|
|
7
|
+
const sections = createFileSections(files, cwd);
|
|
8
|
+
const stats = sections
|
|
9
|
+
.map((section) => {
|
|
10
|
+
const tokens = tokenizer(section.sectionText, tokenizerOptions);
|
|
11
|
+
const percent = inputTokenBudget ? (tokens / inputTokenBudget) * 100 : undefined;
|
|
12
|
+
return {
|
|
13
|
+
path: section.absolutePath,
|
|
14
|
+
displayPath: section.displayPath,
|
|
15
|
+
tokens,
|
|
16
|
+
percent,
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
20
|
+
const totalTokens = stats.reduce((sum, entry) => sum + entry.tokens, 0);
|
|
21
|
+
return { stats, totalTokens };
|
|
22
|
+
}
|
|
23
|
+
export function printFileTokenStats({ stats, totalTokens }, { inputTokenBudget, log = console.log }) {
|
|
24
|
+
if (!stats.length) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
log(chalk.bold('File Token Usage'));
|
|
28
|
+
for (const entry of stats) {
|
|
29
|
+
const percentLabel = inputTokenBudget && entry.percent != null ? `${entry.percent.toFixed(2)}%` : 'n/a';
|
|
30
|
+
log(`${entry.tokens.toLocaleString().padStart(10)} ${percentLabel.padStart(8)} ${entry.displayPath}`);
|
|
31
|
+
}
|
|
32
|
+
if (inputTokenBudget) {
|
|
33
|
+
const totalPercent = (totalTokens / inputTokenBudget) * 100;
|
|
34
|
+
log(`Total: ${totalTokens.toLocaleString()} tokens (${totalPercent.toFixed(2)}% of ${inputTokenBudget.toLocaleString()})`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
log(`Total: ${totalTokens.toLocaleString()} tokens`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Minimal helper to stringify arbitrary input for tokenizer consumption.
|
|
2
|
+
// Anthropic's tokenizer expects a string; we accept unknown and coerce safely.
|
|
3
|
+
export function stringifyTokenizerInput(input) {
|
|
4
|
+
if (typeof input === 'string')
|
|
5
|
+
return input;
|
|
6
|
+
if (input === null || input === undefined)
|
|
7
|
+
return '';
|
|
8
|
+
if (typeof input === 'number' || typeof input === 'boolean' || typeof input === 'bigint') {
|
|
9
|
+
return String(input);
|
|
10
|
+
}
|
|
11
|
+
if (typeof input === 'object') {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(input);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// fall through to generic stringification
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (typeof input === 'function') {
|
|
20
|
+
return input.toString();
|
|
21
|
+
}
|
|
22
|
+
return String(input);
|
|
23
|
+
}
|
|
24
|
+
export default stringifyTokenizerInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './oracle/types.js';
|
|
2
|
+
export { MODEL_CONFIGS, DEFAULT_MODEL, PRO_MODELS, DEFAULT_SYSTEM_PROMPT, TOKENIZER_OPTIONS, } from './oracle/config.js';
|
|
3
|
+
export { readFiles, createFileSections } from './oracle/files.js';
|
|
4
|
+
export { buildPrompt, buildRequestBody, renderPromptMarkdown } from './oracle/request.js';
|
|
5
|
+
export { estimateRequestTokens } from './oracle/tokenEstimate.js';
|
|
6
|
+
export { formatUSD, formatNumber, formatElapsed } from './oracle/format.js';
|
|
7
|
+
export { formatFileSection } from './oracle/markdown.js';
|
|
8
|
+
export { getFileTokenStats, printFileTokenStats } from './oracle/tokenStats.js';
|
|
9
|
+
export { OracleResponseError, OracleTransportError, OracleUserError, FileValidationError, BrowserAutomationError, PromptValidationError, describeTransportError, extractResponseMetadata, asOracleUserError, toTransportError, } from './oracle/errors.js';
|
|
10
|
+
export { createDefaultClientFactory } from './oracle/client.js';
|
|
11
|
+
export { runOracle, extractTextOutput } from './oracle/run.js';
|
|
12
|
+
export { resolveGeminiModelId } from './oracle/gemini.js';
|