@steipete/oracle 0.8.4 → 0.8.6

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 (44) hide show
  1. package/README.md +30 -1
  2. package/dist/bin/oracle-cli.js +291 -16
  3. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  4. package/dist/src/bridge/connection.js +103 -0
  5. package/dist/src/bridge/userConfigFile.js +28 -0
  6. package/dist/src/browser/actions/assistantResponse.js +85 -42
  7. package/dist/src/browser/actions/promptComposer.js +141 -32
  8. package/dist/src/browser/chromeLifecycle.js +78 -9
  9. package/dist/src/browser/config.js +14 -0
  10. package/dist/src/browser/detect.js +164 -0
  11. package/dist/src/browser/index.js +394 -24
  12. package/dist/src/browser/profileState.js +93 -0
  13. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  14. package/dist/src/cli/bridge/client.js +73 -0
  15. package/dist/src/cli/bridge/codexConfig.js +43 -0
  16. package/dist/src/cli/bridge/doctor.js +107 -0
  17. package/dist/src/cli/bridge/host.js +259 -0
  18. package/dist/src/cli/browserConfig.js +21 -0
  19. package/dist/src/cli/browserDefaults.js +21 -0
  20. package/dist/src/cli/engine.js +17 -1
  21. package/dist/src/cli/options.js +14 -0
  22. package/dist/src/cli/runOptions.js +4 -0
  23. package/dist/src/cli/sessionRunner.js +149 -0
  24. package/dist/src/cli/tui/index.js +1 -0
  25. package/dist/src/mcp/tools/consult.js +81 -15
  26. package/dist/src/mcp/tools/sessions.js +15 -6
  27. package/dist/src/mcp/types.js +4 -0
  28. package/dist/src/mcp/utils.js +12 -2
  29. package/dist/src/oracle/background.js +1 -2
  30. package/dist/src/oracle/client.js +5 -2
  31. package/dist/src/oracle/files.js +2 -2
  32. package/dist/src/oracle/modelResolver.js +33 -1
  33. package/dist/src/oracle/run.js +1 -0
  34. package/dist/src/remote/client.js +6 -5
  35. package/dist/src/remote/health.js +113 -0
  36. package/dist/src/remote/remoteServiceConfig.js +31 -0
  37. package/dist/src/remote/server.js +28 -1
  38. package/dist/src/sessionManager.js +72 -7
  39. package/dist/src/sessionStore.js +2 -2
  40. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  42. package/package.json +21 -21
  43. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  44. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -0,0 +1,113 @@
