@steipete/oracle 0.8.6 → 0.10.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 (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  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/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,12 +1,12 @@
1
- import { APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from 'openai';
2
- import { APIError } from 'openai/error';
3
- import { formatElapsed } from './format.js';
1
+ import { APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from "openai";
2
+ import { APIError } from "openai/error";
3
+ import { formatElapsed } from "./format.js";
4
4
  export class OracleUserError extends Error {
5
5
  category;
6
6
  details;
7
7
  constructor(category, message, details, cause) {
8
8
  super(message);
9
- this.name = 'OracleUserError';
9
+ this.name = "OracleUserError";
10
10
  this.category = category;
11
11
  this.details = details;
12
12
  if (cause) {
@@ -16,20 +16,20 @@ export class OracleUserError extends Error {
16
16
  }
17
17
  export class FileValidationError extends OracleUserError {
18
18
  constructor(message, details, cause) {
19
- super('file-validation', message, details, cause);
20
- this.name = 'FileValidationError';
19
+ super("file-validation", message, details, cause);
20
+ this.name = "FileValidationError";
21
21
  }
22
22
  }
23
23
  export class BrowserAutomationError extends OracleUserError {
24
24
  constructor(message, details, cause) {
25
- super('browser-automation', message, details, cause);
26
- this.name = 'BrowserAutomationError';
25
+ super("browser-automation", message, details, cause);
26
+ this.name = "BrowserAutomationError";
27
27
  }
28
28
  }
29
29
  export class PromptValidationError extends OracleUserError {
30
30
  constructor(message, details, cause) {
31
- super('prompt-validation', message, details, cause);
32
- this.name = 'PromptValidationError';
31
+ super("prompt-validation", message, details, cause);
32
+ this.name = "PromptValidationError";
33
33
  }
34
34
  }
35
35
  export function asOracleUserError(error) {
@@ -42,7 +42,7 @@ export class OracleTransportError extends Error {
42
42
  reason;
43
43
  constructor(reason, message, cause) {
44
44
  super(message);
45
- this.name = 'OracleTransportError';
45
+ this.name = "OracleTransportError";
46
46
  this.reason = reason;
47
47
  if (cause) {
48
48
  this.cause = cause;
@@ -54,7 +54,7 @@ export class OracleResponseError extends Error {
54
54
  response;
55
55
  constructor(message, response) {
56
56
  super(message);
57
- this.name = 'OracleResponseError';
57
+ this.name = "OracleResponseError";
58
58
  this.response = response;
59
59
  this.metadata = extractResponseMetadata(response);
60
60
  }
@@ -79,54 +79,54 @@ export function toTransportError(error, model) {
79
79
  return error;
80
80
  }
81
81
  if (error instanceof APIConnectionTimeoutError) {
82
- return new OracleTransportError('client-timeout', 'OpenAI request timed out before completion.', error);
82
+ return new OracleTransportError("client-timeout", "OpenAI request timed out before completion.", error);
83
83
  }
84
84
  if (error instanceof APIUserAbortError) {
85
- return new OracleTransportError('client-abort', 'The request was aborted before OpenAI finished responding.', error);
85
+ return new OracleTransportError("client-abort", "The request was aborted before OpenAI finished responding.", error);
86
86
  }
87
87
  if (error instanceof APIConnectionError) {
88
- return new OracleTransportError('connection-lost', 'Connection to OpenAI dropped before the response completed.', error);
88
+ return new OracleTransportError("connection-lost", "Connection to OpenAI dropped before the response completed.", error);
89
89
  }
90
- const isApiError = error instanceof APIError || error?.name === 'APIError';
90
+ const isApiError = error instanceof APIError || error?.name === "APIError";
91
91
  if (isApiError) {
92
92
  const apiError = error;
93
93
  const code = apiError.code ?? apiError.error?.code;
94
- const messageText = apiError.message?.toLowerCase?.() ?? '';
94
+ const messageText = apiError.message?.toLowerCase?.() ?? "";
95
95
  const apiMessage = apiError.error?.message ||
96
96
  apiError.message ||
97
- (apiError.status ? `${apiError.status} OpenAI API error` : 'OpenAI API error');
97
+ (apiError.status ? `${apiError.status} OpenAI API error` : "OpenAI API error");
98
98
  // Friendly guidance when a pro-tier model isn't available on this base URL / API key.
99
- if (model === 'gpt-5.2-pro' &&
100
- (code === 'model_not_found' ||
101
- messageText.includes('does not exist') ||
102
- messageText.includes('unknown model') ||
103
- messageText.includes('model_not_found'))) {
104
- return new OracleTransportError('model-unavailable', 'gpt-5.2-pro is not available on this API base/key. Try gpt-5-pro or gpt-5.2, or switch to the browser engine.', apiError);
99
+ if ((model === "gpt-5.5-pro" || model === "gpt-5.4-pro") &&
100
+ (code === "model_not_found" ||
101
+ messageText.includes("does not exist") ||
102
+ messageText.includes("unknown model") ||
103
+ messageText.includes("model_not_found"))) {
104
+ return new OracleTransportError("model-unavailable", `${model} is not available on this API base/key. Try gpt-5.5, gpt-5-pro, or switch to the browser engine.`, apiError);
105
105
  }
106
106
  if (apiError.status === 404 || apiError.status === 405) {
107
- return new OracleTransportError('unsupported-endpoint', 'HTTP 404/405 from the Responses API; this base URL or gateway likely does not expose /v1/responses. Set OPENAI_BASE_URL to api.openai.com/v1, update your Azure API version/deployment, or use the browser engine.', apiError);
107
+ return new OracleTransportError("unsupported-endpoint", "HTTP 404/405 from the Responses API; this base URL or gateway likely does not expose /v1/responses. Set OPENAI_BASE_URL to api.openai.com/v1, update your Azure API version/deployment, or use the browser engine.", apiError);
108
108
  }
109
- return new OracleTransportError('api-error', apiMessage, apiError);
109
+ return new OracleTransportError("api-error", apiMessage, apiError);
110
110
  }
111
- return new OracleTransportError('unknown', error instanceof Error ? error.message : 'Unknown transport failure.', error);
111
+ return new OracleTransportError("unknown", error instanceof Error ? error.message : "Unknown transport failure.", error);
112
112
  }
113
113
  export function describeTransportError(error, deadlineMs) {
114
114
  switch (error.reason) {
115
- case 'client-timeout':
115
+ case "client-timeout":
116
116
  return deadlineMs
117
117
  ? `Client-side timeout: OpenAI streaming call exceeded the ${formatElapsed(deadlineMs)} deadline.`
118
- : 'Client-side timeout: OpenAI streaming call exceeded the configured deadline.';
119
- case 'connection-lost':
120
- return 'Connection to OpenAI ended unexpectedly before the response completed.';
121
- case 'client-abort':
122
- return 'Request was aborted before OpenAI completed the response.';
123
- case 'api-error':
118
+ : "Client-side timeout: OpenAI streaming call exceeded the configured deadline.";
119
+ case "connection-lost":
120
+ return "Connection to OpenAI ended unexpectedly before the response completed.";
121
+ case "client-abort":
122
+ return "Request was aborted before OpenAI completed the response.";
123
+ case "api-error":
124
124
  return error.message;
125
- case 'model-unavailable':
125
+ case "model-unavailable":
126
126
  return error.message;
127
- case 'unsupported-endpoint':
128
- return 'The Responses API returned 404/405 — your base URL/gateway probably lacks /v1/responses (check OPENAI_BASE_URL or switch to api.openai.com / browser engine).';
127
+ case "unsupported-endpoint":
128
+ return "The Responses API returned 404/405 — your base URL/gateway probably lacks /v1/responses (check OPENAI_BASE_URL or switch to api.openai.com / browser engine).";
129
129
  default:
130
- return 'OpenAI streaming call ended with an unknown transport error.';
130
+ return "OpenAI streaming call ended with an unknown transport error.";
131
131
  }
132
132
  }
@@ -1,11 +1,20 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import fg from 'fast-glob';
4
- import { FileValidationError } from './errors.js';
5
- const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import { FileValidationError } from "./errors.js";
5
+ export const DEFAULT_MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
6
6
  const DEFAULT_FS = fs;
7
- const DEFAULT_IGNORED_DIRS = ['node_modules', 'dist', 'coverage', '.git', '.turbo', '.next', 'build', 'tmp'];
8
- export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = MAX_FILE_SIZE_BYTES, readContents = true, } = {}) {
7
+ const DEFAULT_IGNORED_DIRS = new Set([
8
+ "node_modules",
9
+ "dist",
10
+ "coverage",
11
+ ".git",
12
+ ".turbo",
13
+ ".next",
14
+ "build",
15
+ "tmp",
16
+ ]);
17
+ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES, readContents = true, } = {}) {
9
18
  if (!filePaths || filePaths.length === 0) {
10
19
  return [];
11
20
  }
@@ -24,13 +33,13 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
24
33
  }
25
34
  else {
26
35
  if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
27
- throw new Error('Glob patterns and exclusions are only supported for on-disk files.');
36
+ throw new Error("Glob patterns and exclusions are only supported for on-disk files.");
28
37
  }
29
38
  candidatePaths = await expandWithCustomFs(partitioned, fsModule);
30
39
  }
31
40
  const allowedLiteralDirs = partitioned.literalDirectories
32
41
  .map((dir) => path.resolve(dir))
33
- .filter((dir) => DEFAULT_IGNORED_DIRS.includes(path.basename(dir)));
42
+ .filter((dir) => DEFAULT_IGNORED_DIRS.has(path.basename(dir)));
34
43
  const allowedLiteralFiles = partitioned.literalFiles.map((file) => path.resolve(file));
35
44
  const resolvedLiteralDirs = new Set(allowedLiteralDirs);
36
45
  const allowedPaths = new Set([...allowedLiteralDirs, ...allowedLiteralFiles]);
@@ -50,7 +59,7 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
50
59
  return false;
51
60
  });
52
61
  if (filteredCandidates.length === 0) {
53
- throw new FileValidationError('No files matched the provided --file patterns.', {
62
+ throw new FileValidationError("No files matched the provided --file patterns.", {
54
63
  patterns: partitioned.globPatterns,
55
64
  excludes: partitioned.excludePatterns,
56
65
  });
@@ -68,7 +77,7 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
68
77
  if (!stats.isFile()) {
69
78
  continue;
70
79
  }
71
- if (maxFileSizeBytes && typeof stats.size === 'number' && stats.size > maxFileSizeBytes) {
80
+ if (maxFileSizeBytes && typeof stats.size === "number" && stats.size > maxFileSizeBytes) {
72
81
  const relative = path.relative(cwd, filePath) || filePath;
73
82
  oversized.push(`${relative} (${formatBytes(stats.size)})`);
74
83
  continue;
@@ -76,14 +85,14 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
76
85
  accepted.push(filePath);
77
86
  }
78
87
  if (oversized.length > 0) {
79
- throw new FileValidationError(`The following files exceed the 1 MB limit:\n- ${oversized.join('\n- ')}`, {
88
+ throw new FileValidationError(`The following files exceed the ${formatBytes(maxFileSizeBytes)} limit:\n- ${oversized.join("\n- ")}`, {
80
89
  files: oversized,
81
90
  limitBytes: maxFileSizeBytes,
82
91
  });
83
92
  }
84
93
  const files = [];
85
94
  for (const filePath of accepted) {
86
- const content = readContents ? await fsModule.readFile(filePath, 'utf8') : '';
95
+ const content = readContents ? await fsModule.readFile(filePath, "utf8") : "";
87
96
  files.push({ path: filePath, content });
88
97
  }
89
98
  return files;
@@ -100,7 +109,7 @@ async function partitionFileInputs(rawPaths, cwd, fsModule) {
100
109
  if (!raw) {
101
110
  continue;
102
111
  }
103
- if (raw.startsWith('!')) {
112
+ if (raw.startsWith("!")) {
104
113
  const normalized = normalizeGlob(raw.slice(1), cwd);
105
114
  if (normalized) {
106
115
  result.excludePatterns.push(normalized);
@@ -153,11 +162,13 @@ async function expandWithNativeGlob(partitioned, cwd) {
153
162
  }));
154
163
  const resolved = matches.map((match) => path.resolve(cwd, match));
155
164
  const filtered = resolved.filter((filePath) => !isGitignored(filePath, gitignoreSets));
156
- const finalFiles = dotfileOptIn ? filtered : filtered.filter((filePath) => !path.basename(filePath).startsWith('.'));
165
+ const finalFiles = dotfileOptIn
166
+ ? filtered
167
+ : filtered.filter((filePath) => !path.basename(filePath).startsWith("."));
157
168
  return Array.from(new Set(finalFiles));
158
169
  }
159
170
  async function loadGitignoreSets(cwd) {
160
- const gitignorePaths = await fg('**/.gitignore', {
171
+ const gitignorePaths = await fg("**/.gitignore", {
161
172
  cwd,
162
173
  dot: true,
163
174
  absolute: true,
@@ -168,11 +179,11 @@ async function loadGitignoreSets(cwd) {
168
179
  const sets = [];
169
180
  for (const filePath of gitignorePaths) {
170
181
  try {
171
- const raw = await fs.readFile(filePath, 'utf8');
182
+ const raw = await fs.readFile(filePath, "utf8");
172
183
  const patterns = raw
173
- .split('\n')
184
+ .split("\n")
174
185
  .map((line) => line.trim())
175
- .filter((line) => line.length > 0 && !line.startsWith('#'));
186
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
176
187
  if (patterns.length > 0) {
177
188
  sets.push({ dir: path.dirname(filePath), patterns });
178
189
  }
@@ -204,7 +215,7 @@ async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
204
215
  const parts = rel.split(path.sep).filter(Boolean);
205
216
  for (let i = 0; i < parts.length - 1; i += 1) {
206
217
  const part = parts[i];
207
- if (!DEFAULT_IGNORED_DIRS.includes(part)) {
218
+ if (!DEFAULT_IGNORED_DIRS.has(part)) {
208
219
  continue;
209
220
  }
210
221
  const dirPath = path.resolve(cwd, ...parts.slice(0, i + 1));
@@ -212,7 +223,7 @@ async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
212
223
  continue;
213
224
  }
214
225
  try {
215
- const stats = await fsModule.stat(path.join(dirPath, '.gitignore'));
226
+ const stats = await fsModule.stat(path.join(dirPath, ".gitignore"));
216
227
  if (stats.isFile()) {
217
228
  whitelist.add(dirPath);
218
229
  }
@@ -233,7 +244,7 @@ function findIgnoredAncestor(filePath, cwd, _literalDirs, allowedPaths, ignoredW
233
244
  const parts = rel.split(path.sep);
234
245
  for (let idx = 0; idx < parts.length; idx += 1) {
235
246
  const part = parts[idx];
236
- if (!DEFAULT_IGNORED_DIRS.includes(part)) {
247
+ if (!DEFAULT_IGNORED_DIRS.has(part)) {
237
248
  continue;
238
249
  }
239
250
  const ignoredDir = path.resolve(cwd, parts.slice(0, idx + 1).join(path.sep));
@@ -251,9 +262,9 @@ function matchesPattern(relativePath, pattern) {
251
262
  if (!pattern) {
252
263
  return false;
253
264
  }
254
- const normalized = pattern.replace(/\\+/g, '/');
265
+ const normalized = pattern.replace(/\\+/g, "/");
255
266
  // Directory rule
256
- if (normalized.endsWith('/')) {
267
+ if (normalized.endsWith("/")) {
257
268
  const dir = normalized.slice(0, -1);
258
269
  return relativePath === dir || relativePath.startsWith(`${dir}/`);
259
270
  }
@@ -262,16 +273,14 @@ function matchesPattern(relativePath, pattern) {
262
273
  return regex.test(relativePath);
263
274
  }
264
275
  function globToRegex(pattern) {
265
- const withMarkers = pattern.replace(/\*\*/g, '§§DOUBLESTAR§§').replace(/\*/g, '§§SINGLESTAR§§');
266
- const escaped = withMarkers.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
267
- const restored = escaped
268
- .replace(/§§DOUBLESTAR§§/g, '.*')
269
- .replace(/§§SINGLESTAR§§/g, '[^/]*');
276
+ const withMarkers = pattern.replace(/\*\*/g, "§§DOUBLESTAR§§").replace(/\*/g, "§§SINGLESTAR§§");
277
+ const escaped = withMarkers.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
278
+ const restored = escaped.replace(/§§DOUBLESTAR§§/g, ".*").replace(/§§SINGLESTAR§§/g, "[^/]*");
270
279
  return new RegExp(`^${restored}$`);
271
280
  }
272
281
  function includesDotfileSegment(pattern) {
273
- const segments = pattern.split('/');
274
- return segments.some((segment) => segment.startsWith('.') && segment.length > 1);
282
+ const segments = pattern.split("/");
283
+ return segments.some((segment) => segment.startsWith(".") && segment.length > 1);
275
284
  }
276
285
  async function expandWithCustomFs(partitioned, fsModule) {
277
286
  const paths = new Set();
@@ -302,38 +311,38 @@ async function expandDirectoryRecursive(directory, fsModule) {
302
311
  return results;
303
312
  }
304
313
  function makeDirectoryPattern(relative) {
305
- if (relative === '.' || relative === '') {
306
- return '**/*';
314
+ if (relative === "." || relative === "") {
315
+ return "**/*";
307
316
  }
308
317
  return `${stripTrailingSlashes(relative)}/**/*`;
309
318
  }
310
319
  function isNativeFsModule(fsModule) {
311
- return ((fsModule.__nativeFs === true ||
320
+ return (fsModule.__nativeFs === true ||
312
321
  (fsModule.readFile === DEFAULT_FS.readFile &&
313
322
  fsModule.stat === DEFAULT_FS.stat &&
314
- fsModule.readdir === DEFAULT_FS.readdir)));
323
+ fsModule.readdir === DEFAULT_FS.readdir));
315
324
  }
316
325
  function normalizeGlob(pattern, cwd) {
317
326
  if (!pattern) {
318
- return '';
327
+ return "";
319
328
  }
320
329
  let normalized = pattern;
321
330
  if (path.isAbsolute(normalized)) {
322
331
  normalized = path.relative(cwd, normalized);
323
332
  }
324
333
  normalized = toPosix(normalized);
325
- if (normalized.startsWith('./')) {
334
+ if (normalized.startsWith("./")) {
326
335
  normalized = normalized.slice(2);
327
336
  }
328
337
  return normalized;
329
338
  }
330
339
  function toPosix(value) {
331
- return value.replace(/\\/g, '/');
340
+ return value.replace(/\\/g, "/");
332
341
  }
333
342
  function toPosixRelative(absPath, cwd) {
334
343
  const relative = path.relative(cwd, absPath);
335
344
  if (!relative) {
336
- return '.';
345
+ return ".";
337
346
  }
338
347
  return toPosix(relative);
339
348
  }
@@ -343,17 +352,32 @@ function toPosixRelativeOrBasename(absPath, cwd) {
343
352
  }
344
353
  function stripTrailingSlashes(value) {
345
354
  const normalized = toPosix(value);
346
- return normalized.replace(/\/+$/g, '');
355
+ return normalized.replace(/\/+$/g, "");
347
356
  }
348
357
  function formatBytes(size) {
349
358
  if (size >= 1024 * 1024) {
350
- return `${(size / (1024 * 1024)).toFixed(1)} MB`;
359
+ return `${formatScaled(size / (1024 * 1024))} MB`;
351
360
  }
352
361
  if (size >= 1024) {
353
- return `${(size / 1024).toFixed(1)} KB`;
362
+ return `${formatScaled(size / 1024)} KB`;
354
363
  }
355
364
  return `${size} B`;
356
365
  }
366
+ function formatScaled(value) {
367
+ return value.toFixed(1).replace(/\.0$/, "");
368
+ }
369
+ export function normalizeMaxFileSizeBytes(value, source = "max file size") {
370
+ if (value == null || value === "") {
371
+ return undefined;
372
+ }
373
+ const parsed = typeof value === "number"
374
+ ? value
375
+ : Number.parseInt(typeof value === "string" ? value.trim() : String(value), 10);
376
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
377
+ throw new Error(`${source} must be a positive integer number of bytes.`);
378
+ }
379
+ return parsed;
380
+ }
357
381
  function relativePath(targetPath, cwd) {
358
382
  const relative = path.relative(cwd, targetPath);
359
383
  return relative || targetPath;
@@ -363,10 +387,10 @@ export function createFileSections(files, cwd = process.cwd()) {
363
387
  const relative = toPosix(path.relative(cwd, file.path) || file.path);
364
388
  const sectionText = [
365
389
  `### File ${index + 1}: ${relative}`,
366
- '```',
390
+ "```",
367
391
  file.content.trimEnd(),
368
- '```',
369
- ].join('\n');
392
+ "```",
393
+ ].join("\n");
370
394
  return {
371
395
  index: index + 1,
372
396
  absolutePath: file.path,
@@ -1,7 +1,7 @@
1
- import { formatUSD } from './format.js';
1
+ import { formatUSD } from "./format.js";
2
2
  export function formatElapsedCompact(ms) {
3
3
  if (!Number.isFinite(ms) || ms < 0) {
4
- return 'unknown';
4
+ return "unknown";
5
5
  }
6
6
  if (ms < 60_000) {
7
7
  return `${(ms / 1000).toFixed(1)}s`;
@@ -9,24 +9,26 @@ export function formatElapsedCompact(ms) {
9
9
  if (ms < 60 * 60_000) {
10
10
  const minutes = Math.floor(ms / 60_000);
11
11
  const seconds = Math.floor((ms % 60_000) / 1000);
12
- return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
12
+ return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
13
13
  }
14
14
  const hours = Math.floor(ms / (60 * 60_000));
15
15
  const minutes = Math.floor((ms % (60 * 60_000)) / 60_000);
16
- return `${hours}h${minutes.toString().padStart(2, '0')}m`;
16
+ return `${hours}h${minutes.toString().padStart(2, "0")}m`;
17
17
  }
18
18
  export function formatFinishLine({ elapsedMs, model, costUsd, tokensPart, summaryExtraParts, detailParts, }) {
19
19
  const line1Parts = [
20
20
  formatElapsedCompact(elapsedMs),
21
- typeof costUsd === 'number' ? formatUSD(costUsd) : null,
21
+ typeof costUsd === "number" ? formatUSD(costUsd) : null,
22
22
  model,
23
23
  tokensPart,
24
24
  ...(summaryExtraParts ?? []),
25
25
  ];
26
- const line1 = line1Parts.filter((part) => typeof part === 'string' && part.length > 0).join(' · ');
27
- const line2Parts = (detailParts ?? []).filter((part) => typeof part === 'string' && part.length > 0);
26
+ const line1 = line1Parts
27
+ .filter((part) => typeof part === "string" && part.length > 0)
28
+ .join(" · ");
29
+ const line2Parts = (detailParts ?? []).filter((part) => typeof part === "string" && part.length > 0);
28
30
  if (line2Parts.length === 0) {
29
31
  return { line1 };
30
32
  }
31
- return { line1, line2: line2Parts.join(' | ') };
33
+ return { line1, line2: line2Parts.join(" | ") };
32
34
  }
@@ -1,15 +1,15 @@
1
1
  export function formatUSD(value) {
2
2
  if (!Number.isFinite(value)) {
3
- return 'n/a';
3
+ return "n/a";
4
4
  }
5
5
  // Display with 4 decimal places, rounding to $0.0001 minimum granularity.
6
6
  return `$${value.toFixed(4)}`;
7
7
  }
8
8
  export function formatNumber(value, { estimated = false } = {}) {
9
9
  if (value == null) {
10
- return 'n/a';
10
+ return "n/a";
11
11
  }
12
- const suffix = estimated ? ' (est.)' : '';
12
+ const suffix = estimated ? " (est.)" : "";
13
13
  return `${value.toLocaleString()}${suffix}`;
14
14
  }
15
15
  export function formatElapsed(ms) {
@@ -1,30 +1,35 @@
1
- import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
1
+ import { GoogleGenAI, HarmCategory, HarmBlockThreshold, } from "@google/genai";
2
2
  const MODEL_ID_MAP = {
3
- 'gemini-3-pro': 'gemini-3-pro-preview',
4
- 'gpt-5.1-pro': 'gpt-5.1-pro',
5
- 'gpt-5-pro': 'gpt-5-pro',
6
- 'gpt-5.1': 'gpt-5.1',
7
- 'gpt-5.1-codex': 'gpt-5.1-codex',
8
- 'gpt-5.2': 'gpt-5.2',
9
- 'gpt-5.2-instant': 'gpt-5.2-instant',
10
- 'gpt-5.2-pro': 'gpt-5.2-pro',
11
- 'claude-4.5-sonnet': 'claude-4.5-sonnet',
12
- 'claude-4.1-opus': 'claude-4.1-opus',
13
- 'grok-4.1': 'grok-4.1',
3
+ "gemini-3.1-pro": "gemini-3.1-pro-preview",
4
+ "gemini-3-pro": "gemini-3-pro-preview",
5
+ "gpt-5.5": "gpt-5.5",
6
+ "gpt-5.5-pro": "gpt-5.5-pro",
7
+ "gpt-5.4": "gpt-5.4",
8
+ "gpt-5.4-pro": "gpt-5.4-pro",
9
+ "gpt-5.1-pro": "gpt-5.1-pro",
10
+ "gpt-5-pro": "gpt-5-pro",
11
+ "gpt-5.1": "gpt-5.1",
12
+ "gpt-5.1-codex": "gpt-5.1-codex",
13
+ "gpt-5.2": "gpt-5.2",
14
+ "gpt-5.2-instant": "gpt-5.2-instant",
15
+ "gpt-5.2-pro": "gpt-5.2-pro",
16
+ "claude-4.6-sonnet": "claude-4.6-sonnet",
17
+ "claude-4.1-opus": "claude-4.1-opus",
18
+ "grok-4.1": "grok-4.1",
14
19
  };
15
20
  export function resolveGeminiModelId(modelName) {
16
21
  // Map our logical Gemini names to the exact model ids expected by the SDK.
17
22
  return MODEL_ID_MAP[modelName] ?? modelName;
18
23
  }
19
- export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedModelId) {
24
+ export function createGeminiClient(apiKey, modelName = "gemini-3-pro", resolvedModelId) {
20
25
  const modelId = resolvedModelId ?? resolveGeminiModelId(modelName);
21
26
  const genAI = new GoogleGenAI({ apiKey });
22
27
  const adaptBodyToGemini = (body) => {
23
28
  const contents = body.input.map((inputItem) => ({
24
- role: inputItem.role === 'user' ? 'user' : 'model',
29
+ role: inputItem.role === "user" ? "user" : "model",
25
30
  parts: inputItem.content
26
31
  .map((contentPart) => {
27
- if (contentPart.type === 'input_text') {
32
+ if (contentPart.type === "input_text") {
28
33
  return { text: contentPart.text };
29
34
  }
30
35
  return null;
@@ -33,7 +38,7 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
33
38
  }));
34
39
  const tools = body.tools
35
40
  ?.map((tool) => {
36
- if (tool.type === 'web_search_preview') {
41
+ if (tool.type === "web_search_preview") {
37
42
  return {
38
43
  googleSearch: {},
39
44
  };
@@ -60,7 +65,7 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
60
65
  },
61
66
  ];
62
67
  const systemInstruction = body.instructions
63
- ? { role: 'system', parts: [{ text: body.instructions }] }
68
+ ? { role: "system", parts: [{ text: body.instructions }] }
64
69
  : undefined;
65
70
  return {
66
71
  model: modelId,
@@ -80,18 +85,19 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
80
85
  candidate.content?.parts?.forEach((part) => {
81
86
  if (part.text) {
82
87
  outputText.push(part.text);
83
- output.push({ type: 'text', text: part.text });
88
+ output.push({ type: "text", text: part.text });
84
89
  }
85
90
  });
86
91
  });
87
92
  const usage = {
88
93
  input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
89
94
  output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
90
- total_tokens: (geminiResponse.usageMetadata?.promptTokenCount || 0) + (geminiResponse.usageMetadata?.candidatesTokenCount || 0),
95
+ total_tokens: (geminiResponse.usageMetadata?.promptTokenCount || 0) +
96
+ (geminiResponse.usageMetadata?.candidatesTokenCount || 0),
91
97
  };
92
98
  return {
93
99
  id: geminiResponse.responseId ?? `gemini-${Date.now()}`,
94
- status: 'completed',
100
+ status: "completed",
95
101
  output_text: outputText,
96
102
  output,
97
103
  usage,
@@ -105,15 +111,15 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
105
111
  };
106
112
  return {
107
113
  id: responseId ?? `gemini-${Date.now()}`,
108
- status: 'completed',
114
+ status: "completed",
109
115
  output_text: [text],
110
- output: [{ type: 'text', text }],
116
+ output: [{ type: "text", text }],
111
117
  usage,
112
118
  };
113
119
  };
114
120
  const enrichGeminiError = (error) => {
115
121
  const message = error instanceof Error ? error.message : String(error);
116
- if (message.includes('404')) {
122
+ if (message.includes("404")) {
117
123
  return new Error(`Gemini model not available to this API key/region. Confirm preview access and model ID (${modelId}). Original: ${message}`);
118
124
  }
119
125
  return error instanceof Error ? error : new Error(message);
@@ -123,7 +129,7 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
123
129
  stream: (body) => {
124
130
  const geminiBody = adaptBodyToGemini(body);
125
131
  let finalResponsePromise = null;
126
- let aggregatedText = '';
132
+ let aggregatedText = "";
127
133
  let lastUsage;
128
134
  let responseId;
129
135
  async function* iterator() {
@@ -138,7 +144,7 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
138
144
  const text = chunk.text;
139
145
  if (text) {
140
146
  aggregatedText += text;
141
- yield { type: 'chunk', delta: text };
147
+ yield { type: "chunk", delta: text };
142
148
  }
143
149
  if (chunk.usageMetadata) {
144
150
  lastUsage = chunk.usageMetadata;
@@ -157,13 +163,14 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
157
163
  if (!finalResponsePromise) {
158
164
  // In case the user calls finalResponse before iterating, we need to consume the stream
159
165
  // This is a bit edge-casey but safe.
160
- for await (const _ of generator) { }
166
+ for await (const _ of generator) {
167
+ }
161
168
  }
162
169
  if (!finalResponsePromise) {
163
- throw new Error('Response promise not initialized');
170
+ throw new Error("Response promise not initialized");
164
171
  }
165
172
  return finalResponsePromise;
166
- }
173
+ },
167
174
  };
168
175
  },
169
176
  create: async (body) => {
@@ -180,8 +187,8 @@ export function createGeminiClient(apiKey, modelName = 'gemini-3-pro', resolvedM
180
187
  retrieve: async (id) => {
181
188
  return {
182
189
  id,
183
- status: 'error',
184
- error: { message: 'Retrieve by ID not supported for Gemini API yet.' },
190
+ status: "error",
191
+ error: { message: "Retrieve by ID not supported for Gemini API yet." },
185
192
  };
186
193
  },
187
194
  },