@steipete/oracle 1.0.8 → 1.2.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 +32 -4
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +178 -21
- package/dist/bin/oracle-mcp.js +6 -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/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/cookies.js +1 -1
- package/dist/src/browser/index.js +2 -1
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +4 -2
- package/dist/src/cli/dryRun.js +41 -5
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/runOptions.js +29 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +222 -10
- package/dist/src/cli/sessionRunner.js +21 -2
- package/dist/src/cli/tui/index.js +436 -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/files.js +143 -6
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +104 -71
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/sessionManager.js +65 -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 +27 -9
- 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
package/dist/src/oracle/run.js
CHANGED
|
@@ -8,11 +8,13 @@ import { APIConnectionError, APIConnectionTimeoutError } from 'openai';
|
|
|
8
8
|
import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from './config.js';
|
|
9
9
|
import { readFiles } from './files.js';
|
|
10
10
|
import { buildPrompt, buildRequestBody } from './request.js';
|
|
11
|
+
import { estimateRequestTokens } from './tokenEstimate.js';
|
|
11
12
|
import { formatElapsed, formatUSD } from './format.js';
|
|
12
13
|
import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
|
|
13
14
|
import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
|
|
14
15
|
import { createDefaultClientFactory } from './client.js';
|
|
15
16
|
import { startHeartbeat } from '../heartbeat.js';
|
|
17
|
+
import { startOscProgress } from './oscProgress.js';
|
|
16
18
|
import { getCliVersion } from '../version.js';
|
|
17
19
|
import { createFsAdapter } from './fsAdapter.js';
|
|
18
20
|
const isTty = process.stdout.isTTY;
|
|
@@ -84,19 +86,23 @@ export async function runOracle(options, deps = {}) {
|
|
|
84
86
|
logVerbose(`Attached files use ${totalFileTokens.toLocaleString()} tokens`);
|
|
85
87
|
const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
86
88
|
const promptWithFiles = buildPrompt(options.prompt, files, cwd);
|
|
87
|
-
const tokenizerInput = [
|
|
88
|
-
{ role: 'system', content: systemPrompt },
|
|
89
|
-
{ role: 'user', content: promptWithFiles },
|
|
90
|
-
];
|
|
91
|
-
const estimatedInputTokens = modelConfig.tokenizer(tokenizerInput, TOKENIZER_OPTIONS);
|
|
92
|
-
logVerbose(`Estimated tokens (prompt + files): ${estimatedInputTokens.toLocaleString()}`);
|
|
93
89
|
const fileCount = files.length;
|
|
94
90
|
const cliVersion = getCliVersion();
|
|
95
91
|
const richTty = process.stdout.isTTY && chalk.level > 0;
|
|
96
92
|
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
93
|
+
const requestBody = buildRequestBody({
|
|
94
|
+
modelConfig,
|
|
95
|
+
systemPrompt,
|
|
96
|
+
userPrompt: promptWithFiles,
|
|
97
|
+
searchEnabled,
|
|
98
|
+
maxOutputTokens: options.maxOutput,
|
|
99
|
+
background: useBackground,
|
|
100
|
+
storeResponse: useBackground,
|
|
101
|
+
});
|
|
102
|
+
const estimatedInputTokens = estimateRequestTokens(requestBody, modelConfig);
|
|
97
103
|
const tokenLabel = richTty ? chalk.green(estimatedInputTokens.toLocaleString()) : estimatedInputTokens.toLocaleString();
|
|
98
104
|
const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
|
|
99
|
-
const headerLine = `
|
|
105
|
+
const headerLine = `oracle (${cliVersion}) consulting ${headerModelLabel}'s crystal ball with ${tokenLabel} tokens and ${fileLabel} files...`;
|
|
100
106
|
const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
|
|
101
107
|
if (!isPreview) {
|
|
102
108
|
log(headerLine);
|
|
@@ -111,15 +117,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
111
117
|
if (estimatedInputTokens > inputTokenBudget) {
|
|
112
118
|
throw new PromptValidationError(`Input too large (${estimatedInputTokens.toLocaleString()} tokens). Limit is ${inputTokenBudget.toLocaleString()} tokens.`, { estimatedInputTokens, inputTokenBudget });
|
|
113
119
|
}
|
|
114
|
-
|
|
115
|
-
modelConfig,
|
|
116
|
-
systemPrompt,
|
|
117
|
-
userPrompt: promptWithFiles,
|
|
118
|
-
searchEnabled,
|
|
119
|
-
maxOutputTokens: options.maxOutput,
|
|
120
|
-
background: useBackground,
|
|
121
|
-
storeResponse: useBackground,
|
|
122
|
-
});
|
|
120
|
+
logVerbose(`Estimated tokens (request body): ${estimatedInputTokens.toLocaleString()}`);
|
|
123
121
|
if (isPreview && previewMode) {
|
|
124
122
|
if (previewMode === 'json' || previewMode === 'full') {
|
|
125
123
|
log('Request JSON');
|
|
@@ -142,6 +140,11 @@ export async function runOracle(options, deps = {}) {
|
|
|
142
140
|
}
|
|
143
141
|
const openAiClient = client ?? clientFactory(apiKey);
|
|
144
142
|
logVerbose('Dispatching request to OpenAI Responses API...');
|
|
143
|
+
const stopOscProgress = startOscProgress({
|
|
144
|
+
label: useBackground ? 'Waiting for OpenAI (background)' : 'Waiting for OpenAI',
|
|
145
|
+
targetMs: useBackground ? BACKGROUND_MAX_WAIT_MS : 10 * 60_000,
|
|
146
|
+
write,
|
|
147
|
+
});
|
|
145
148
|
const runStart = now();
|
|
146
149
|
let response = null;
|
|
147
150
|
let elapsedMs = 0;
|
|
@@ -154,74 +157,98 @@ export async function runOracle(options, deps = {}) {
|
|
|
154
157
|
answerHeaderPrinted = true;
|
|
155
158
|
}
|
|
156
159
|
};
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
elapsedMs = now() - runStart;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const stream = await openAiClient.responses.stream(requestBody);
|
|
170
|
-
let heartbeatActive = false;
|
|
171
|
-
let stopHeartbeat = null;
|
|
172
|
-
const stopHeartbeatNow = () => {
|
|
173
|
-
if (!heartbeatActive) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
heartbeatActive = false;
|
|
177
|
-
stopHeartbeat?.();
|
|
178
|
-
stopHeartbeat = null;
|
|
179
|
-
};
|
|
180
|
-
if (options.heartbeatIntervalMs && options.heartbeatIntervalMs > 0) {
|
|
181
|
-
heartbeatActive = true;
|
|
182
|
-
stopHeartbeat = startHeartbeat({
|
|
183
|
-
intervalMs: options.heartbeatIntervalMs,
|
|
184
|
-
log: (message) => log(message),
|
|
185
|
-
isActive: () => heartbeatActive,
|
|
186
|
-
makeMessage: (elapsedMs) => {
|
|
187
|
-
const elapsedText = formatElapsed(elapsedMs);
|
|
188
|
-
return `API connection active — ${elapsedText} elapsed. Expect up to ~10 min before GPT-5 responds.`;
|
|
189
|
-
},
|
|
160
|
+
try {
|
|
161
|
+
if (useBackground) {
|
|
162
|
+
response = await executeBackgroundResponse({
|
|
163
|
+
client: openAiClient,
|
|
164
|
+
requestBody,
|
|
165
|
+
log,
|
|
166
|
+
wait,
|
|
167
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
168
|
+
now,
|
|
190
169
|
});
|
|
170
|
+
elapsedMs = now() - runStart;
|
|
191
171
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
172
|
+
else {
|
|
173
|
+
const stream = await openAiClient.responses.stream(requestBody);
|
|
174
|
+
let heartbeatActive = false;
|
|
175
|
+
let stopHeartbeat = null;
|
|
176
|
+
const stopHeartbeatNow = () => {
|
|
177
|
+
if (!heartbeatActive) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
heartbeatActive = false;
|
|
181
|
+
stopHeartbeat?.();
|
|
182
|
+
stopHeartbeat = null;
|
|
183
|
+
};
|
|
184
|
+
if (options.heartbeatIntervalMs && options.heartbeatIntervalMs > 0) {
|
|
185
|
+
heartbeatActive = true;
|
|
186
|
+
stopHeartbeat = startHeartbeat({
|
|
187
|
+
intervalMs: options.heartbeatIntervalMs,
|
|
188
|
+
log: (message) => log(message),
|
|
189
|
+
isActive: () => heartbeatActive,
|
|
190
|
+
makeMessage: (elapsedMs) => {
|
|
191
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
192
|
+
return `API connection active — ${elapsedText} elapsed. Expect up to ~10 min before GPT-5 responds.`;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
for await (const event of stream) {
|
|
198
|
+
if (event.type === 'response.output_text.delta') {
|
|
199
|
+
stopOscProgress();
|
|
200
|
+
stopHeartbeatNow();
|
|
201
|
+
sawTextDelta = true;
|
|
202
|
+
ensureAnswerHeader();
|
|
203
|
+
if (!options.silent && typeof event.delta === 'string') {
|
|
204
|
+
write(event.delta);
|
|
205
|
+
}
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
catch (streamError) {
|
|
210
|
+
if (typeof stream.abort === 'function') {
|
|
211
|
+
stream.abort();
|
|
212
|
+
}
|
|
213
|
+
stopHeartbeatNow();
|
|
214
|
+
const transportError = toTransportError(streamError);
|
|
215
|
+
log(chalk.yellow(describeTransportError(transportError)));
|
|
216
|
+
throw transportError;
|
|
207
217
|
}
|
|
218
|
+
response = await stream.finalResponse();
|
|
208
219
|
stopHeartbeatNow();
|
|
209
|
-
|
|
210
|
-
log(chalk.yellow(describeTransportError(transportError)));
|
|
211
|
-
throw transportError;
|
|
220
|
+
elapsedMs = now() - runStart;
|
|
212
221
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
stopOscProgress();
|
|
216
225
|
}
|
|
217
226
|
if (!response) {
|
|
218
227
|
throw new Error('OpenAI did not return a response.');
|
|
219
228
|
}
|
|
220
229
|
logVerbose(`Response status: ${response.status ?? 'completed'}`);
|
|
221
230
|
if (response.status && response.status !== 'completed') {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
// OpenAI can reply `in_progress` even after the stream closes; give it a brief grace poll.
|
|
232
|
+
if (response.id && response.status === 'in_progress') {
|
|
233
|
+
const polishingStart = now();
|
|
234
|
+
const pollIntervalMs = 2_000;
|
|
235
|
+
const maxWaitMs = 60_000;
|
|
236
|
+
log(chalk.dim('Response still in_progress; polling until completion...'));
|
|
237
|
+
// Short polling loop — we don't want to hang forever, just catch late finalization.
|
|
238
|
+
while (now() - polishingStart < maxWaitMs) {
|
|
239
|
+
await wait(pollIntervalMs);
|
|
240
|
+
const refreshed = await openAiClient.responses.retrieve(response.id);
|
|
241
|
+
if (refreshed.status === 'completed') {
|
|
242
|
+
response = refreshed;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (response.status !== 'completed') {
|
|
248
|
+
const detail = response.error?.message || response.incomplete_details?.reason || response.status;
|
|
249
|
+
log(chalk.yellow(`OpenAI ended the run early (status=${response.status}${response.incomplete_details?.reason ? `, reason=${response.incomplete_details.reason}` : ''}).`));
|
|
250
|
+
throw new OracleResponseError(`Response did not complete: ${detail}`, response);
|
|
251
|
+
}
|
|
225
252
|
}
|
|
226
253
|
const answerText = extractTextOutput(response);
|
|
227
254
|
if (!options.silent) {
|
|
@@ -250,6 +277,12 @@ export async function runOracle(options, deps = {}) {
|
|
|
250
277
|
.map((value, index) => formatTokenValue(value, usage, index))
|
|
251
278
|
.join('/');
|
|
252
279
|
statsParts.push(`tok(i/o/r/t)=${tokensDisplay}`);
|
|
280
|
+
const actualInput = usage.input_tokens;
|
|
281
|
+
if (actualInput !== undefined) {
|
|
282
|
+
const delta = actualInput - estimatedInputTokens;
|
|
283
|
+
const deltaText = delta === 0 ? '' : delta > 0 ? ` (+${delta.toLocaleString()})` : ` (${delta.toLocaleString()})`;
|
|
284
|
+
statsParts.push(`est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`);
|
|
285
|
+
}
|
|
253
286
|
if (!searchEnabled) {
|
|
254
287
|
statsParts.push('search=off');
|
|
255
288
|
}
|
|
@@ -260,7 +293,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
260
293
|
return {
|
|
261
294
|
mode: 'live',
|
|
262
295
|
response,
|
|
263
|
-
usage: { inputTokens, outputTokens, reasoningTokens, totalTokens },
|
|
296
|
+
usage: { inputTokens, outputTokens, reasoningTokens, totalTokens, cost },
|
|
264
297
|
elapsedMs,
|
|
265
298
|
};
|
|
266
299
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TOKENIZER_OPTIONS } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Estimate input tokens from the full request body instead of just system/user text.
|
|
4
|
+
* This is a conservative approximation: we tokenize the key textual fields and add a fixed buffer
|
|
5
|
+
* to cover structural JSON overhead and server-side wrappers (tools/reasoning/background/store).
|
|
6
|
+
*/
|
|
7
|
+
export function estimateRequestTokens(requestBody, modelConfig, bufferTokens = 200) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (requestBody.instructions) {
|
|
10
|
+
parts.push(requestBody.instructions);
|
|
11
|
+
}
|
|
12
|
+
for (const turn of requestBody.input ?? []) {
|
|
13
|
+
for (const content of turn.content ?? []) {
|
|
14
|
+
if (typeof content.text === 'string') {
|
|
15
|
+
parts.push(content.text);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (requestBody.tools && requestBody.tools.length > 0) {
|
|
20
|
+
parts.push(JSON.stringify(requestBody.tools));
|
|
21
|
+
}
|
|
22
|
+
if (requestBody.reasoning) {
|
|
23
|
+
parts.push(JSON.stringify(requestBody.reasoning));
|
|
24
|
+
}
|
|
25
|
+
if (requestBody.background) {
|
|
26
|
+
parts.push('background:true');
|
|
27
|
+
}
|
|
28
|
+
if (requestBody.store) {
|
|
29
|
+
parts.push('store:true');
|
|
30
|
+
}
|
|
31
|
+
const concatenated = parts.join('\n');
|
|
32
|
+
const baseEstimate = modelConfig.tokenizer(concatenated, TOKENIZER_OPTIONS);
|
|
33
|
+
return baseEstimate + bufferTokens;
|
|
34
|
+
}
|
|
@@ -5,6 +5,7 @@ import { createWriteStream } from 'node:fs';
|
|
|
5
5
|
const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
6
6
|
const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
|
|
7
7
|
const MAX_STATUS_LIMIT = 1000;
|
|
8
|
+
const ZOMBIE_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes
|
|
8
9
|
const DEFAULT_SLUG = 'session';
|
|
9
10
|
const MAX_SLUG_WORDS = 5;
|
|
10
11
|
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
@@ -67,7 +68,7 @@ async function ensureUniqueSessionId(baseSlug) {
|
|
|
67
68
|
}
|
|
68
69
|
return candidate;
|
|
69
70
|
}
|
|
70
|
-
export async function initializeSession(options, cwd) {
|
|
71
|
+
export async function initializeSession(options, cwd, notifications) {
|
|
71
72
|
await ensureSessionStorage();
|
|
72
73
|
const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
73
74
|
const sessionId = await ensureUniqueSessionId(baseSlug);
|
|
@@ -84,6 +85,7 @@ export async function initializeSession(options, cwd) {
|
|
|
84
85
|
cwd,
|
|
85
86
|
mode,
|
|
86
87
|
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
88
|
+
notifications,
|
|
87
89
|
options: {
|
|
88
90
|
prompt: options.prompt,
|
|
89
91
|
file: options.file ?? [],
|
|
@@ -99,7 +101,9 @@ export async function initializeSession(options, cwd) {
|
|
|
99
101
|
verbose: options.verbose,
|
|
100
102
|
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
101
103
|
browserInlineFiles: options.browserInlineFiles,
|
|
104
|
+
browserBundleFiles: options.browserBundleFiles,
|
|
102
105
|
background: options.background,
|
|
106
|
+
search: options.search,
|
|
103
107
|
},
|
|
104
108
|
};
|
|
105
109
|
await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
|
|
@@ -110,7 +114,8 @@ export async function initializeSession(options, cwd) {
|
|
|
110
114
|
export async function readSessionMetadata(sessionId) {
|
|
111
115
|
try {
|
|
112
116
|
const raw = await fs.readFile(metaPath(sessionId), 'utf8');
|
|
113
|
-
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return await markZombie(parsed, { persist: false }); // transient check; do not touch disk on single read
|
|
114
119
|
}
|
|
115
120
|
catch {
|
|
116
121
|
return null;
|
|
@@ -138,8 +143,9 @@ export async function listSessionsMetadata() {
|
|
|
138
143
|
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
139
144
|
const metas = [];
|
|
140
145
|
for (const entry of entries) {
|
|
141
|
-
|
|
146
|
+
let meta = await readSessionMetadata(entry);
|
|
142
147
|
if (meta) {
|
|
148
|
+
meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
|
|
143
149
|
metas.push(meta);
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -164,6 +170,15 @@ export async function readSessionLog(sessionId) {
|
|
|
164
170
|
return '';
|
|
165
171
|
}
|
|
166
172
|
}
|
|
173
|
+
export async function readSessionRequest(sessionId) {
|
|
174
|
+
try {
|
|
175
|
+
const raw = await fs.readFile(requestPath(sessionId), 'utf8');
|
|
176
|
+
return JSON.parse(raw);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
167
182
|
export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
|
|
168
183
|
await ensureSessionStorage();
|
|
169
184
|
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
@@ -203,3 +218,50 @@ export async function wait(ms) {
|
|
|
203
218
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
204
219
|
}
|
|
205
220
|
export { ORACLE_HOME, SESSIONS_DIR, MAX_STATUS_LIMIT };
|
|
221
|
+
export { ZOMBIE_MAX_AGE_MS };
|
|
222
|
+
export async function getSessionPaths(sessionId) {
|
|
223
|
+
const dir = sessionDir(sessionId);
|
|
224
|
+
const metadata = metaPath(sessionId);
|
|
225
|
+
const log = logPath(sessionId);
|
|
226
|
+
const request = requestPath(sessionId);
|
|
227
|
+
const required = [metadata, log, request];
|
|
228
|
+
const missing = [];
|
|
229
|
+
for (const file of required) {
|
|
230
|
+
if (!(await fileExists(file))) {
|
|
231
|
+
missing.push(path.basename(file));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (missing.length > 0) {
|
|
235
|
+
throw new Error(`Session "${sessionId}" is missing: ${missing.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
return { dir, metadata, log, request };
|
|
238
|
+
}
|
|
239
|
+
async function markZombie(meta, { persist }) {
|
|
240
|
+
if (!isZombie(meta)) {
|
|
241
|
+
return meta;
|
|
242
|
+
}
|
|
243
|
+
const updated = {
|
|
244
|
+
...meta,
|
|
245
|
+
status: 'error',
|
|
246
|
+
errorMessage: 'Session marked as zombie (>30m stale)',
|
|
247
|
+
completedAt: new Date().toISOString(),
|
|
248
|
+
};
|
|
249
|
+
if (persist) {
|
|
250
|
+
await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
|
|
251
|
+
}
|
|
252
|
+
return updated;
|
|
253
|
+
}
|
|
254
|
+
function isZombie(meta) {
|
|
255
|
+
if (meta.status !== 'running') {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
const reference = meta.startedAt ?? meta.createdAt;
|
|
259
|
+
if (!reference) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const startedMs = Date.parse(reference);
|
|
263
|
+
if (Number.isNaN(startedMs)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
|
|
267
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleIdentifier</key>
|
|
6
|
+
<string>com.steipete.oracle.notifier</string>
|
|
7
|
+
<key>CFBundleName</key>
|
|
8
|
+
<string>OracleNotifier</string>
|
|
9
|
+
<key>CFBundleDisplayName</key>
|
|
10
|
+
<string>Oracle Notifier</string>
|
|
11
|
+
<key>CFBundleExecutable</key>
|
|
12
|
+
<string>OracleNotifier</string>
|
|
13
|
+
<key>CFBundleIconFile</key>
|
|
14
|
+
<string>OracleIcon</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>APPL</string>
|
|
17
|
+
<key>LSMinimumSystemVersion</key>
|
|
18
|
+
<string>13.0</string>
|
|
19
|
+
</dict>
|
|
20
|
+
</plist>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>files</key>
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/OracleIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
edUHAMetayIv3xtc3Vb92VXRLfM=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
12
|
+
<key>files2</key>
|
|
13
|
+
<dict>
|
|
14
|
+
<key>Resources/OracleIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
AVPJK/6w6IOsDLmZTW4hL+Za+/4wHMxZiIp0t6m3NRA=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
21
|
+
</dict>
|
|
22
|
+
<key>rules</key>
|
|
23
|
+
<dict>
|
|
24
|
+
<key>^Resources/</key>
|
|
25
|
+
<true/>
|
|
26
|
+
<key>^Resources/.*\.lproj/</key>
|
|
27
|
+
<dict>
|
|
28
|
+
<key>optional</key>
|
|
29
|
+
<true/>
|
|
30
|
+
<key>weight</key>
|
|
31
|
+
<real>1000</real>
|
|
32
|
+
</dict>
|
|
33
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
34
|
+
<dict>
|
|
35
|
+
<key>omit</key>
|
|
36
|
+
<true/>
|
|
37
|
+
<key>weight</key>
|
|
38
|
+
<real>1100</real>
|
|
39
|
+
</dict>
|
|
40
|
+
<key>^Resources/Base\.lproj/</key>
|
|
41
|
+
<dict>
|
|
42
|
+
<key>weight</key>
|
|
43
|
+
<real>1010</real>
|
|
44
|
+
</dict>
|
|
45
|
+
<key>^version.plist$</key>
|
|
46
|
+
<true/>
|
|
47
|
+
</dict>
|
|
48
|
+
<key>rules2</key>
|
|
49
|
+
<dict>
|
|
50
|
+
<key>.*\.dSYM($|/)</key>
|
|
51
|
+
<dict>
|
|
52
|
+
<key>weight</key>
|
|
53
|
+
<real>11</real>
|
|
54
|
+
</dict>
|
|
55
|
+
<key>^(.*/)?\.DS_Store$</key>
|
|
56
|
+
<dict>
|
|
57
|
+
<key>omit</key>
|
|
58
|
+
<true/>
|
|
59
|
+
<key>weight</key>
|
|
60
|
+
<real>2000</real>
|
|
61
|
+
</dict>
|
|
62
|
+
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
63
|
+
<dict>
|
|
64
|
+
<key>nested</key>
|
|
65
|
+
<true/>
|
|
66
|
+
<key>weight</key>
|
|
67
|
+
<real>10</real>
|
|
68
|
+
</dict>
|
|
69
|
+
<key>^.*</key>
|
|
70
|
+
<true/>
|
|
71
|
+
<key>^Info\.plist$</key>
|
|
72
|
+
<dict>
|
|
73
|
+
<key>omit</key>
|
|
74
|
+
<true/>
|
|
75
|
+
<key>weight</key>
|
|
76
|
+
<real>20</real>
|
|
77
|
+
</dict>
|
|
78
|
+
<key>^PkgInfo$</key>
|
|
79
|
+
<dict>
|
|
80
|
+
<key>omit</key>
|
|
81
|
+
<true/>
|
|
82
|
+
<key>weight</key>
|
|
83
|
+
<real>20</real>
|
|
84
|
+
</dict>
|
|
85
|
+
<key>^Resources/</key>
|
|
86
|
+
<dict>
|
|
87
|
+
<key>weight</key>
|
|
88
|
+
<real>20</real>
|
|
89
|
+
</dict>
|
|
90
|
+
<key>^Resources/.*\.lproj/</key>
|
|
91
|
+
<dict>
|
|
92
|
+
<key>optional</key>
|
|
93
|
+
<true/>
|
|
94
|
+
<key>weight</key>
|
|
95
|
+
<real>1000</real>
|
|
96
|
+
</dict>
|
|
97
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
98
|
+
<dict>
|
|
99
|
+
<key>omit</key>
|
|
100
|
+
<true/>
|
|
101
|
+
<key>weight</key>
|
|
102
|
+
<real>1100</real>
|
|
103
|
+
</dict>
|
|
104
|
+
<key>^Resources/Base\.lproj/</key>
|
|
105
|
+
<dict>
|
|
106
|
+
<key>weight</key>
|
|
107
|
+
<real>1010</real>
|
|
108
|
+
</dict>
|
|
109
|
+
<key>^[^/]+$</key>
|
|
110
|
+
<dict>
|
|
111
|
+
<key>nested</key>
|
|
112
|
+
<true/>
|
|
113
|
+
<key>weight</key>
|
|
114
|
+
<real>10</real>
|
|
115
|
+
</dict>
|
|
116
|
+
<key>^embedded\.provisionprofile$</key>
|
|
117
|
+
<dict>
|
|
118
|
+
<key>weight</key>
|
|
119
|
+
<real>20</real>
|
|
120
|
+
</dict>
|
|
121
|
+
<key>^version\.plist$</key>
|
|
122
|
+
<dict>
|
|
123
|
+
<key>weight</key>
|
|
124
|
+
<real>20</real>
|
|
125
|
+
</dict>
|
|
126
|
+
</dict>
|
|
127
|
+
</dict>
|
|
128
|
+
</plist>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UserNotifications
|
|
3
|
+
|
|
4
|
+
let args = CommandLine.arguments
|
|
5
|
+
// Usage: OracleNotifier <title> <message> [soundName]
|
|
6
|
+
if args.count < 3 {
|
|
7
|
+
fputs("usage: OracleNotifier <title> <message> [soundName]\n", stderr)
|
|
8
|
+
exit(1)
|
|
9
|
+
}
|
|
10
|
+
let title = args[1]
|
|
11
|
+
let message = args[2]
|
|
12
|
+
let soundName = args.count >= 4 ? args[3] : "Glass"
|
|
13
|
+
|
|
14
|
+
let center = UNUserNotificationCenter.current()
|
|
15
|
+
let group = DispatchGroup()
|
|
16
|
+
group.enter()
|
|
17
|
+
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
|
18
|
+
if let error = error {
|
|
19
|
+
fputs("auth error: \(error)\n", stderr)
|
|
20
|
+
group.leave()
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
if !granted {
|
|
24
|
+
fputs("authorization not granted\n", stderr)
|
|
25
|
+
group.leave()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
let content = UNMutableNotificationContent()
|
|
29
|
+
content.title = title
|
|
30
|
+
content.body = message
|
|
31
|
+
if !soundName.isEmpty {
|
|
32
|
+
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName))
|
|
33
|
+
} else {
|
|
34
|
+
content.sound = UNNotificationSound.default
|
|
35
|
+
}
|
|
36
|
+
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
37
|
+
center.add(request) { addError in
|
|
38
|
+
if let addError = addError {
|
|
39
|
+
fputs("add error: \(addError)\n", stderr)
|
|
40
|
+
}
|
|
41
|
+
group.leave()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_ = group.wait(timeout: .now() + 2)
|
|
45
|
+
RunLoop.current.run(until: Date().addingTimeInterval(1))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Oracle Notifier helper (macOS, arm64)
|
|
2
|
+
|
|
3
|
+
Builds a tiny signed helper app for macOS notifications with the Oracle icon.
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd vendor/oracle-notifier
|
|
9
|
+
# Optional: notarize by setting App Store Connect key credentials
|
|
10
|
+
export APP_STORE_CONNECT_API_KEY_P8="$(cat AuthKey_XXXXXX.p8)" # with literal newlines or \n escaped
|
|
11
|
+
export APP_STORE_CONNECT_KEY_ID=XXXXXX
|
|
12
|
+
export APP_STORE_CONNECT_ISSUER_ID=YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
|
|
13
|
+
./build-notifier.sh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Requires Xcode command line tools (swiftc) and a macOS Developer ID certificate. Without a valid cert, the build fails (no ad-hoc fallback).
|
|
17
|
+
- If `APP_STORE_CONNECT_*` vars are set, the script notarizes and staples the ticket.
|
|
18
|
+
- Output: `OracleNotifier.app` (arm64 only), bundled with `OracleIcon.icns`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
The CLI prefers this helper on macOS; if it fails or is missing, it falls back to toasted-notifier/terminal-notifier.
|
|
22
|
+
|
|
23
|
+
## Permissions
|
|
24
|
+
After first run, allow notifications for “Oracle Notifier” in System Settings → Notifications.
|