1
+ import http from 'node:http';
2
+ import net from 'node:net';
3
+ import { parseHostPort } from '../bridge/connection.js';
4
+ export async function checkTcpConnection(host, timeoutMs = 2000) {
5
+ const { hostname, port } = parseHostPort(host);
6
+ return await new Promise((resolve) => {
7
+ const socket = net.createConnection({ host: hostname, port });
8
+ const onError = (err) => {
9
+ cleanup();
10
+ resolve({ ok: false, error: err.message });
11
+ };
12
+ const onConnect = () => {
13
+ cleanup();
14
+ resolve({ ok: true });
15
+ };
16
+ const onTimeout = () => {
17
+ cleanup();
18
+ resolve({ ok: false, error: `timeout after ${timeoutMs}ms` });
19
+ };
20
+ const cleanup = () => {
21
+ socket.removeAllListeners();
22
+ socket.end();
23
+ socket.destroy();
24
+ socket.unref();
25
+ };
26
+ socket.setTimeout(timeoutMs);
27
+ socket.once('error', onError);
28
+ socket.once('connect', onConnect);
29
+ socket.once('timeout', onTimeout);
30
+ });
31
+ }
32
+ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
33
+ const { hostname, port } = parseHostPort(host);
34
+ const headers = { accept: 'application/json' };
35
+ if (token) {
36
+ headers.authorization = `Bearer ${token}`;
37
+ }
38
+ try {
39
+ const response = await requestJson({
40
+ hostname,
41
+ port,
42
+ path: '/health',
43
+ headers,
44
+ timeoutMs,
45
+ });
46
+ if (response.statusCode === 200 && typeof response.json === 'object' && response.json) {
47
+ const ok = response.json.ok === true;
48
+ const version = response.json.version;
49
+ const uptimeSeconds = response.json.uptimeSeconds;
50
+ return {
51
+ ok,
52
+ statusCode: response.statusCode,
53
+ version: typeof version === 'string' ? version : undefined,
54
+ uptimeSeconds: typeof uptimeSeconds === 'number' ? uptimeSeconds : undefined,
55
+ };
56
+ }
57
+ if (response.statusCode === 404) {
58
+ return {
59
+ ok: false,
60
+ statusCode: response.statusCode,
61
+ error: 'remote host does not expose /health (upgrade oracle on the host and retry)',
62
+ };
63
+ }
64
+ const error = extractErrorMessage(response.json, response.bodyText) ?? `HTTP ${response.statusCode}`;
65
+ return { ok: false, statusCode: response.statusCode, error };
66
+ }
67
+ catch (error) {
68
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
69
+ }
70
+ }
71
+ function extractErrorMessage(json, bodyText) {
72
+ if (json && typeof json === 'object') {
73
+ const err = json.error;
74
+ if (typeof err === 'string' && err.trim().length > 0) {
75
+ return err.trim();
76
+ }
77
+ }
78
+ const trimmed = bodyText.trim();
79
+ return trimmed.length ? trimmed : null;
80
+ }
81
+ async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
82
+ return await new Promise((resolve, reject) => {
83
+ const req = http.request({
84
+ hostname,
85
+ port,
86
+ path,
87
+ method: 'GET',
88
+ headers,
89
+ }, (res) => {
90
+ res.setEncoding('utf8');
91
+ let body = '';
92
+ res.on('data', (chunk) => {
93
+ body += chunk;
94
+ });
95
+ res.on('end', () => {
96
+ const statusCode = res.statusCode ?? 0;
97
+ let json = null;
98
+ try {
99
+ json = body.length ? JSON.parse(body) : null;
100
+ }
101
+ catch {
102
+ json = null;
103
+ }
104
+ resolve({ statusCode, json, bodyText: body });
105
+ });
106
+ });
107
+ req.setTimeout(timeoutMs, () => {
108
+ req.destroy(new Error(`timeout after ${timeoutMs}ms`));
109
+ });
110
+ req.on('error', reject);
111
+ req.end();
112
+ });
113
+ }
@@ -0,0 +1,31 @@
1
+ function normalizeString(value) {
2
+ if (typeof value !== 'string')
3
+ return undefined;
4
+ const trimmed = value.trim();
5
+ return trimmed.length ? trimmed : undefined;
6
+ }
7
+ export function resolveRemoteServiceConfig({ cliHost, cliToken, userConfig, env = process.env, }) {
8
+ const configBrowserHost = normalizeString(userConfig?.browser?.remoteHost);
9
+ const configBrowserToken = normalizeString(userConfig?.browser?.remoteToken);
10
+ const envHost = normalizeString(env.ORACLE_REMOTE_HOST);
11
+ const envToken = normalizeString(env.ORACLE_REMOTE_TOKEN);
12
+ const cliHostValue = normalizeString(cliHost);
13
+ const cliTokenValue = normalizeString(cliToken);
14
+ const host = cliHostValue ?? configBrowserHost ?? envHost;
15
+ const token = cliTokenValue ?? configBrowserToken ?? envToken;
16
+ const hostSource = cliHostValue
17
+ ? 'cli'
18
+ : configBrowserHost
19
+ ? 'config.browser'
20
+ : envHost
21
+ ? 'env'
22
+ : 'unset';
23
+ const tokenSource = cliTokenValue
24
+ ? 'cli'
25
+ : configBrowserToken
26
+ ? 'config.browser'
27
+ : envToken
28
+ ? 'env'
29
+ : 'unset';
30
+ return { host, token, sources: { host: hostSource, token: tokenSource } };
31
+ }
@@ -9,6 +9,7 @@ import chalk from 'chalk';
9
9
  import { runBrowserMode } from '../browserMode.js';
10
10
  import { getCookies } from '@steipete/sweet-cookie';
