@steipete/oracle 1.0.5 → 1.0.7
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/README.md +2 -1
- package/dist/bin/oracle-cli.js +4 -1
- package/dist/src/browser/index.js +14 -2
- package/dist/src/browser/prompt.js +33 -1
- package/dist/src/browser/sessionRunner.js +3 -0
- package/dist/src/cli/help.js +4 -3
- package/dist/src/cli/sessionDisplay.js +15 -1
- package/dist/src/oracle/run.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# oracle — Whispering your tokens to the silicon sage
|
|
1
|
+
# oracle 🧿 — Whispering your tokens to the silicon sage
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="./README-header.png" alt="Oracle CLI header banner" width="1100">
|
|
@@ -85,4 +85,5 @@ pnpm test:coverage
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
87
|
If you’re looking for an even more powerful context-management tool, check out https://repoprompt.com
|
|
88
|
+
|
|
88
89
|
Name inspired by: https://ampcode.com/news/oracle
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -39,7 +39,10 @@ program.hook('preAction', (thisCommand) => {
|
|
|
39
39
|
thisCommand.setOptionValue('prompt', positional);
|
|
40
40
|
}
|
|
41
41
|
if (shouldRequirePrompt(rawCliArgs, opts)) {
|
|
42
|
-
|
|
42
|
+
console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
|
|
43
|
+
thisCommand.help({ error: false });
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
43
46
|
}
|
|
44
47
|
});
|
|
45
48
|
program
|
|
@@ -200,6 +200,19 @@ function isWebSocketClosureError(error) {
|
|
|
200
200
|
message.includes('websocket error') ||
|
|
201
201
|
message.includes('target closed'));
|
|
202
202
|
}
|
|
203
|
+
export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
204
|
+
const elapsedMs = now - startedAt;
|
|
205
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
206
|
+
const progress = Math.min(1, elapsedMs / 600_000); // soft target: 10 minutes
|
|
207
|
+
const barSegments = 10;
|
|
208
|
+
const filled = Math.round(progress * barSegments);
|
|
209
|
+
const bar = `${'█'.repeat(filled).padEnd(barSegments, '░')}`;
|
|
210
|
+
const pct = Math.round(progress * 100)
|
|
211
|
+
.toString()
|
|
212
|
+
.padStart(3, ' ');
|
|
213
|
+
const statusLabel = message ? ` — ${message}` : '';
|
|
214
|
+
return `[${elapsedText} / ~10m] ${bar} ${pct}%${statusLabel}${locatorSuffix}`;
|
|
215
|
+
}
|
|
203
216
|
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
204
217
|
let stopped = false;
|
|
205
218
|
let pending = false;
|
|
@@ -215,7 +228,6 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
215
228
|
const nextMessage = await readThinkingStatus(Runtime);
|
|
216
229
|
if (nextMessage && nextMessage !== lastMessage) {
|
|
217
230
|
lastMessage = nextMessage;
|
|
218
|
-
const elapsedText = formatElapsed(Date.now() - startedAt);
|
|
219
231
|
let locatorSuffix = '';
|
|
220
232
|
if (includeDiagnostics) {
|
|
221
233
|
try {
|
|
@@ -226,7 +238,7 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
226
238
|
locatorSuffix = ' | assistant-turn=error';
|
|
227
239
|
}
|
|
228
240
|
}
|
|
229
|
-
logger(
|
|
241
|
+
logger(formatThinkingLog(startedAt, Date.now(), nextMessage, locatorSuffix));
|
|
230
242
|
}
|
|
231
243
|
}
|
|
232
244
|
catch {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { readFiles, createFileSections, DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from '../oracle.js';
|
|
2
5
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
3
6
|
const cwd = deps.cwd ?? process.cwd();
|
|
@@ -39,6 +42,25 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
39
42
|
displayPath: section.displayPath,
|
|
40
43
|
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
41
44
|
}));
|
|
45
|
+
const MAX_BROWSER_ATTACHMENTS = 10;
|
|
46
|
+
if (!inlineFiles && attachments.length > MAX_BROWSER_ATTACHMENTS) {
|
|
47
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
48
|
+
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
49
|
+
const bundleLines = [];
|
|
50
|
+
sections.forEach((section) => {
|
|
51
|
+
bundleLines.push(`### File: ${section.displayPath}`);
|
|
52
|
+
bundleLines.push(section.content.trimEnd());
|
|
53
|
+
bundleLines.push('');
|
|
54
|
+
});
|
|
55
|
+
const bundleText = `${bundleLines.join('\n').trimEnd()}\n`;
|
|
56
|
+
await fs.writeFile(bundlePath, bundleText, 'utf8');
|
|
57
|
+
attachments.length = 0;
|
|
58
|
+
attachments.push({
|
|
59
|
+
path: bundlePath,
|
|
60
|
+
displayPath: 'attachments-bundle.txt',
|
|
61
|
+
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
42
64
|
const inlineFileCount = inlineFiles ? sections.length : 0;
|
|
43
65
|
const tokenizer = MODEL_CONFIGS[runOptions.model].tokenizer;
|
|
44
66
|
const tokenizerUserContent = inlineFileCount > 0 && inlineBlock
|
|
@@ -52,5 +74,15 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
52
74
|
? tokenizerMessages
|
|
53
75
|
: [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
|
|
54
76
|
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(inlineBlock);
|
|
55
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
markdown,
|
|
79
|
+
composerText,
|
|
80
|
+
estimatedInputTokens,
|
|
81
|
+
attachments,
|
|
82
|
+
inlineFileCount,
|
|
83
|
+
tokenEstimateIncludesInlineFiles,
|
|
84
|
+
bundled: !inlineFiles && attachments.length === 1 && sections.length > MAX_BROWSER_ATTACHMENTS && attachments[0]?.displayPath
|
|
85
|
+
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
86
|
+
: null,
|
|
87
|
+
};
|
|
56
88
|
}
|
|
@@ -15,6 +15,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
15
15
|
if (promptArtifacts.attachments.length > 0) {
|
|
16
16
|
const attachmentList = promptArtifacts.attachments.map((attachment) => attachment.displayPath).join(', ');
|
|
17
17
|
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
18
|
+
if (promptArtifacts.bundled) {
|
|
19
|
+
log(chalk.yellow(`[browser] More than 10 files provided; bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath} to satisfy ChatGPT upload limits.`));
|
|
20
|
+
}
|
|
18
21
|
}
|
|
19
22
|
else if (runOptions.file && runOptions.file.length > 0 && runOptions.browserInlineFiles) {
|
|
20
23
|
log(chalk.dim('[verbose] Browser inline file fallback enabled (pasting file contents).'));
|
package/dist/src/cli/help.js
CHANGED
|
@@ -43,11 +43,12 @@ function renderHelpBanner(version, colors) {
|
|
|
43
43
|
}
|
|
44
44
|
function renderHelpFooter(program, colors) {
|
|
45
45
|
const tips = [
|
|
46
|
-
`${colors.bullet('•')} Attach source
|
|
47
|
-
`${colors.bullet('•')}
|
|
46
|
+
`${colors.bullet('•')} Attach lots of source (whole directories beat single files) and keep total input under ~196k tokens.`,
|
|
47
|
+
`${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.`,
|
|
48
48
|
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
49
49
|
`${colors.bullet('•')} Non-preview runs spawn detached sessions so they keep streaming even if your terminal closes — reattach anytime via ${colors.accent('pnpm oracle session <slug>')}.`,
|
|
50
|
-
`${colors.bullet('•')}
|
|
50
|
+
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
51
|
+
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
51
52
|
`${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
|
|
52
53
|
].join('\n');
|
|
53
54
|
const formatExample = (command, description) => `${colors.command(` ${command}`)}\n${colors.muted(` ${description}`)}`;
|
|
@@ -51,7 +51,6 @@ export async function attachSession(sessionId, options) {
|
|
|
51
51
|
}
|
|
52
52
|
const initialStatus = metadata.status;
|
|
53
53
|
if (!options?.suppressMetadata) {
|
|
54
|
-
console.log(chalk.bold(`Session: ${sessionId}`));
|
|
55
54
|
const reattachLine = buildReattachLine(metadata);
|
|
56
55
|
if (reattachLine) {
|
|
57
56
|
console.log(chalk.blue(reattachLine));
|
|
@@ -72,6 +71,13 @@ export async function attachSession(sessionId, options) {
|
|
|
72
71
|
console.log(dim(`User error: ${userErrorSummary}`));
|
|
73
72
|
}
|
|
74
73
|
}
|
|
74
|
+
const shouldTrimIntro = initialStatus === 'completed' || initialStatus === 'error';
|
|
75
|
+
if (shouldTrimIntro) {
|
|
76
|
+
const fullLog = await readSessionLog(sessionId);
|
|
77
|
+
const trimmed = trimBeforeFirstAnswer(fullLog);
|
|
78
|
+
process.stdout.write(trimmed);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
75
81
|
let lastLength = 0;
|
|
76
82
|
const printNew = async () => {
|
|
77
83
|
const text = await readSessionLog(sessionId);
|
|
@@ -171,6 +177,14 @@ export function buildReattachLine(metadata) {
|
|
|
171
177
|
}
|
|
172
178
|
return null;
|
|
173
179
|
}
|
|
180
|
+
export function trimBeforeFirstAnswer(logText) {
|
|
181
|
+
const marker = 'Answer:';
|
|
182
|
+
const index = logText.indexOf(marker);
|
|
183
|
+
if (index === -1) {
|
|
184
|
+
return logText;
|
|
185
|
+
}
|
|
186
|
+
return logText.slice(index);
|
|
187
|
+
}
|
|
174
188
|
function formatRelativeDuration(referenceIso) {
|
|
175
189
|
const timestamp = Date.parse(referenceIso);
|
|
176
190
|
if (Number.isNaN(timestamp)) {
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -92,7 +92,11 @@ export async function runOracle(options, deps = {}) {
|
|
|
92
92
|
logVerbose(`Estimated tokens (prompt + files): ${estimatedInputTokens.toLocaleString()}`);
|
|
93
93
|
const fileCount = files.length;
|
|
94
94
|
const cliVersion = getCliVersion();
|
|
95
|
-
const
|
|
95
|
+
const richTty = process.stdout.isTTY && chalk.level > 0;
|
|
96
|
+
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
97
|
+
const tokenLabel = richTty ? chalk.green(estimatedInputTokens.toLocaleString()) : estimatedInputTokens.toLocaleString();
|
|
98
|
+
const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
|
|
99
|
+
const headerLine = `Oracle (${cliVersion}) consulting ${headerModelLabel}'s crystal ball with ${tokenLabel} tokens and ${fileLabel} files...`;
|
|
96
100
|
const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
|
|
97
101
|
if (!isPreview) {
|
|
98
102
|
log(headerLine);
|
|
@@ -145,6 +149,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
145
149
|
let answerHeaderPrinted = false;
|
|
146
150
|
const ensureAnswerHeader = () => {
|
|
147
151
|
if (!options.silent && !answerHeaderPrinted) {
|
|
152
|
+
log('');
|
|
148
153
|
log(chalk.bold('Answer:'));
|
|
149
154
|
answerHeaderPrinted = true;
|
|
150
155
|
}
|
|
@@ -325,7 +330,7 @@ async function executeBackgroundResponse(params) {
|
|
|
325
330
|
isActive: () => heartbeatActive,
|
|
326
331
|
makeMessage: (elapsedMs) => {
|
|
327
332
|
const elapsedText = formatElapsed(elapsedMs);
|
|
328
|
-
return `OpenAI background run still in progress — ${elapsedText} elapsed
|
|
333
|
+
return `OpenAI background run still in progress — ${elapsedText} elapsed.`;
|
|
329
334
|
},
|
|
330
335
|
});
|
|
331
336
|
}
|
package/package.json
CHANGED