@steipete/oracle 0.7.1 → 0.7.3

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.
Files changed (30) hide show
  1. package/dist/src/browser/actions/assistantResponse.js +53 -33
  2. package/dist/src/browser/actions/attachments.js +276 -133
  3. package/dist/src/browser/actions/modelSelection.js +33 -2
  4. package/dist/src/browser/actions/promptComposer.js +38 -45
  5. package/dist/src/browser/chromeLifecycle.js +2 -0
  6. package/dist/src/browser/config.js +7 -2
  7. package/dist/src/browser/index.js +12 -2
  8. package/dist/src/browser/pageActions.js +1 -1
  9. package/dist/src/browser/reattach.js +192 -17
  10. package/dist/src/browser/utils.js +10 -0
  11. package/dist/src/browserMode.js +1 -1
  12. package/dist/src/cli/browserConfig.js +11 -6
  13. package/dist/src/cli/notifier.js +8 -2
  14. package/dist/src/cli/oscUtils.js +1 -19
  15. package/dist/src/cli/sessionDisplay.js +6 -3
  16. package/dist/src/cli/sessionTable.js +5 -1
  17. package/dist/src/oracle/files.js +8 -1
  18. package/dist/src/oracle/modelResolver.js +11 -4
  19. package/dist/src/oracle/multiModelRunner.js +3 -14
  20. package/dist/src/oracle/oscProgress.js +12 -61
  21. package/dist/src/oracle/run.js +62 -34
  22. package/dist/src/sessionManager.js +91 -2
  23. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  24. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  25. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  26. package/package.json +43 -26
  27. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  28. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  29. package/vendor/oracle-notifier/README.md +24 -0
  30. package/vendor/oracle-notifier/build-notifier.sh +0 -0
@@ -2,24 +2,13 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
4
4
  import { sessionStore } from '../sessionStore.js';
5
- const OSC_PROGRESS_PREFIX = '\u001b]9;4;';
6
- const OSC_PROGRESS_END = '\u001b\\';
5
+ import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from 'osc-progress';
7
6
  function forwardOscProgress(chunk, shouldForward) {
8
7
  if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
9
8
  return;
10
9
  }
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;
10
+ for (const seq of findOscProgressSequences(chunk)) {
11
+ process.stdout.write(seq.raw);
23
12
  }
24
13
  }
25
14
  const defaultDeps = {
@@ -1,66 +1,17 @@
1
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
- }
2
+ import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from 'osc-progress';
9
3
  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;
4
+ return supportsOscProgressShared(env, isTty, {
5
+ disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
6
+ forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
7
+ });
30
8
  }
31
9
  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
- // 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
- };
10
+ return startOscProgressShared({
11
+ ...options,
12
+ // Preserve Oracle's previous default: progress emits to stdout.
13
+ write: options.write ?? ((text) => process.stdout.write(text)),
14
+ disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
15
+ forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
16
+ });
66
17
  }
@@ -19,8 +19,10 @@ import { createFsAdapter } from './fsAdapter.js';
19
19
  import { resolveGeminiModelId } from './gemini.js';
20
20
  import { resolveClaudeModelId } from './claude.js';
21
21
  import { renderMarkdownAnsi } from '../cli/markdownRenderer.js';
22
+ import { createLiveRenderer } from 'markdansi';
22
23
  import { executeBackgroundResponse } from './background.js';
23
24
  import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from './runUtils.js';