11
11
  import { CHATGPT_URL } from '../browser/constants.js';
12
+ import { getCliVersion } from '../version.js';
12
13
  import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from '../browser/profileState.js';
13
14
  import { normalizeChatgptUrl } from '../browser/utils.js';
14
15
  async function findAvailablePort() {
@@ -32,6 +33,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
32
33
  const server = http.createServer();
33
34
  const logger = options.logger ?? console.log;
34
35
  const authToken = options.token ?? randomBytes(16).toString('hex');
36
+ const startedAt = Date.now();
35
37
  const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
36
38
  const color = process.stdout.isTTY
37
39
  ? (formatter, msg) => formatter(msg)
@@ -50,6 +52,24 @@ export async function createRemoteServer(options = {}, deps = {}) {
50
52
  res.end(JSON.stringify({ ok: true }));
51
53
  return;
52
54
  }
55
+ if (req.method === 'GET' && req.url === '/health') {
56
+ const authHeader = req.headers.authorization ?? '';
57
+ if (authHeader !== `Bearer ${authToken}`) {
58
+ if (verbose) {
59
+ logger(`[serve] Unauthorized /health attempt from ${formatSocket(req)} (missing/invalid token)`);
60
+ }
61
+ res.writeHead(401, { 'Content-Type': 'application/json' });
62
+ res.end(JSON.stringify({ error: 'unauthorized' }));
63
+ return;
64
+ }
65
+ res.writeHead(200, { 'Content-Type': 'application/json' });
66
+ res.end(JSON.stringify({
67
+ ok: true,
68
+ version: getCliVersion(),
69
+ uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
70
+ }));
71
+ return;
72
+ }
53
73
  if (req.method !== 'POST' || req.url !== '/runs') {
54
74
  res.statusCode = 404;
55
75
  res.end();
@@ -352,7 +372,14 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
352
372
  }
353
373
  catch (error) {
354
374
  const message = error instanceof Error ? error.message : String(error);
355
- logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
375
+ const missingDbMatch = message.match(/Unable to locate Chrome cookie DB at (.+?)(?:\.|$)/);
376
+ if (missingDbMatch) {
377
+ const lookedPath = missingDbMatch[1];
378
+ logger(`Chrome cookies not found at ${lookedPath}. Set --browser-cookie-path to your Chrome profile or log in manually.`);
379
+ }
380
+ else {
381
+ logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
382
+ }
356
383
  if (process.platform === 'linux' && isWsl()) {
357
384
  logger('WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.');
358
385
  }
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { createWriteStream } from 'node:fs';
4
4
  import net from 'node:net';
5
- import { DEFAULT_MODEL } from './oracle.js';
5
+ import { DEFAULT_MODEL, formatElapsed } from './oracle.js';
6
6
  import { safeModelSlug } from './oracle/modelResolver.js';
7
7
  import { getOracleHomeDir } from './oracleHome.js';
8
8
  export function getSessionsDir() {
@@ -151,9 +151,9 @@ export async function updateModelRunMetadata(sessionId, model, updates) {
151
151
  export async function readModelRunMetadata(sessionId, model) {
152
152
  return readModelRunFile(sessionId, model);
153
153
  }
154
- export async function initializeSession(options, cwd, notifications) {
154
+ export async function initializeSession(options, cwd, notifications, baseSlugOverride) {
155
155
  await ensureSessionStorage();
156
- const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
156
+ const baseSlug = baseSlugOverride || createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
157
157
  const sessionId = await ensureUniqueSessionId(baseSlug);
158
158
  const dir = sessionDir(sessionId);
159
159
  await ensureDir(dir);
@@ -201,7 +201,18 @@ export async function initializeSession(options, cwd, notifications) {
201
201
  search: options.search,
202
202
  baseUrl: options.baseUrl,
203
203
  azure: options.azure,
204
+ timeoutSeconds: options.timeoutSeconds,
205
+ httpTimeoutMs: options.httpTimeoutMs,
206
+ zombieTimeoutMs: options.zombieTimeoutMs,
207
+ zombieUseLastActivity: options.zombieUseLastActivity,
204
208
  writeOutputPath: options.writeOutputPath,
209
+ waitPreference: options.waitPreference,
210
+ youtube: options.youtube,
211
+ generateImage: options.generateImage,
212
+ editImage: options.editImage,
213
+ outputPath: options.outputPath,
214
+ aspectRatio: options.aspectRatio,
215
+ geminiShowThoughts: options.geminiShowThoughts,
205
216
  },
206
217
  };
207
218
  await ensureDir(modelsDir(sessionId));
@@ -442,7 +453,7 @@ export async function getSessionPaths(sessionId) {
442
453
  return { dir, metadata, log, request };
443
454
  }
444
455
  async function markZombie(meta, { persist }) {
445
- if (!isZombie(meta)) {
456
+ if (!(await isZombie(meta))) {
446
457
  return meta;
447
458
  }
448
459
  if (meta.mode === 'browser') {
@@ -461,10 +472,11 @@ async function markZombie(meta, { persist }) {
461
472
  }
462
473
  }
463
474
  }
475
+ const maxAgeMs = resolveZombieMaxAgeMs(meta);
464
476
  const updated = {
465
477
  ...meta,
466
478
  status: 'error',
467
- errorMessage: 'Session marked as zombie (>60m stale)',
479
+ errorMessage: `Session marked as zombie (> ${formatElapsed(maxAgeMs)} stale)`,
468
480
  completedAt: new Date().toISOString(),
469
481
  };
470
482
  if (persist) {
@@ -510,7 +522,7 @@ async function markDeadBrowser(meta, { persist }) {
510
522
  }
511
523
  return updated;
512
524
  }
513
- function isZombie(meta) {
525
+ async function isZombie(meta) {
514
526
  if (meta.status !== 'running') {
515
527
  return false;
516
528
  }
@@ -522,7 +534,60 @@ function isZombie(meta) {
522
534
  if (Number.isNaN(startedMs)) {
523
535
  return false;
524
536
  }
525
- return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
537
+ const useLastActivity = meta.options?.zombieUseLastActivity === true;
538
+ const lastActivityMs = useLastActivity ? await getLastActivityMs(meta) : null;
539
+ const anchorMs = lastActivityMs ?? startedMs;
540
+ const maxAgeMs = resolveZombieMaxAgeMs(meta);
541
+ return Date.now() - anchorMs > maxAgeMs;
542
+ }
543
+ function resolveZombieMaxAgeMs(meta) {
544
+ const explicit = meta.options?.zombieTimeoutMs;
545
+ const hasExplicit = typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0;
546
+ let maxAgeMs = hasExplicit ? explicit : ZOMBIE_MAX_AGE_MS;
547
+ if (!hasExplicit) {
548
+ const timeoutSeconds = meta.options?.timeoutSeconds;
549
+ if (typeof timeoutSeconds === 'number' && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) {
550
+ const timeoutMs = timeoutSeconds * 1000;
551
+ if (timeoutMs > maxAgeMs) {
552
+ maxAgeMs = timeoutMs;
553
+ }
554
+ }
555
+ }
556
+ return maxAgeMs;
557
+ }
558
+ async function getLastActivityMs(meta) {
559
+ const candidates = new Set();
560
+ candidates.add(logPath(meta.id));
561
+ const modelNames = new Set();
562
+ if (typeof meta.model === 'string' && meta.model.length > 0) {
563
+ modelNames.add(meta.model);
564
+ }
565
+ if (Array.isArray(meta.models)) {
566
+ for (const entry of meta.models) {
567
+ if (entry?.model) {
568
+ modelNames.add(entry.model);
569
+ }
570
+ }
571
+ }
572
+ for (const modelName of modelNames) {
573
+ candidates.add(modelLogPath(meta.id, modelName));
574
+ }
575
+ let latest = 0;
576
+ let sawStat = false;
577
+ for (const candidate of candidates) {
578
+ try {
579
+ const stats = await fs.stat(candidate);
580
+ const mtimeMs = Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : stats.mtime.getTime();
581
+ if (Number.isFinite(mtimeMs)) {
582
+ latest = Math.max(latest, mtimeMs);
583
+ sawStat = true;
584
+ }
585
+ }
586
+ catch {
587
+ // ignore missing logs; fallback to startedAt
588
+ }
589
+ }
590
+ return sawStat ? latest : null;
526
591
  }
527
592
  function isProcessAlive(pid) {
528
593
  if (!pid)
@@ -3,8 +3,8 @@ class FileSessionStore {
3
3
  ensureStorage() {
4
4
  return ensureSessionStorage();
5
5
  }
6
- createSession(options, cwd, notifications) {
7
- return initializeSession(options, cwd, notifications);
6
+ createSession(options, cwd, notifications, baseSlugOverride) {
7
+ return initializeSession(options, cwd, notifications, baseSlugOverride);
8
8
  }
9
9
  readSession(sessionId) {
10
10
  return readSessionMetadata(sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
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",
@@ -62,49 +62,49 @@
62
62
  "homepage": "https://github.com/steipete/oracle#readme",
63
63
  "dependencies": {
64
64
  "@anthropic-ai/tokenizer": "^0.0.4",
65
- "@google/genai": "^1.34.0",
65
+ "@google/genai": "^1.40.0",
66
66
  "@google/generative-ai": "^0.24.1",
67
- "@modelcontextprotocol/sdk": "^1.25.1",
67
+ "@modelcontextprotocol/sdk": "^1.26.0",
68
68
  "@steipete/sweet-cookie": "^0.1.0",
69
69
  "chalk": "^5.6.2",
70
70
  "chrome-launcher": "^1.2.1",
71
71
  "chrome-remote-interface": "^0.33.3",
72
- "clipboardy": "^5.0.2",
73
- "commander": "^14.0.2",
74
- "dotenv": "^17.2.3",
72
+ "clipboardy": "^5.2.1",
73
+ "commander": "^14.0.3",
74
+ "dotenv": "^17.2.4",
75
75
  "fast-glob": "^3.3.3",
76
76
  "gpt-tokenizer": "^3.4.0",
77
- "inquirer": "13.1.0",
77
+ "inquirer": "13.2.2",
78
78
  "json5": "^2.2.3",
79
79
  "kleur": "^4.1.5",
80
- "markdansi": "0.2.0",
81
- "openai": "^6.15.0",
82
- "osc-progress": "^0.2.0",
80
+ "markdansi": "0.2.1",
81
+ "openai": "^6.18.0",
82
+ "osc-progress": "^0.3.0",
83
83
  "qs": "^6.14.1",
84
- "shiki": "^3.20.0",
84
+ "shiki": "^3.22.0",
85
85
  "toasted-notifier": "^10.1.0",
86
86
  "tokentally": "^0.1.1",
87
- "zod": "^4.3.5"
87
+ "zod": "^4.3.6"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@anthropic-ai/tokenizer": "^0.0.4",
91
- "@biomejs/biome": "^2.3.11",
91
+ "@biomejs/biome": "^2.3.14",
92
92
  "@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
93
93
  "@types/chrome-remote-interface": "^0.33.0",
94
94
  "@types/inquirer": "^9.0.9",
95
- "@types/node": "^25.0.3",
96
- "@vitest/coverage-v8": "4.0.16",
97
- "devtools-protocol": "0.0.1561482",
98
- "es-toolkit": "^1.43.0",
99
- "esbuild": "^0.27.2",
100
- "puppeteer-core": "^24.34.0",
95
+ "@types/node": "^25.2.1",
96
+ "@vitest/coverage-v8": "4.0.18",
97
+ "devtools-protocol": "0.0.1580600",
98
+ "es-toolkit": "^1.44.0",
99
+ "esbuild": "^0.27.3",
100
+ "puppeteer-core": "^24.37.2",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3",
103
- "vitest": "^4.0.16"
103
+ "vitest": "^4.0.18"
104
104
  },
105
105
  "pnpm": {
106
106
  "overrides": {
107
- "devtools-protocol": "0.0.1561482"
107
+ "devtools-protocol": "0.0.1568893"
108
108
  },
109
109
  "onlyBuiltDependencies": [
110
110
  "@cdktf/node-pty-prebuilt-multiarch",