@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,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSection, } from '../oracle.js';
|
|
5
|
+
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
6
|
+
import { buildAttachmentPlan } from './policies.js';
|
|
7
|
+
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
8
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
9
|
+
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
10
|
+
const files = await readFilesFn(runOptions.file ?? [], { cwd });
|
|
11
|
+
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
12
|
+
const userPrompt = basePrompt;
|
|
13
|
+
const systemPrompt = runOptions.system?.trim() || '';
|
|
14
|
+
const sections = createFileSections(files, cwd);
|
|
15
|
+
const markdown = buildPromptMarkdown(systemPrompt, userPrompt, sections);
|
|
16
|
+
const inlineFiles = Boolean(runOptions.browserInlineFiles);
|
|
17
|
+
const composerSections = [];
|
|
18
|
+
if (systemPrompt)
|
|
19
|
+
composerSections.push(systemPrompt);
|
|
20
|
+
if (userPrompt)
|
|
21
|
+
composerSections.push(userPrompt);
|
|
22
|
+
const attachmentPlan = buildAttachmentPlan(sections, {
|
|
23
|
+
inlineFiles,
|
|
24
|
+
bundleRequested: Boolean(runOptions.browserBundleFiles),
|
|
25
|
+
});
|
|
26
|
+
if (attachmentPlan.inlineBlock) {
|
|
27
|
+
composerSections.push(attachmentPlan.inlineBlock);
|
|
28
|
+
}
|
|
29
|
+
const composerText = composerSections.join('\n\n').trim();
|
|
30
|
+
const attachments = attachmentPlan.attachments.slice();
|
|
31
|
+
const shouldBundle = attachmentPlan.shouldBundle;
|
|
32
|
+
let bundleText = null;
|
|
33
|
+
if (shouldBundle) {
|
|
34
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
35
|
+
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
36
|
+
const bundleLines = [];
|
|
37
|
+
sections.forEach((section) => {
|
|
38
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
39
|
+
bundleLines.push('');
|
|
40
|
+
});
|
|
41
|
+
bundleText = `${bundleLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
42
|
+
await fs.writeFile(bundlePath, bundleText, 'utf8');
|
|
43
|
+
attachments.length = 0;
|
|
44
|
+
attachments.push({
|
|
45
|
+
path: bundlePath,
|
|
46
|
+
displayPath: bundlePath,
|
|
47
|
+
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const inlineFileCount = attachmentPlan.inlineFileCount;
|
|
51
|
+
const tokenizer = MODEL_CONFIGS[runOptions.model].tokenizer;
|
|
52
|
+
const tokenizerUserContent = inlineFileCount > 0 && attachmentPlan.inlineBlock
|
|
53
|
+
? [userPrompt, attachmentPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
54
|
+
: userPrompt;
|
|
55
|
+
const tokenizerMessages = [
|
|
56
|
+
systemPrompt ? { role: 'system', content: systemPrompt } : null,
|
|
57
|
+
tokenizerUserContent ? { role: 'user', content: tokenizerUserContent } : null,
|
|
58
|
+
].filter(Boolean);
|
|
59
|
+
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0
|
|
60
|
+
? tokenizerMessages
|
|
61
|
+
: [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
|
|
62
|
+
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(attachmentPlan.inlineBlock);
|
|
63
|
+
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
64
|
+
const attachmentText = bundleText ??
|
|
65
|
+
sections
|
|
66
|
+
.map((section) => formatFileSection(section.displayPath, section.content).trimEnd())
|
|
67
|
+
.join('\n\n');
|
|
68
|
+
const attachmentTokens = tokenizer([{ role: 'user', content: attachmentText }], TOKENIZER_OPTIONS);
|
|
69
|
+
estimatedInputTokens += attachmentTokens;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
markdown,
|
|
73
|
+
composerText,
|
|
74
|
+
estimatedInputTokens,
|
|
75
|
+
attachments,
|
|
76
|
+
inlineFileCount,
|
|
77
|
+
tokenEstimateIncludesInlineFiles,
|
|
78
|
+
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
79
|
+
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
80
|
+
: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { formatBytes } from './utils.js';
|
|
2
|
+
export function buildTokenEstimateSuffix(artifacts) {
|
|
3
|
+
if (artifacts.tokenEstimateIncludesInlineFiles && artifacts.inlineFileCount > 0) {
|
|
4
|
+
const count = artifacts.inlineFileCount;
|
|
5
|
+
const plural = count === 1 ? '' : 's';
|
|
6
|
+
return ` (includes ${count} inline file${plural})`;
|
|
7
|
+
}
|
|
8
|
+
if (artifacts.attachments.length > 0) {
|
|
9
|
+
const count = artifacts.attachments.length;
|
|
10
|
+
const plural = count === 1 ? '' : 's';
|
|
11
|
+
return ` (prompt only; ${count} attachment${plural} excluded)`;
|
|
12
|
+
}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
export function formatAttachmentLabel(attachment) {
|
|
16
|
+
if (typeof attachment.sizeBytes !== 'number' || Number.isNaN(attachment.sizeBytes)) {
|
|
17
|
+
return attachment.displayPath;
|
|
18
|
+
}
|
|
19
|
+
return `${attachment.displayPath} (${formatBytes(attachment.sizeBytes)})`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { formatElapsed } from '../oracle.js';
|
|
3
|
+
import { runBrowserMode } from '../browserMode.js';
|
|
4
|
+
import { assembleBrowserPrompt } from './prompt.js';
|
|
5
|
+
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
6
|
+
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
7
|
+
if (runOptions.model.startsWith('gemini')) {
|
|
8
|
+
throw new BrowserAutomationError('Gemini models are not available in browser mode. Re-run with --engine api.', {
|
|
9
|
+
stage: 'preflight',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
13
|
+
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
14
|
+
const promptArtifacts = await assemblePrompt(runOptions, { cwd });
|
|
15
|
+
if (runOptions.verbose) {
|
|
16
|
+
log(chalk.dim(`[verbose] Browser config: ${JSON.stringify({
|
|
17
|
+
...browserConfig,
|
|
18
|
+
})}`));
|
|
19
|
+
log(chalk.dim(`[verbose] Browser prompt length: ${promptArtifacts.composerText.length} chars`));
|
|
20
|
+
if (promptArtifacts.attachments.length > 0) {
|
|
21
|
+
const attachmentList = promptArtifacts.attachments.map((attachment) => attachment.displayPath).join(', ');
|
|
22
|
+
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
23
|
+
if (promptArtifacts.bundled) {
|
|
24
|
+
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (runOptions.file && runOptions.file.length > 0 && runOptions.browserInlineFiles) {
|
|
28
|
+
log(chalk.dim('[verbose] Browser inline file fallback enabled (pasting file contents).'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (promptArtifacts.bundled) {
|
|
32
|
+
log(chalk.dim(`Packed ${promptArtifacts.bundled.originalCount} files into 1 bundle (contents counted in token estimate).`));
|
|
33
|
+
}
|
|
34
|
+
const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
35
|
+
const automationLogger = ((message) => {
|
|
36
|
+
if (!runOptions.verbose)
|
|
37
|
+
return;
|
|
38
|
+
if (typeof message === 'string') {
|
|
39
|
+
log(message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
automationLogger.verbose = Boolean(runOptions.verbose);
|
|
43
|
+
automationLogger.sessionLog = runOptions.verbose ? log : (() => { });
|
|
44
|
+
log(headerLine);
|
|
45
|
+
log(chalk.dim('This run can take up to an hour (usually ~10 minutes).'));
|
|
46
|
+
if (runOptions.verbose) {
|
|
47
|
+
log(chalk.dim('Chrome automation does not stream output; this may take a minute...'));
|
|
48
|
+
}
|
|
49
|
+
let browserResult;
|
|
50
|
+
try {
|
|
51
|
+
browserResult = await executeBrowser({
|
|
52
|
+
prompt: promptArtifacts.composerText,
|
|
53
|
+
attachments: promptArtifacts.attachments,
|
|
54
|
+
config: browserConfig,
|
|
55
|
+
log: automationLogger,
|
|
56
|
+
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
57
|
+
verbose: runOptions.verbose,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof BrowserAutomationError) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const message = error instanceof Error ? error.message : 'Browser automation failed.';
|
|
65
|
+
throw new BrowserAutomationError(message, { stage: 'execute-browser' }, error);
|
|
66
|
+
}
|
|
67
|
+
if (!runOptions.silent) {
|
|
68
|
+
log(chalk.bold('Answer:'));
|
|
69
|
+
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim('(no text output)'));
|
|
70
|
+
log('');
|
|
71
|
+
}
|
|
72
|
+
const answerText = browserResult.answerMarkdown || browserResult.answerText || '';
|
|
73
|
+
const usage = {
|
|
74
|
+
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
75
|
+
outputTokens: browserResult.answerTokens,
|
|
76
|
+
reasoningTokens: 0,
|
|
77
|
+
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
78
|
+
};
|
|
79
|
+
const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
|
|
80
|
+
const tokensLabel = runOptions.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
81
|
+
const statsParts = [`${runOptions.model}[browser]`, `${tokensLabel}=${tokensDisplay}`];
|
|
82
|
+
if (runOptions.file && runOptions.file.length > 0) {
|
|
83
|
+
statsParts.push(`files=${runOptions.file.length}`);
|
|
84
|
+
}
|
|
85
|
+
log(chalk.blue(`Finished in ${formatElapsed(browserResult.tookMs)} (${statsParts.join(' | ')})`));
|
|
86
|
+
return {
|
|
87
|
+
usage,
|
|
88
|
+
elapsedMs: browserResult.tookMs,
|
|
89
|
+
runtime: {
|
|
90
|
+
chromePid: browserResult.chromePid,
|
|
91
|
+
chromePort: browserResult.chromePort,
|
|
92
|
+
userDataDir: browserResult.userDataDir,
|
|
93
|
+
},
|
|
94
|
+
answerText,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export function parseDuration(input, fallback) {
|
|
2
|
+
if (!input) {
|
|
3
|
+
return fallback;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
const lowercase = trimmed.toLowerCase();
|
|
10
|
+
if (/^[0-9]+$/.test(lowercase)) {
|
|
11
|
+
return Number(lowercase);
|
|
12
|
+
}
|
|
13
|
+
const normalized = lowercase.replace(/\s+/g, '');
|
|
14
|
+
const singleMatch = /^([0-9]+)(ms|s|m|h)$/i.exec(normalized);
|
|
15
|
+
if (singleMatch && singleMatch[0].length === normalized.length) {
|
|
16
|
+
const value = Number(singleMatch[1]);
|
|
17
|
+
return convertUnit(value, singleMatch[2]);
|
|
18
|
+
}
|
|
19
|
+
const multiDuration = /([0-9]+)(ms|h|m|s)/g;
|
|
20
|
+
let total = 0;
|
|
21
|
+
let lastIndex = 0;
|
|
22
|
+
let match = multiDuration.exec(normalized);
|
|
23
|
+
while (match !== null) {
|
|
24
|
+
total += convertUnit(Number(match[1]), match[2]);
|
|
25
|
+
lastIndex = multiDuration.lastIndex;
|
|
26
|
+
match = multiDuration.exec(normalized);
|
|
27
|
+
}
|
|
28
|
+
if (total > 0 && lastIndex === normalized.length) {
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
function convertUnit(value, unitRaw) {
|
|
34
|
+
const unit = unitRaw?.toLowerCase();
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case 'ms':
|
|
37
|
+
return value;
|
|
38
|
+
case 's':
|
|
39
|
+
return value * 1000;
|
|
40
|
+
case 'm':
|
|
41
|
+
return value * 60_000;
|
|
42
|
+
case 'h':
|
|
43
|
+
return value * 3_600_000;
|
|
44
|
+
default:
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function delay(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
export function estimateTokenCount(text) {
|
|
52
|
+
if (!text) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
const estimate = Math.max(words.length * 0.75, text.length / 4);
|
|
57
|
+
return Math.max(1, Math.round(estimate));
|
|
58
|
+
}
|
|
59
|
+
export async function withRetries(task, options = {}) {
|
|
60
|
+
const { retries = 2, delayMs = 250, onRetry } = options;
|
|
61
|
+
let attempt = 0;
|
|
62
|
+
while (attempt <= retries) {
|
|
63
|
+
try {
|
|
64
|
+
return await task();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (attempt === retries) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
attempt += 1;
|
|
71
|
+
onRetry?.(attempt, error);
|
|
72
|
+
await delay(delayMs * attempt);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error('withRetries exhausted without result');
|
|
76
|
+
}
|
|
77
|
+
export function formatBytes(size) {
|
|
78
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
79
|
+
return 'n/a';
|
|
80
|
+
}
|
|
81
|
+
if (size < 1024) {
|
|
82
|
+
return `${size} B`;
|
|
83
|
+
}
|
|
84
|
+
if (size < 1024 * 1024) {
|
|
85
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
86
|
+
}
|
|
87
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalizes a ChatGPT URL, ensuring it is absolute, uses http/https, and trims whitespace.
|
|
91
|
+
* Falls back to the provided default when input is empty/undefined.
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeChatgptUrl(raw, fallback) {
|
|
94
|
+
const candidate = raw?.trim();
|
|
95
|
+
if (!candidate) {
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(candidate);
|
|
99
|
+
const withScheme = hasScheme ? candidate : `https://${candidate}`;
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(withScheme);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Invalid ChatGPT URL: "${raw}". Provide an absolute http(s) URL.`);
|
|
106
|
+
}
|
|
107
|
+
if (!/^https?:$/i.test(parsed.protocol)) {
|
|
108
|
+
throw new Error(`Invalid ChatGPT URL protocol: "${parsed.protocol}". Use http or https.`);
|
|
109
|
+
}
|
|
110
|
+
// Preserve user-provided path/query; URL#toString will normalize trailing slashes appropriately.
|
|
111
|
+
return parsed.toString();
|
|
112
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { existsSync, promises as fsp } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import sqlite3 from 'sqlite3';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
let cachedUnprotect = null;
|
|
10
|
+
function getUnprotectData() {
|
|
11
|
+
if (cachedUnprotect) {
|
|
12
|
+
return cachedUnprotect;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
// win-dpapi is CommonJS; require it explicitly and support both named/default shapes.
|
|
16
|
+
const dpapiModule = createRequire(import.meta.url)('win-dpapi');
|
|
17
|
+
const unprotect = dpapiModule?.Dpapi?.unprotectData ??
|
|
18
|
+
dpapiModule?.default?.unprotectData ??
|
|
19
|
+
((_) => {
|
|
20
|
+
throw new Error('win-dpapi unprotectData not available');
|
|
21
|
+
});
|
|
22
|
+
cachedUnprotect = unprotect;
|
|
23
|
+
return unprotect;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (process.platform !== 'win32') {
|
|
27
|
+
// On macOS/Linux we don't need DPAPI; return a function that makes misuse obvious.
|
|
28
|
+
cachedUnprotect = () => {
|
|
29
|
+
throw new Error('win-dpapi is unavailable on non-Windows platforms');
|
|
30
|
+
};
|
|
31
|
+
return cachedUnprotect;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function loadWindowsCookies(dbPath, filterNames) {
|
|
37
|
+
if (process.platform !== 'win32') {
|
|
38
|
+
throw new Error('loadWindowsCookies is only supported on Windows');
|
|
39
|
+
}
|
|
40
|
+
const localStatePath = await locateLocalState(dbPath);
|
|
41
|
+
const cookiesCopy = await copyLockedFile(dbPath, 'Cookies'); // name the copy exactly "Cookies"
|
|
42
|
+
const cookiesDirForFallback = await ensureCookiesDirForFallback(cookiesCopy);
|
|
43
|
+
const localStateCopy = await copyLockedFile(localStatePath, 'Local State');
|
|
44
|
+
const aesKey = await extractWindowsAesKey(localStateCopy);
|
|
45
|
+
const rows = await readChromeCookiesDb(cookiesCopy, filterNames);
|
|
46
|
+
const cookies = [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const enc = row.encrypted_value && row.encrypted_value.length > 0 ? row.encrypted_value : Buffer.from(row.value ?? '', 'utf8');
|
|
49
|
+
const decrypted = enc.length > 0 ? decryptCookie(enc, aesKey) : '';
|
|
50
|
+
cookies.push({
|
|
51
|
+
name: row.name,
|
|
52
|
+
value: decrypted,
|
|
53
|
+
domain: row.host_key?.startsWith('.') ? row.host_key.slice(1) : row.host_key,
|
|
54
|
+
path: row.path ?? '/',
|
|
55
|
+
expires: normalizeExpiration(row.expires_utc),
|
|
56
|
+
secure: Boolean(row.is_secure),
|
|
57
|
+
httpOnly: Boolean(row.is_httponly),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log(`[cookies] windows decrypt decoded ${cookies.length} cookies from ${cookiesCopy} (fallback dir ${cookiesDirForFallback})`);
|
|
63
|
+
}
|
|
64
|
+
return cookies.filter((c) => c.value);
|
|
65
|
+
}
|
|
66
|
+
function decryptCookie(value, aesKey) {
|
|
67
|
+
const prefix = value.slice(0, 3).toString();
|
|
68
|
+
if (prefix === 'v10' || prefix === 'v11') {
|
|
69
|
+
const iv = value.slice(3, 15);
|
|
70
|
+
const tag = value.slice(value.length - 16);
|
|
71
|
+
const data = value.slice(15, value.length - 16);
|
|
72
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
|
|
73
|
+
decipher.setAuthTag(tag);
|
|
74
|
+
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
75
|
+
return decrypted.toString('utf8');
|
|
76
|
+
}
|
|
77
|
+
const unprotectData = getUnprotectData();
|
|
78
|
+
const unprotected = unprotectData(value, undefined, 'CurrentUser');
|
|
79
|
+
return Buffer.from(unprotected).toString('utf8');
|
|
80
|
+
}
|
|
81
|
+
async function locateLocalState(dbPath) {
|
|
82
|
+
// Prefer sibling Local State near the profile path; fall back to default location.
|
|
83
|
+
const guess = path.resolve(path.join(path.dirname(dbPath), '..', 'Local State'));
|
|
84
|
+
if (existsSync(guess))
|
|
85
|
+
return guess;
|
|
86
|
+
const localState = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Local State');
|
|
87
|
+
if (existsSync(localState))
|
|
88
|
+
return localState;
|
|
89
|
+
throw new Error('Chrome Local State not found for AES key');
|
|
90
|
+
}
|
|
91
|
+
async function extractWindowsAesKey(localStatePath) {
|
|
92
|
+
const raw = await fs.readFile(localStatePath, 'utf8');
|
|
93
|
+
const state = JSON.parse(raw);
|
|
94
|
+
const encKeyB64 = state?.os_crypt?.encrypted_key;
|
|
95
|
+
if (!encKeyB64)
|
|
96
|
+
throw new Error('encrypted_key missing in Local State');
|
|
97
|
+
const encKey = Buffer.from(encKeyB64, 'base64');
|
|
98
|
+
const dpapiBlob = encKey.slice(5); // strip "DPAPI"
|
|
99
|
+
const unprotectData = getUnprotectData();
|
|
100
|
+
const unprotected = unprotectData(dpapiBlob, undefined, 'CurrentUser');
|
|
101
|
+
return Buffer.from(unprotected);
|
|
102
|
+
}
|
|
103
|
+
async function readChromeCookiesDb(dbPath, filterNames) {
|
|
104
|
+
const db = await openSqlite(dbPath);
|
|
105
|
+
const placeholders = filterNames && filterNames.size > 0 ? `AND name IN (${Array.from(filterNames).map(() => '?').join(',')})` : '';
|
|
106
|
+
const params = filterNames ? Array.from(filterNames) : [];
|
|
107
|
+
const sql = `SELECT name,value,encrypted_value,host_key,path,expires_utc,is_secure,is_httponly FROM cookies WHERE host_key LIKE '%chatgpt.com%' ${placeholders}`;
|
|
108
|
+
try {
|
|
109
|
+
return await allSqlite(db, sql, params);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
db.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function openSqlite(dbPath) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
118
|
+
if (err)
|
|
119
|
+
reject(err);
|
|
120
|
+
else
|
|
121
|
+
resolve(db);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function copyLockedFile(sourcePath, targetName) {
|
|
126
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookie-'));
|
|
127
|
+
const tempPath = targetName ? path.join(tempDir, targetName) : path.join(tempDir, path.basename(sourcePath));
|
|
128
|
+
const psScript = `
|
|
129
|
+
$src = '${sourcePath.replace(/'/g, "''")}'
|
|
130
|
+
$dst = '${tempPath.replace(/'/g, "''")}'
|
|
131
|
+
try {
|
|
132
|
+
$fs = [System.IO.File]::Open($src, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
133
|
+
$bytes = New-Object byte[] $fs.Length
|
|
134
|
+
$null = $fs.Read($bytes, 0, $fs.Length)
|
|
135
|
+
$fs.Close()
|
|
136
|
+
[System.IO.File]::WriteAllBytes($dst, $bytes)
|
|
137
|
+
exit 0
|
|
138
|
+
} catch {
|
|
139
|
+
exit 1
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
143
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
|
|
144
|
+
timeout: 15000,
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
});
|
|
147
|
+
if (result.status !== 0) {
|
|
148
|
+
// Fallback: best-effort normal copy
|
|
149
|
+
await fs.copyFile(sourcePath, tempPath);
|
|
150
|
+
}
|
|
151
|
+
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.log(`[cookies] copied locked file ${sourcePath} -> ${tempPath}`);
|
|
154
|
+
}
|
|
155
|
+
return tempPath;
|
|
156
|
+
}
|
|
157
|
+
// Create a directory containing a file named "Cookies" for chrome-cookies-secure fallback
|
|
158
|
+
export async function ensureCookiesDirForFallback(cookieFile) {
|
|
159
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-secure-'));
|
|
160
|
+
const target = path.join(dir, 'Cookies');
|
|
161
|
+
await fs.copyFile(cookieFile, target);
|
|
162
|
+
return dir;
|
|
163
|
+
}
|
|
164
|
+
function allSqlite(db, sql, params) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
db.all(sql, params, (err, rows) => {
|
|
167
|
+
if (err)
|
|
168
|
+
reject(err);
|
|
169
|
+
else
|
|
170
|
+
resolve((rows ?? []));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function normalizeExpiration(expires) {
|
|
175
|
+
if (!expires || Number.isNaN(expires)) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const value = Number(expires);
|
|
179
|
+
if (value <= 0)
|
|
180
|
+
return undefined;
|
|
181
|
+
if (value > 1_000_000_000_000) {
|
|
182
|
+
return Math.round(value / 1_000_000 - 11644473600);
|
|
183
|
+
}
|
|
184
|
+
if (value > 1_000_000_000) {
|
|
185
|
+
return Math.round(value / 1000);
|
|
186
|
+
}
|
|
187
|
+
return Math.round(value);
|
|
188
|
+
}
|
|
189
|
+
export async function materializeCookieFile(sourcePath) {
|
|
190
|
+
if (process.platform !== 'win32')
|
|
191
|
+
return sourcePath;
|
|
192
|
+
const resolved = await resolveDirectoryCandidate(sourcePath);
|
|
193
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
|
|
194
|
+
const tempPath = path.join(tempDir, 'Cookies');
|
|
195
|
+
await fs.copyFile(resolved, tempPath);
|
|
196
|
+
return tempPath;
|
|
197
|
+
}
|
|
198
|
+
async function resolveDirectoryCandidate(inputPath) {
|
|
199
|
+
const stat = await fs.stat(inputPath).catch(() => null);
|
|
200
|
+
if (!stat?.isDirectory())
|
|
201
|
+
return inputPath;
|
|
202
|
+
const network = path.join(inputPath, 'Network', 'Cookies');
|
|
203
|
+
const direct = path.join(inputPath, 'Cookies');
|
|
204
|
+
if (await fileExists(network))
|
|
205
|
+
return network;
|
|
206
|
+
if (await fileExists(direct))
|
|
207
|
+
return direct;
|
|
208
|
+
return inputPath;
|
|
209
|
+
}
|
|
210
|
+
async function fileExists(candidate) {
|
|
211
|
+
try {
|
|
212
|
+
const stat = await fsp.stat(candidate);
|
|
213
|
+
return stat.isFile();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runBrowserMode, CHATGPT_URL, DEFAULT_MODEL_TARGET, parseDuration, normalizeChatgptUrl, } from './browser/index.js';
|