@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,221 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getCliVersion } from '../../version.js';
|
|
3
|
+
import { LoggingMessageNotificationParamsSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { ensureBrowserAvailable, mapConsultToRunOptions } from '../utils.js';
|
|
5
|
+
import { sessionStore } from '../../sessionStore.js';
|
|
6
|
+
async function readSessionLogTail(sessionId, maxBytes) {
|
|
7
|
+
try {
|
|
8
|
+
const log = await sessionStore.readLog(sessionId);
|
|
9
|
+
if (log.length <= maxBytes) {
|
|
10
|
+
return log;
|
|
11
|
+
}
|
|
12
|
+
return log.slice(-maxBytes);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
import { performSessionRun } from '../../cli/sessionRunner.js';
|
|
19
|
+
import { CHATGPT_URL } from '../../browser/constants.js';
|
|
20
|
+
import { consultInputSchema } from '../types.js';
|
|
21
|
+
import { loadUserConfig } from '../../config.js';
|
|
22
|
+
import { resolveNotificationSettings } from '../../cli/notifier.js';
|
|
23
|
+
import { mapModelToBrowserLabel, resolveBrowserModelLabel } from '../../cli/browserConfig.js';
|
|
24
|
+
// Use raw shapes so the MCP SDK (with its bundled Zod) wraps them and emits valid JSON Schema.
|
|
25
|
+
const consultInputShape = {
|
|
26
|
+
prompt: z.string().min(1, 'Prompt is required.'),
|
|
27
|
+
files: z.array(z.string()).default([]),
|
|
28
|
+
model: z.string().optional(),
|
|
29
|
+
models: z.array(z.string()).optional(),
|
|
30
|
+
engine: z.enum(['api', 'browser']).optional(),
|
|
31
|
+
browserModelLabel: z.string().optional(),
|
|
32
|
+
search: z.boolean().optional(),
|
|
33
|
+
slug: z.string().optional(),
|
|
34
|
+
};
|
|
35
|
+
const consultModelSummaryShape = z.object({
|
|
36
|
+
model: z.string(),
|
|
37
|
+
status: z.string(),
|
|
38
|
+
startedAt: z.string().optional(),
|
|
39
|
+
completedAt: z.string().optional(),
|
|
40
|
+
usage: z
|
|
41
|
+
.object({
|
|
42
|
+
inputTokens: z.number().optional(),
|
|
43
|
+
outputTokens: z.number().optional(),
|
|
44
|
+
reasoningTokens: z.number().optional(),
|
|
45
|
+
totalTokens: z.number().optional(),
|
|
46
|
+
cost: z.number().optional(),
|
|
47
|
+
})
|
|
48
|
+
.optional(),
|
|
49
|
+
response: z
|
|
50
|
+
.object({
|
|
51
|
+
id: z.string().optional(),
|
|
52
|
+
requestId: z.string().optional(),
|
|
53
|
+
status: z.string().optional(),
|
|
54
|
+
})
|
|
55
|
+
.optional(),
|
|
56
|
+
error: z
|
|
57
|
+
.object({
|
|
58
|
+
category: z.string().optional(),
|
|
59
|
+
message: z.string().optional(),
|
|
60
|
+
})
|
|
61
|
+
.optional(),
|
|
62
|
+
logPath: z.string().optional(),
|
|
63
|
+
});
|
|
64
|
+
const consultOutputShape = {
|
|
65
|
+
sessionId: z.string(),
|
|
66
|
+
status: z.string(),
|
|
67
|
+
output: z.string(),
|
|
68
|
+
models: z.array(consultModelSummaryShape).optional(),
|
|
69
|
+
};
|
|
70
|
+
export function summarizeModelRunsForConsult(runs) {
|
|
71
|
+
if (!runs || runs.length === 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return runs.map((run) => {
|
|
75
|
+
const response = run.response
|
|
76
|
+
? {
|
|
77
|
+
id: run.response.id ?? undefined,
|
|
78
|
+
requestId: run.response.requestId ?? undefined,
|
|
79
|
+
status: run.response.status ?? undefined,
|
|
80
|
+
}
|
|
81
|
+
: undefined;
|
|
82
|
+
const error = run.error
|
|
83
|
+
? {
|
|
84
|
+
category: run.error.category,
|
|
85
|
+
message: run.error.message,
|
|
86
|
+
}
|
|
87
|
+
: undefined;
|
|
88
|
+
return {
|
|
89
|
+
model: run.model,
|
|
90
|
+
status: run.status ?? 'unknown',
|
|
91
|
+
startedAt: run.startedAt,
|
|
92
|
+
completedAt: run.completedAt,
|
|
93
|
+
usage: run.usage,
|
|
94
|
+
response,
|
|
95
|
+
error,
|
|
96
|
+
logPath: run.log?.path,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function registerConsultTool(server) {
|
|
101
|
+
server.registerTool('consult', {
|
|
102
|
+
title: 'Run an oracle session',
|
|
103
|
+
description: 'Run a one-shot Oracle session (API or browser). Attach files/dirs for context, optional model/engine overrides, and an optional slug. Background handling follows the CLI defaults; browser runs only start when Chrome is available.',
|
|
104
|
+
// Cast to any to satisfy SDK typings across differing Zod versions.
|
|
105
|
+
inputSchema: consultInputShape,
|
|
106
|
+
outputSchema: consultOutputShape,
|
|
107
|
+
}, async (input) => {
|
|
108
|
+
const textContent = (text) => [{ type: 'text', text }];
|
|
109
|
+
const { prompt, files, model, models, engine, search, browserModelLabel, slug } = consultInputSchema.parse(input);
|
|
110
|
+
const { config: userConfig } = await loadUserConfig();
|
|
111
|
+
const { runOptions, resolvedEngine } = mapConsultToRunOptions({
|
|
112
|
+
prompt,
|
|
113
|
+
files: files ?? [],
|
|
114
|
+
model,
|
|
115
|
+
models,
|
|
116
|
+
engine,
|
|
117
|
+
search,
|
|
118
|
+
userConfig,
|
|
119
|
+
env: process.env,
|
|
120
|
+
});
|
|
121
|
+
const cwd = process.cwd();
|
|
122
|
+
const browserGuard = ensureBrowserAvailable(resolvedEngine);
|
|
123
|
+
if (resolvedEngine === 'browser' && browserGuard) {
|
|
124
|
+
return {
|
|
125
|
+
isError: true,
|
|
126
|
+
content: textContent(browserGuard),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
let browserConfig;
|
|
130
|
+
if (resolvedEngine === 'browser') {
|
|
131
|
+
const preferredLabel = (browserModelLabel ?? model)?.trim();
|
|
132
|
+
const desiredModelLabel = resolveBrowserModelLabel(preferredLabel, runOptions.model);
|
|
133
|
+
// Keep the browser path minimal; only forward a desired model label for the ChatGPT picker.
|
|
134
|
+
browserConfig = {
|
|
135
|
+
url: CHATGPT_URL,
|
|
136
|
+
cookieSync: true,
|
|
137
|
+
headless: false,
|
|
138
|
+
hideWindow: false,
|
|
139
|
+
keepBrowser: false,
|
|
140
|
+
desiredModel: desiredModelLabel || mapModelToBrowserLabel(runOptions.model),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const notifications = resolveNotificationSettings({
|
|
144
|
+
cliNotify: undefined,
|
|
145
|
+
cliNotifySound: undefined,
|
|
146
|
+
env: process.env,
|
|
147
|
+
config: userConfig.notify,
|
|
148
|
+
});
|
|
149
|
+
const sessionMeta = await sessionStore.createSession({
|
|
150
|
+
...runOptions,
|
|
151
|
+
mode: resolvedEngine,
|
|
152
|
+
slug,
|
|
153
|
+
browserConfig,
|
|
154
|
+
}, cwd, notifications);
|
|
155
|
+
const logWriter = sessionStore.createLogWriter(sessionMeta.id);
|
|
156
|
+
// Best-effort: emit MCP logging notifications for live chunks but never block the run.
|
|
157
|
+
const sendLog = (text, level = 'info') => server.server
|
|
158
|
+
.sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
|
|
159
|
+
level,
|
|
160
|
+
data: { text, bytes: Buffer.byteLength(text, 'utf8') },
|
|
161
|
+
}))
|
|
162
|
+
.catch(() => { });
|
|
163
|
+
// Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
|
|
164
|
+
const log = (line) => {
|
|
165
|
+
logWriter.logLine(line);
|
|
166
|
+
if (line !== undefined) {
|
|
167
|
+
sendLog(line);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const write = (chunk) => {
|
|
171
|
+
logWriter.writeChunk(chunk);
|
|
172
|
+
sendLog(chunk, 'debug');
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
await performSessionRun({
|
|
177
|
+
sessionMeta,
|
|
178
|
+
runOptions,
|
|
179
|
+
mode: resolvedEngine,
|
|
180
|
+
browserConfig,
|
|
181
|
+
cwd,
|
|
182
|
+
log,
|
|
183
|
+
write,
|
|
184
|
+
version: getCliVersion(),
|
|
185
|
+
notifications,
|
|
186
|
+
muteStdout: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
log(`Run failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
+
return {
|
|
192
|
+
isError: true,
|
|
193
|
+
content: textContent(`Session ${sessionMeta.id} failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
logWriter.stream.end();
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const finalMeta = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
|
|
201
|
+
const summary = `Session ${sessionMeta.id} (${finalMeta.status})`;
|
|
202
|
+
const logTail = await readSessionLogTail(sessionMeta.id, 4000);
|
|
203
|
+
const modelsSummary = summarizeModelRunsForConsult(finalMeta.models);
|
|
204
|
+
return {
|
|
205
|
+
content: textContent([summary, logTail || '(log empty)'].join('\n').trim()),
|
|
206
|
+
structuredContent: {
|
|
207
|
+
sessionId: sessionMeta.id,
|
|
208
|
+
status: finalMeta.status,
|
|
209
|
+
output: logTail ?? '',
|
|
210
|
+
models: modelsSummary,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
isError: true,
|
|
217
|
+
content: textContent(`Session completed but metadata fetch failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { sessionStore } from '../../sessionStore.js';
|
|
4
|
+
// URIs:
|
|
5
|
+
// - oracle-session://<id>/metadata
|
|
6
|
+
// - oracle-session://<id>/log
|
|
7
|
+
// - oracle-session://<id>/request
|
|
8
|
+
export function registerSessionResources(server) {
|
|
9
|
+
const template = new ResourceTemplate('oracle-session://{id}/{kind}', { list: undefined });
|
|
10
|
+
server.registerResource('oracle-session', template, {
|
|
11
|
+
title: 'oracle session resources',
|
|
12
|
+
description: 'Read stored session metadata, log, or request payload.',
|
|
13
|
+
}, async (uri, variables) => {
|
|
14
|
+
const idRaw = variables?.id;
|
|
15
|
+
const kindRaw = variables?.kind;
|
|
16
|
+
// uri-template variables arrive as string | string[]; collapse to first value.
|
|
17
|
+
const id = Array.isArray(idRaw) ? idRaw[0] : idRaw;
|
|
18
|
+
const kind = Array.isArray(kindRaw) ? kindRaw[0] : kindRaw;
|
|
19
|
+
if (!id || !kind) {
|
|
20
|
+
throw new Error('Missing id or kind');
|
|
21
|
+
}
|
|
22
|
+
switch (kind) {
|
|
23
|
+
case 'metadata': {
|
|
24
|
+
const metadata = await sessionStore.readSession(id);
|
|
25
|
+
if (!metadata) {
|
|
26
|
+
throw new Error(`Session "${id}" not found.`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
contents: [
|
|
30
|
+
{
|
|
31
|
+
uri: uri.href,
|
|
32
|
+
text: JSON.stringify(metadata, null, 2),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case 'log': {
|
|
38
|
+
const log = await sessionStore.readLog(id);
|
|
39
|
+
return {
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: uri.href,
|
|
43
|
+
text: log,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case 'request': {
|
|
49
|
+
const request = await sessionStore.readRequest(id);
|
|
50
|
+
if (request) {
|
|
51
|
+
return {
|
|
52
|
+
contents: [
|
|
53
|
+
{
|
|
54
|
+
uri: uri.href,
|
|
55
|
+
text: JSON.stringify(request, null, 2),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const paths = await sessionStore.getPaths(id);
|
|
61
|
+
const raw = await fs.readFile(paths.request, 'utf8');
|
|
62
|
+
return {
|
|
63
|
+
contents: [
|
|
64
|
+
{
|
|
65
|
+
uri: uri.href,
|
|
66
|
+
text: raw,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unsupported resource kind: ${kind}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { sessionStore } from '../../sessionStore.js';
|
|
3
|
+
import { sessionsInputSchema } from '../types.js';
|
|
4
|
+
const sessionsInputShape = {
|
|
5
|
+
id: z.string().optional(),
|
|
6
|
+
hours: z.number().optional(),
|
|
7
|
+
limit: z.number().optional(),
|
|
8
|
+
includeAll: z.boolean().optional(),
|
|
9
|
+
detail: z.boolean().optional(),
|
|
10
|
+
};
|
|
11
|
+
const sessionsOutputShape = {
|
|
12
|
+
entries: z
|
|
13
|
+
.array(z.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
createdAt: z.string(),
|
|
16
|
+
status: z.string(),
|
|
17
|
+
model: z.string().optional(),
|
|
18
|
+
mode: z.string().optional(),
|
|
19
|
+
}))
|
|
20
|
+
.optional(),
|
|
21
|
+
total: z.number().optional(),
|
|
22
|
+
truncated: z.boolean().optional(),
|
|
23
|
+
session: z
|
|
24
|
+
.object({
|
|
25
|
+
metadata: z.record(z.string(), z.any()),
|
|
26
|
+
log: z.string(),
|
|
27
|
+
request: z.record(z.string(), z.any()).optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
};
|
|
31
|
+
export function registerSessionsTool(server) {
|
|
32
|
+
server.registerTool('sessions', {
|
|
33
|
+
title: 'List or fetch oracle sessions',
|
|
34
|
+
description: 'List stored sessions (same defaults as `oracle status`) or, with id/slug, return a summary row. Pass detail:true to include metadata, log, and stored request for that session.',
|
|
35
|
+
inputSchema: sessionsInputShape,
|
|
36
|
+
outputSchema: sessionsOutputShape,
|
|
37
|
+
}, async (input) => {
|
|
38
|
+
const textContent = (text) => [{ type: 'text', text }];
|
|
39
|
+
const { id, hours = 24, limit = 100, includeAll = false, detail = false } = sessionsInputSchema.parse(input);
|
|
40
|
+
if (id) {
|
|
41
|
+
if (!detail) {
|
|
42
|
+
const metadata = await sessionStore.readSession(id);
|
|
43
|
+
if (!metadata) {
|
|
44
|
+
throw new Error(`Session "${id}" not found.`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: textContent(`${metadata.createdAt} | ${metadata.status} | ${metadata.model ?? 'n/a'} | ${metadata.id}`),
|
|
48
|
+
structuredContent: {
|
|
49
|
+
entries: [
|
|
50
|
+
{
|
|
51
|
+
id: metadata.id,
|
|
52
|
+
createdAt: metadata.createdAt,
|
|
53
|
+
status: metadata.status,
|
|
54
|
+
model: metadata.model,
|
|
55
|
+
mode: metadata.mode,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
total: 1,
|
|
59
|
+
truncated: false,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const metadata = await sessionStore.readSession(id);
|
|
64
|
+
if (!metadata) {
|
|
65
|
+
throw new Error(`Session "${id}" not found.`);
|
|
66
|
+
}
|
|
67
|
+
const log = await sessionStore.readLog(id);
|
|
68
|
+
const request = (await sessionStore.readRequest(id)) ?? undefined;
|
|
69
|
+
return {
|
|
70
|
+
content: textContent(log),
|
|
71
|
+
structuredContent: { session: { metadata, log, request } },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const metas = await sessionStore.listSessions();
|
|
75
|
+
const { entries, truncated, total } = sessionStore.filterSessions(metas, { hours, includeAll, limit });
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: entries.map((entry) => `${entry.createdAt} | ${entry.status} | ${entry.model ?? 'n/a'} | ${entry.id}`).join('\n'),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
structuredContent: {
|
|
84
|
+
entries: entries.map((entry) => ({
|
|
85
|
+
id: entry.id,
|
|
86
|
+
createdAt: entry.createdAt,
|
|
87
|
+
status: entry.status,
|
|
88
|
+
model: entry.model,
|
|
89
|
+
mode: entry.mode,
|
|
90
|
+
})),
|
|
91
|
+
total,
|
|
92
|
+
truncated,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const consultInputSchema = z.object({
|
|
3
|
+
prompt: z.string().min(1, 'Prompt is required.'),
|
|
4
|
+
files: z.array(z.string()).default([]),
|
|
5
|
+
model: z.string().optional(),
|
|
6
|
+
models: z.array(z.string()).optional(),
|
|
7
|
+
engine: z.enum(['api', 'browser']).optional(),
|
|
8
|
+
browserModelLabel: z.string().optional(),
|
|
9
|
+
search: z.boolean().optional(),
|
|
10
|
+
slug: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
export const sessionsInputSchema = z.object({
|
|
13
|
+
id: z.string().optional(),
|
|
14
|
+
hours: z.number().optional(),
|
|
15
|
+
limit: z.number().optional(),
|
|
16
|
+
includeAll: z.boolean().optional(),
|
|
17
|
+
detail: z.boolean().optional(),
|
|
18
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolveRunOptionsFromConfig } from '../cli/runOptions.js';
|
|
2
|
+
import { Launcher } from 'chrome-launcher';
|
|
3
|
+
export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, userConfig, env = process.env, }) {
|
|
4
|
+
// Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
|
|
5
|
+
// then overlay MCP-only overrides such as explicit search toggles.
|
|
6
|
+
const mergedModels = Array.isArray(models) && models.length > 0
|
|
7
|
+
? [model, ...models].filter((entry) => Boolean(entry?.trim()))
|
|
8
|
+
: models;
|
|
9
|
+
const result = resolveRunOptionsFromConfig({ prompt, files, model, models: mergedModels, engine, userConfig, env });
|
|
10
|
+
if (typeof search === 'boolean') {
|
|
11
|
+
result.runOptions.search = search;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
export function ensureBrowserAvailable(engine) {
|
|
16
|
+
if (engine !== 'browser') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (process.env.CHROME_PATH) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const found = Launcher.getFirstInstallation();
|
|
23
|
+
if (!found) {
|
|
24
|
+
return 'Browser engine unavailable: no Chrome installation found and CHROME_PATH is unset.';
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { APIConnectionError, APIConnectionTimeoutError } from 'openai';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { formatElapsed } from './format.js';
|
|
4
|
+
import { startHeartbeat } from '../heartbeat.js';
|
|
5
|
+
import { OracleResponseError, OracleTransportError, describeTransportError, toTransportError, } from './errors.js';
|
|
6
|
+
const BACKGROUND_MAX_WAIT_MS = 30 * 60 * 1000;
|
|
7
|
+
const BACKGROUND_POLL_INTERVAL_MS = 5000;
|
|
8
|
+
const BACKGROUND_RETRY_BASE_MS = 3000;
|
|
9
|
+
const BACKGROUND_RETRY_MAX_MS = 15000;
|
|
10
|
+
export async function executeBackgroundResponse(params) {
|
|
11
|
+
const { client, requestBody, log, wait, heartbeatIntervalMs, now, maxWaitMs } = params;
|
|
12
|
+
const initialResponse = await client.responses.create(requestBody);
|
|
13
|
+
if (!initialResponse || !initialResponse.id) {
|
|
14
|
+
throw new OracleResponseError('API did not return a response ID for the background run.', initialResponse);
|
|
15
|
+
}
|
|
16
|
+
const responseId = initialResponse.id;
|
|
17
|
+
log(chalk.dim(`API scheduled background response ${responseId} (status=${initialResponse.status ?? 'unknown'}). Monitoring up to ${Math.round(BACKGROUND_MAX_WAIT_MS / 60000)} minutes for completion...`));
|
|
18
|
+
let heartbeatActive = false;
|
|
19
|
+
let stopHeartbeat = null;
|
|
20
|
+
const stopHeartbeatNow = () => {
|
|
21
|
+
if (!heartbeatActive)
|
|
22
|
+
return;
|
|
23
|
+
heartbeatActive = false;
|
|
24
|
+
stopHeartbeat?.();
|
|
25
|
+
stopHeartbeat = null;
|
|
26
|
+
};
|
|
27
|
+
if (heartbeatIntervalMs && heartbeatIntervalMs > 0) {
|
|
28
|
+
heartbeatActive = true;
|
|
29
|
+
stopHeartbeat = startHeartbeat({
|
|
30
|
+
intervalMs: heartbeatIntervalMs,
|
|
31
|
+
log: (message) => log(message),
|
|
32
|
+
isActive: () => heartbeatActive,
|
|
33
|
+
makeMessage: (elapsedMs) => {
|
|
34
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
35
|
+
return `API background run still in progress — ${elapsedText} elapsed.`;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return await pollBackgroundResponse({
|
|
41
|
+
client,
|
|
42
|
+
responseId,
|
|
43
|
+
initialResponse,
|
|
44
|
+
log,
|
|
45
|
+
wait,
|
|
46
|
+
now,
|
|
47
|
+
maxWaitMs,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
stopHeartbeatNow();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function pollBackgroundResponse(params) {
|
|
55
|
+
const { client, responseId, initialResponse, log, wait, now, maxWaitMs } = params;
|
|
56
|
+
const startMark = now();
|
|
57
|
+
let response = initialResponse;
|
|
58
|
+
let firstCycle = true;
|
|
59
|
+
let lastStatus = response.status;
|
|
60
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
|
|
61
|
+
while (true) {
|
|
62
|
+
const status = response.status ?? 'completed';
|
|
63
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: firstCycle toggles immediately; keep for clarity in logs.
|
|
64
|
+
if (firstCycle) {
|
|
65
|
+
firstCycle = false;
|
|
66
|
+
log(chalk.dim(`API background response status=${status}. We'll keep retrying automatically.`));
|
|
67
|
+
}
|
|
68
|
+
else if (status !== lastStatus && status !== 'completed') {
|
|
69
|
+
log(chalk.dim(`API background response status=${status}.`));
|
|
70
|
+
}
|
|
71
|
+
lastStatus = status;
|
|
72
|
+
if (status === 'completed') {
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
if (status !== 'in_progress' && status !== 'queued') {
|
|
76
|
+
const detail = response.error?.message || response.incomplete_details?.reason || status;
|
|
77
|
+
throw new OracleResponseError(`Response did not complete: ${detail}`, response);
|
|
78
|
+
}
|
|
79
|
+
if (now() - startMark >= maxWaitMs) {
|
|
80
|
+
throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
|
|
81
|
+
}
|
|
82
|
+
await wait(BACKGROUND_POLL_INTERVAL_MS);
|
|
83
|
+
if (now() - startMark >= maxWaitMs) {
|
|
84
|
+
throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
|
|
85
|
+
}
|
|
86
|
+
const { response: nextResponse, reconnected } = await retrieveBackgroundResponseWithRetry({
|
|
87
|
+
client,
|
|
88
|
+
responseId,
|
|
89
|
+
wait,
|
|
90
|
+
now,
|
|
91
|
+
maxWaitMs,
|
|
92
|
+
startMark,
|
|
93
|
+
log,
|
|
94
|
+
});
|
|
95
|
+
if (reconnected) {
|
|
96
|
+
const nextStatus = nextResponse.status ?? 'in_progress';
|
|
97
|
+
log(chalk.dim(`Reconnected to API background response (status=${nextStatus}). API is still working...`));
|
|
98
|
+
}
|
|
99
|
+
response = nextResponse;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function retrieveBackgroundResponseWithRetry(params) {
|
|
103
|
+
const { client, responseId, wait, now, maxWaitMs, startMark, log } = params;
|
|
104
|
+
let retries = 0;
|
|
105
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: intentional retry loop
|
|
106
|
+
while (true) {
|
|
107
|
+
try {
|
|
108
|
+
const next = await client.responses.retrieve(responseId);
|
|
109
|
+
return { response: next, reconnected: retries > 0 };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const transportError = asRetryableTransportError(error);
|
|
113
|
+
if (!transportError) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
retries += 1;
|
|
117
|
+
const delay = Math.min(BACKGROUND_RETRY_BASE_MS * 2 ** (retries - 1), BACKGROUND_RETRY_MAX_MS);
|
|
118
|
+
log(chalk.yellow(`${describeTransportError(transportError, maxWaitMs)} Retrying in ${formatElapsed(delay)}...`));
|
|
119
|
+
await wait(delay);
|
|
120
|
+
if (now() - startMark >= maxWaitMs) {
|
|
121
|
+
throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function asRetryableTransportError(error) {
|
|
127
|
+
if (error instanceof OracleTransportError) {
|
|
128
|
+
return error;
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof APIConnectionError || error instanceof APIConnectionTimeoutError) {
|
|
131
|
+
return toTransportError(error);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const DEFAULT_CLAUDE_ENDPOINT = 'https://api.anthropic.com/v1/messages';
|
|
2
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
3
|
+
function extractPrompt(body) {
|
|
4
|
+
const first = body.input?.[0]?.content?.[0];
|
|
5
|
+
if (first && first.type === 'input_text') {
|
|
6
|
+
return first.text ?? '';
|
|
7
|
+
}
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
async function callClaude({ apiKey, model, prompt, endpoint, stream = false, }) {
|
|
11
|
+
const url = endpoint?.trim() || DEFAULT_CLAUDE_ENDPOINT;
|
|
12
|
+
const payload = {
|
|
13
|
+
model,
|
|
14
|
+
max_tokens: 2048,
|
|
15
|
+
messages: [
|
|
16
|
+
{
|
|
17
|
+
role: 'user',
|
|
18
|
+
content: prompt,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
stream,
|
|
22
|
+
};
|
|
23
|
+
return fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'content-type': 'application/json',
|
|
27
|
+
'x-api-key': apiKey,
|
|
28
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function parseClaudeResponse(raw) {
|
|
34
|
+
const json = (await raw.json());
|
|
35
|
+
if (json.error) {
|
|
36
|
+
throw new Error(json.error.message || 'Claude request failed');
|
|
37
|
+
}
|
|
38
|
+
const textParts = json.content?.map((part) => part.text ?? '').filter(Boolean) ?? [];
|
|
39
|
+
const outputText = textParts.join('');
|
|
40
|
+
return {
|
|
41
|
+
id: json.id ?? `claude-${Date.now()}`,
|
|
42
|
+
status: 'completed',
|
|
43
|
+
output_text: [outputText],
|
|
44
|
+
output: [{ type: 'text', text: outputText }],
|
|
45
|
+
usage: {
|
|
46
|
+
input_tokens: json.usage?.input_tokens ?? 0,
|
|
47
|
+
output_tokens: json.usage?.output_tokens ?? 0,
|
|
48
|
+
total_tokens: (json.usage?.input_tokens ?? 0) + (json.usage?.output_tokens ?? 0),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function createClaudeClient(apiKey, modelName, resolvedModelId, baseUrl) {
|
|
53
|
+
const modelId = resolveClaudeModelId(resolvedModelId ?? modelName);
|
|
54
|
+
const stream = async (body) => {
|
|
55
|
+
const prompt = extractPrompt(body);
|
|
56
|
+
const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
|
|
57
|
+
const parsed = await parseClaudeResponse(resp);
|
|
58
|
+
const iterator = async function* () {
|
|
59
|
+
if (parsed.output_text?.[0]) {
|
|
60
|
+
yield { type: 'response.output_text.delta', delta: parsed.output_text[0] };
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
[Symbol.asyncIterator]: () => iterator(),
|
|
66
|
+
finalResponse: async () => parsed,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const create = async (body) => {
|
|
70
|
+
const prompt = extractPrompt(body);
|
|
71
|
+
const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
|
|
72
|
+
return parseClaudeResponse(resp);
|
|
73
|
+
};
|
|
74
|
+
const retrieve = async (id) => ({
|
|
75
|
+
id,
|
|
76
|
+
status: 'error',
|
|
77
|
+
error: { message: 'Retrieve by ID not supported for Claude API yet.' },
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
responses: {
|
|
81
|
+
stream,
|
|
82
|
+
create,
|
|
83
|
+
retrieve,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function resolveClaudeModelId(modelName) {
|
|
88
|
+
if (modelName === 'claude-4.5-sonnet' || modelName === 'claude-sonnet-4-5-20241022') {
|
|
89
|
+
return 'claude-sonnet-4-5';
|
|
90
|
+
}
|
|
91
|
+
if (modelName === 'claude-4.1-opus' || modelName === 'claude-opus-4-1-20240808') {
|
|
92
|
+
return 'claude-opus-4-1';
|
|
93
|
+
}
|
|
94
|
+
return modelName;
|
|
95
|
+
}
|