@indykish/oracle 0.9.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1252 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +125 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1378 -0
  14. package/dist/scripts/test-browser.js +103 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1067 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
  20. package/dist/src/browser/actions/attachments.js +1910 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +485 -0
  23. package/dist/src/browser/actions/navigation.js +445 -0
  24. package/dist/src/browser/actions/promptComposer.js +485 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +344 -0
  28. package/dist/src/browser/config.js +103 -0
  29. package/dist/src/browser/constants.js +71 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1741 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +43 -0
  37. package/dist/src/browser/profileState.js +280 -0
  38. package/dist/src/browser/prompt.js +152 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/reattach.js +186 -0
  41. package/dist/src/browser/reattachHelpers.js +382 -0
  42. package/dist/src/browser/sessionRunner.js +119 -0
  43. package/dist/src/browser/types.js +1 -0
  44. package/dist/src/browser/utils.js +122 -0
  45. package/dist/src/browserMode.js +1 -0
  46. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  47. package/dist/src/cli/bridge/client.js +73 -0
  48. package/dist/src/cli/bridge/codexConfig.js +43 -0
  49. package/dist/src/cli/bridge/doctor.js +107 -0
  50. package/dist/src/cli/bridge/host.js +259 -0
  51. package/dist/src/cli/browserConfig.js +278 -0
  52. package/dist/src/cli/browserDefaults.js +81 -0
  53. package/dist/src/cli/bundleWarnings.js +9 -0
  54. package/dist/src/cli/clipboard.js +10 -0
  55. package/dist/src/cli/detach.js +11 -0
  56. package/dist/src/cli/dryRun.js +105 -0
  57. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  58. package/dist/src/cli/engine.js +41 -0
  59. package/dist/src/cli/errorUtils.js +9 -0
  60. package/dist/src/cli/format.js +13 -0
  61. package/dist/src/cli/help.js +77 -0
  62. package/dist/src/cli/hiddenAliases.js +22 -0
  63. package/dist/src/cli/markdownBundle.js +17 -0
  64. package/dist/src/cli/markdownRenderer.js +97 -0
  65. package/dist/src/cli/notifier.js +306 -0
  66. package/dist/src/cli/options.js +281 -0
  67. package/dist/src/cli/oscUtils.js +2 -0
  68. package/dist/src/cli/promptRequirement.js +17 -0
  69. package/dist/src/cli/renderFlags.js +9 -0
  70. package/dist/src/cli/renderOutput.js +26 -0
  71. package/dist/src/cli/rootAlias.js +30 -0
  72. package/dist/src/cli/runOptions.js +78 -0
  73. package/dist/src/cli/sessionCommand.js +111 -0
  74. package/dist/src/cli/sessionDisplay.js +567 -0
  75. package/dist/src/cli/sessionRunner.js +602 -0
  76. package/dist/src/cli/sessionTable.js +92 -0
  77. package/dist/src/cli/tagline.js +258 -0
  78. package/dist/src/cli/tui/index.js +486 -0
  79. package/dist/src/cli/writeOutputPath.js +21 -0
  80. package/dist/src/config.js +26 -0
  81. package/dist/src/gemini-web/client.js +328 -0
  82. package/dist/src/gemini-web/executor.js +285 -0
  83. package/dist/src/gemini-web/index.js +1 -0
  84. package/dist/src/gemini-web/types.js +1 -0
  85. package/dist/src/heartbeat.js +43 -0
  86. package/dist/src/mcp/server.js +40 -0
  87. package/dist/src/mcp/tools/consult.js +290 -0
  88. package/dist/src/mcp/tools/sessionResources.js +75 -0
  89. package/dist/src/mcp/tools/sessions.js +105 -0
  90. package/dist/src/mcp/types.js +22 -0
  91. package/dist/src/mcp/utils.js +37 -0
  92. package/dist/src/oracle/background.js +141 -0
  93. package/dist/src/oracle/claude.js +101 -0
  94. package/dist/src/oracle/client.js +197 -0
  95. package/dist/src/oracle/config.js +227 -0
  96. package/dist/src/oracle/errors.js +132 -0
  97. package/dist/src/oracle/files.js +378 -0
  98. package/dist/src/oracle/finishLine.js +32 -0
  99. package/dist/src/oracle/format.js +30 -0
  100. package/dist/src/oracle/fsAdapter.js +10 -0
  101. package/dist/src/oracle/gemini.js +195 -0
  102. package/dist/src/oracle/logging.js +36 -0
  103. package/dist/src/oracle/markdown.js +46 -0
  104. package/dist/src/oracle/modelResolver.js +183 -0
  105. package/dist/src/oracle/multiModelRunner.js +153 -0
  106. package/dist/src/oracle/oscProgress.js +24 -0
  107. package/dist/src/oracle/promptAssembly.js +13 -0
  108. package/dist/src/oracle/request.js +50 -0
  109. package/dist/src/oracle/run.js +596 -0
  110. package/dist/src/oracle/runUtils.js +31 -0
  111. package/dist/src/oracle/tokenEstimate.js +37 -0
  112. package/dist/src/oracle/tokenStats.js +39 -0
  113. package/dist/src/oracle/tokenStringifier.js +24 -0
  114. package/dist/src/oracle/types.js +1 -0
  115. package/dist/src/oracle.js +12 -0
  116. package/dist/src/oracleHome.js +13 -0
  117. package/dist/src/remote/client.js +129 -0
  118. package/dist/src/remote/health.js +113 -0
  119. package/dist/src/remote/remoteServiceConfig.js +31 -0
  120. package/dist/src/remote/server.js +533 -0
  121. package/dist/src/remote/types.js +1 -0
  122. package/dist/src/sessionManager.js +637 -0
  123. package/dist/src/sessionStore.js +56 -0
  124. package/dist/src/version.js +39 -0
  125. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  126. package/dist/vendor/oracle-notifier/README.md +24 -0
  127. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  128. package/package.json +115 -0
  129. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  130. package/vendor/oracle-notifier/README.md +24 -0
  131. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,132 @@
