@steipete/oracle 0.9.0 → 0.11.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.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,21 +1,24 @@
1
- import { z } from 'zod';
2
- import { sessionStore } from '../../sessionStore.js';
3
- import { sessionsInputSchema } from '../types.js';
1
+ import { z } from "zod";
2
+ import { sessionStore } from "../../sessionStore.js";
3
+ import { sessionsInputSchema } from "../types.js";
4
4
  const sessionsInputShape = {
5
5
  id: z
6
6
  .string()
7
7
  .optional()
8
- .describe('Session id or slug. If set, returns a single session (use detail:true to include metadata/request).'),
9
- hours: z.number().optional().describe('Look back this many hours when listing sessions (default: 24).'),
10
- limit: z.number().optional().describe('Maximum sessions to return when listing (default: 100).'),
8
+ .describe("Session id or slug. If set, returns a single session (use detail:true to include metadata/request)."),
9
+ hours: z
10
+ .number()
11
+ .optional()
12
+ .describe("Look back this many hours when listing sessions (default: 24)."),
13
+ limit: z.number().optional().describe("Maximum sessions to return when listing (default: 100)."),
11
14
  includeAll: z
12
15
  .boolean()
13
16
  .optional()
14
- .describe('Include sessions outside the time window when listing (mirrors `oracle status --all`).'),
17
+ .describe("Include sessions outside the time window when listing (mirrors `oracle status --all`)."),
15
18
  detail: z
16
19
  .boolean()
17
20
  .optional()
18
- .describe('When id is set, include session metadata + stored request + full log text.'),
21
+ .describe("When id is set, include session metadata + stored request + full log text."),
19
22
  };
