@steipete/oracle 0.8.4 → 0.8.5

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 (35) hide show
  1. package/README.md +7 -0
  2. package/dist/bin/oracle-cli.js +102 -9
  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 +13 -5
  7. package/dist/src/browser/chromeLifecycle.js +62 -9
  8. package/dist/src/browser/detect.js +164 -0
  9. package/dist/src/browser/index.js +55 -2
  10. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  11. package/dist/src/cli/bridge/client.js +73 -0
  12. package/dist/src/cli/bridge/codexConfig.js +43 -0
  13. package/dist/src/cli/bridge/doctor.js +107 -0
  14. package/dist/src/cli/bridge/host.js +259 -0
  15. package/dist/src/cli/engine.js +17 -1
  16. package/dist/src/cli/options.js +14 -0
  17. package/dist/src/cli/runOptions.js +4 -0
  18. package/dist/src/mcp/tools/consult.js +80 -15
  19. package/dist/src/mcp/tools/sessions.js +15 -6
  20. package/dist/src/mcp/types.js +4 -0
  21. package/dist/src/mcp/utils.js +12 -2
  22. package/dist/src/oracle/background.js +1 -2
  23. package/dist/src/oracle/client.js +5 -2
  24. package/dist/src/oracle/files.js +2 -2
  25. package/dist/src/oracle/run.js +1 -0
  26. package/dist/src/remote/client.js +6 -5
  27. package/dist/src/remote/health.js +113 -0
  28. package/dist/src/remote/remoteServiceConfig.js +31 -0
  29. package/dist/src/remote/server.js +28 -1
  30. package/dist/src/sessionManager.js +63 -5
  31. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  32. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  33. package/package.json +13 -13
  34. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  35. 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() {
@@ -201,6 +201,10 @@ 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,
205
209
  },
206
210
  };
@@ -442,7 +446,7 @@ export async function getSessionPaths(sessionId) {
442
446
  return { dir, metadata, log, request };
443
447
  }
444
448
  async function markZombie(meta, { persist }) {
445
- if (!isZombie(meta)) {
449
+ if (!(await isZombie(meta))) {
446
450
  return meta;
447
451
  }
448
452
  if (meta.mode === 'browser') {
@@ -461,10 +465,11 @@ async function markZombie(meta, { persist }) {
461
465
  }
462
466
  }
463
467
  }
468
+ const maxAgeMs = resolveZombieMaxAgeMs(meta);
464
469
  const updated = {
465
470
  ...meta,
466
471
  status: 'error',
467
- errorMessage: 'Session marked as zombie (>60m stale)',
472
+ errorMessage: `Session marked as zombie (> ${formatElapsed(maxAgeMs)} stale)`,
468
473
  completedAt: new Date().toISOString(),
469
474
  };
470
475
  if (persist) {
@@ -510,7 +515,7 @@ async function markDeadBrowser(meta, { persist }) {
510
515
  }
511
516
  return updated;
512
517
  }
513
- function isZombie(meta) {
518
+ async function isZombie(meta) {
514
519
  if (meta.status !== 'running') {
515
520
  return false;
516
521
  }
@@ -522,7 +527,60 @@ function isZombie(meta) {
522
527
  if (Number.isNaN(startedMs)) {
523
528
  return false;
524
529
  }
525
- return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
530
+ const useLastActivity = meta.options?.zombieUseLastActivity === true;
531
+ const lastActivityMs = useLastActivity ? await getLastActivityMs(meta) : null;
532
+ const anchorMs = lastActivityMs ?? startedMs;
533
+ const maxAgeMs = resolveZombieMaxAgeMs(meta);
534
+ return Date.now() - anchorMs > maxAgeMs;
535
+ }
536
+ function resolveZombieMaxAgeMs(meta) {
537
+ const explicit = meta.options?.zombieTimeoutMs;
538
+ const hasExplicit = typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0;
539
+ let maxAgeMs = hasExplicit ? explicit : ZOMBIE_MAX_AGE_MS;
540
+ if (!hasExplicit) {
541
+ const timeoutSeconds = meta.options?.timeoutSeconds;
542
+ if (typeof timeoutSeconds === 'number' && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) {
543
+ const timeoutMs = timeoutSeconds * 1000;
544
+ if (timeoutMs > maxAgeMs) {
545
+ maxAgeMs = timeoutMs;
546
+ }
547
+ }
548
+ }
549
+ return maxAgeMs;
550
+ }
551
+ async function getLastActivityMs(meta) {
552
+ const candidates = new Set();
553
+ candidates.add(logPath(meta.id));
554
+ const modelNames = new Set();
555
+ if (typeof meta.model === 'string' && meta.model.length > 0) {
556
+ modelNames.add(meta.model);
557
+ }
558
+ if (Array.isArray(meta.models)) {
559
+ for (const entry of meta.models) {
560
+ if (entry?.model) {
561
+ modelNames.add(entry.model);
562
+ }
563
+ }
564
+ }
565
+ for (const modelName of modelNames) {
566
+ candidates.add(modelLogPath(meta.id, modelName));
567
+ }
568
+ let latest = 0;
569
+ let sawStat = false;
570
+ for (const candidate of candidates) {
571
+ try {
572
+ const stats = await fs.stat(candidate);
573
+ const mtimeMs = Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : stats.mtime.getTime();
574
+ if (Number.isFinite(mtimeMs)) {
575
+ latest = Math.max(latest, mtimeMs);
576
+ sawStat = true;
577
+ }
578
+ }
579
+ catch {
580
+ // ignore missing logs; fallback to startedAt
581
+ }
582
+ }
583
+ return sawStat ? latest : null;
526
584
  }
527
585
  function isProcessAlive(pid) {
528
586
  if (!pid)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
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,9 +62,9 @@
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.35.0",
66
66
  "@google/generative-ai": "^0.24.1",
67
- "@modelcontextprotocol/sdk": "^1.25.1",
67
+ "@modelcontextprotocol/sdk": "^1.25.2",
68
68
  "@steipete/sweet-cookie": "^0.1.0",
69
69
  "chalk": "^5.6.2",
70
70
  "chrome-launcher": "^1.2.1",
@@ -74,14 +74,14 @@
74
74
  "dotenv": "^17.2.3",
75
75
  "fast-glob": "^3.3.3",
76
76
  "gpt-tokenizer": "^3.4.0",
77
- "inquirer": "13.1.0",
77
+ "inquirer": "13.2.0",
78
78
  "json5": "^2.2.3",
79
79
  "kleur": "^4.1.5",
80
- "markdansi": "0.2.0",
81
- "openai": "^6.15.0",
80
+ "markdansi": "0.2.1",
81
+ "openai": "^6.16.0",
82
82
  "osc-progress": "^0.2.0",
83
83
  "qs": "^6.14.1",
84
- "shiki": "^3.20.0",
84
+ "shiki": "^3.21.0",
85
85
  "toasted-notifier": "^10.1.0",
86
86
  "tokentally": "^0.1.1",
87
87
  "zod": "^4.3.5"
@@ -92,19 +92,19 @@
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",
95
+ "@types/node": "^25.0.6",
96
+ "@vitest/coverage-v8": "4.0.17",
97
+ "devtools-protocol": "0.0.1568893",
98
98
  "es-toolkit": "^1.43.0",
99
99
  "esbuild": "^0.27.2",
100
- "puppeteer-core": "^24.34.0",
100
+ "puppeteer-core": "^24.35.0",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3",
103
- "vitest": "^4.0.16"
103
+ "vitest": "^4.0.17"
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",