@steipete/oracle 1.1.0 → 1.3.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 +40 -7
- package/assets-oracle-icon.png +0 -0
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +315 -47
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/config.js +6 -0
- package/dist/src/browser/cookies.js +50 -12
- package/dist/src/browser/index.js +19 -5
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +14 -3
- package/dist/src/cli/browserConfig.js +109 -2
- package/dist/src/cli/detach.js +12 -0
- package/dist/src/cli/dryRun.js +60 -8
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +3 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +79 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/options.js +22 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/runOptions.js +43 -0
- package/dist/src/cli/sessionCommand.js +1 -1
- package/dist/src/cli/sessionDisplay.js +94 -7
- package/dist/src/cli/sessionRunner.js +32 -2
- package/dist/src/cli/tui/index.js +457 -0
- package/dist/src/config.js +27 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +158 -0
- package/dist/src/mcp/tools/sessionResources.js +64 -0
- package/dist/src/mcp/tools/sessions.js +106 -0
- package/dist/src/mcp/types.js +17 -0
- package/dist/src/mcp/utils.js +24 -0
- package/dist/src/oracle/client.js +24 -6
- package/dist/src/oracle/config.js +10 -0
- package/dist/src/oracle/files.js +151 -8
- package/dist/src/oracle/format.js +2 -7
- package/dist/src/oracle/fsAdapter.js +4 -1
- package/dist/src/oracle/gemini.js +161 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/oscProgress.js +7 -1
- package/dist/src/oracle/run.js +148 -64
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/oracle.js +1 -0
- package/dist/src/sessionManager.js +50 -3
- 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 +22 -6
- 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/build-notifier.sh +93 -0
package/dist/src/oracle/files.js
CHANGED
|
@@ -4,12 +4,13 @@ import fg from 'fast-glob';
|
|
|
4
4
|
import { FileValidationError } from './errors.js';
|
|
5
5
|
const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
6
6
|
const DEFAULT_FS = fs;
|
|
7
|
+
const DEFAULT_IGNORED_DIRS = ['node_modules', 'dist', 'coverage', '.git', '.turbo', '.next', 'build', 'tmp'];
|
|
7
8
|
export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = MAX_FILE_SIZE_BYTES } = {}) {
|
|
8
9
|
if (!filePaths || filePaths.length === 0) {
|
|
9
10
|
return [];
|
|
10
11
|
}
|
|
11
12
|
const partitioned = await partitionFileInputs(filePaths, cwd, fsModule);
|
|
12
|
-
const useNativeFilesystem = fsModule === DEFAULT_FS;
|
|
13
|
+
const useNativeFilesystem = fsModule === DEFAULT_FS || isNativeFsModule(fsModule);
|
|
13
14
|
let candidatePaths = [];
|
|
14
15
|
if (useNativeFilesystem) {
|
|
15
16
|
candidatePaths = await expandWithNativeGlob(partitioned, cwd);
|
|
@@ -20,7 +21,28 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
|
|
|
20
21
|
}
|
|
21
22
|
candidatePaths = await expandWithCustomFs(partitioned, fsModule);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
const allowedLiteralDirs = partitioned.literalDirectories
|
|
25
|
+
.map((dir) => path.resolve(dir))
|
|
26
|
+
.filter((dir) => DEFAULT_IGNORED_DIRS.includes(path.basename(dir)));
|
|
27
|
+
const allowedLiteralFiles = partitioned.literalFiles.map((file) => path.resolve(file));
|
|
28
|
+
const resolvedLiteralDirs = new Set(allowedLiteralDirs);
|
|
29
|
+
const allowedPaths = new Set([...allowedLiteralDirs, ...allowedLiteralFiles]);
|
|
30
|
+
const ignoredWhitelist = await buildIgnoredWhitelist(candidatePaths, cwd, fsModule);
|
|
31
|
+
const ignoredLog = new Set();
|
|
32
|
+
const filteredCandidates = candidatePaths.filter((filePath) => {
|
|
33
|
+
const ignoredDir = findIgnoredAncestor(filePath, cwd, resolvedLiteralDirs, allowedPaths, ignoredWhitelist);
|
|
34
|
+
if (!ignoredDir) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const displayFile = relativePath(filePath, cwd);
|
|
38
|
+
const key = `${ignoredDir}|${displayFile}`;
|
|
39
|
+
if (!ignoredLog.has(key)) {
|
|
40
|
+
console.log(`Skipping default-ignored path: ${displayFile} (matches ${ignoredDir})`);
|
|
41
|
+
ignoredLog.add(key);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
if (filteredCandidates.length === 0) {
|
|
24
46
|
throw new FileValidationError('No files matched the provided --file patterns.', {
|
|
25
47
|
patterns: partitioned.globPatterns,
|
|
26
48
|
excludes: partitioned.excludePatterns,
|
|
@@ -28,7 +50,7 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
|
|
|
28
50
|
}
|
|
29
51
|
const oversized = [];
|
|
30
52
|
const accepted = [];
|
|
31
|
-
for (const filePath of
|
|
53
|
+
for (const filePath of filteredCandidates) {
|
|
32
54
|
let stats;
|
|
33
55
|
try {
|
|
34
56
|
stats = await fsModule.stat(filePath);
|
|
@@ -111,15 +133,130 @@ async function expandWithNativeGlob(partitioned, cwd) {
|
|
|
111
133
|
if (patterns.length === 0) {
|
|
112
134
|
return [];
|
|
113
135
|
}
|
|
114
|
-
const
|
|
136
|
+
const dotfileOptIn = patterns.some((pattern) => includesDotfileSegment(pattern));
|
|
137
|
+
const gitignoreSets = await loadGitignoreSets(cwd);
|
|
138
|
+
const matches = (await fg(patterns, {
|
|
115
139
|
cwd,
|
|
116
|
-
absolute:
|
|
140
|
+
absolute: false,
|
|
117
141
|
dot: true,
|
|
118
142
|
ignore: partitioned.excludePatterns,
|
|
119
143
|
onlyFiles: true,
|
|
120
144
|
followSymbolicLinks: false,
|
|
121
|
-
});
|
|
122
|
-
|
|
145
|
+
}));
|
|
146
|
+
const resolved = matches.map((match) => path.resolve(cwd, match));
|
|
147
|
+
const filtered = resolved.filter((filePath) => !isGitignored(filePath, gitignoreSets));
|
|
148
|
+
const finalFiles = dotfileOptIn ? filtered : filtered.filter((filePath) => !path.basename(filePath).startsWith('.'));
|
|
149
|
+
return Array.from(new Set(finalFiles));
|
|
150
|
+
}
|
|
151
|
+
async function loadGitignoreSets(cwd) {
|
|
152
|
+
const gitignorePaths = await fg('**/.gitignore', { cwd, dot: true, absolute: true, onlyFiles: true, followSymbolicLinks: false });
|
|
153
|
+
const sets = [];
|
|
154
|
+
for (const filePath of gitignorePaths) {
|
|
155
|
+
try {
|
|
156
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
157
|
+
const patterns = raw
|
|
158
|
+
.split('\n')
|
|
159
|
+
.map((line) => line.trim())
|
|
160
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
161
|
+
if (patterns.length > 0) {
|
|
162
|
+
sets.push({ dir: path.dirname(filePath), patterns });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Ignore unreadable .gitignore files
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Ensure deterministic parent-before-child ordering
|
|
170
|
+
return sets.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
171
|
+
}
|
|
172
|
+
function isGitignored(filePath, sets) {
|
|
173
|
+
for (const { dir, patterns } of sets) {
|
|
174
|
+
if (!filePath.startsWith(dir)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const relative = path.relative(dir, filePath) || path.basename(filePath);
|
|
178
|
+
if (matchesAny(relative, patterns)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
|
|
185
|
+
const whitelist = new Set();
|
|
186
|
+
for (const filePath of filePaths) {
|
|
187
|
+
const absolute = path.resolve(filePath);
|
|
188
|
+
const rel = path.relative(cwd, absolute);
|
|
189
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
190
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
191
|
+
const part = parts[i];
|
|
192
|
+
if (!DEFAULT_IGNORED_DIRS.includes(part)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const dirPath = path.resolve(cwd, ...parts.slice(0, i + 1));
|
|
196
|
+
if (whitelist.has(dirPath)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const stats = await fsModule.stat(path.join(dirPath, '.gitignore'));
|
|
201
|
+
if (stats.isFile()) {
|
|
202
|
+
whitelist.add(dirPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// no .gitignore at this level; keep ignored
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return whitelist;
|
|
211
|
+
}
|
|
212
|
+
function findIgnoredAncestor(filePath, cwd, _literalDirs, allowedPaths, ignoredWhitelist) {
|
|
213
|
+
const absolute = path.resolve(filePath);
|
|
214
|
+
if (Array.from(allowedPaths).some((allowed) => absolute === allowed || absolute.startsWith(`${allowed}${path.sep}`))) {
|
|
215
|
+
return null; // explicitly requested path overrides default ignore when the ignored dir itself was passed
|
|
216
|
+
}
|
|
217
|
+
const rel = path.relative(cwd, absolute);
|
|
218
|
+
const parts = rel.split(path.sep);
|
|
219
|
+
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
220
|
+
const part = parts[idx];
|
|
221
|
+
if (!DEFAULT_IGNORED_DIRS.includes(part)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const ignoredDir = path.resolve(cwd, parts.slice(0, idx + 1).join(path.sep));
|
|
225
|
+
if (ignoredWhitelist.has(ignoredDir)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
return part;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function matchesAny(relativePath, patterns) {
|
|
233
|
+
return patterns.some((pattern) => matchesPattern(relativePath, pattern));
|
|
234
|
+
}
|
|
235
|
+
function matchesPattern(relativePath, pattern) {
|
|
236
|
+
if (!pattern) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const normalized = pattern.replace(/\\+/g, '/');
|
|
240
|
+
// Directory rule
|
|
241
|
+
if (normalized.endsWith('/')) {
|
|
242
|
+
const dir = normalized.slice(0, -1);
|
|
243
|
+
return relativePath === dir || relativePath.startsWith(`${dir}/`);
|
|
244
|
+
}
|
|
245
|
+
// Simple glob support (* and **)
|
|
246
|
+
const regex = globToRegex(normalized);
|
|
247
|
+
return regex.test(relativePath);
|
|
248
|
+
}
|
|
249
|
+
function globToRegex(pattern) {
|
|
250
|
+
const withMarkers = pattern.replace(/\*\*/g, '§§DOUBLESTAR§§').replace(/\*/g, '§§SINGLESTAR§§');
|
|
251
|
+
const escaped = withMarkers.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
252
|
+
const restored = escaped
|
|
253
|
+
.replace(/§§DOUBLESTAR§§/g, '.*')
|
|
254
|
+
.replace(/§§SINGLESTAR§§/g, '[^/]*');
|
|
255
|
+
return new RegExp(`^${restored}$`);
|
|
256
|
+
}
|
|
257
|
+
function includesDotfileSegment(pattern) {
|
|
258
|
+
const segments = pattern.split('/');
|
|
259
|
+
return segments.some((segment) => segment.startsWith('.') && segment.length > 1);
|
|
123
260
|
}
|
|
124
261
|
async function expandWithCustomFs(partitioned, fsModule) {
|
|
125
262
|
const paths = new Set();
|
|
@@ -155,6 +292,12 @@ function makeDirectoryPattern(relative) {
|
|
|
155
292
|
}
|
|
156
293
|
return `${stripTrailingSlashes(relative)}/**/*`;
|
|
157
294
|
}
|
|
295
|
+
function isNativeFsModule(fsModule) {
|
|
296
|
+
return ((fsModule.__nativeFs === true ||
|
|
297
|
+
(fsModule.readFile === DEFAULT_FS.readFile &&
|
|
298
|
+
fsModule.stat === DEFAULT_FS.stat &&
|
|
299
|
+
fsModule.readdir === DEFAULT_FS.readdir)));
|
|
300
|
+
}
|
|
158
301
|
function normalizeGlob(pattern, cwd) {
|
|
159
302
|
if (!pattern) {
|
|
160
303
|
return '';
|
|
@@ -202,7 +345,7 @@ function relativePath(targetPath, cwd) {
|
|
|
202
345
|
}
|
|
203
346
|
export function createFileSections(files, cwd = process.cwd()) {
|
|
204
347
|
return files.map((file, index) => {
|
|
205
|
-
const relative = path.relative(cwd, file.path) || file.path;
|
|
348
|
+
const relative = toPosix(path.relative(cwd, file.path) || file.path);
|
|
206
349
|
const sectionText = [
|
|
207
350
|
`### File ${index + 1}: ${relative}`,
|
|
208
351
|
'```',
|
|
@@ -2,13 +2,8 @@ export function formatUSD(value) {
|
|
|
2
2
|
if (!Number.isFinite(value)) {
|
|
3
3
|
return 'n/a';
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
if (value >= 0.01) {
|
|
9
|
-
return `$${value.toFixed(3)}`;
|
|
10
|
-
}
|
|
11
|
-
return `$${value.toFixed(6)}`;
|
|
5
|
+
// Display with 4 decimal places, rounding to $0.0001 minimum granularity.
|
|
6
|
+
return `$${value.toFixed(4)}`;
|
|
12
7
|
}
|
|
13
8
|
export function formatNumber(value, { estimated = false } = {}) {
|
|
14
9
|
if (value == null) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export function createFsAdapter(fsModule) {
|
|
2
|
-
|
|
2
|
+
const adapter = {
|
|
3
3
|
stat: (targetPath) => fsModule.stat(targetPath),
|
|
4
4
|
readdir: (targetPath) => fsModule.readdir(targetPath),
|
|
5
5
|
readFile: (targetPath, encoding) => fsModule.readFile(targetPath, encoding),
|
|
6
6
|
};
|
|
7
|
+
// Mark adapters so downstream callers can treat them as native filesystem access.
|
|
8
|
+
adapter.__nativeFs = true;
|
|
9
|
+
return adapter;
|
|
7
10
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, } from '@google/generative-ai';
|
|
2
|
+
const MODEL_ID_MAP = {
|
|
3
|
+
'gemini-3-pro': 'gemini-3-pro-preview',
|
|
4
|
+
'gpt-5-pro': 'gpt-5-pro', // unused, normalize TS map
|
|
5
|
+
'gpt-5.1': 'gpt-5.1',
|
|
6
|
+
};
|
|
7
|
+
export function resolveGeminiModelId(modelName) {
|
|
8
|
+
// Map our logical Gemini names to the exact model ids expected by the SDK.
|
|
9
|
+
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
10
|
+
}
|
|
11
|
+
export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedModelId) {
|
|
12
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
13
|
+
const modelId = resolvedModelId ?? resolveGeminiModelId(modelName);
|
|
14
|
+
const model = genAI.getGenerativeModel({ model: modelId });
|
|
15
|
+
const adaptBodyToGemini = (body) => {
|
|
16
|
+
const contents = body.input.map((inputItem) => ({
|
|
17
|
+
role: inputItem.role === 'user' ? 'user' : 'model',
|
|
18
|
+
parts: inputItem.content
|
|
19
|
+
.map((contentPart) => {
|
|
20
|
+
if (contentPart.type === 'input_text') {
|
|
21
|
+
return { text: contentPart.text };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
})
|
|
25
|
+
.filter((part) => part !== null),
|
|
26
|
+
}));
|
|
27
|
+
const tools = body.tools
|
|
28
|
+
?.map((tool) => {
|
|
29
|
+
if (tool.type === 'web_search_preview') {
|
|
30
|
+
return {
|
|
31
|
+
googleSearch: {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
})
|
|
36
|
+
.filter((t) => Object.keys(t).length > 0);
|
|
37
|
+
const generationConfig = {
|
|
38
|
+
maxOutputTokens: body.max_output_tokens,
|
|
39
|
+
};
|
|
40
|
+
const safetySettings = [
|
|
41
|
+
{
|
|
42
|
+
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
43
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
47
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
51
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
55
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const systemInstruction = body.instructions || undefined;
|
|
59
|
+
return { systemInstruction, contents, tools, generationConfig, safetySettings };
|
|
60
|
+
};
|
|
61
|
+
const adaptGeminiResponseToOracle = (geminiResponse) => {
|
|
62
|
+
const outputText = [];
|
|
63
|
+
const output = [];
|
|
64
|
+
geminiResponse.candidates?.forEach((candidate) => {
|
|
65
|
+
candidate.content?.parts?.forEach((part) => {
|
|
66
|
+
if (part.text) {
|
|
67
|
+
outputText.push(part.text);
|
|
68
|
+
output.push({ type: 'text', text: part.text });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const usage = {
|
|
73
|
+
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
|
74
|
+
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
|
75
|
+
total_tokens: (geminiResponse.usageMetadata?.promptTokenCount || 0) + (geminiResponse.usageMetadata?.candidatesTokenCount || 0),
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
id: `gemini-${Date.now()}`, // Gemini doesn't always provide a stable ID in the response object
|
|
79
|
+
status: 'completed',
|
|
80
|
+
output_text: outputText,
|
|
81
|
+
output,
|
|
82
|
+
usage,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const enrichGeminiError = (error) => {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
if (message.includes('404')) {
|
|
88
|
+
return new Error(`Gemini model not available to this API key/region. Confirm preview access and model ID (${modelId}). Original: ${message}`);
|
|
89
|
+
}
|
|
90
|
+
return error instanceof Error ? error : new Error(message);
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
responses: {
|
|
94
|
+
stream: (body) => {
|
|
95
|
+
const geminiBody = adaptBodyToGemini(body);
|
|
96
|
+
let finalResponsePromise = null;
|
|
97
|
+
const collectChunkText = (chunk) => {
|
|
98
|
+
const parts = [];
|
|
99
|
+
chunk.candidates?.forEach((candidate) => {
|
|
100
|
+
candidate.content?.parts?.forEach((part) => {
|
|
101
|
+
if (part.text) {
|
|
102
|
+
parts.push(part.text);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
return parts.join('');
|
|
107
|
+
};
|
|
108
|
+
async function* iterator() {
|
|
109
|
+
let streamingResp;
|
|
110
|
+
try {
|
|
111
|
+
streamingResp = await model.generateContentStream(geminiBody);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw enrichGeminiError(error);
|
|
115
|
+
}
|
|
116
|
+
for await (const chunk of streamingResp.stream) {
|
|
117
|
+
const text = collectChunkText(chunk);
|
|
118
|
+
if (text) {
|
|
119
|
+
yield { type: 'chunk', delta: text };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finalResponsePromise = streamingResp.response.then(adaptGeminiResponseToOracle);
|
|
123
|
+
}
|
|
124
|
+
const generator = iterator();
|
|
125
|
+
return {
|
|
126
|
+
[Symbol.asyncIterator]: () => generator,
|
|
127
|
+
finalResponse: async () => {
|
|
128
|
+
// Ensure the stream has been consumed or at least started to get the promise
|
|
129
|
+
if (!finalResponsePromise) {
|
|
130
|
+
// In case the user calls finalResponse before iterating, we need to consume the stream
|
|
131
|
+
// This is a bit edge-casey but safe.
|
|
132
|
+
for await (const _ of generator) { }
|
|
133
|
+
}
|
|
134
|
+
if (!finalResponsePromise) {
|
|
135
|
+
throw new Error('Response promise not initialized');
|
|
136
|
+
}
|
|
137
|
+
return finalResponsePromise;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
create: async (body) => {
|
|
142
|
+
const geminiBody = adaptBodyToGemini(body);
|
|
143
|
+
let result;
|
|
144
|
+
try {
|
|
145
|
+
result = await model.generateContent(geminiBody);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw enrichGeminiError(error);
|
|
149
|
+
}
|
|
150
|
+
return adaptGeminiResponseToOracle(result.response);
|
|
151
|
+
},
|
|
152
|
+
retrieve: async (id) => {
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
status: 'error',
|
|
156
|
+
error: { message: 'Retrieve by ID not supported for Gemini API yet.' },
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function maskApiKey(key) {
|
|
2
|
+
if (!key)
|
|
3
|
+
return null;
|
|
4
|
+
if (key.length <= 8)
|
|
5
|
+
return `${key[0] ?? ''}***${key[key.length - 1] ?? ''}`;
|
|
6
|
+
const prefix = key.slice(0, 4);
|
|
7
|
+
const suffix = key.slice(-4);
|
|
8
|
+
return `${prefix}****${suffix}`;
|
|
9
|
+
}
|
|
10
|
+
export function formatBaseUrlForLog(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return '';
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(raw);
|
|
15
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
16
|
+
let path = '';
|
|
17
|
+
if (segments.length > 0) {
|
|
18
|
+
path = `/${segments[0]}`;
|
|
19
|
+
if (segments.length > 1) {
|
|
20
|
+
path += '/...';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const allowedQueryKeys = ['api-version'];
|
|
24
|
+
const maskedQuery = allowedQueryKeys
|
|
25
|
+
.filter((key) => parsed.searchParams.has(key))
|
|
26
|
+
.map((key) => `${key}=***`);
|
|
27
|
+
const query = maskedQuery.length > 0 ? `?${maskedQuery.join('&')}` : '';
|
|
28
|
+
return `${parsed.protocol}//${parsed.host}${path}${query}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (trimmed.length <= 64)
|
|
33
|
+
return trimmed;
|
|
34
|
+
return `${trimmed.slice(0, 32)}…${trimmed.slice(-8)}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -29,11 +29,17 @@ export function supportsOscProgress(env = process.env, isTty = process.stdout.is
|
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
31
|
export function startOscProgress(options = {}) {
|
|
32
|
-
const { label = 'Waiting for
|
|
32
|
+
const { label = 'Waiting for API', targetMs = 10 * 60_000, write = (text) => process.stdout.write(text), indeterminate = false, } = options;
|
|
33
33
|
if (!supportsOscProgress(options.env, options.isTty)) {
|
|
34
34
|
return () => { };
|
|
35
35
|
}
|
|
36
36
|
const cleanLabel = sanitizeLabel(label);
|
|
37
|
+
if (indeterminate) {
|
|
38
|
+
write(`${OSC}3;;${cleanLabel}${ST}`);
|
|
39
|
+
return () => {
|
|
40
|
+
write(`${OSC}0;0;${cleanLabel}${ST}`);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
37
43
|
const target = Math.max(targetMs, 1_000);
|
|
38
44
|
const send = (state, percent) => {
|
|
39
45
|
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|