@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 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
@@ -39,7 +39,10 @@ program.hook('preAction', (thisCommand) => {
39
39
  thisCommand.setOptionValue('prompt', positional);
40
40
  }
41
41
  if (shouldRequirePrompt(rawCliArgs, opts)) {
42
- throw new Error('Prompt is required. Provide it via --prompt "<text>".');
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(`[${elapsedText} / ~10m] Pro thinking: ${nextMessage}${locatorSuffix}`);
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 { markdown, composerText, estimatedInputTokens, attachments, inlineFileCount, tokenEstimateIncludesInlineFiles };
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).'));
@@ -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 files for best results, but keep total input under ~196k tokens.`,
47
- `${colors.bullet('•')} The model has no built-in knowledge of your project—open with the architecture, key components, and why you’re asking.`,
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('•')} Ask the model for a memorable 3–5 word slug and pass it via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
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)) {
@@ -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 headerLine = `Oracle (${cliVersion}) consulting ${modelConfig.model}'s crystal ball with ${estimatedInputTokens.toLocaleString()} tokens and ${fileCount} files...`;
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 (id=${responseId}).`;
333
+ return `OpenAI background run still in progress — ${elapsedText} elapsed.`;
329
334
  },
330
335
  });
331
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5 Pro and GPT-5.1 high reasoning modes.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/oracle-cli.js",