@steipete/oracle 1.0.7 → 1.1.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/README.md +3 -0
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +9 -3
- 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/src/cli/help.js +1 -0
- package/dist/src/cli/markdownRenderer.js +18 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +129 -4
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +63 -51
- package/dist/src/sessionManager.js +17 -0
- package/package.json +14 -22
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { FileValidationError } from './errors.js';
|
|
5
|
+
const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
6
|
+
const DEFAULT_FS = fs;
|
|
7
|
+
export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = MAX_FILE_SIZE_BYTES } = {}) {
|
|
8
|
+
if (!filePaths || filePaths.length === 0) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const partitioned = await partitionFileInputs(filePaths, cwd, fsModule);
|
|
12
|
+
const useNativeFilesystem = fsModule === DEFAULT_FS;
|
|
13
|
+
let candidatePaths = [];
|
|
14
|
+
if (useNativeFilesystem) {
|
|
15
|
+
candidatePaths = await expandWithNativeGlob(partitioned, cwd);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
|
|
19
|
+
throw new Error('Glob patterns and exclusions are only supported for on-disk files.');
|
|
20
|
+
}
|
|
21
|
+
candidatePaths = await expandWithCustomFs(partitioned, fsModule);
|
|
22
|
+
}
|
|
23
|
+
if (candidatePaths.length === 0) {
|
|
24
|
+
throw new FileValidationError('No files matched the provided --file patterns.', {
|
|
25
|
+
patterns: partitioned.globPatterns,
|
|
26
|
+
excludes: partitioned.excludePatterns,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const oversized = [];
|
|
30
|
+
const accepted = [];
|
|
31
|
+
for (const filePath of candidatePaths) {
|
|
32
|
+
let stats;
|
|
33
|
+
try {
|
|
34
|
+
stats = await fsModule.stat(filePath);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new FileValidationError(`Missing file or directory: ${relativePath(filePath, cwd)}`, { path: filePath }, error);
|
|
38
|
+
}
|
|
39
|
+
if (!stats.isFile()) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (maxFileSizeBytes && typeof stats.size === 'number' && stats.size > maxFileSizeBytes) {
|
|
43
|
+
const relative = path.relative(cwd, filePath) || filePath;
|
|
44
|
+
oversized.push(`${relative} (${formatBytes(stats.size)})`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
accepted.push(filePath);
|
|
48
|
+
}
|
|
49
|
+
if (oversized.length > 0) {
|
|
50
|
+
throw new FileValidationError(`The following files exceed the 1 MB limit:\n- ${oversized.join('\n- ')}`, {
|
|
51
|
+
files: oversized,
|
|
52
|
+
limitBytes: maxFileSizeBytes,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const files = [];
|
|
56
|
+
for (const filePath of accepted) {
|
|
57
|
+
const content = await fsModule.readFile(filePath, 'utf8');
|
|
58
|
+
files.push({ path: filePath, content });
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
async function partitionFileInputs(rawPaths, cwd, fsModule) {
|
|
63
|
+
const result = {
|
|
64
|
+
globPatterns: [],
|
|
65
|
+
excludePatterns: [],
|
|
66
|
+
literalFiles: [],
|
|
67
|
+
literalDirectories: [],
|
|
68
|
+
};
|
|
69
|
+
for (const entry of rawPaths) {
|
|
70
|
+
const raw = entry?.trim();
|
|
71
|
+
if (!raw) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (raw.startsWith('!')) {
|
|
75
|
+
const normalized = normalizeGlob(raw.slice(1), cwd);
|
|
76
|
+
if (normalized) {
|
|
77
|
+
result.excludePatterns.push(normalized);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (fg.isDynamicPattern(raw)) {
|
|
82
|
+
result.globPatterns.push(normalizeGlob(raw, cwd));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const absolutePath = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
86
|
+
let stats;
|
|
87
|
+
try {
|
|
88
|
+
stats = await fsModule.stat(absolutePath);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new FileValidationError(`Missing file or directory: ${raw}`, { path: absolutePath }, error);
|
|
92
|
+
}
|
|
93
|
+
if (stats.isDirectory()) {
|
|
94
|
+
result.literalDirectories.push(absolutePath);
|
|
95
|
+
}
|
|
96
|
+
else if (stats.isFile()) {
|
|
97
|
+
result.literalFiles.push(absolutePath);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
throw new FileValidationError(`Not a file or directory: ${raw}`, { path: absolutePath });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
async function expandWithNativeGlob(partitioned, cwd) {
|
|
106
|
+
const patterns = [
|
|
107
|
+
...partitioned.globPatterns,
|
|
108
|
+
...partitioned.literalFiles.map((absPath) => toPosixRelativeOrBasename(absPath, cwd)),
|
|
109
|
+
...partitioned.literalDirectories.map((absDir) => makeDirectoryPattern(toPosixRelative(absDir, cwd))),
|
|
110
|
+
].filter(Boolean);
|
|
111
|
+
if (patterns.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const matches = await fg(patterns, {
|
|
115
|
+
cwd,
|
|
116
|
+
absolute: true,
|
|
117
|
+
dot: true,
|
|
118
|
+
ignore: partitioned.excludePatterns,
|
|
119
|
+
onlyFiles: true,
|
|
120
|
+
followSymbolicLinks: false,
|
|
121
|
+
});
|
|
122
|
+
return Array.from(new Set(matches.map((match) => path.resolve(match))));
|
|
123
|
+
}
|
|
124
|
+
async function expandWithCustomFs(partitioned, fsModule) {
|
|
125
|
+
const paths = new Set();
|
|
126
|
+
partitioned.literalFiles.forEach((file) => {
|
|
127
|
+
paths.add(file);
|
|
128
|
+
});
|
|
129
|
+
for (const directory of partitioned.literalDirectories) {
|
|
130
|
+
const nested = await expandDirectoryRecursive(directory, fsModule);
|
|
131
|
+
nested.forEach((entry) => {
|
|
132
|
+
paths.add(entry);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return Array.from(paths);
|
|
136
|
+
}
|
|
137
|
+
async function expandDirectoryRecursive(directory, fsModule) {
|
|
138
|
+
const entries = await fsModule.readdir(directory);
|
|
139
|
+
const results = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const childPath = path.join(directory, entry);
|
|
142
|
+
const stats = await fsModule.stat(childPath);
|
|
143
|
+
if (stats.isDirectory()) {
|
|
144
|
+
results.push(...(await expandDirectoryRecursive(childPath, fsModule)));
|
|
145
|
+
}
|
|
146
|
+
else if (stats.isFile()) {
|
|
147
|
+
results.push(childPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
function makeDirectoryPattern(relative) {
|
|
153
|
+
if (relative === '.' || relative === '') {
|
|
154
|
+
return '**/*';
|
|
155
|
+
}
|
|
156
|
+
return `${stripTrailingSlashes(relative)}/**/*`;
|
|
157
|
+
}
|
|
158
|
+
function normalizeGlob(pattern, cwd) {
|
|
159
|
+
if (!pattern) {
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
let normalized = pattern;
|
|
163
|
+
if (path.isAbsolute(normalized)) {
|
|
164
|
+
normalized = path.relative(cwd, normalized);
|
|
165
|
+
}
|
|
166
|
+
normalized = toPosix(normalized);
|
|
167
|
+
if (normalized.startsWith('./')) {
|
|
168
|
+
normalized = normalized.slice(2);
|
|
169
|
+
}
|
|
170
|
+
return normalized;
|
|
171
|
+
}
|
|
172
|
+
function toPosix(value) {
|
|
173
|
+
return value.replace(/\\/g, '/');
|
|
174
|
+
}
|
|
175
|
+
function toPosixRelative(absPath, cwd) {
|
|
176
|
+
const relative = path.relative(cwd, absPath);
|
|
177
|
+
if (!relative) {
|
|
178
|
+
return '.';
|
|
179
|
+
}
|
|
180
|
+
return toPosix(relative);
|
|
181
|
+
}
|
|
182
|
+
function toPosixRelativeOrBasename(absPath, cwd) {
|
|
183
|
+
const relative = path.relative(cwd, absPath);
|
|
184
|
+
return toPosix(relative || path.basename(absPath));
|
|
185
|
+
}
|
|
186
|
+
function stripTrailingSlashes(value) {
|
|
187
|
+
const normalized = toPosix(value);
|
|
188
|
+
return normalized.replace(/\/+$/g, '');
|
|
189
|
+
}
|
|
190
|
+
function formatBytes(size) {
|
|
191
|
+
if (size >= 1024 * 1024) {
|
|
192
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
193
|
+
}
|
|
194
|
+
if (size >= 1024) {
|
|
195
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
196
|
+
}
|
|
197
|
+
return `${size} B`;
|
|
198
|
+
}
|
|
199
|
+
function relativePath(targetPath, cwd) {
|
|
200
|
+
const relative = path.relative(cwd, targetPath);
|
|
201
|
+
return relative || targetPath;
|
|
202
|
+
}
|
|
203
|
+
export function createFileSections(files, cwd = process.cwd()) {
|
|
204
|
+
return files.map((file, index) => {
|
|
205
|
+
const relative = path.relative(cwd, file.path) || file.path;
|
|
206
|
+
const sectionText = [
|
|
207
|
+
`### File ${index + 1}: ${relative}`,
|
|
208
|
+
'```',
|
|
209
|
+
file.content.trimEnd(),
|
|
210
|
+
'```',
|
|
211
|
+
].join('\n');
|
|
212
|
+
return {
|
|
213
|
+
index: index + 1,
|
|
214
|
+
absolutePath: file.path,
|
|
215
|
+
displayPath: relative,
|
|
216
|
+
sectionText,
|
|
217
|
+
content: file.content,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function formatUSD(value) {
|
|
2
|
+
if (!Number.isFinite(value)) {
|
|
3
|
+
return 'n/a';
|
|
4
|
+
}
|
|
5
|
+
if (value >= 0.1) {
|
|
6
|
+
return `$${value.toFixed(2)}`;
|
|
7
|
+
}
|
|
8
|
+
if (value >= 0.01) {
|
|
9
|
+
return `$${value.toFixed(3)}`;
|
|
10
|
+
}
|
|
11
|
+
return `$${value.toFixed(6)}`;
|
|
12
|
+
}
|
|
13
|
+
export function formatNumber(value, { estimated = false } = {}) {
|
|
14
|
+
if (value == null) {
|
|
15
|
+
return 'n/a';
|
|
16
|
+
}
|
|
17
|
+
const suffix = estimated ? ' (est.)' : '';
|
|
18
|
+
return `${value.toLocaleString()}${suffix}`;
|
|
19
|
+
}
|
|
20
|
+
export function formatElapsed(ms) {
|
|
21
|
+
const totalSeconds = ms / 1000;
|
|
22
|
+
if (totalSeconds < 60) {
|
|
23
|
+
return `${totalSeconds.toFixed(2)}s`;
|
|
24
|
+
}
|
|
25
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
26
|
+
let seconds = Math.round(totalSeconds - minutes * 60);
|
|
27
|
+
let adjustedMinutes = minutes;
|
|
28
|
+
if (seconds === 60) {
|
|
29
|
+
adjustedMinutes += 1;
|
|
30
|
+
seconds = 0;
|
|
31
|
+
}
|
|
32
|
+
return `${adjustedMinutes}m ${seconds}s`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
const OSC = '\u001b]9;4;';
|
|
3
|
+
const ST = '\u001b\\';
|
|
4
|
+
function sanitizeLabel(label) {
|
|
5
|
+
const withoutEscape = label.split('\u001b').join('');
|
|
6
|
+
const withoutBellAndSt = withoutEscape.replaceAll('\u0007', '').replaceAll('\u009c', '');
|
|
7
|
+
return withoutBellAndSt.replaceAll(']', '').trim();
|
|
8
|
+
}
|
|
9
|
+
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
|
|
10
|
+
if (!isTty) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (env.ORACLE_NO_OSC_PROGRESS === '1') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (env.ORACLE_FORCE_OSC_PROGRESS === '1') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
|
|
20
|
+
if (termProgram.includes('ghostty')) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (termProgram.includes('wezterm')) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (env.WT_SESSION) {
|
|
27
|
+
return true; // Windows Terminal exposes this
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
export function startOscProgress(options = {}) {
|
|
32
|
+
const { label = 'Waiting for OpenAI', targetMs = 10 * 60_000, write = (text) => process.stdout.write(text) } = options;
|
|
33
|
+
if (!supportsOscProgress(options.env, options.isTty)) {
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
36
|
+
const cleanLabel = sanitizeLabel(label);
|
|
37
|
+
const target = Math.max(targetMs, 1_000);
|
|
38
|
+
const send = (state, percent) => {
|
|
39
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
40
|
+
write(`${OSC}${state};${clamped};${cleanLabel}${ST}`);
|
|
41
|
+
};
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
send(1, 0); // activate progress bar
|
|
44
|
+
const timer = setInterval(() => {
|
|
45
|
+
const elapsed = Date.now() - startedAt;
|
|
46
|
+
const percent = Math.min(99, (elapsed / target) * 100);
|
|
47
|
+
send(1, percent);
|
|
48
|
+
}, 900);
|
|
49
|
+
timer.unref?.();
|
|
50
|
+
let stopped = false;
|
|
51
|
+
return () => {
|
|
52
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: multiple callers may try to stop
|
|
53
|
+
if (stopped) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
stopped = true;
|
|
57
|
+
clearInterval(timer);
|
|
58
|
+
send(0, 0); // clear the progress bar
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { DEFAULT_SYSTEM_PROMPT } from './config.js';
|
|
3
|
+
import { createFileSections, readFiles } from './files.js';
|
|
4
|
+
import { createFsAdapter } from './fsAdapter.js';
|
|
5
|
+
export function buildPrompt(basePrompt, files, cwd = process.cwd()) {
|
|
6
|
+
if (!files.length) {
|
|
7
|
+
return basePrompt;
|
|
8
|
+
}
|
|
9
|
+
const sections = createFileSections(files, cwd);
|
|
10
|
+
const sectionText = sections.map((section) => section.sectionText).join('\n\n');
|
|
11
|
+
return `${basePrompt.trim()}\n\n${sectionText}`;
|
|
12
|
+
}
|
|
13
|
+
export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, }) {
|
|
14
|
+
return {
|
|
15
|
+
model: modelConfig.model,
|
|
16
|
+
instructions: systemPrompt,
|
|
17
|
+
input: [
|
|
18
|
+
{
|
|
19
|
+
role: 'user',
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'input_text',
|
|
23
|
+
text: userPrompt,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
tools: searchEnabled ? [{ type: 'web_search_preview' }] : undefined,
|
|
29
|
+
reasoning: modelConfig.reasoning || undefined,
|
|
30
|
+
max_output_tokens: maxOutputTokens,
|
|
31
|
+
background: background ? true : undefined,
|
|
32
|
+
store: storeResponse ? true : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function renderPromptMarkdown(options, deps = {}) {
|
|
36
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
37
|
+
const fsModule = deps.fs ?? createFsAdapter(fs);
|
|
38
|
+
const files = await readFiles(options.file ?? [], { cwd, fsModule });
|
|
39
|
+
const sections = createFileSections(files, cwd);
|
|
40
|
+
const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
41
|
+
const userPrompt = (options.prompt ?? '').trim();
|
|
42
|
+
const lines = ['[SYSTEM]', systemPrompt, ''];
|
|
43
|
+
lines.push('[USER]', userPrompt, '');
|
|
44
|
+
sections.forEach((section) => {
|
|
45
|
+
lines.push(`[FILE: ${section.displayPath}]`, section.content.trimEnd(), '');
|
|
46
|
+
});
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|