@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,193 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { CHATGPT_URL, DEFAULT_MODEL_TARGET, normalizeChatgptUrl, parseDuration } from '../browserMode.js';
|
|
5
|
+
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
6
|
+
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
8
|
+
const BROWSER_MODEL_LABELS = {
|
|
9
|
+
'gpt-5-pro': 'GPT-5 Pro',
|
|
10
|
+
'gpt-5.1-pro': 'GPT-5.1 Pro',
|
|
11
|
+
'gpt-5.1': 'GPT-5.1',
|
|
12
|
+
'gemini-3-pro': 'Gemini 3 Pro',
|
|
13
|
+
};
|
|
14
|
+
export async function buildBrowserConfig(options) {
|
|
15
|
+
const desiredModelOverride = options.browserModelLabel?.trim();
|
|
16
|
+
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? '';
|
|
17
|
+
const baseModel = options.model.toLowerCase();
|
|
18
|
+
const shouldUseOverride = normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
19
|
+
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
20
|
+
const inline = await resolveInlineCookies({
|
|
21
|
+
inlineArg: options.browserInlineCookies,
|
|
22
|
+
inlineFileArg: options.browserInlineCookiesFile,
|
|
23
|
+
envPayload: process.env.ORACLE_BROWSER_COOKIES_JSON,
|
|
24
|
+
envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
});
|
|
27
|
+
let remoteChrome;
|
|
28
|
+
if (options.remoteChrome) {
|
|
29
|
+
remoteChrome = parseRemoteChromeTarget(options.remoteChrome);
|
|
30
|
+
}
|
|
31
|
+
const rawUrl = options.chatgptUrl ?? options.browserUrl;
|
|
32
|
+
const url = rawUrl ? normalizeChatgptUrl(rawUrl, CHATGPT_URL) : undefined;
|
|
33
|
+
return {
|
|
34
|
+
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
35
|
+
chromePath: options.browserChromePath ?? null,
|
|
36
|
+
chromeCookiePath: options.browserCookiePath ?? null,
|
|
37
|
+
url,
|
|
38
|
+
timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
|
|
39
|
+
inputTimeoutMs: options.browserInputTimeout
|
|
40
|
+
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
41
|
+
: undefined,
|
|
42
|
+
cookieSync: options.browserNoCookieSync ? false : undefined,
|
|
43
|
+
cookieNames,
|
|
44
|
+
inlineCookies: inline?.cookies,
|
|
45
|
+
inlineCookiesSource: inline?.source ?? null,
|
|
46
|
+
headless: undefined, // disable headless; Cloudflare blocks it
|
|
47
|
+
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
48
|
+
hideWindow: options.browserHideWindow ? true : undefined,
|
|
49
|
+
desiredModel: shouldUseOverride ? desiredModelOverride : mapModelToBrowserLabel(options.model),
|
|
50
|
+
debug: options.verbose ? true : undefined,
|
|
51
|
+
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
52
|
+
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
53
|
+
remoteChrome,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function mapModelToBrowserLabel(model) {
|
|
57
|
+
return BROWSER_MODEL_LABELS[model] ?? DEFAULT_MODEL_TARGET;
|
|
58
|
+
}
|
|
59
|
+
export function resolveBrowserModelLabel(input, model) {
|
|
60
|
+
const trimmed = input?.trim?.() ?? '';
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
return mapModelToBrowserLabel(model);
|
|
63
|
+
}
|
|
64
|
+
const normalizedInput = trimmed.toLowerCase();
|
|
65
|
+
if (normalizedInput === model.toLowerCase()) {
|
|
66
|
+
return mapModelToBrowserLabel(model);
|
|
67
|
+
}
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
function parseRemoteChromeTarget(raw) {
|
|
71
|
+
const target = raw.trim();
|
|
72
|
+
if (!target) {
|
|
73
|
+
throw new Error('Invalid remote-chrome value: expected host:port but received an empty string.');
|
|
74
|
+
}
|
|
75
|
+
const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
|
|
76
|
+
let host;
|
|
77
|
+
let portSegment;
|
|
78
|
+
if (ipv6Match) {
|
|
79
|
+
host = ipv6Match[1]?.trim();
|
|
80
|
+
portSegment = ipv6Match[2]?.trim();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const lastColon = target.lastIndexOf(':');
|
|
84
|
+
if (lastColon === -1) {
|
|
85
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`);
|
|
86
|
+
}
|
|
87
|
+
host = target.slice(0, lastColon).trim();
|
|
88
|
+
portSegment = target.slice(lastColon + 1).trim();
|
|
89
|
+
if (host.includes(':')) {
|
|
90
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Wrap IPv6 addresses in brackets, e.g. --remote-chrome "[2001:db8::1]:9222".`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!host) {
|
|
94
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Host portion is missing; expected host:port.`);
|
|
95
|
+
}
|
|
96
|
+
const port = Number.parseInt(portSegment ?? '', 10);
|
|
97
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
98
|
+
throw new Error(`Invalid remote-chrome port: "${portSegment ?? ''}". Expected a number between 1 and 65535.`);
|
|
99
|
+
}
|
|
100
|
+
return { host, port };
|
|
101
|
+
}
|
|
102
|
+
function parseCookieNames(raw) {
|
|
103
|
+
if (!raw)
|
|
104
|
+
return undefined;
|
|
105
|
+
const names = raw
|
|
106
|
+
.split(',')
|
|
107
|
+
.map((entry) => entry.trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
return names.length ? names : undefined;
|
|
110
|
+
}
|
|
111
|
+
async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envFile, cwd, }) {
|
|
112
|
+
const tryLoad = async (source, allowPathResolution) => {
|
|
113
|
+
if (!source)
|
|
114
|
+
return undefined;
|
|
115
|
+
const trimmed = source.trim();
|
|
116
|
+
if (!trimmed)
|
|
117
|
+
return undefined;
|
|
118
|
+
if (allowPathResolution) {
|
|
119
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(cwd, trimmed);
|
|
120
|
+
try {
|
|
121
|
+
const stat = await fs.stat(resolved);
|
|
122
|
+
if (stat.isFile()) {
|
|
123
|
+
const fileContent = await fs.readFile(resolved, 'utf8');
|
|
124
|
+
const parsed = parseInlineCookiesPayload(fileContent);
|
|
125
|
+
if (parsed)
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// not a file; treat as payload below
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return parseInlineCookiesPayload(trimmed);
|
|
134
|
+
};
|
|
135
|
+
const sources = [
|
|
136
|
+
{ value: inlineFileArg, allowPath: true, source: 'inline-file' },
|
|
137
|
+
{ value: inlineArg, allowPath: true, source: 'inline-arg' },
|
|
138
|
+
{ value: envFile, allowPath: true, source: 'env-file' },
|
|
139
|
+
{ value: envPayload, allowPath: false, source: 'env-payload' },
|
|
140
|
+
];
|
|
141
|
+
for (const { value, allowPath, source } of sources) {
|
|
142
|
+
const parsed = await tryLoad(value, allowPath);
|
|
143
|
+
if (parsed)
|
|
144
|
+
return { cookies: parsed, source };
|
|
145
|
+
}
|
|
146
|
+
// fallback: ~/.oracle/cookies.{json,base64}
|
|
147
|
+
const oracleHome = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
148
|
+
const candidates = ['cookies.json', 'cookies.base64'];
|
|
149
|
+
for (const file of candidates) {
|
|
150
|
+
const fullPath = path.join(oracleHome, file);
|
|
151
|
+
try {
|
|
152
|
+
const stat = await fs.stat(fullPath);
|
|
153
|
+
if (!stat.isFile())
|
|
154
|
+
continue;
|
|
155
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
156
|
+
const parsed = parseInlineCookiesPayload(content);
|
|
157
|
+
if (parsed)
|
|
158
|
+
return { cookies: parsed, source: `home:${file}` };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// ignore missing/invalid
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
function parseInlineCookiesPayload(raw) {
|
|
167
|
+
if (!raw)
|
|
168
|
+
return undefined;
|
|
169
|
+
const text = raw.trim();
|
|
170
|
+
if (!text)
|
|
171
|
+
return undefined;
|
|
172
|
+
let jsonPayload = text;
|
|
173
|
+
// Attempt base64 decode first; fall back to raw text on failure.
|
|
174
|
+
try {
|
|
175
|
+
const decoded = Buffer.from(text, 'base64').toString('utf8');
|
|
176
|
+
if (decoded.trim().startsWith('[')) {
|
|
177
|
+
jsonPayload = decoded;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// not base64; continue with raw text
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(jsonPayload);
|
|
185
|
+
if (Array.isArray(parsed)) {
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// invalid json; skip silently to keep this hidden flag non-fatal
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function warnIfOversizeBundle(estimatedTokens, threshold = 196_000, log = console.log) {
|
|
3
|
+
if (Number.isNaN(estimatedTokens) || estimatedTokens <= threshold) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const msg = `Warning: bundle is ~${estimatedTokens.toLocaleString()} tokens (>${threshold.toLocaleString()}); may exceed model limits.`;
|
|
7
|
+
log(chalk.red(msg));
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PRO_MODELS } from '../oracle.js';
|
|
2
|
+
export function shouldDetachSession({
|
|
3
|
+
// Params kept for future policy tweaks; currently only model/disableDetachEnv matter.
|
|
4
|
+
engine, model, waitPreference: _waitPreference, disableDetachEnv, }) {
|
|
5
|
+
if (disableDetachEnv)
|
|
6
|
+
return false;
|
|
7
|
+
// Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
|
|
8
|
+
if (PRO_MODELS.has(model) && engine === 'api')
|
|
9
|
+
return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from '../oracle.js';
|
|
3
|
+
import { assembleBrowserPrompt } from '../browser/prompt.js';
|
|
4
|
+
import { buildTokenEstimateSuffix, formatAttachmentLabel } from '../browser/promptSummary.js';
|
|
5
|
+
import { buildCookiePlan } from '../browser/policies.js';
|
|
6
|
+
export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
|
|
7
|
+
if (engine === 'browser') {
|
|
8
|
+
await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await runApiDryRun({ runOptions, cwd, version, log }, deps);
|
|
12
|
+
}
|
|
13
|
+
async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
14
|
+
const readFilesImpl = deps.readFilesImpl ?? readFiles;
|
|
15
|
+
const files = await readFilesImpl(runOptions.file ?? [], { cwd });
|
|
16
|
+
const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
17
|
+
const combinedPrompt = buildPrompt(runOptions.prompt ?? '', files, cwd);
|
|
18
|
+
const tokenizer = MODEL_CONFIGS[runOptions.model].tokenizer;
|
|
19
|
+
const estimatedInputTokens = tokenizer([
|
|
20
|
+
{ role: 'system', content: systemPrompt },
|
|
21
|
+
{ role: 'user', content: combinedPrompt },
|
|
22
|
+
], TOKENIZER_OPTIONS);
|
|
23
|
+
const headerLine = `[dry-run] Oracle (${version}) would call ${runOptions.model} with ~${estimatedInputTokens.toLocaleString()} tokens and ${files.length} files.`;
|
|
24
|
+
log(chalk.cyan(headerLine));
|
|
25
|
+
if (files.length === 0) {
|
|
26
|
+
log(chalk.dim('[dry-run] No files matched the provided --file patterns.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const inputBudget = runOptions.maxInput ?? MODEL_CONFIGS[runOptions.model].inputLimit;
|
|
30
|
+
const stats = getFileTokenStats(files, {
|
|
31
|
+
cwd,
|
|
32
|
+
tokenizer,
|
|
33
|
+
tokenizerOptions: TOKENIZER_OPTIONS,
|
|
34
|
+
inputTokenBudget: inputBudget,
|
|
35
|
+
});
|
|
36
|
+
printFileTokenStats(stats, { inputTokenBudget: inputBudget, log });
|
|
37
|
+
}
|
|
38
|
+
async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig, }, deps) {
|
|
39
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
40
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
41
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
42
|
+
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
43
|
+
log(chalk.cyan(headerLine));
|
|
44
|
+
logBrowserCookieStrategy(browserConfig, log, 'dry-run');
|
|
45
|
+
logBrowserFileSummary(artifacts, log, 'dry-run');
|
|
46
|
+
}
|
|
47
|
+
function logBrowserCookieStrategy(browserConfig, log, label) {
|
|
48
|
+
if (!browserConfig)
|
|
49
|
+
return;
|
|
50
|
+
const plan = buildCookiePlan(browserConfig);
|
|
51
|
+
log(chalk.bold(`[${label}] ${plan.description}`));
|
|
52
|
+
}
|
|
53
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
54
|
+
if (artifacts.attachments.length > 0) {
|
|
55
|
+
const prefix = artifacts.bundled ? `[${label}] Bundled upload:` : `[${label}] Attachments to upload:`;
|
|
56
|
+
log(chalk.bold(prefix));
|
|
57
|
+
artifacts.attachments.forEach((attachment) => {
|
|
58
|
+
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
59
|
+
});
|
|
60
|
+
if (artifacts.bundled) {
|
|
61
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (artifacts.inlineFileCount > 0) {
|
|
66
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
67
|
+
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? '' : 's'} pasted directly into the composer.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
71
|
+
}
|
|
72
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
73
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
74
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
75
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
76
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
77
|
+
log(chalk.cyan(headerLine));
|
|
78
|
+
logBrowserFileSummary(artifacts, log, 'preview');
|
|
79
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
80
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
81
|
+
path: attachment.path,
|
|
82
|
+
displayPath: attachment.displayPath,
|
|
83
|
+
sizeBytes: attachment.sizeBytes,
|
|
84
|
+
}));
|
|
85
|
+
const previewPayload = {
|
|
86
|
+
model: runOptions.model,
|
|
87
|
+
engine: 'browser',
|
|
88
|
+
composerText: artifacts.composerText,
|
|
89
|
+
attachments: attachmentSummary,
|
|
90
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
91
|
+
bundled: artifacts.bundled,
|
|
92
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
93
|
+
};
|
|
94
|
+
log('');
|
|
95
|
+
log(chalk.bold('Preview JSON'));
|
|
96
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
97
|
+
}
|
|
98
|
+
if (previewMode === 'full') {
|
|
99
|
+
log('');
|
|
100
|
+
log(chalk.bold('Composer Text'));
|
|
101
|
+
log(artifacts.composerText || chalk.dim('(empty prompt)'));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export async function shouldBlockDuplicatePrompt({ prompt, force, sessionStore, log = console.log, }) {
|
|
3
|
+
if (force)
|
|
4
|
+
return false;
|
|
5
|
+
const normalized = prompt?.trim();
|
|
6
|
+
if (!normalized)
|
|
7
|
+
return false;
|
|
8
|
+
const running = (await sessionStore.listSessions()).filter((entry) => entry.status === 'running');
|
|
9
|
+
const duplicate = running.find((entry) => (entry.options?.prompt?.trim?.() ?? '') === normalized);
|
|
10
|
+
if (!duplicate)
|
|
11
|
+
return false;
|
|
12
|
+
log(chalk.yellow(`A session with the same prompt is already running (${duplicate.id}). Reattach with "oracle session ${duplicate.id}" or rerun with --force to start another run.`));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PRO_MODELS } from '../oracle.js';
|
|
2
|
+
export function defaultWaitPreference(model, engine) {
|
|
3
|
+
// Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
|
|
4
|
+
if (engine === 'api' && PRO_MODELS.has(model)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return true; // browser or non-pro models are fast enough to block by default
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Determine which engine to use based on CLI flags and the environment.
|
|
11
|
+
*
|
|
12
|
+
* Precedence:
|
|
13
|
+
* 1) Legacy --browser flag forces browser.
|
|
14
|
+
* 2) Explicit --engine value.
|
|
15
|
+
* 3) OPENAI_API_KEY decides: api when set, otherwise browser.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveEngine({ engine, browserFlag, env, }) {
|
|
18
|
+
if (browserFlag) {
|
|
19
|
+
return 'browser';
|
|
20
|
+
}
|
|
21
|
+
if (engine) {
|
|
22
|
+
return engine;
|
|
23
|
+
}
|
|
24
|
+
return env.OPENAI_API_KEY ? 'api' : 'browser';
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const LOGGED_SYMBOL = Symbol('oracle.alreadyLogged');
|
|
2
|
+
export function markErrorLogged(error) {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
error[LOGGED_SYMBOL] = true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isErrorLogged(error) {
|
|
8
|
+
return Boolean(error instanceof Error && error[LOGGED_SYMBOL]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatCompactNumber(value) {
|
|
2
|
+
if (Number.isNaN(value) || !Number.isFinite(value))
|
|
3
|
+
return '0';
|
|
4
|
+
const abs = Math.abs(value);
|
|
5
|
+
const stripTrailingZero = (text) => text.replace(/\.0$/, '');
|
|
6
|
+
if (abs >= 1_000_000) {
|
|
7
|
+
return `${stripTrailingZero((value / 1_000_000).toFixed(1))}m`;
|
|
8
|
+
}
|
|
9
|
+
if (abs >= 1_000) {
|
|
10
|
+
return `${stripTrailingZero((value / 1_000).toFixed(1))}k`;
|
|
11
|
+
}
|
|
12
|
+
return value.toLocaleString();
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
const createColorWrapper = (isTty) => (styler) => (text) => isTty ? styler(text) : text;
|
|
3
|
+
export function applyHelpStyling(program, version, isTty) {
|
|
4
|
+
const wrap = createColorWrapper(isTty);
|
|
5
|
+
const colors = {
|
|
6
|
+
banner: wrap((text) => kleur.bold().blue(text)),
|
|
7
|
+
subtitle: wrap((text) => kleur.dim(text)),
|
|
8
|
+
section: wrap((text) => kleur.bold().white(text)),
|
|
9
|
+
bullet: wrap((text) => kleur.blue(text)),
|
|
10
|
+
command: wrap((text) => kleur.bold().blue(text)),
|
|
11
|
+
option: wrap((text) => kleur.cyan(text)),
|
|
12
|
+
argument: wrap((text) => kleur.magenta(text)),
|
|
13
|
+
description: wrap((text) => kleur.white(text)),
|
|
14
|
+
muted: wrap((text) => kleur.gray(text)),
|
|
15
|
+
accent: wrap((text) => kleur.cyan(text)),
|
|
16
|
+
};
|
|
17
|
+
program.configureHelp({
|
|
18
|
+
styleTitle(title) {
|
|
19
|
+
return colors.section(title);
|
|
20
|
+
},
|
|
21
|
+
styleDescriptionText(text) {
|
|
22
|
+
return colors.description(text);
|
|
23
|
+
},
|
|
24
|
+
styleCommandText(text) {
|
|
25
|
+
return colors.command(text);
|
|
26
|
+
},
|
|
27
|
+
styleSubcommandText(text) {
|
|
28
|
+
return colors.command(text);
|
|
29
|
+
},
|
|
30
|
+
styleOptionText(text) {
|
|
31
|
+
return colors.option(text);
|
|
32
|
+
},
|
|
33
|
+
styleArgumentText(text) {
|
|
34
|
+
return colors.argument(text);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
program.addHelpText('beforeAll', () => renderHelpBanner(version, colors));
|
|
38
|
+
program.addHelpText('after', () => renderHelpFooter(program, colors));
|
|
39
|
+
}
|
|
40
|
+
function renderHelpBanner(version, colors) {
|
|
41
|
+
const subtitle = 'GPT-5.1 Pro/GPT-5.1 for tough questions with code/file context.';
|
|
42
|
+
return `${colors.banner(`Oracle CLI v${version}`)} ${colors.subtitle(`— ${subtitle}`)}\n`;
|
|
43
|
+
}
|
|
44
|
+
function renderHelpFooter(program, colors) {
|
|
45
|
+
const tips = [
|
|
46
|
+
`${colors.bullet('•')} Oracle cannot see your project unless you pass ${colors.accent('--file …')} — attach the files/dirs you want it to read.`,
|
|
47
|
+
`${colors.bullet('•')} Attach lots of source (whole directories beat single files) and keep total input under ~196k tokens.`,
|
|
48
|
+
`${colors.bullet('•')} Oracle starts empty—open with a short project briefing (stack, services, build steps), spell out the question and prior attempts, and why it matters; the more explanation and context you provide, the better the response will be.`,
|
|
49
|
+
`${colors.bullet('•')} Spell out the project + platform + version requirements (repo name, target OS/toolchain versions, API dependencies) so Oracle doesn’t guess defaults.`,
|
|
50
|
+
`${colors.bullet('•')} When comparing multiple repos/files, spell out each repo + path + role (e.g., “Project A SettingsView → apps/project-a/Sources/SettingsView.swift; Project B SettingsView → ../project-b/mac/...”) so the model knows exactly which file is which.`,
|
|
51
|
+
`${colors.bullet('•')} Best results: 6–30 sentences plus key source files; very short prompts often yield generic answers.`,
|
|
52
|
+
`${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
|
|
53
|
+
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
54
|
+
`${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5.1-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
|
|
55
|
+
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
56
|
+
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
57
|
+
`${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
|
|
58
|
+
`${colors.bullet('•')} If any Oracle session is already running, do not start new API runs. Attach to the existing browser session instead; only trigger API calls when you explicitly mean to.`,
|
|
59
|
+
`${colors.bullet('•')} Duplicate prompt guard: if the same prompt is already running, new runs are blocked unless you pass ${colors.accent('--force')}—prefer reattaching instead of spawning duplicates.`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
const formatExample = (command, description) => `${colors.command(` ${command}`)}\n${colors.muted(` ${description}`)}`;
|
|
62
|
+
const examples = [
|
|
63
|
+
formatExample(`${program.name()} --render --copy --prompt "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"`, 'Build the bundle, print it, and copy it for manual paste into ChatGPT.'),
|
|
64
|
+
formatExample(`${program.name()} --prompt "Cross-check the data layer assumptions" --models gpt-5.1-pro,gemini-3-pro --file "src/**/*.ts"`, 'Run multiple API models in one go and aggregate cost/usage.'),
|
|
65
|
+
formatExample(`${program.name()} status --hours 72 --limit 50`, 'Show sessions from the last 72h (capped at 50 entries).'),
|
|
66
|
+
formatExample(`${program.name()} session <sessionId>`, 'Attach to a running/completed session and stream the saved transcript.'),
|
|
67
|
+
formatExample(`${program.name()} --prompt "Ship review" --slug "release-readiness-audit"`, 'Encourage the model to hand you a 3–5 word slug and pass it along with --slug.'),
|
|
68
|
+
formatExample(`${program.name()} --prompt "Tabs frozen: compare Project A SettingsView (apps/project-a/Sources/SettingsView.swift) vs Project B SettingsView (../project-b/mac/App/Presentation/Views/SettingsView.swift)" --file apps/project-a/Sources/SettingsView.swift --file ../project-b/mac/App/Presentation/Views/SettingsView.swift`, 'Spell out what each attached file is (repo + path + role) before asking for comparisons so the model knows exactly what it is reading.'),
|
|
69
|
+
].join('\n\n');
|
|
70
|
+
return `
|
|
71
|
+
${colors.section('Tips')}
|
|
72
|
+
${tips}
|
|
73
|
+
|
|
74
|
+
${colors.section('Examples')}
|
|
75
|
+
${examples}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize hidden alias flags so they behave like their primary counterparts.
|
|
3
|
+
*
|
|
4
|
+
* - `--message` maps to `--prompt` when no prompt is provided.
|
|
5
|
+
* - `--include` extends the `--file` list.
|
|
6
|
+
* - `--mode` maps to `--engine` for backward compatibility with older docs/UX.
|
|
7
|
+
*/
|
|
8
|
+
export function applyHiddenAliases(options, setOptionValue) {
|
|
9
|
+
if (options.include && options.include.length > 0) {
|
|
10
|
+
const mergedFiles = [...(options.file ?? []), ...options.include];
|
|
11
|
+
options.file = mergedFiles;
|
|
12
|
+
setOptionValue?.('file', mergedFiles);
|
|
13
|
+
}
|
|
14
|
+
if (!options.prompt && options.message) {
|
|
15
|
+
options.prompt = options.message;
|
|
16
|
+
setOptionValue?.('prompt', options.message);
|
|
17
|
+
}
|
|
18
|
+
if (!options.engine && options.mode) {
|
|
19
|
+
options.engine = options.mode;
|
|
20
|
+
setOptionValue?.('engine', options.mode);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { DEFAULT_SYSTEM_PROMPT } from '../oracle/config.js';
|
|
3
|
+
import { buildPrompt } from '../oracle/request.js';
|
|
4
|
+
import { createFileSections, readFiles } from '../oracle/files.js';
|
|
5
|
+
import { createFsAdapter } from '../oracle/fsAdapter.js';
|
|
6
|
+
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
7
|
+
export async function buildMarkdownBundle(options, deps = {}) {
|
|
8
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
9
|
+
const fsModule = deps.fs ?? createFsAdapter(fs);
|
|
10
|
+
const files = await readFiles(options.file ?? [], { cwd, fsModule });
|
|
11
|
+
const sections = createFileSections(files, cwd);
|
|
12
|
+
const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
13
|
+
const userPrompt = (options.prompt ?? '').trim();
|
|
14
|
+
const markdown = buildPromptMarkdown(systemPrompt, userPrompt, sections);
|
|
15
|
+
const promptWithFiles = buildPrompt(userPrompt, files, cwd);
|
|
16
|
+
return { markdown, promptWithFiles, systemPrompt, files };
|
|
17
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { render as renderMarkdown } from 'markdansi';
|
|
3
|
+
import { bundledLanguages, bundledThemes, createHighlighter, } from 'shiki';
|
|
4
|
+
const DEFAULT_THEME = 'github-dark';
|
|
5
|
+
const HIGHLIGHT_LANGS = ['ts', 'tsx', 'js', 'jsx', 'json', 'swift'];
|
|
6
|
+
const SUPPORTED_LANG_ALIASES = {
|
|
7
|
+
ts: 'ts',
|
|
8
|
+
typescript: 'ts',
|
|
9
|
+
tsx: 'tsx',
|
|
10
|
+
js: 'js',
|
|
11
|
+
javascript: 'js',
|
|
12
|
+
jsx: 'jsx',
|
|
13
|
+
json: 'json',
|
|
14
|
+
swift: 'swift',
|
|
15
|
+
};
|
|
16
|
+
const shikiPromise = createHighlighter({
|
|
17
|
+
themes: [bundledThemes[DEFAULT_THEME]],
|
|
18
|
+
langs: HIGHLIGHT_LANGS.map((lang) => bundledLanguages[lang]),
|
|
19
|
+
});
|
|
20
|
+
let shiki = null;
|
|
21
|
+
void shikiPromise
|
|
22
|
+
.then((instance) => {
|
|
23
|
+
shiki = instance;
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {
|
|
26
|
+
shiki = null;
|
|
27
|
+
});
|
|
28
|
+
export async function ensureShikiReady() {
|
|
29
|
+
if (shiki)
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
shiki = await shikiPromise;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
shiki = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function normalizeLanguage(lang) {
|
|
39
|
+
if (!lang)
|
|
40
|
+
return null;
|
|
41
|
+
const key = lang.toLowerCase();
|
|
42
|
+
return SUPPORTED_LANG_ALIASES[key] ?? null;
|
|
43
|
+
}
|
|
44
|
+
function styleToken(text, fontStyle = 0) {
|
|
45
|
+
let styled = text;
|
|
46
|
+
if (fontStyle & 1)
|
|
47
|
+
styled = chalk.italic(styled);
|
|
48
|
+
if (fontStyle & 2)
|
|
49
|
+
styled = chalk.bold(styled);
|
|
50
|
+
if (fontStyle & 4)
|
|
51
|
+
styled = chalk.underline(styled);
|
|
52
|
+
if (fontStyle & 8)
|
|
53
|
+
styled = chalk.strikethrough(styled);
|
|
54
|
+
return styled;
|
|
55
|
+
}
|
|
56
|
+
function shikiHighlighter(code, lang) {
|
|
57
|
+
if (!process.stdout.isTTY || !shiki)
|
|
58
|
+
return code;
|
|
59
|
+
const normalizedLang = normalizeLanguage(lang);
|
|
60
|
+
if (!normalizedLang)
|
|
61
|
+
return code;
|
|
62
|
+
try {
|
|
63
|
+
if (!shiki.getLoadedLanguages().includes(normalizedLang)) {
|
|
64
|
+
return code;
|
|
65
|
+
}
|
|
66
|
+
const { tokens } = shiki.codeToTokens(code, { lang: normalizedLang, theme: DEFAULT_THEME });
|
|
67
|
+
return tokens
|
|
68
|
+
.map((line) => line
|
|
69
|
+
.map((token) => {
|
|
70
|
+
const colored = token.color ? chalk.hex(token.color)(token.content) : token.content;
|
|
71
|
+
return styleToken(colored, token.fontStyle);
|
|
72
|
+
})
|
|
73
|
+
.join(''))
|
|
74
|
+
.join('\n');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return code;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function renderMarkdownAnsi(markdown) {
|
|
81
|
+
try {
|
|
82
|
+
const color = Boolean(process.stdout.isTTY);
|
|
83
|
+
const width = process.stdout.columns;
|
|
84
|
+
const hyperlinks = color; // enable OSC 8 only when we have color/TTY
|
|
85
|
+
return renderMarkdown(markdown, {
|
|
86
|
+
color,
|
|
87
|
+
width,
|
|
88
|
+
wrap: true,
|
|
89
|
+
hyperlinks,
|
|
90
|
+
highlighter: color ? shikiHighlighter : undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Last-resort fallback: return the raw markdown so we never crash.
|
|
95
|
+
return markdown;
|
|
96
|
+
}
|
|
97
|
+
}
|