25
+ import { estimateUsdCost } from 'tokentally';
24
26
  import { defaultOpenRouterBaseUrl, isKnownModel, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from './modelResolver.js';
25
27
  const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
26
28
  const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
@@ -309,8 +311,8 @@ export async function runOracle(options, deps = {}) {
309
311
  let response = null;
310
312
  let elapsedMs = 0;
311
313
  let sawTextDelta = false;
312
- // Buffer streamed text so we can re-render markdown once the stream ends (TTY + non-plain mode).
313
- const streamedChunks = [];
314
+ let streamedText = '';
315
+ let lastLiveFrameAtMs = 0;
314
316
  let answerHeaderPrinted = false;
315
317
  const allowAnswerHeader = options.suppressAnswerHeader !== true;
316
318
  const timeoutExceeded = () => now() - runStart >= timeoutMs;
@@ -378,31 +380,52 @@ export async function runOracle(options, deps = {}) {
378
380
  },
379
381
  });
380
382
  }
383
+ let liveRenderer = null;
381
384
  try {
385
+ liveRenderer =
386
+ isTty && !renderPlain
387
+ ? createLiveRenderer({
388
+ write: stdoutWrite,
389
+ width: process.stdout.columns ?? 80,
390
+ renderFrame: renderMarkdownAnsi,
391
+ })
392
+ : null;
382
393
  for await (const event of stream) {
383
394
  throwIfTimedOut();
384
395
  const isTextDelta = event.type === 'chunk' || event.type === 'response.output_text.delta';
385
- if (isTextDelta) {
386
- stopOscProgress();
387
- stopHeartbeatNow();
388
- sawTextDelta = true;
389
- ensureAnswerHeader();
390
- if (!options.silent && typeof event.delta === 'string') {
391
- // Always keep the log/bookkeeping sink up to date.
392
- sinkWrite(event.delta);
393
- if (renderPlain) {
394
- // Plain mode: stream directly to stdout regardless of write sink.
395
- stdoutWrite(event.delta);
396
- }
397
- else if (isTty) {
398
- // Buffer for end-of-stream markdown rendering on TTY.
399
- streamedChunks.push(event.delta);
400
- }
401
- else {
402
- // Non-TTY streams should still surface output; fall back to raw stdout.
403
- stdoutWrite(event.delta);
404
- }
396
+ if (!isTextDelta)
397
+ continue;
398
+ stopOscProgress();
399
+ stopHeartbeatNow();
400
+ sawTextDelta = true;
401
+ ensureAnswerHeader();
402
+ if (options.silent || typeof event.delta !== 'string')
403
+ continue;
404
+ // Always keep the log/bookkeeping sink up to date.
405
+ sinkWrite(event.delta);
406
+ if (renderPlain) {
407
+ // Plain mode: stream directly to stdout regardless of write sink.
408
+ stdoutWrite(event.delta);
409
+ continue;
410
+ }
411
+ if (liveRenderer) {
412
+ streamedText += event.delta;
413
+ const currentMs = now();
414
+ const due = currentMs - lastLiveFrameAtMs >= 120;
415
+ const hasNewline = event.delta.includes('\n');
416
+ if (hasNewline || due) {
417
+ liveRenderer.render(streamedText);
418
+ lastLiveFrameAtMs = currentMs;
405
419
  }
420
+ continue;
421
+ }
422
+ // Non-TTY streams should still surface output; fall back to raw stdout.
423
+ stdoutWrite(event.delta);
424
+ }
425
+ if (liveRenderer) {
426
+ streamedText = streamedText.trim();
427
+ if (streamedText.length > 0) {
428
+ liveRenderer.render(streamedText);
406
429
  }
407
430
  }
408
431
  throwIfTimedOut();
@@ -414,6 +437,9 @@ export async function runOracle(options, deps = {}) {
414
437
  log(chalk.yellow(describeTransportError(transportError, timeoutMs)));
415
438
  throw transportError;
416
439
  }
440
+ finally {
441
+ liveRenderer?.finish();
442
+ }
417
443
  response = await stream.finalResponse();
418
444
  throwIfTimedOut();
419
445
  stopHeartbeatNow();
@@ -428,21 +454,17 @@ export async function runOracle(options, deps = {}) {
428
454
  }
429
455
  // We only add spacing when streamed text was printed.
430
456
  if (sawTextDelta && !options.silent) {
431
- const fullStreamedText = streamedChunks.join('');
432
- const shouldRenderAfterStream = isTty && !renderPlain && fullStreamedText.length > 0;
433
- if (shouldRenderAfterStream) {
434
- const rendered = renderMarkdownAnsi(fullStreamedText);
435
- stdoutWrite(rendered);
436
- if (!rendered.endsWith('\n')) {
437
- stdoutWrite('\n');
438
- }
439
- log('');
440
- }
441
- else if (renderPlain) {
457
+ const shouldRenderAfterStream = isTty && !renderPlain && streamedText.length > 0;
458
+ if (renderPlain) {
442
459
  // Plain streaming already wrote chunks; ensure clean separation.
443
460
  stdoutWrite('\n');
444
461
  }
462
+ else if (!shouldRenderAfterStream) {
463
+ // Non-TTY streams should still surface output; ensure separation.
464
+ log('');
465
+ }
445
466
  else {
467
+ // Live-rendered mode already drew the final frame; only separate from logs.
446
468
  log('');
447
469
  }
448
470
  }
@@ -503,7 +525,10 @@ export async function runOracle(options, deps = {}) {
503
525
  const totalTokens = usage.total_tokens ?? inputTokens + outputTokens + reasoningTokens;
504
526
  const pricing = modelConfig.pricing ?? undefined;
505
527
  const cost = pricing
506
- ? inputTokens * pricing.inputPerToken + outputTokens * pricing.outputPerToken
528
+ ? estimateUsdCost({
529
+ usage: { inputTokens, outputTokens, reasoningTokens, totalTokens },
530
+ pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
531
+ })?.totalUsd
507
532
  : undefined;
508
533
  const elapsedDisplay = formatElapsed(elapsedMs);
509
534
  const statsParts = [];
@@ -541,6 +566,9 @@ export async function runOracle(options, deps = {}) {
541
566
  statsParts.push(`files=${files.length}`);
542
567
  }
543
568
  const sessionPrefix = options.sessionId ? `${options.sessionId} ` : '';
569
+ if (!options.silent) {
570
+ log('');
571
+ }
544
572
  log(chalk.blue(`Finished ${sessionPrefix}in ${elapsedDisplay} (${statsParts.join(' | ')})`));
545
573
  return {
546
574
  mode: 'live',
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { createWriteStream } from 'node:fs';
4
+ import net from 'node:net';
4
5
  import { DEFAULT_MODEL } from './oracle.js';
5
6
  import { safeModelSlug } from './oracle/modelResolver.js';
6
7
  import { getOracleHomeDir } from './oracleHome.js';
@@ -15,6 +16,7 @@ const MODEL_JSON_EXTENSION = '.json';
15
16
  const MODEL_LOG_EXTENSION = '.log';
16
17
  const MAX_STATUS_LIMIT = 1000;
17
18
  const ZOMBIE_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
19
+ const CHROME_RUNTIME_TIMEOUT_MS = 250;
18
20
  const DEFAULT_SLUG = 'session';
19
21
  const MAX_SLUG_WORDS = 5;
20
22
  const MIN_CUSTOM_SLUG_WORDS = 3;
@@ -245,7 +247,8 @@ async function readModernSessionMetadata(sessionId) {
245
247
  return null;
246
248
  }
247
249
  const enriched = await attachModelRuns(parsed, sessionId);
248
- return await markZombie(enriched, { persist: false });
250
+ const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
251
+ return await markZombie(runtimeChecked, { persist: false });
249
252
  }
250
253
  catch {
251
254
  return null;
@@ -256,7 +259,8 @@ async function readLegacySessionMetadata(sessionId) {
256
259
  const raw = await fs.readFile(legacySessionPath(sessionId), 'utf8');
257
260
  const parsed = JSON.parse(raw);
258
261
  const enriched = await attachModelRuns(parsed, sessionId);
259
- return await markZombie(enriched, { persist: false });
262
+ const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
263
+ return await markZombie(runtimeChecked, { persist: false });
260
264
  }
261
265
  catch {
262
266
  return null;
@@ -294,6 +298,7 @@ export async function listSessionsMetadata() {
294
298
  for (const entry of entries) {
295
299
  let meta = await readSessionMetadata(entry);
296
300
  if (meta) {
301
+ meta = await markDeadBrowser(meta, { persist: true });
297
302
  meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
298
303
  metas.push(meta);
299
304
  }
@@ -451,6 +456,44 @@ async function markZombie(meta, { persist }) {
451
456
  }
452
457
  return updated;
453
458
  }
459
+ async function markDeadBrowser(meta, { persist }) {
460
+ if (meta.status !== 'running' || meta.mode !== 'browser') {
461
+ return meta;
462
+ }
463
+ const runtime = meta.browser?.runtime;
464
+ if (!runtime) {
465
+ return meta;
466
+ }
467
+ const signals = [];
468
+ if (runtime.chromePid) {
469
+ signals.push(isProcessAlive(runtime.chromePid));
470
+ }
471
+ if (runtime.chromePort) {
472
+ const host = runtime.chromeHost ?? '127.0.0.1';
473
+ signals.push(await isPortOpen(host, runtime.chromePort));
474
+ }
475
+ if (signals.length === 0 || signals.some(Boolean)) {
476
+ return meta;
477
+ }
478
+ const response = meta.response
479
+ ? {
480
+ ...meta.response,
481
+ status: 'error',
482
+ incompleteReason: meta.response.incompleteReason ?? 'chrome-disconnected',
483
+ }
484
+ : { status: 'error', incompleteReason: 'chrome-disconnected' };
485
+ const updated = {
486
+ ...meta,
487
+ status: 'error',
488
+ errorMessage: 'Browser session ended (Chrome is no longer reachable)',
489
+ completedAt: new Date().toISOString(),
490
+ response,
491
+ };
492
+ if (persist) {
493
+ await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
494
+ }
495
+ return updated;
496
+ }
454
497
  function isZombie(meta) {
455
498
  if (meta.status !== 'running') {
456
499
  return false;
@@ -465,3 +508,49 @@ function isZombie(meta) {
465
508
  }
466
509
  return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
467
510
  }
511
+ function isProcessAlive(pid) {
512
+ if (!pid)
513
+ return false;
514
+ try {
515
+ process.kill(pid, 0);
516
+ return true;
517
+ }
518
+ catch (error) {
519
+ const code = error instanceof Error ? error.code : undefined;
520
+ if (code === 'ESRCH' || code === 'EINVAL') {
521
+ return false;
522
+ }
523
+ if (code === 'EPERM') {
524
+ return true;
525
+ }
526
+ return true;
527
+ }
528
+ }
529
+ async function isPortOpen(host, port) {
530
+ if (!port || port <= 0 || port > 65535) {
531
+ return false;
532
+ }
533
+ return new Promise((resolve) => {
534
+ const socket = net.createConnection({ host, port });
535
+ let settled = false;
536
+ const cleanup = (result) => {
537
+ if (settled)
538
+ return;
539
+ settled = true;
540
+ socket.removeAllListeners();
541
+ socket.end();
542
+ socket.destroy();
543
+ socket.unref();
544
+ resolve(result);
545
+ };
546
+ const timer = setTimeout(() => cleanup(false), CHROME_RUNTIME_TIMEOUT_MS);
547
+ socket.once('connect', () => {
548
+ clearTimeout(timer);
549
+ cleanup(true);
550
+ });
551
+ socket.once('error', () => {
552
+ clearTimeout(timer);
553
+ cleanup(false);
554
+ });
555
+ });
556
+ }
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/oracle-cli.js",
@@ -8,6 +8,26 @@
8
8
  "oracle": "dist/bin/oracle-cli.js",
9
9
  "oracle-mcp": "dist/bin/oracle-mcp.js"
10
10
  },
11
+ "scripts": {
12
+ "docs:list": "tsx scripts/docs-list.ts",
13
+ "build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
14
+ "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
15
+ "start": "pnpm run build && node ./dist/scripts/run-cli.js",
16
+ "oracle": "pnpm start",
17
+ "check": "pnpm run typecheck",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "pnpm run typecheck && biome lint .",
20
+ "test": "vitest run",
21
+ "test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
22
+ "test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
23
+ "test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
24
+ "test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
25
+ "test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
26
+ "test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
27
+ "test:coverage": "vitest run --coverage",
28
+ "prepare": "pnpm run build",
29
+ "mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
30
+ },
11
31
  "files": [
12
32
  "dist/**/*",
13
33
  "assets-oracle-icon.png",
@@ -57,25 +77,27 @@
57
77
  "json5": "^2.2.3",
58
78
  "keytar": "^7.9.0",
59
79
  "kleur": "^4.1.5",
60
- "markdansi": "^0.1.3",
61
- "openai": "^6.14.0",
80
+ "markdansi": "0.1.7",
81
+ "openai": "^6.15.0",
82
+ "osc-progress": "^0.1.0",
62
83
  "shiki": "^3.20.0",
63
84
  "sqlite3": "^5.1.7",
64
85
  "toasted-notifier": "^10.1.0",
86
+ "tokentally": "github:steipete/tokentally#v0.1.0",
65
87
  "zod": "^4.2.1"
66
88
  },
67
89
  "devDependencies": {
68
90
  "@anthropic-ai/tokenizer": "^0.0.4",
69
- "@biomejs/biome": "^2.3.9",
91
+ "@biomejs/biome": "^2.3.10",
70
92
  "@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
71
93
  "@types/chrome-remote-interface": "^0.33.0",
72
94
  "@types/inquirer": "^9.0.9",
73
95
  "@types/node": "^25.0.3",
74
96
  "@vitest/coverage-v8": "4.0.16",
75
- "devtools-protocol": "0.0.1559729",
97
+ "devtools-protocol": "0.0.1561482",
76
98
  "es-toolkit": "^1.43.0",
77
99
  "esbuild": "^0.27.2",
78
- "puppeteer-core": "^24.33.0",
100
+ "puppeteer-core": "^24.34.0",
79
101
  "tsx": "^4.21.0",
80
102
  "typescript": "^5.9.3",
81
103
  "vitest": "^4.0.16"
@@ -83,23 +105,18 @@
83
105
  "optionalDependencies": {
84
106
  "win-dpapi": "npm:@primno/dpapi@2.0.1"
85
107
  },
86
- "scripts": {
87
- "docs:list": "tsx scripts/docs-list.ts",
88
- "build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
89
- "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
90
- "start": "pnpm run build && node ./dist/scripts/run-cli.js",
91
- "oracle": "pnpm start",
92
- "check": "pnpm run typecheck",
93
- "typecheck": "tsc --noEmit",
94
- "lint": "pnpm run typecheck && biome lint .",
95
- "test": "vitest run",
96
- "test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
97
- "test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
98
- "test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
99
- "test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
100
- "test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
101
- "test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
102
- "test:coverage": "vitest run --coverage",
103
- "mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
104
- }
105
- }
108
+ "pnpm": {
109
+ "overrides": {
110
+ "devtools-protocol": "0.0.1561482",
111
+ "win-dpapi": "npm:@primno/dpapi@2.0.1"
112
+ },
113
+ "onlyBuiltDependencies": [
114
+ "@cdktf/node-pty-prebuilt-multiarch",
115
+ "esbuild",
116
+ "keytar",
117
+ "sqlite3",
118
+ "win-dpapi"
119
+ ]
120
+ },
121
+ "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
122
+ }
@@ -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.
File without changes