@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,185 @@
|
|
|
1
|
+
import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
2
|
+
const MODEL_ID_MAP = {
|
|
3
|
+
'gemini-3-pro': 'gemini-3-pro-preview',
|
|
4
|
+
'gpt-5.1-pro': 'gpt-5.1-pro', // unused, normalize TS map
|
|
5
|
+
'gpt-5-pro': 'gpt-5-pro',
|
|
6
|
+
'gpt-5.1': 'gpt-5.1',
|
|
7
|
+
'gpt-5.1-codex': 'gpt-5.1-codex',
|
|
8
|
+
'claude-4.5-sonnet': 'claude-4.5-sonnet',
|
|
9
|
+
'claude-4.1-opus': 'claude-4.1-opus',
|
|
10
|
+
};
|
|
11
|
+
export function resolveGeminiModelId(modelName) {
|
|
12
|
+
// Map our logical Gemini names to the exact model ids expected by the SDK.
|
|
13
|
+
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
14
|
+
}
|
|
15
|
+
export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedModelId) {
|
|
16
|
+
const modelId = resolvedModelId ?? resolveGeminiModelId(modelName);
|
|
17
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
18
|
+
const adaptBodyToGemini = (body) => {
|
|
19
|
+
const contents = body.input.map((inputItem) => ({
|
|
20
|
+
role: inputItem.role === 'user' ? 'user' : 'model',
|
|
21
|
+
parts: inputItem.content
|
|
22
|
+
.map((contentPart) => {
|
|
23
|
+
if (contentPart.type === 'input_text') {
|
|
24
|
+
return { text: contentPart.text };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
})
|
|
28
|
+
.filter((part) => part !== null),
|
|
29
|
+
}));
|
|
30
|
+
const tools = body.tools
|
|
31
|
+
?.map((tool) => {
|
|
32
|
+
if (tool.type === 'web_search_preview') {
|
|
33
|
+
return {
|
|
34
|
+
googleSearch: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
})
|
|
39
|
+
.filter((t) => Object.keys(t).length > 0);
|
|
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
|
|
59
|
+
? { role: 'system', parts: [{ text: body.instructions }] }
|
|
60
|
+
: undefined;
|
|
61
|
+
return {
|
|
62
|
+
model: modelId,
|
|
63
|
+
contents,
|
|
64
|
+
config: {
|
|
65
|
+
maxOutputTokens: body.max_output_tokens,
|
|
66
|
+
safetySettings,
|
|
67
|
+
tools,
|
|
68
|
+
systemInstruction,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const adaptGeminiResponseToOracle = (geminiResponse) => {
|
|
73
|
+
const outputText = [];
|
|
74
|
+
const output = [];
|
|
75
|
+
geminiResponse.candidates?.forEach((candidate) => {
|
|
76
|
+
candidate.content?.parts?.forEach((part) => {
|
|
77
|
+
if (part.text) {
|
|
78
|
+
outputText.push(part.text);
|
|
79
|
+
output.push({ type: 'text', text: part.text });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
const usage = {
|
|
84
|
+
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
|
85
|
+
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
|
86
|
+
total_tokens: (geminiResponse.usageMetadata?.promptTokenCount || 0) + (geminiResponse.usageMetadata?.candidatesTokenCount || 0),
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
id: geminiResponse.responseId ?? `gemini-${Date.now()}`,
|
|
90
|
+
status: 'completed',
|
|
91
|
+
output_text: outputText,
|
|
92
|
+
output,
|
|
93
|
+
usage,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const adaptAggregatedTextToOracle = (text, usageMetadata, responseId) => {
|
|
97
|
+
const usage = {
|
|
98
|
+
input_tokens: usageMetadata?.promptTokenCount ?? 0,
|
|
99
|
+
output_tokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
100
|
+
total_tokens: (usageMetadata?.promptTokenCount ?? 0) + (usageMetadata?.candidatesTokenCount ?? 0),
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
id: responseId ?? `gemini-${Date.now()}`,
|
|
104
|
+
status: 'completed',
|
|
105
|
+
output_text: [text],
|
|
106
|
+
output: [{ type: 'text', text }],
|
|
107
|
+
usage,
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const enrichGeminiError = (error) => {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
if (message.includes('404')) {
|
|
113
|
+
return new Error(`Gemini model not available to this API key/region. Confirm preview access and model ID (${modelId}). Original: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
return error instanceof Error ? error : new Error(message);
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
responses: {
|
|
119
|
+
stream: (body) => {
|
|
120
|
+
const geminiBody = adaptBodyToGemini(body);
|
|
121
|
+
let finalResponsePromise = null;
|
|
122
|
+
let aggregatedText = '';
|
|
123
|
+
let lastUsage;
|
|
124
|
+
let responseId;
|
|
125
|
+
async function* iterator() {
|
|
126
|
+
let streamingResp;
|
|
127
|
+
try {
|
|
128
|
+
streamingResp = await genAI.models.generateContentStream(geminiBody);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw enrichGeminiError(error);
|
|
132
|
+
}
|
|
133
|
+
for await (const chunk of streamingResp) {
|
|
134
|
+
const text = chunk.text;
|
|
135
|
+
if (text) {
|
|
136
|
+
aggregatedText += text;
|
|
137
|
+
yield { type: 'chunk', delta: text };
|
|
138
|
+
}
|
|
139
|
+
if (chunk.usageMetadata) {
|
|
140
|
+
lastUsage = chunk.usageMetadata;
|
|
141
|
+
}
|
|
142
|
+
if (chunk.responseId) {
|
|
143
|
+
responseId = chunk.responseId;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finalResponsePromise = Promise.resolve(adaptAggregatedTextToOracle(aggregatedText, lastUsage, responseId));
|
|
147
|
+
}
|
|
148
|
+
const generator = iterator();
|
|
149
|
+
return {
|
|
150
|
+
[Symbol.asyncIterator]: () => generator,
|
|
151
|
+
finalResponse: async () => {
|
|
152
|
+
// Ensure the stream has been consumed or at least started to get the promise
|
|
153
|
+
if (!finalResponsePromise) {
|
|
154
|
+
// In case the user calls finalResponse before iterating, we need to consume the stream
|
|
155
|
+
// This is a bit edge-casey but safe.
|
|
156
|
+
for await (const _ of generator) { }
|
|
157
|
+
}
|
|
158
|
+
if (!finalResponsePromise) {
|
|
159
|
+
throw new Error('Response promise not initialized');
|
|
160
|
+
}
|
|
161
|
+
return finalResponsePromise;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
create: async (body) => {
|
|
166
|
+
const geminiBody = adaptBodyToGemini(body);
|
|
167
|
+
let result;
|
|
168
|
+
try {
|
|
169
|
+
result = await genAI.models.generateContent(geminiBody);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw enrichGeminiError(error);
|
|
173
|
+
}
|
|
174
|
+
return adaptGeminiResponseToOracle(result);
|
|
175
|
+
},
|
|
176
|
+
retrieve: async (id) => {
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
status: 'error',
|
|
180
|
+
error: { message: 'Retrieve by ID not supported for Gemini API yet.' },
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const EXT_TO_LANG = {
|
|
3
|
+
'.ts': 'ts',
|
|
4
|
+
'.tsx': 'tsx',
|
|
5
|
+
'.js': 'js',
|
|
6
|
+
'.jsx': 'jsx',
|
|
7
|
+
'.json': 'json',
|
|
8
|
+
'.swift': 'swift',
|
|
9
|
+
'.md': 'md',
|
|
10
|
+
'.sh': 'bash',
|
|
11
|
+
'.bash': 'bash',
|
|
12
|
+
'.zsh': 'bash',
|
|
13
|
+
'.py': 'python',
|
|
14
|
+
'.rb': 'ruby',
|
|
15
|
+
'.rs': 'rust',
|
|
16
|
+
'.go': 'go',
|
|
17
|
+
'.java': 'java',
|
|
18
|
+
'.c': 'c',
|
|
19
|
+
'.h': 'c',
|
|
20
|
+
'.cpp': 'cpp',
|
|
21
|
+
'.hpp': 'cpp',
|
|
22
|
+
'.css': 'css',
|
|
23
|
+
'.scss': 'scss',
|
|
24
|
+
'.sql': 'sql',
|
|
25
|
+
'.yaml': 'yaml',
|
|
26
|
+
'.yml': 'yaml',
|
|
27
|
+
};
|
|
28
|
+
function detectFenceLanguage(displayPath) {
|
|
29
|
+
const ext = path.extname(displayPath).toLowerCase();
|
|
30
|
+
return EXT_TO_LANG[ext] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function pickFence(content) {
|
|
33
|
+
// Choose a fence longer than any backtick run inside the file so the block can't prematurely close.
|
|
34
|
+
const matches = [...content.matchAll(/`+/g)];
|
|
35
|
+
const maxTicks = matches.reduce((max, m) => Math.max(max, m[0].length), 0);
|
|
36
|
+
const fenceLength = Math.max(3, maxTicks + 1);
|
|
37
|
+
return '`'.repeat(fenceLength);
|
|
38
|
+
}
|
|
39
|
+
export function formatFileSection(displayPath, content) {
|
|
40
|
+
const fence = pickFence(content);
|
|
41
|
+
const lang = detectFenceLanguage(displayPath);
|
|
42
|
+
const normalized = content.replace(/\s+$/u, '');
|
|
43
|
+
const header = `### File: ${displayPath}`;
|
|
44
|
+
const fenceOpen = lang ? `${fence}${lang}` : fence;
|
|
45
|
+
return [header, fenceOpen, normalized, fence, ''].join('\n');
|
|
46
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
|
|
4
|
+
import { sessionStore } from '../sessionStore.js';
|
|
5
|
+
const OSC_PROGRESS_PREFIX = '\u001b]9;4;';
|
|
6
|
+
const OSC_PROGRESS_END = '\u001b\\';
|
|
7
|
+
function forwardOscProgress(chunk, shouldForward) {
|
|
8
|
+
if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let searchFrom = 0;
|
|
12
|
+
while (searchFrom < chunk.length) {
|
|
13
|
+
const start = chunk.indexOf(OSC_PROGRESS_PREFIX, searchFrom);
|
|
14
|
+
if (start === -1) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
const end = chunk.indexOf(OSC_PROGRESS_END, start + OSC_PROGRESS_PREFIX.length);
|
|
18
|
+
if (end === -1) {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(chunk.slice(start, end + OSC_PROGRESS_END.length));
|
|
22
|
+
searchFrom = end + OSC_PROGRESS_END.length;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const defaultDeps = {
|
|
26
|
+
store: sessionStore,
|
|
27
|
+
runOracleImpl: runOracle,
|
|
28
|
+
now: () => Date.now(),
|
|
29
|
+
};
|
|
30
|
+
export async function runMultiModelApiSession(params, deps = defaultDeps) {
|
|
31
|
+
const { sessionMeta, runOptions, models, cwd } = params;
|
|
32
|
+
const { onModelDone } = params;
|
|
33
|
+
const store = deps.store ?? sessionStore;
|
|
34
|
+
const runOracleImpl = deps.runOracleImpl ?? runOracle;
|
|
35
|
+
const now = deps.now ?? (() => Date.now());
|
|
36
|
+
const startMark = now();
|
|
37
|
+
const executions = models.map((model) => startModelExecution({
|
|
38
|
+
sessionMeta,
|
|
39
|
+
runOptions,
|
|
40
|
+
model,
|
|
41
|
+
cwd,
|
|
42
|
+
store,
|
|
43
|
+
runOracleImpl,
|
|
44
|
+
}));
|
|
45
|
+
const settled = await Promise.allSettled(executions.map((exec) => exec.promise.then(async (value) => {
|
|
46
|
+
if (onModelDone) {
|
|
47
|
+
await onModelDone(value);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}, (error) => {
|
|
51
|
+
throw error;
|
|
52
|
+
})));
|
|
53
|
+
const fulfilled = [];
|
|
54
|
+
const rejected = [];
|
|
55
|
+
settled.forEach((result, index) => {
|
|
56
|
+
const exec = executions[index];
|
|
57
|
+
if (result.status === 'fulfilled') {
|
|
58
|
+
fulfilled.push(result.value);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
rejected.push({ model: exec.model, reason: result.reason });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
fulfilled,
|
|
66
|
+
rejected,
|
|
67
|
+
elapsedMs: now() - startMark,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOracleImpl, }) {
|
|
71
|
+
const logWriter = store.createLogWriter(sessionMeta.id, model);
|
|
72
|
+
const perModelOptions = {
|
|
73
|
+
...runOptions,
|
|
74
|
+
model,
|
|
75
|
+
models: undefined,
|
|
76
|
+
sessionId: `${sessionMeta.id}:${model}`,
|
|
77
|
+
};
|
|
78
|
+
const perModelLog = (message) => {
|
|
79
|
+
logWriter.logLine(message ?? '');
|
|
80
|
+
};
|
|
81
|
+
const mirrorOscProgress = process.stdout.isTTY === true;
|
|
82
|
+
const perModelWrite = (chunk) => {
|
|
83
|
+
logWriter.writeChunk(chunk);
|
|
84
|
+
forwardOscProgress(chunk, mirrorOscProgress);
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
const promise = (async () => {
|
|
88
|
+
await store.updateModelRun(sessionMeta.id, model, {
|
|
89
|
+
status: 'running',
|
|
90
|
+
queuedAt: new Date().toISOString(),
|
|
91
|
+
startedAt: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
const result = await runOracleImpl({
|
|
94
|
+
...perModelOptions,
|
|
95
|
+
effectiveModelId: model,
|
|
96
|
+
// Drop per-model preamble; the aggregate runner prints the shared header and tips once.
|
|
97
|
+
suppressHeader: true,
|
|
98
|
+
suppressAnswerHeader: true,
|
|
99
|
+
suppressTips: true,
|
|
100
|
+
}, {
|
|
101
|
+
cwd,
|
|
102
|
+
log: perModelLog,
|
|
103
|
+
write: perModelWrite,
|
|
104
|
+
});
|
|
105
|
+
if (result.mode !== 'live') {
|
|
106
|
+
throw new Error('Unexpected preview result while running a session.');
|
|
107
|
+
}
|
|
108
|
+
const answerText = extractTextOutput(result.response);
|
|
109
|
+
await store.updateModelRun(sessionMeta.id, model, {
|
|
110
|
+
status: 'completed',
|
|
111
|
+
completedAt: new Date().toISOString(),
|
|
112
|
+
usage: result.usage,
|
|
113
|
+
response: extractResponseMetadata(result.response),
|
|
114
|
+
transport: undefined,
|
|
115
|
+
error: undefined,
|
|
116
|
+
log: await describeLog(sessionMeta.id, logWriter.logPath, store),
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
model,
|
|
120
|
+
usage: result.usage,
|
|
121
|
+
answerText,
|
|
122
|
+
logPath: logWriter.logPath,
|
|
123
|
+
};
|
|
124
|
+
})()
|
|
125
|
+
.catch(async (error) => {
|
|
126
|
+
const userError = asOracleUserError(error);
|
|
127
|
+
const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
|
|
128
|
+
const transportMetadata = error instanceof OracleTransportError ? { reason: error.reason } : undefined;
|
|
129
|
+
await store.updateModelRun(sessionMeta.id, model, {
|
|
130
|
+
status: 'error',
|
|
131
|
+
completedAt: new Date().toISOString(),
|
|
132
|
+
response: responseMetadata,
|
|
133
|
+
transport: transportMetadata,
|
|
134
|
+
error: userError
|
|
135
|
+
? {
|
|
136
|
+
category: userError.category,
|
|
137
|
+
message: userError.message,
|
|
138
|
+
details: userError.details,
|
|
139
|
+
}
|
|
140
|
+
: undefined,
|
|
141
|
+
log: await describeLog(sessionMeta.id, logWriter.logPath, store),
|
|
142
|
+
});
|
|
143
|
+
throw error;
|
|
144
|
+
})
|
|
145
|
+
.finally(() => {
|
|
146
|
+
logWriter.stream.end();
|
|
147
|
+
});
|
|
148
|
+
return { model, promise };
|
|
149
|
+
}
|
|
150
|
+
async function describeLog(sessionId, logFilePath, store) {
|
|
151
|
+
const { dir } = await store.getPaths(sessionId);
|
|
152
|
+
const relative = path.relative(dir, logFilePath);
|
|
153
|
+
try {
|
|
154
|
+
const stats = await fsStat(logFilePath);
|
|
155
|
+
return { path: relative, bytes: stats.size };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { path: relative };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function fsStat(target) {
|
|
162
|
+
const stats = await fs.stat(target);
|
|
163
|
+
return { size: stats.size };
|
|
164
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 API', targetMs = 10 * 60_000, write = (text) => process.stdout.write(text), indeterminate = false, } = options;
|
|
33
|
+
if (!supportsOscProgress(options.env, options.isTty)) {
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
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
|
+
}
|
|
43
|
+
const target = Math.max(targetMs, 1_000);
|
|
44
|
+
const send = (state, percent) => {
|
|
45
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
46
|
+
write(`${OSC}${state};${clamped};${cleanLabel}${ST}`);
|
|
47
|
+
};
|
|
48
|
+
const startedAt = Date.now();
|
|
49
|
+
send(1, 0); // activate progress bar
|
|
50
|
+
const timer = setInterval(() => {
|
|
51
|
+
const elapsed = Date.now() - startedAt;
|
|
52
|
+
const percent = Math.min(99, (elapsed / target) * 100);
|
|
53
|
+
send(1, percent);
|
|
54
|
+
}, 900);
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
let stopped = false;
|
|
57
|
+
return () => {
|
|
58
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: multiple callers may try to stop
|
|
59
|
+
if (stopped) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
stopped = true;
|
|
63
|
+
clearInterval(timer);
|
|
64
|
+
send(0, 0); // clear the progress bar
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatFileSection } from './markdown.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the shared markdown structure for system/user/file sections.
|
|
4
|
+
* Collapses excessive blank lines and trims trailing whitespace to keep
|
|
5
|
+
* snapshots stable across CLI and browser modes.
|
|
6
|
+
*/
|
|
7
|
+
export function buildPromptMarkdown(systemPrompt, userPrompt, sections) {
|
|
8
|
+
const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
|
|
9
|
+
sections.forEach((section) => {
|
|
10
|
+
lines.push(formatFileSection(section.displayPath, section.content));
|
|
11
|
+
});
|
|
12
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { DEFAULT_SYSTEM_PROMPT } from './config.js';
|
|
3
|
+
import { createFileSections, readFiles } from './files.js';
|
|
4
|
+
import { formatFileSection } from './markdown.js';
|
|
5
|
+
import { createFsAdapter } from './fsAdapter.js';
|
|
6
|
+
export function buildPrompt(basePrompt, files, cwd = process.cwd()) {
|
|
7
|
+
if (!files.length) {
|
|
8
|
+
return basePrompt;
|
|
9
|
+
}
|
|
10
|
+
const sections = createFileSections(files, cwd);
|
|
11
|
+
const sectionText = sections.map((section) => section.sectionText).join('\n\n');
|
|
12
|
+
return `${basePrompt.trim()}\n\n${sectionText}`;
|
|
13
|
+
}
|
|
14
|
+
export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, }) {
|
|
15
|
+
return {
|
|
16
|
+
model: modelConfig.apiModel ?? modelConfig.model,
|
|
17
|
+
instructions: systemPrompt,
|
|
18
|
+
input: [
|
|
19
|
+
{
|
|
20
|
+
role: 'user',
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: 'input_text',
|
|
24
|
+
text: userPrompt,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
tools: searchEnabled ? [{ type: 'web_search_preview' }] : undefined,
|
|
30
|
+
reasoning: modelConfig.reasoning || undefined,
|
|
31
|
+
max_output_tokens: maxOutputTokens,
|
|
32
|
+
background: background ? true : undefined,
|
|
33
|
+
store: storeResponse ? true : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function renderPromptMarkdown(options, deps = {}) {
|
|
37
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
38
|
+
const fsModule = deps.fs ?? createFsAdapter(fs);
|
|
39
|
+
const files = await readFiles(options.file ?? [], { cwd, fsModule });
|
|
40
|
+
const sections = createFileSections(files, cwd);
|
|
41
|
+
const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
42
|
+
const userPrompt = (options.prompt ?? '').trim();
|
|
43
|
+
const lines = ['[SYSTEM]', systemPrompt, ''];
|
|
44
|
+
lines.push('[USER]', userPrompt, '');
|
|
45
|
+
sections.forEach((section) => {
|
|
46
|
+
lines.push(formatFileSection(section.displayPath, section.content));
|
|
47
|
+
});
|
|
48
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
49
|
+
}
|