1
+ import { APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from 'openai';
2
+ import { APIError } from 'openai/error';
3
+ import { formatElapsed } from './format.js';
4
+ export class OracleUserError extends Error {
5
+ category;
6
+ details;
7
+ constructor(category, message, details, cause) {
8
+ super(message);
9
+ this.name = 'OracleUserError';
10
+ this.category = category;
11
+ this.details = details;
12
+ if (cause) {
13
+ this.cause = cause;
14
+ }
15
+ }
16
+ }
17
+ export class FileValidationError extends OracleUserError {
18
+ constructor(message, details, cause) {
19
+ super('file-validation', message, details, cause);
20
+ this.name = 'FileValidationError';
21
+ }
22
+ }
23
+ export class BrowserAutomationError extends OracleUserError {
24
+ constructor(message, details, cause) {
25
+ super('browser-automation', message, details, cause);
26
+ this.name = 'BrowserAutomationError';
27
+ }
28
+ }
29
+ export class PromptValidationError extends OracleUserError {
30
+ constructor(message, details, cause) {
31
+ super('prompt-validation', message, details, cause);
32
+ this.name = 'PromptValidationError';
33
+ }
34
+ }
35
+ export function asOracleUserError(error) {
36
+ if (error instanceof OracleUserError) {
37
+ return error;
38
+ }
39
+ return null;
40
+ }
41
+ export class OracleTransportError extends Error {
42
+ reason;
43
+ constructor(reason, message, cause) {
44
+ super(message);
45
+ this.name = 'OracleTransportError';
46
+ this.reason = reason;
47
+ if (cause) {
48
+ this.cause = cause;
49
+ }
50
+ }
51
+ }
52
+ export class OracleResponseError extends Error {
53
+ metadata;
54
+ response;
55
+ constructor(message, response) {
56
+ super(message);
57
+ this.name = 'OracleResponseError';
58
+ this.response = response;
59
+ this.metadata = extractResponseMetadata(response);
60
+ }
61
+ }
62
+ export function extractResponseMetadata(response) {
63
+ if (!response) {
64
+ return {};
65
+ }
66
+ const metadata = {
67
+ responseId: response.id,
68
+ status: response.status,
69
+ incompleteReason: response.incomplete_details?.reason ?? undefined,
70
+ };
71
+ const requestId = response._request_id;
72
+ if (requestId !== undefined) {
73
+ metadata.requestId = requestId;
74
+ }
75
+ return metadata;
76
+ }
77
+ export function toTransportError(error, model) {
78
+ if (error instanceof OracleTransportError) {
79
+ return error;
80
+ }
81
+ if (error instanceof APIConnectionTimeoutError) {
82
+ return new OracleTransportError('client-timeout', 'OpenAI request timed out before completion.', error);
83
+ }
84
+ if (error instanceof APIUserAbortError) {
85
+ return new OracleTransportError('client-abort', 'The request was aborted before OpenAI finished responding.', error);
86
+ }
87
+ if (error instanceof APIConnectionError) {
88
+ return new OracleTransportError('connection-lost', 'Connection to OpenAI dropped before the response completed.', error);
89
+ }
90
+ const isApiError = error instanceof APIError || error?.name === 'APIError';
91
+ if (isApiError) {
92
+ const apiError = error;
93
+ const code = apiError.code ?? apiError.error?.code;
94
+ const messageText = apiError.message?.toLowerCase?.() ?? '';
95
+ const apiMessage = apiError.error?.message ||
96
+ apiError.message ||
97
+ (apiError.status ? `${apiError.status} OpenAI API error` : 'OpenAI API error');
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);
105
+ }
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);
108
+ }
109
+ return new OracleTransportError('api-error', apiMessage, apiError);
110
+ }
111
+ return new OracleTransportError('unknown', error instanceof Error ? error.message : 'Unknown transport failure.', error);
112
+ }
113
+ export function describeTransportError(error, deadlineMs) {
114
+ switch (error.reason) {
115
+ case 'client-timeout':
116
+ return deadlineMs
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':
124
+ return error.message;
125
+ case 'model-unavailable':
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).';
129
+ default:
130
+ return 'OpenAI streaming call ended with an unknown transport error.';
131
+ }
132
+ }
@@ -0,0 +1,378 @@
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
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, } = {}) {
9
+ if (!filePaths || filePaths.length === 0) {
10
+ return [];
11
+ }
12
+ const partitioned = await partitionFileInputs(filePaths, cwd, fsModule);
13
+ const useNativeFilesystem = fsModule === DEFAULT_FS || isNativeFsModule(fsModule);
14
+ let candidatePaths = [];
15
+ if (useNativeFilesystem) {
16
+ if (partitioned.globPatterns.length === 0 &&
17
+ partitioned.excludePatterns.length === 0 &&
18
+ partitioned.literalDirectories.length === 0) {
19
+ candidatePaths = Array.from(new Set(partitioned.literalFiles));
20
+ }
21
+ else {
22
+ candidatePaths = await expandWithNativeGlob(partitioned, cwd);
23
+ }
24
+ }
25
+ else {
26
+ if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
27
+ throw new Error('Glob patterns and exclusions are only supported for on-disk files.');
28
+ }
29
+ candidatePaths = await expandWithCustomFs(partitioned, fsModule);
30
+ }
31
+ const allowedLiteralDirs = partitioned.literalDirectories
32
+ .map((dir) => path.resolve(dir))
33
+ .filter((dir) => DEFAULT_IGNORED_DIRS.includes(path.basename(dir)));
34
+ const allowedLiteralFiles = partitioned.literalFiles.map((file) => path.resolve(file));
35
+ const resolvedLiteralDirs = new Set(allowedLiteralDirs);
36
+ const allowedPaths = new Set([...allowedLiteralDirs, ...allowedLiteralFiles]);
37
+ const ignoredWhitelist = await buildIgnoredWhitelist(candidatePaths, cwd, fsModule);
38
+ const ignoredLog = new Set();
39
+ const filteredCandidates = candidatePaths.filter((filePath) => {
40
+ const ignoredDir = findIgnoredAncestor(filePath, cwd, resolvedLiteralDirs, allowedPaths, ignoredWhitelist);
41
+ if (!ignoredDir) {
42
+ return true;
43
+ }
44
+ const displayFile = relativePath(filePath, cwd);
45
+ const key = `${ignoredDir}|${displayFile}`;
46
+ if (!ignoredLog.has(key)) {
47
+ console.log(`Skipping default-ignored path: ${displayFile} (matches ${ignoredDir})`);
48
+ ignoredLog.add(key);
49
+ }
50
+ return false;
51
+ });
52
+ if (filteredCandidates.length === 0) {
53
+ throw new FileValidationError('No files matched the provided --file patterns.', {
54
+ patterns: partitioned.globPatterns,
55
+ excludes: partitioned.excludePatterns,
56
+ });
57
+ }
58
+ const oversized = [];
59
+ const accepted = [];
60
+ for (const filePath of filteredCandidates) {
61
+ let stats;
62
+ try {
63
+ stats = await fsModule.stat(filePath);
64
+ }
65
+ catch (error) {
66
+ throw new FileValidationError(`Missing file or directory: ${relativePath(filePath, cwd)}`, { path: filePath }, error);
67
+ }
68
+ if (!stats.isFile()) {
69
+ continue;
70
+ }
71
+ if (maxFileSizeBytes && typeof stats.size === 'number' && stats.size > maxFileSizeBytes) {
72
+ const relative = path.relative(cwd, filePath) || filePath;
73
+ oversized.push(`${relative} (${formatBytes(stats.size)})`);
74
+ continue;
75
+ }
76
+ accepted.push(filePath);
77
+ }
78
+ if (oversized.length > 0) {
79
+ throw new FileValidationError(`The following files exceed the 1 MB limit:\n- ${oversized.join('\n- ')}`, {
80
+ files: oversized,
81
+ limitBytes: maxFileSizeBytes,
82
+ });
83
+ }
84
+ const files = [];
85
+ for (const filePath of accepted) {
86
+ const content = readContents ? await fsModule.readFile(filePath, 'utf8') : '';
87
+ files.push({ path: filePath, content });
88
+ }
89
+ return files;
90
+ }
91
+ async function partitionFileInputs(rawPaths, cwd, fsModule) {
92
+ const result = {
93
+ globPatterns: [],
94
+ excludePatterns: [],
95
+ literalFiles: [],
96
+ literalDirectories: [],
97
+ };
98
+ for (const entry of rawPaths) {
99
+ const raw = entry?.trim();
100
+ if (!raw) {
101
+ continue;
102
+ }
103
+ if (raw.startsWith('!')) {
104
+ const normalized = normalizeGlob(raw.slice(1), cwd);
105
+ if (normalized) {
106
+ result.excludePatterns.push(normalized);
107
+ }
108
+ continue;
109
+ }
110
+ if (fg.isDynamicPattern(raw)) {
111
+ result.globPatterns.push(normalizeGlob(raw, cwd));
112
+ continue;
113
+ }
114
+ const absolutePath = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
115
+ let stats;
116
+ try {
117
+ stats = await fsModule.stat(absolutePath);
118
+ }
119
+ catch (error) {
120
+ throw new FileValidationError(`Missing file or directory: ${raw}`, { path: absolutePath }, error);
121
+ }
122
+ if (stats.isDirectory()) {
123
+ result.literalDirectories.push(absolutePath);
124
+ }
125
+ else if (stats.isFile()) {
126
+ result.literalFiles.push(absolutePath);
127
+ }
128
+ else {
129
+ throw new FileValidationError(`Not a file or directory: ${raw}`, { path: absolutePath });
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ async function expandWithNativeGlob(partitioned, cwd) {
135
+ const patterns = [
136
+ ...partitioned.globPatterns,
137
+ ...partitioned.literalFiles.map((absPath) => toPosixRelativeOrBasename(absPath, cwd)),
138
+ ...partitioned.literalDirectories.map((absDir) => makeDirectoryPattern(toPosixRelative(absDir, cwd))),
139
+ ].filter(Boolean);
140
+ if (patterns.length === 0) {
141
+ return [];
142
+ }
143
+ const dotfileOptIn = patterns.some((pattern) => includesDotfileSegment(pattern));
144
+ const gitignoreSets = await loadGitignoreSets(cwd);
145
+ const matches = (await fg(patterns, {
146
+ cwd,
147
+ absolute: false,
148
+ dot: true,
149
+ ignore: partitioned.excludePatterns,
150
+ onlyFiles: true,
151
+ followSymbolicLinks: false,
152
+ suppressErrors: true,
153
+ }));
154
+ const resolved = matches.map((match) => path.resolve(cwd, match));
155
+ const filtered = resolved.filter((filePath) => !isGitignored(filePath, gitignoreSets));
156
+ const finalFiles = dotfileOptIn ? filtered : filtered.filter((filePath) => !path.basename(filePath).startsWith('.'));
157
+ return Array.from(new Set(finalFiles));
158
+ }
159
+ async function loadGitignoreSets(cwd) {
160
+ const gitignorePaths = await fg('**/.gitignore', {
161
+ cwd,
162
+ dot: true,
163
+ absolute: true,
164
+ onlyFiles: true,
165
+ followSymbolicLinks: false,
166
+ suppressErrors: true,
167
+ });
168
+ const sets = [];
169
+ for (const filePath of gitignorePaths) {
170
+ try {
171
+ const raw = await fs.readFile(filePath, 'utf8');
172
+ const patterns = raw
173
+ .split('\n')
174
+ .map((line) => line.trim())
175
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
176
+ if (patterns.length > 0) {
177
+ sets.push({ dir: path.dirname(filePath), patterns });
178
+ }
179
+ }
180
+ catch {
181
+ // Ignore unreadable .gitignore files
182
+ }
183
+ }
184
+ // Ensure deterministic parent-before-child ordering
185
+ return sets.sort((a, b) => a.dir.localeCompare(b.dir));
186
+ }
187
+ function isGitignored(filePath, sets) {
188
+ for (const { dir, patterns } of sets) {
189
+ if (!filePath.startsWith(dir)) {
190
+ continue;
191
+ }
192
+ const relative = path.relative(dir, filePath) || path.basename(filePath);
193
+ if (matchesAny(relative, patterns)) {
194
+ return true;
195
+ }
196
+ }
197
+ return false;
198
+ }
199
+ async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
200
+ const whitelist = new Set();
201
+ for (const filePath of filePaths) {
202
+ const absolute = path.resolve(filePath);
203
+ const rel = path.relative(cwd, absolute);
204
+ const parts = rel.split(path.sep).filter(Boolean);
205
+ for (let i = 0; i < parts.length - 1; i += 1) {
206
+ const part = parts[i];
207
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
208
+ continue;
209
+ }
210
+ const dirPath = path.resolve(cwd, ...parts.slice(0, i + 1));
211
+ if (whitelist.has(dirPath)) {
212
+ continue;
213
+ }
214
+ try {
215
+ const stats = await fsModule.stat(path.join(dirPath, '.gitignore'));
216
+ if (stats.isFile()) {
217
+ whitelist.add(dirPath);
218
+ }
219
+ }
220
+ catch {
221
+ // no .gitignore at this level; keep ignored
222
+ }
223
+ }
224
+ }
225
+ return whitelist;
226
+ }
227
+ function findIgnoredAncestor(filePath, cwd, _literalDirs, allowedPaths, ignoredWhitelist) {
228
+ const absolute = path.resolve(filePath);
229
+ if (Array.from(allowedPaths).some((allowed) => absolute === allowed || absolute.startsWith(`${allowed}${path.sep}`))) {
230
+ return null; // explicitly requested path overrides default ignore when the ignored dir itself was passed
231
+ }
232
+ const rel = path.relative(cwd, absolute);
233
+ const parts = rel.split(path.sep);
234
+ for (let idx = 0; idx < parts.length; idx += 1) {
235
+ const part = parts[idx];
236
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
237
+ continue;
238
+ }
239
+ const ignoredDir = path.resolve(cwd, parts.slice(0, idx + 1).join(path.sep));
240
+ if (ignoredWhitelist.has(ignoredDir)) {
241
+ continue;
242
+ }
243
+ return part;
244
+ }
245
+ return null;
246
+ }
247
+ function matchesAny(relativePath, patterns) {
248
+ return patterns.some((pattern) => matchesPattern(relativePath, pattern));
249
+ }
250
+ function matchesPattern(relativePath, pattern) {
251
+ if (!pattern) {
252
+ return false;
253
+ }
254
+ const normalized = pattern.replace(/\\+/g, '/');
255
+ // Directory rule
256
+ if (normalized.endsWith('/')) {
257
+ const dir = normalized.slice(0, -1);
258
+ return relativePath === dir || relativePath.startsWith(`${dir}/`);
259
+ }
260
+ // Simple glob support (* and **)
261
+ const regex = globToRegex(normalized);
262
+ return regex.test(relativePath);
263
+ }
264
+ 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, '[^/]*');
270
+ return new RegExp(`^${restored}$`);
271
+ }
272
+ function includesDotfileSegment(pattern) {
273
+ const segments = pattern.split('/');
274
+ return segments.some((segment) => segment.startsWith('.') && segment.length > 1);
275
+ }
276
+ async function expandWithCustomFs(partitioned, fsModule) {
277
+ const paths = new Set();
278
+ partitioned.literalFiles.forEach((file) => {
279
+ paths.add(file);
280
+ });
281
+ for (const directory of partitioned.literalDirectories) {
282
+ const nested = await expandDirectoryRecursive(directory, fsModule);
283
+ nested.forEach((entry) => {
284
+ paths.add(entry);
285
+ });
286
+ }
287
+ return Array.from(paths);
288
+ }
289
+ async function expandDirectoryRecursive(directory, fsModule) {
290
+ const entries = await fsModule.readdir(directory);
291
+ const results = [];
292
+ for (const entry of entries) {
293
+ const childPath = path.join(directory, entry);
294
+ const stats = await fsModule.stat(childPath);
295
+ if (stats.isDirectory()) {
296
+ results.push(...(await expandDirectoryRecursive(childPath, fsModule)));
297
+ }
298
+ else if (stats.isFile()) {
299
+ results.push(childPath);
300
+ }
301
+ }
302
+ return results;
303
+ }
304
+ function makeDirectoryPattern(relative) {
305
+ if (relative === '.' || relative === '') {
306
+ return '**/*';
307
+ }
308
+ return `${stripTrailingSlashes(relative)}/**/*`;
309
+ }
310
+ function isNativeFsModule(fsModule) {
311
+ return ((fsModule.__nativeFs === true ||
312
+ (fsModule.readFile === DEFAULT_FS.readFile &&
313
+ fsModule.stat === DEFAULT_FS.stat &&
314
+ fsModule.readdir === DEFAULT_FS.readdir)));
315
+ }
316
+ function normalizeGlob(pattern, cwd) {
317
+ if (!pattern) {
318
+ return '';
319
+ }
320
+ let normalized = pattern;
321
+ if (path.isAbsolute(normalized)) {
322
+ normalized = path.relative(cwd, normalized);
323
+ }
324
+ normalized = toPosix(normalized);
325
+ if (normalized.startsWith('./')) {
326
+ normalized = normalized.slice(2);
327
+ }
328
+ return normalized;
329
+ }
330
+ function toPosix(value) {
331
+ return value.replace(/\\/g, '/');
332
+ }
333
+ function toPosixRelative(absPath, cwd) {
334
+ const relative = path.relative(cwd, absPath);
335
+ if (!relative) {
336
+ return '.';
337
+ }
338
+ return toPosix(relative);
339
+ }
340
+ function toPosixRelativeOrBasename(absPath, cwd) {
341
+ const relative = path.relative(cwd, absPath);
342
+ return toPosix(relative || path.basename(absPath));
343
+ }
344
+ function stripTrailingSlashes(value) {
345
+ const normalized = toPosix(value);
346
+ return normalized.replace(/\/+$/g, '');
347
+ }
348
+ function formatBytes(size) {
349
+ if (size >= 1024 * 1024) {
350
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
351
+ }
352
+ if (size >= 1024) {
353
+ return `${(size / 1024).toFixed(1)} KB`;
354
+ }
355
+ return `${size} B`;
356
+ }
357
+ function relativePath(targetPath, cwd) {
358
+ const relative = path.relative(cwd, targetPath);
359
+ return relative || targetPath;
360
+ }
361
+ export function createFileSections(files, cwd = process.cwd()) {
362
+ return files.map((file, index) => {
363
+ const relative = toPosix(path.relative(cwd, file.path) || file.path);
364
+ const sectionText = [
365
+ `### File ${index + 1}: ${relative}`,
366
+ '```',
367
+ file.content.trimEnd(),
368
+ '```',
369
+ ].join('\n');
370
+ return {
371
+ index: index + 1,
372
+ absolutePath: file.path,
373
+ displayPath: relative,
374
+ sectionText,
375
+ content: file.content,
376
+ };
377
+ });
378
+ }
@@ -0,0 +1,32 @@
1
+ import { formatUSD } from './format.js';
2
+ export function formatElapsedCompact(ms) {
3
+ if (!Number.isFinite(ms) || ms < 0) {
4
+ return 'unknown';
5
+ }
6
+ if (ms < 60_000) {
7
+ return `${(ms / 1000).toFixed(1)}s`;
8
+ }
9
+ if (ms < 60 * 60_000) {
10
+ const minutes = Math.floor(ms / 60_000);
11
+ const seconds = Math.floor((ms % 60_000) / 1000);
12
+ return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
13
+ }
14
+ const hours = Math.floor(ms / (60 * 60_000));
15
+ const minutes = Math.floor((ms % (60 * 60_000)) / 60_000);
16
+ return `${hours}h${minutes.toString().padStart(2, '0')}m`;
17
+ }
18
+ export function formatFinishLine({ elapsedMs, model, costUsd, tokensPart, summaryExtraParts, detailParts, }) {
19
+ const line1Parts = [
20
+ formatElapsedCompact(elapsedMs),
21
+ typeof costUsd === 'number' ? formatUSD(costUsd) : null,
22
+ model,
23
+ tokensPart,
24
+ ...(summaryExtraParts ?? []),
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);
28
+ if (line2Parts.length === 0) {
29
+ return { line1 };
30
+ }
31
+ return { line1, line2: line2Parts.join(' | ') };
32
+ }
@@ -0,0 +1,30 @@
1
+ export function formatUSD(value) {
2
+ if (!Number.isFinite(value)) {
3
+ return 'n/a';
4
+ }
5
+ // Display with 4 decimal places, rounding to $0.0001 minimum granularity.
6
+ return `$${value.toFixed(4)}`;
7
+ }
8
+ export function formatNumber(value, { estimated = false } = {}) {
9
+ if (value == null) {
10
+ return 'n/a';
11
+ }
12
+ const suffix = estimated ? ' (est.)' : '';
13
+ return `${value.toLocaleString()}${suffix}`;
14
+ }
15
+ export function formatElapsed(ms) {
16
+ if (ms >= 60 * 60 * 1000) {
17
+ const hours = Math.floor(ms / (60 * 60 * 1000));
18
+ const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
19
+ return `${hours}h ${minutes}m`;
20
+ }
21
+ if (ms >= 60 * 1000) {
22
+ const minutes = Math.floor(ms / (60 * 1000));
23
+ const seconds = Math.floor((ms % (60 * 1000)) / 1000);
24
+ return `${minutes}m ${seconds}s`;
25
+ }
26
+ if (ms >= 1000) {
27
+ return `${Math.floor(ms / 1000)}s`;
28
+ }
29
+ return `${Math.round(ms)}ms`;
30
+ }
@@ -0,0 +1,10 @@
1
+ export function createFsAdapter(fsModule) {
2
+ const adapter = {
3
+ stat: (targetPath) => fsModule.stat(targetPath),
4
+ readdir: (targetPath) => fsModule.readdir(targetPath),
5
+ readFile: (targetPath, encoding) => fsModule.readFile(targetPath, encoding),
6
+ };
7
+ // Mark adapters so downstream callers can treat them as native filesystem access.
8
+ adapter.__nativeFs = true;
9
+ return adapter;
10
+ }