20
23
  const sessionsOutputShape = {
21
24
  entries: z
@@ -38,14 +41,14 @@ const sessionsOutputShape = {
38
41
  .optional(),
39
42
  };
40
43
  export function registerSessionsTool(server) {
41
- server.registerTool('sessions', {
42
- title: 'List or fetch oracle sessions',
43
- description: 'Inspect Oracle session history stored under `ORACLE_HOME_DIR` (shared with the CLI). List recent sessions or fetch one by id/slug (optionally including metadata + request + log).',
44
+ server.registerTool("sessions", {
45
+ title: "List or fetch oracle sessions",
46
+ description: "Inspect Oracle session history stored under `ORACLE_HOME_DIR` (shared with the CLI). List recent sessions or fetch one by id/slug (optionally including metadata + request + log).",
44
47
  inputSchema: sessionsInputShape,
45
48
  outputSchema: sessionsOutputShape,
46
49
  }, async (input) => {
47
- const textContent = (text) => [{ type: 'text', text }];
48
- const { id, hours = 24, limit = 100, includeAll = false, detail = false } = sessionsInputSchema.parse(input);
50
+ const textContent = (text) => [{ type: "text", text }];
51
+ const { id, hours = 24, limit = 100, includeAll = false, detail = false, } = sessionsInputSchema.parse(input);
49
52
  if (id) {
50
53
  if (!detail) {
51
54
  const metadata = await sessionStore.readSession(id);
@@ -53,7 +56,7 @@ export function registerSessionsTool(server) {
53
56
  throw new Error(`Session "${id}" not found.`);
54
57
  }
55
58
  return {
56
- content: textContent(`${metadata.createdAt} | ${metadata.status} | ${metadata.model ?? 'n/a'} | ${metadata.id}`),
59
+ content: textContent(`${metadata.createdAt} | ${metadata.status} | ${metadata.model ?? "n/a"} | ${metadata.id}`),
57
60
  structuredContent: {
58
61
  entries: [
59
62
  {
@@ -81,12 +84,18 @@ export function registerSessionsTool(server) {
81
84
  };
82
85
  }
83
86
  const metas = await sessionStore.listSessions();
84
- const { entries, truncated, total } = sessionStore.filterSessions(metas, { hours, includeAll, limit });
87
+ const { entries, truncated, total } = sessionStore.filterSessions(metas, {
88
+ hours,
89
+ includeAll,
90
+ limit,
91
+ });
85
92
  return {
86
93
  content: [
87
94
  {
88
- type: 'text',
89
- text: entries.map((entry) => `${entry.createdAt} | ${entry.status} | ${entry.model ?? 'n/a'} | ${entry.id}`).join('\n'),
95
+ type: "text",
96
+ text: entries
97
+ .map((entry) => `${entry.createdAt} | ${entry.status} | ${entry.model ?? "n/a"} | ${entry.id}`)
98
+ .join("\n"),
90
99
  },
91
100
  ],
92
101
  structuredContent: {
@@ -1,15 +1,22 @@
1
- import { z } from 'zod';
1
+ import { z } from "zod";
2
+ export const CONSULT_PRESETS = ["chatgpt-pro-heavy"];
2
3
  export const consultInputSchema = z.object({
3
- prompt: z.string().min(1, 'Prompt is required.'),
4
+ preset: z.enum(CONSULT_PRESETS).optional(),
5
+ prompt: z.string().min(1, "Prompt is required."),
4
6
  files: z.array(z.string()).default([]),
5
7
  model: z.string().optional(),
6
8
  models: z.array(z.string()).optional(),
7
- engine: z.enum(['api', 'browser']).optional(),
9
+ engine: z.enum(["api", "browser"]).optional(),
8
10
  browserModelLabel: z.string().optional(),
9
- browserAttachments: z.enum(['auto', 'never', 'always']).optional(),
11
+ browserAttachments: z.enum(["auto", "never", "always"]).optional(),
10
12
  browserBundleFiles: z.boolean().optional(),
11
- browserThinkingTime: z.enum(['light', 'standard', 'extended', 'heavy']).optional(),
13
+ browserThinkingTime: z.enum(["light", "standard", "extended", "heavy"]).optional(),
14
+ browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
15
+ browserResearchMode: z.enum(["deep"]).optional(),
16
+ browserArchive: z.enum(["auto", "always", "never"]).optional(),
17
+ browserFollowUps: z.array(z.string()).optional(),
12
18
  browserKeepBrowser: z.boolean().optional(),
19
+ dryRun: z.boolean().optional(),
13
20
  search: z.boolean().optional(),
14
21
  slug: z.string().optional(),
15
22
  });
@@ -1,25 +1,38 @@
1
- import { resolveRunOptionsFromConfig } from '../cli/runOptions.js';
2
- import { Launcher } from 'chrome-launcher';
3
- export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, userConfig, env = process.env, }) {
1
+ import { resolveRunOptionsFromConfig } from "../cli/runOptions.js";
2
+ import { Launcher } from "chrome-launcher";
3
+ export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserFollowUps, userConfig, env = process.env, }) {
4
4
  // Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
5
5
  // then overlay MCP-only overrides such as explicit search toggles.
6
6
  const mergedModels = Array.isArray(models) && models.length > 0
7
7
  ? [model, ...models].filter((entry) => Boolean(entry?.trim()))
8
8
  : models;
9
- const result = resolveRunOptionsFromConfig({ prompt, files, model, models: mergedModels, engine, userConfig, env });
10
- if (typeof search === 'boolean') {
9
+ const result = resolveRunOptionsFromConfig({
10
+ prompt,
11
+ files,
12
+ model,
13
+ models: mergedModels,
14
+ engine,
15
+ userConfig,
16
+ env,
17
+ });
18
+ if (typeof search === "boolean") {
11
19
  result.runOptions.search = search;
12
20
  }
13
21
  if (browserAttachments) {
14
22
  result.runOptions.browserAttachments = browserAttachments;
15
23
  }
16
- if (typeof browserBundleFiles === 'boolean') {
24
+ if (typeof browserBundleFiles === "boolean") {
17
25
  result.runOptions.browserBundleFiles = browserBundleFiles;
18
26
  }
27
+ if (Array.isArray(browserFollowUps)) {
28
+ result.runOptions.browserFollowUps = browserFollowUps
29
+ .map((entry) => entry.trim())
30
+ .filter(Boolean);
31
+ }
19
32
  return result;
20
33
  }
21
34
  export function ensureBrowserAvailable(engine, options) {
22
- if (engine !== 'browser') {
35
+ if (engine !== "browser") {
23
36
  return null;
24
37
  }
25
38
  const remoteHost = options?.remoteHost?.trim() || process.env.ORACLE_REMOTE_HOST?.trim();
@@ -31,7 +44,7 @@ export function ensureBrowserAvailable(engine, options) {
31
44
  }
32
45
  const found = Launcher.getFirstInstallation();
33
46
  if (!found) {
34
- return 'Browser engine unavailable: no Chrome installation found and CHROME_PATH is unset.';
47
+ return "Browser engine unavailable: no Chrome installation found and CHROME_PATH is unset.";
35
48
  }
36
49
  return null;
37
50
  }
@@ -1,8 +1,8 @@
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';
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
6
  const BACKGROUND_POLL_INTERVAL_MS = 5000;
7
7
  const BACKGROUND_RETRY_BASE_MS = 3000;
8
8
  const BACKGROUND_RETRY_MAX_MS = 15000;
@@ -18,10 +18,10 @@ export async function executeBackgroundResponse(params) {
18
18
  throw transportError;
19
19
  }
20
20
  if (!initialResponse || !initialResponse.id) {
21
- throw new OracleResponseError('API did not return a response ID for the background run.', initialResponse);
21
+ throw new OracleResponseError("API did not return a response ID for the background run.", initialResponse);
22
22
  }
23
23
  const responseId = initialResponse.id;
24
- log(chalk.dim(`API scheduled background response ${responseId} (status=${initialResponse.status ?? 'unknown'}). Monitoring up to ${Math.round(maxWaitMs / 60000)} minutes for completion...`));
24
+ log(chalk.dim(`API scheduled background response ${responseId} (status=${initialResponse.status ?? "unknown"}). Monitoring up to ${Math.round(maxWaitMs / 60000)} minutes for completion...`));
25
25
  let heartbeatActive = false;
26
26
  let stopHeartbeat = null;
27
27
  const stopHeartbeatNow = () => {
@@ -66,29 +66,29 @@ async function pollBackgroundResponse(params) {
66
66
  let lastStatus = response.status;
67
67
  // biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
68
68
  while (true) {
69
- const status = response.status ?? 'completed';
69
+ const status = response.status ?? "completed";
70
70
  // firstCycle toggles immediately; keep for clarity in logs.
71
71
  if (firstCycle) {
72
72
  firstCycle = false;
73
73
  log(chalk.dim(`API background response status=${status}. We'll keep retrying automatically.`));
74
74
  }
75
- else if (status !== lastStatus && status !== 'completed') {
75
+ else if (status !== lastStatus && status !== "completed") {
76
76
  log(chalk.dim(`API background response status=${status}.`));
77
77
  }
78
78
  lastStatus = status;
79
- if (status === 'completed') {
79
+ if (status === "completed") {
80
80
  return response;
81
81
  }
82
- if (status !== 'in_progress' && status !== 'queued') {
82
+ if (status !== "in_progress" && status !== "queued") {
83
83
  const detail = response.error?.message || response.incomplete_details?.reason || status;
84
84
  throw new OracleResponseError(`Response did not complete: ${detail}`, response);
85
85
  }
86
86
  if (now() - startMark >= maxWaitMs) {
87
- throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
87
+ throw new OracleTransportError("client-timeout", "Timed out waiting for API background response to finish.");
88
88
  }
89
89
  await wait(BACKGROUND_POLL_INTERVAL_MS);
90
90
  if (now() - startMark >= maxWaitMs) {
91
- throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
91
+ throw new OracleTransportError("client-timeout", "Timed out waiting for API background response to finish.");
92
92
  }
93
93
  const { response: nextResponse, reconnected } = await retrieveBackgroundResponseWithRetry({
94
94
  client,
@@ -100,7 +100,7 @@ async function pollBackgroundResponse(params) {
100
100
  log,
101
101
  });
102
102
  if (reconnected) {
103
- const nextStatus = nextResponse.status ?? 'in_progress';
103
+ const nextStatus = nextResponse.status ?? "in_progress";
104
104
  log(chalk.dim(`Reconnected to API background response (status=${nextStatus}). API is still working...`));
105
105
  }
106
106
  response = nextResponse;
@@ -125,7 +125,7 @@ async function retrieveBackgroundResponseWithRetry(params) {
125
125
  log(chalk.yellow(`${describeTransportError(transportError, maxWaitMs)} Retrying in ${formatElapsed(delay)}...`));
126
126
  await wait(delay);
127
127
  if (now() - startMark >= maxWaitMs) {
128
- throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
128
+ throw new OracleTransportError("client-timeout", "Timed out waiting for API background response to finish.");
129
129
  }
130
130
  }
131
131
  }
@@ -1,11 +1,11 @@
1
- const DEFAULT_CLAUDE_ENDPOINT = 'https://api.anthropic.com/v1/messages';
2
- const ANTHROPIC_VERSION = '2023-06-01';
1
+ const DEFAULT_CLAUDE_ENDPOINT = "https://api.anthropic.com/v1/messages";
2
+ const ANTHROPIC_VERSION = "2023-06-01";
3
3
  function extractPrompt(body) {
4
4
  const first = body.input?.[0]?.content?.[0];
5
- if (first && first.type === 'input_text') {
6
- return first.text ?? '';
5
+ if (first && first.type === "input_text") {
6
+ return first.text ?? "";
7
7
  }
8
- return '';
8
+ return "";
9
9
  }
10
10
  async function callClaude({ apiKey, model, prompt, endpoint, stream = false, }) {
11
11
  const url = endpoint?.trim() || DEFAULT_CLAUDE_ENDPOINT;
@@ -14,34 +14,45 @@ async function callClaude({ apiKey, model, prompt, endpoint, stream = false, })
14
14
  max_tokens: 2048,
15
15
  messages: [
16
16
  {
17
- role: 'user',
17
+ role: "user",
18
18
  content: prompt,
19
19
  },
20
20
  ],
21
21
  stream,
22
22
  };
23
23
  return fetch(url, {
24
- method: 'POST',
24
+ method: "POST",
25
25
  headers: {
26
- 'content-type': 'application/json',
27
- 'x-api-key': apiKey,
28
- 'anthropic-version': ANTHROPIC_VERSION,
26
+ "content-type": "application/json",
27
+ "x-api-key": apiKey,
28
+ "anthropic-version": ANTHROPIC_VERSION,
29
29
  },
30
30
  body: JSON.stringify(payload),
31
31
  });
32
32
  }
33
33
  async function parseClaudeResponse(raw) {
34
- const json = (await raw.json());
34
+ const body = await raw.text();
35
+ if (!body.trim()) {
36
+ throw new Error(`Claude request failed (${raw.status} ${raw.statusText || "unknown status"}): empty response`);
37
+ }
38
+ let json;
39
+ try {
40
+ json = JSON.parse(body);
41
+ }
42
+ catch (error) {
43
+ const snippet = body.slice(0, 160).replace(/\s+/g, " ").trim();
44
+ throw new Error(`Claude request failed (${raw.status} ${raw.statusText || "unknown status"}): invalid JSON response${snippet ? `: ${snippet}` : ""}`, { cause: error });
45
+ }
35
46
  if (json.error) {
36
- throw new Error(json.error.message || 'Claude request failed');
47
+ throw new Error(json.error.message || "Claude request failed");
37
48
  }
38
- const textParts = json.content?.map((part) => part.text ?? '').filter(Boolean) ?? [];
39
- const outputText = textParts.join('');
49
+ const textParts = json.content?.map((part) => part.text ?? "").filter(Boolean) ?? [];
50
+ const outputText = textParts.join("");
40
51
  return {
41
52
  id: json.id ?? `claude-${Date.now()}`,
42
- status: 'completed',
53
+ status: "completed",
43
54
  output_text: [outputText],
44
- output: [{ type: 'text', text: outputText }],
55
+ output: [{ type: "text", text: outputText }],
45
56
  usage: {
46
57
  input_tokens: json.usage?.input_tokens ?? 0,
47
58
  output_tokens: json.usage?.output_tokens ?? 0,
@@ -53,11 +64,17 @@ export function createClaudeClient(apiKey, modelName, resolvedModelId, baseUrl)
53
64
  const modelId = resolveClaudeModelId(resolvedModelId ?? modelName);
54
65
  const stream = async (body) => {
55
66
  const prompt = extractPrompt(body);
56
- const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
67
+ const resp = await callClaude({
68
+ apiKey,
69
+ model: modelId,
70
+ prompt,
71
+ stream: false,
72
+ endpoint: baseUrl,
73
+ });
57
74
  const parsed = await parseClaudeResponse(resp);
58
75
  const iterator = async function* () {
59
76
  if (parsed.output_text?.[0]) {
60
- yield { type: 'response.output_text.delta', delta: parsed.output_text[0] };
77
+ yield { type: "response.output_text.delta", delta: parsed.output_text[0] };
61
78
  }
62
79
  return;
63
80
  };
@@ -68,13 +85,19 @@ export function createClaudeClient(apiKey, modelName, resolvedModelId, baseUrl)
68
85
  };
69
86
  const create = async (body) => {
70
87
  const prompt = extractPrompt(body);
71
- const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
88
+ const resp = await callClaude({
89
+ apiKey,
90
+ model: modelId,
91
+ prompt,
92
+ stream: false,
93
+ endpoint: baseUrl,
94
+ });
72
95
  return parseClaudeResponse(resp);
73
96
  };
74
97
  const retrieve = async (id) => ({
75
98
  id,
76
- status: 'error',
77
- error: { message: 'Retrieve by ID not supported for Claude API yet.' },
99
+ status: "error",
100
+ error: { message: "Retrieve by ID not supported for Claude API yet." },
78
101
  });
79
102
  return {
80
103
  responses: {
@@ -85,11 +108,16 @@ export function createClaudeClient(apiKey, modelName, resolvedModelId, baseUrl)
85
108
  };
86
109
  }
87
110
  export function resolveClaudeModelId(modelName) {
88
- if (modelName === 'claude-4.5-sonnet' || modelName === 'claude-sonnet-4-5-20241022') {
89
- return 'claude-sonnet-4-5';
111
+ if (modelName === "claude-4.6-sonnet" || modelName === "claude-sonnet-4-6") {
112
+ return "claude-sonnet-4-6";
113
+ }
114
+ if (modelName === "claude-4.5-sonnet" ||
115
+ modelName === "claude-sonnet-4-5" ||
116
+ modelName === "claude-sonnet-4-5-20250929") {
117
+ return "claude-sonnet-4-5";
90
118
  }
91
- if (modelName === 'claude-4.1-opus' || modelName === 'claude-opus-4-1-20240808') {
92
- return 'claude-opus-4-1';
119
+ if (modelName === "claude-4.1-opus" || modelName === "claude-opus-4-1-20240808") {
120
+ return "claude-opus-4-1";
93
121
  }
94
122
  return modelName;
95
123
  }
@@ -1,19 +1,19 @@
1
- import OpenAI from 'openai';
2
- import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
- import { createGeminiClient } from './gemini.js';
5
- import { createClaudeClient } from './claude.js';
6
- import { isOpenRouterBaseUrl } from './modelResolver.js';
1
+ import OpenAI from "openai";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { createGeminiClient } from "./gemini.js";
5
+ import { createClaudeClient } from "./claude.js";
6
+ import { isOpenRouterBaseUrl } from "./modelResolver.js";
7
7
  /**
8
8
  * Known native API base URLs that should still use their dedicated SDKs.
9
9
  * Any other custom base URL is treated as an OpenAI-compatible proxy and
10
10
  * all models are routed through the chat/completions adapter.
11
11
  */
12
12
  const NATIVE_API_HOSTS = [
13
- 'api.openai.com',
14
- 'api.anthropic.com',
15
- 'generativelanguage.googleapis.com',
16
- 'api.x.ai',
13
+ "api.openai.com",
14
+ "api.anthropic.com",
15
+ "generativelanguage.googleapis.com",
16
+ "api.x.ai",
17
17
  ];
18
18
  export function isCustomBaseUrl(baseUrl) {
19
19
  if (!baseUrl)
@@ -27,7 +27,7 @@ export function isCustomBaseUrl(baseUrl) {
27
27
  }
28
28
  }
29
29
  export function buildAzureResponsesBaseUrl(endpoint) {
30
- return `${endpoint.replace(/\/+$/, '')}/openai/v1`;
30
+ return `${endpoint.replace(/\/+$/, "")}/openai/v1`;
31
31
  }
32
32
  export function createDefaultClientFactory() {
33
33
  const customFactory = loadCustomClientFactory();
@@ -40,17 +40,21 @@ export function createDefaultClientFactory() {
40
40
  // route ALL models through the OpenAI chat/completions adapter instead of native SDKs
41
41
  // which would reject the proxy's API key.
42
42
  if (!openRouter && !customProxy) {
43
- if (options?.model?.startsWith('gemini')) {
43
+ if (options?.model?.startsWith("gemini")) {
44
44
  // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
45
45
  return createGeminiClient(key, options.model, options.resolvedModelId);
46
46
  }
47
- if (options?.model?.startsWith('claude')) {
47
+ if (options?.model?.startsWith("claude")) {
48
48
  return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
49
49
  }
50
50
  }
51
51
  let instance;
52
- const defaultHeaders = openRouter ? buildOpenRouterHeaders() : undefined;
53
- const httpTimeoutMs = typeof options?.httpTimeoutMs === 'number' && Number.isFinite(options.httpTimeoutMs) && options.httpTimeoutMs > 0
52
+ const defaultHeaders = openRouter
53
+ ? buildOpenRouterHeaders()
54
+ : undefined;
55
+ const httpTimeoutMs = typeof options?.httpTimeoutMs === "number" &&
56
+ Number.isFinite(options.httpTimeoutMs) &&
57
+ options.httpTimeoutMs > 0
54
58
  ? options.httpTimeoutMs
55
59
  : 20 * 60 * 1000;
56
60
  if (options?.azure?.endpoint) {
@@ -82,13 +86,15 @@ export function createDefaultClientFactory() {
82
86
  }
83
87
  function buildOpenRouterHeaders() {
84
88
  const headers = {};
85
- const referer = process.env.OPENROUTER_REFERER ?? process.env.OPENROUTER_HTTP_REFERER ?? 'https://github.com/steipete/oracle';
86
- const title = process.env.OPENROUTER_TITLE ?? 'Oracle CLI';
89
+ const referer = process.env.OPENROUTER_REFERER ??
90
+ process.env.OPENROUTER_HTTP_REFERER ??
91
+ "https://github.com/steipete/oracle";
92
+ const title = process.env.OPENROUTER_TITLE ?? "Oracle CLI";
87
93
  if (referer) {
88
- headers['HTTP-Referer'] = referer;
94
+ headers["HTTP-Referer"] = referer;
89
95
  }
90
96
  if (title) {
91
- headers['X-Title'] = title;
97
+ headers["X-Title"] = title;
92
98
  }
93
99
  return headers;
94
100
  }
@@ -97,19 +103,19 @@ function loadCustomClientFactory() {
97
103
  if (!override) {
98
104
  return null;
99
105
  }
100
- if (override === 'INLINE_TEST_FACTORY') {
106
+ if (override === "INLINE_TEST_FACTORY") {
101
107
  return () => ({
102
108
  responses: {
103
- create: async () => ({ id: 'inline-test', status: 'completed' }),
109
+ create: async () => ({ id: "inline-test", status: "completed" }),
104
110
  stream: async () => ({
105
111
  [Symbol.asyncIterator]: () => ({
106
112
  async next() {
107
113
  return { done: true, value: undefined };
108
114
  },
109
115
  }),
110
- finalResponse: async () => ({ id: 'inline-test', status: 'completed' }),
116
+ finalResponse: async () => ({ id: "inline-test", status: "completed" }),
111
117
  }),
112
- retrieve: async (id) => ({ id, status: 'completed' }),
118
+ retrieve: async (id) => ({ id, status: "completed" }),
113
119
  },
114
120
  });
115
121
  }
@@ -117,14 +123,14 @@ function loadCustomClientFactory() {
117
123
  const require = createRequire(import.meta.url);
118
124
  const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
119
125
  const moduleExports = require(resolved);
120
- const factory = typeof moduleExports === 'function'
126
+ const factory = typeof moduleExports === "function"
121
127
  ? moduleExports
122
- : typeof moduleExports?.default === 'function'
128
+ : typeof moduleExports?.default === "function"
123
129
  ? moduleExports.default
124
- : typeof moduleExports?.createClientFactory === 'function'
130
+ : typeof moduleExports?.createClientFactory === "function"
125
131
  ? moduleExports.createClientFactory
126
132
  : null;
127
- if (typeof factory === 'function') {
133
+ if (typeof factory === "function") {
128
134
  return factory;
129
135
  }
130
136
  console.warn(`Custom client factory at ${resolved} did not export a function.`);
@@ -140,14 +146,17 @@ function buildOpenRouterCompletionClient(instance) {
140
146
  const adaptRequest = (body) => {
141
147
  const messages = [];
142
148
  if (body.instructions) {
143
- messages.push({ role: 'system', content: body.instructions });
149
+ messages.push({ role: "system", content: body.instructions });
144
150
  }
145
151
  for (const entry of body.input) {
146
152
  const textParts = entry.content
147
- .map((c) => (c.type === 'input_text' ? c.text : ''))
153
+ .map((c) => (c.type === "input_text" ? c.text : ""))
148
154
  .filter((t) => t)
149
- .join('\n\n');
150
- messages.push({ role: entry.role ?? 'user', content: textParts });
155
+ .join("\n\n");
156
+ messages.push({
157
+ role: entry.role ?? "user",
158
+ content: textParts,
159
+ });
151
160
  }
152
161
  const base = {
153
162
  model: body.model,
@@ -159,7 +168,7 @@ function buildOpenRouterCompletionClient(instance) {
159
168
  return { streaming, nonStreaming };
160
169
  };
161
170
  const adaptResponse = (response) => {
162
- const text = response.choices?.[0]?.message?.content ?? '';
171
+ const text = response.choices?.[0]?.message?.content ?? "";
163
172
  const usage = {
164
173
  input_tokens: response.usage?.prompt_tokens ?? 0,
165
174
  output_tokens: response.usage?.completion_tokens ?? 0,
@@ -167,9 +176,9 @@ function buildOpenRouterCompletionClient(instance) {
167
176
  };
168
177
  return {
169
178
  id: response.id ?? `openrouter-${Date.now()}`,
170
- status: 'completed',
179
+ status: "completed",
171
180
  output_text: [text],
172
- output: [{ type: 'text', text }],
181
+ output: [{ type: "text", text }],
173
182
  usage,
174
183
  };
175
184
  };
@@ -177,15 +186,15 @@ function buildOpenRouterCompletionClient(instance) {
177
186
  const { streaming } = adaptRequest(body);
178
187
  let finalUsage;
179
188
  let finalId;
180
- let aggregated = '';
189
+ let aggregated = "";
181
190
  async function* iterator() {
182
191
  const completion = await instance.chat.completions.create(streaming);
183
192
  for await (const chunk of completion) {
184
193
  finalId = chunk.id ?? finalId;
185
- const delta = chunk.choices?.[0]?.delta?.content ?? '';
194
+ const delta = chunk.choices?.[0]?.delta?.content ?? "";
186
195
  if (delta) {
187
196
  aggregated += delta;
188
- yield { type: 'chunk', delta };
197
+ yield { type: "chunk", delta };
189
198
  }
190
199
  if (chunk.usage) {
191
200
  finalUsage = chunk.usage;
@@ -200,11 +209,11 @@ function buildOpenRouterCompletionClient(instance) {
200
209
  async finalResponse() {
201
210
  return adaptResponse({
202
211
  id: finalId ?? `openrouter-${Date.now()}`,
203
- choices: [{ message: { role: 'assistant', content: aggregated } }],
212
+ choices: [{ message: { role: "assistant", content: aggregated } }],
204
213
  usage: finalUsage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
205
214
  created: Math.floor(Date.now() / 1000),
206
- model: '',
207
- object: 'chat.completion',
215
+ model: "",
216
+ object: "chat.completion",
208
217
  });
209
218
  },
210
219
  };
@@ -219,7 +228,7 @@ function buildOpenRouterCompletionClient(instance) {
219
228
  stream,
220
229
  create,
221
230
  retrieve: async () => {
222
- throw new Error('retrieve is not supported for OpenRouter chat/completions fallback.');
231
+ throw new Error("retrieve is not supported for OpenRouter chat/completions fallback.");
223
232
  },
224
233
  },
225
234
  };