@steipete/oracle 0.4.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,87 @@
1
+ import OpenAI, { AzureOpenAI } 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
+ export function createDefaultClientFactory() {
7
+ const customFactory = loadCustomClientFactory();
8
+ if (customFactory)
9
+ return customFactory;
10
+ return (key, options) => {
11
+ if (options?.model?.startsWith('gemini')) {
12
+ // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
13
+ return createGeminiClient(key, options.model, options.resolvedModelId);
14
+ }
15
+ if (options?.model?.startsWith('claude')) {
16
+ return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
17
+ }
18
+ let instance;
19
+ if (options?.azure?.endpoint) {
20
+ instance = new AzureOpenAI({
21
+ apiKey: key,
22
+ endpoint: options.azure.endpoint,
23
+ apiVersion: options.azure.apiVersion,
24
+ deployment: options.azure.deployment,
25
+ timeout: 20 * 60 * 1000,
26
+ });
27
+ }
28
+ else {
29
+ instance = new OpenAI({
30
+ apiKey: key,
31
+ timeout: 20 * 60 * 1000,
32
+ baseURL: options?.baseUrl,
33
+ });
34
+ }
35
+ return {
36
+ responses: {
37
+ stream: (body) => instance.responses.stream(body),
38
+ create: (body) => instance.responses.create(body),
39
+ retrieve: (id) => instance.responses.retrieve(id),
40
+ },
41
+ };
42
+ };
43
+ }
44
+ function loadCustomClientFactory() {
45
+ const override = process.env.ORACLE_CLIENT_FACTORY;
46
+ if (!override) {
47
+ return null;
48
+ }
49
+ if (override === 'INLINE_TEST_FACTORY') {
50
+ return () => ({
51
+ responses: {
52
+ create: async () => ({ id: 'inline-test', status: 'completed' }),
53
+ stream: async () => ({
54
+ [Symbol.asyncIterator]: () => ({
55
+ async next() {
56
+ return { done: true, value: undefined };
57
+ },
58
+ }),
59
+ finalResponse: async () => ({ id: 'inline-test', status: 'completed' }),
60
+ }),
61
+ retrieve: async (id) => ({ id, status: 'completed' }),
62
+ },
63
+ });
64
+ }
65
+ try {
66
+ const require = createRequire(import.meta.url);
67
+ const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
68
+ const moduleExports = require(resolved);
69
+ const factory = typeof moduleExports === 'function'
70
+ ? moduleExports
71
+ : typeof moduleExports?.default === 'function'
72
+ ? moduleExports.default
73
+ : typeof moduleExports?.createClientFactory === 'function'
74
+ ? moduleExports.createClientFactory
75
+ : null;
76
+ if (typeof factory === 'function') {
77
+ return factory;
78
+ }
79
+ console.warn(`Custom client factory at ${resolved} did not export a function.`);
80
+ }
81
+ catch (error) {
82
+ console.warn(`Failed to load ORACLE_CLIENT_FACTORY module "${override}":`, error);
83
+ }
84
+ return null;
85
+ }
86
+ // Exposed for tests
87
+ export { loadCustomClientFactory as __loadCustomClientFactory };
@@ -0,0 +1,92 @@
1
+ import { countTokens as countTokensGpt5 } from 'gpt-tokenizer/model/gpt-5';
2
+ import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
3
+ import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
4
+ import { stringifyTokenizerInput } from './tokenStringifier.js';
5
+ export const DEFAULT_MODEL = 'gpt-5.1-pro';
6
+ export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'claude-4.1-opus']);
7
+ const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
8
+ export const MODEL_CONFIGS = {
9
+ 'gpt-5.1-pro': {
10
+ model: 'gpt-5.1-pro',
11
+ tokenizer: countTokensGpt5Pro,
12
+ inputLimit: 196000,
13
+ pricing: {
14
+ inputPerToken: 15 / 1_000_000,
15
+ outputPerToken: 120 / 1_000_000,
16
+ },
17
+ reasoning: null,
18
+ },
19
+ 'gpt-5-pro': {
20
+ model: 'gpt-5-pro',
21
+ tokenizer: countTokensGpt5Pro,
22
+ inputLimit: 196000,
23
+ pricing: {
24
+ inputPerToken: 15 / 1_000_000,
25
+ outputPerToken: 120 / 1_000_000,
26
+ },
27
+ reasoning: null,
28
+ },
29
+ 'gpt-5.1': {
30
+ model: 'gpt-5.1',
31
+ tokenizer: countTokensGpt5,
32
+ inputLimit: 196000,
33
+ pricing: {
34
+ inputPerToken: 1.25 / 1_000_000,
35
+ outputPerToken: 10 / 1_000_000,
36
+ },
37
+ reasoning: { effort: 'high' },
38
+ },
39
+ 'gpt-5.1-codex': {
40
+ model: 'gpt-5.1-codex',
41
+ tokenizer: countTokensGpt5,
42
+ inputLimit: 196000,
43
+ pricing: {
44
+ inputPerToken: 1.25 / 1_000_000,
45
+ outputPerToken: 10 / 1_000_000,
46
+ },
47
+ reasoning: { effort: 'high' },
48
+ },
49
+ 'gemini-3-pro': {
50
+ model: 'gemini-3-pro',
51
+ tokenizer: countTokensGpt5Pro,
52
+ inputLimit: 200000,
53
+ pricing: {
54
+ inputPerToken: 2 / 1_000_000,
55
+ outputPerToken: 12 / 1_000_000,
56
+ },
57
+ reasoning: null,
58
+ supportsBackground: false,
59
+ supportsSearch: true,
60
+ },
61
+ 'claude-4.5-sonnet': {
62
+ model: 'claude-4.5-sonnet',
63
+ apiModel: 'claude-sonnet-4-5',
64
+ tokenizer: countTokensAnthropic,
65
+ inputLimit: 200000,
66
+ pricing: {
67
+ inputPerToken: 3 / 1_000_000,
68
+ outputPerToken: 15 / 1_000_000,
69
+ },
70
+ reasoning: null,
71
+ supportsBackground: false,
72
+ supportsSearch: false,
73
+ },
74
+ 'claude-4.1-opus': {
75
+ model: 'claude-4.1-opus',
76
+ apiModel: 'claude-opus-4-1',
77
+ tokenizer: countTokensAnthropic,
78
+ inputLimit: 200000,
79
+ pricing: {
80
+ inputPerToken: 15 / 1_000_000,
81
+ outputPerToken: 75 / 1_000_000,
82
+ },
83
+ reasoning: { effort: 'high' },
84
+ supportsBackground: false,
85
+ supportsSearch: false,
86
+ },
87
+ };
88
+ export const DEFAULT_SYSTEM_PROMPT = [
89
+ 'You are Oracle, a focused one-shot problem solver.',
90
+ 'Emphasize direct answers, cite any files referenced, and clearly note when the search tool was used.',
91
+ ].join(' ');
92
+ export const TOKENIZER_OPTIONS = { allowedSpecial: 'all' };
@@ -0,0 +1,104 @@
1
+ import { APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, } from 'openai';
2
+ import { formatElapsed } from './format.js';
3
+ export class OracleUserError extends Error {
4
+ category;
5
+ details;
6
+ constructor(category, message, details, cause) {
7
+ super(message);
8
+ this.name = 'OracleUserError';
9
+ this.category = category;
10
+ this.details = details;
11
+ if (cause) {
12
+ this.cause = cause;
13
+ }
14
+ }
15
+ }
16
+ export class FileValidationError extends OracleUserError {
17
+ constructor(message, details, cause) {
18
+ super('file-validation', message, details, cause);
19
+ this.name = 'FileValidationError';
20
+ }
21
+ }
22
+ export class BrowserAutomationError extends OracleUserError {
23
+ constructor(message, details, cause) {
24
+ super('browser-automation', message, details, cause);
25
+ this.name = 'BrowserAutomationError';
26
+ }
27
+ }
28
+ export class PromptValidationError extends OracleUserError {
29
+ constructor(message, details, cause) {
30
+ super('prompt-validation', message, details, cause);
31
+ this.name = 'PromptValidationError';
32
+ }
33
+ }
34
+ export function asOracleUserError(error) {
35
+ if (error instanceof OracleUserError) {
36
+ return error;
37
+ }
38
+ return null;
39
+ }
40
+ export class OracleTransportError extends Error {
41
+ reason;
42
+ constructor(reason, message, cause) {
43
+ super(message);
44
+ this.name = 'OracleTransportError';
45
+ this.reason = reason;
46
+ if (cause) {
47
+ this.cause = cause;
48
+ }
49
+ }
50
+ }
51
+ export class OracleResponseError extends Error {
52
+ metadata;
53
+ response;
54
+ constructor(message, response) {
55
+ super(message);
56
+ this.name = 'OracleResponseError';
57
+ this.response = response;
58
+ this.metadata = extractResponseMetadata(response);
59
+ }
60
+ }
61
+ export function extractResponseMetadata(response) {
62
+ if (!response) {
63
+ return {};
64
+ }
65
+ const metadata = {
66
+ responseId: response.id,
67
+ status: response.status,
68
+ incompleteReason: response.incomplete_details?.reason ?? undefined,
69
+ };
70
+ const requestId = response._request_id;
71
+ if (requestId !== undefined) {
72
+ metadata.requestId = requestId;
73
+ }
74
+ return metadata;
75
+ }
76
+ export function toTransportError(error) {
77
+ if (error instanceof OracleTransportError) {
78
+ return error;
79
+ }
80
+ if (error instanceof APIConnectionTimeoutError) {
81
+ return new OracleTransportError('client-timeout', 'OpenAI request timed out before completion.', error);
82
+ }
83
+ if (error instanceof APIUserAbortError) {
84
+ return new OracleTransportError('client-abort', 'The request was aborted before OpenAI finished responding.', error);
85
+ }
86
+ if (error instanceof APIConnectionError) {
87
+ return new OracleTransportError('connection-lost', 'Connection to OpenAI dropped before the response completed.', error);
88
+ }
89
+ return new OracleTransportError('unknown', error instanceof Error ? error.message : 'Unknown transport failure.', error);
90
+ }
91
+ export function describeTransportError(error, deadlineMs) {
92
+ switch (error.reason) {
93
+ case 'client-timeout':
94
+ return deadlineMs
95
+ ? `Client-side timeout: OpenAI streaming call exceeded the ${formatElapsed(deadlineMs)} deadline.`
96
+ : 'Client-side timeout: OpenAI streaming call exceeded the configured deadline.';
97
+ case 'connection-lost':
98
+ return 'Connection to OpenAI ended unexpectedly before the response completed.';
99
+ case 'client-abort':
100
+ return 'Request was aborted before OpenAI completed the response.';
101
+ default:
102
+ return 'OpenAI streaming call ended with an unknown transport error.';
103
+ }
104
+ }
@@ -0,0 +1,371 @@
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 } = {}) {
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
+ candidatePaths = await expandWithNativeGlob(partitioned, cwd);
17
+ }
18
+ else {
19
+ if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
20
+ throw new Error('Glob patterns and exclusions are only supported for on-disk files.');
21
+ }
22
+ candidatePaths = await expandWithCustomFs(partitioned, fsModule);
23
+ }
24
+ const allowedLiteralDirs = partitioned.literalDirectories
25
+ .map((dir) => path.resolve(dir))
26
+ .filter((dir) => DEFAULT_IGNORED_DIRS.includes(path.basename(dir)));
27
+ const allowedLiteralFiles = partitioned.literalFiles.map((file) => path.resolve(file));
28
+ const resolvedLiteralDirs = new Set(allowedLiteralDirs);
29
+ const allowedPaths = new Set([...allowedLiteralDirs, ...allowedLiteralFiles]);
30
+ const ignoredWhitelist = await buildIgnoredWhitelist(candidatePaths, cwd, fsModule);
31
+ const ignoredLog = new Set();
32
+ const filteredCandidates = candidatePaths.filter((filePath) => {
33
+ const ignoredDir = findIgnoredAncestor(filePath, cwd, resolvedLiteralDirs, allowedPaths, ignoredWhitelist);
34
+ if (!ignoredDir) {
35
+ return true;
36
+ }
37
+ const displayFile = relativePath(filePath, cwd);
38
+ const key = `${ignoredDir}|${displayFile}`;
39
+ if (!ignoredLog.has(key)) {
40
+ console.log(`Skipping default-ignored path: ${displayFile} (matches ${ignoredDir})`);
41
+ ignoredLog.add(key);
42
+ }
43
+ return false;
44
+ });
45
+ if (filteredCandidates.length === 0) {
46
+ throw new FileValidationError('No files matched the provided --file patterns.', {
47
+ patterns: partitioned.globPatterns,
48
+ excludes: partitioned.excludePatterns,
49
+ });
50
+ }
51
+ const oversized = [];
52
+ const accepted = [];
53
+ for (const filePath of filteredCandidates) {
54
+ let stats;
55
+ try {
56
+ stats = await fsModule.stat(filePath);
57
+ }
58
+ catch (error) {
59
+ throw new FileValidationError(`Missing file or directory: ${relativePath(filePath, cwd)}`, { path: filePath }, error);
60
+ }
61
+ if (!stats.isFile()) {
62
+ continue;
63
+ }
64
+ if (maxFileSizeBytes && typeof stats.size === 'number' && stats.size > maxFileSizeBytes) {
65
+ const relative = path.relative(cwd, filePath) || filePath;
66
+ oversized.push(`${relative} (${formatBytes(stats.size)})`);
67
+ continue;
68
+ }
69
+ accepted.push(filePath);
70
+ }
71
+ if (oversized.length > 0) {
72
+ throw new FileValidationError(`The following files exceed the 1 MB limit:\n- ${oversized.join('\n- ')}`, {
73
+ files: oversized,
74
+ limitBytes: maxFileSizeBytes,
75
+ });
76
+ }
77
+ const files = [];
78
+ for (const filePath of accepted) {
79
+ const content = await fsModule.readFile(filePath, 'utf8');
80
+ files.push({ path: filePath, content });
81
+ }
82
+ return files;
83
+ }
84
+ async function partitionFileInputs(rawPaths, cwd, fsModule) {
85
+ const result = {
86
+ globPatterns: [],
87
+ excludePatterns: [],
88
+ literalFiles: [],
89
+ literalDirectories: [],
90
+ };
91
+ for (const entry of rawPaths) {
92
+ const raw = entry?.trim();
93
+ if (!raw) {
94
+ continue;
95
+ }
96
+ if (raw.startsWith('!')) {
97
+ const normalized = normalizeGlob(raw.slice(1), cwd);
98
+ if (normalized) {
99
+ result.excludePatterns.push(normalized);
100
+ }
101
+ continue;
102
+ }
103
+ if (fg.isDynamicPattern(raw)) {
104
+ result.globPatterns.push(normalizeGlob(raw, cwd));
105
+ continue;
106
+ }
107
+ const absolutePath = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
108
+ let stats;
109
+ try {
110
+ stats = await fsModule.stat(absolutePath);
111
+ }
112
+ catch (error) {
113
+ throw new FileValidationError(`Missing file or directory: ${raw}`, { path: absolutePath }, error);
114
+ }
115
+ if (stats.isDirectory()) {
116
+ result.literalDirectories.push(absolutePath);
117
+ }
118
+ else if (stats.isFile()) {
119
+ result.literalFiles.push(absolutePath);
120
+ }
121
+ else {
122
+ throw new FileValidationError(`Not a file or directory: ${raw}`, { path: absolutePath });
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ async function expandWithNativeGlob(partitioned, cwd) {
128
+ const patterns = [
129
+ ...partitioned.globPatterns,
130
+ ...partitioned.literalFiles.map((absPath) => toPosixRelativeOrBasename(absPath, cwd)),
131
+ ...partitioned.literalDirectories.map((absDir) => makeDirectoryPattern(toPosixRelative(absDir, cwd))),
132
+ ].filter(Boolean);
133
+ if (patterns.length === 0) {
134
+ return [];
135
+ }
136
+ const dotfileOptIn = patterns.some((pattern) => includesDotfileSegment(pattern));
137
+ const gitignoreSets = await loadGitignoreSets(cwd);
138
+ const matches = (await fg(patterns, {
139
+ cwd,
140
+ absolute: false,
141
+ dot: true,
142
+ ignore: partitioned.excludePatterns,
143
+ onlyFiles: true,
144
+ followSymbolicLinks: false,
145
+ suppressErrors: true,
146
+ }));
147
+ const resolved = matches.map((match) => path.resolve(cwd, match));
148
+ const filtered = resolved.filter((filePath) => !isGitignored(filePath, gitignoreSets));
149
+ const finalFiles = dotfileOptIn ? filtered : filtered.filter((filePath) => !path.basename(filePath).startsWith('.'));
150
+ return Array.from(new Set(finalFiles));
151
+ }
152
+ async function loadGitignoreSets(cwd) {
153
+ const gitignorePaths = await fg('**/.gitignore', {
154
+ cwd,
155
+ dot: true,
156
+ absolute: true,
157
+ onlyFiles: true,
158
+ followSymbolicLinks: false,
159
+ suppressErrors: true,
160
+ });
161
+ const sets = [];
162
+ for (const filePath of gitignorePaths) {
163
+ try {
164
+ const raw = await fs.readFile(filePath, 'utf8');
165
+ const patterns = raw
166
+ .split('\n')
167
+ .map((line) => line.trim())
168
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
169
+ if (patterns.length > 0) {
170
+ sets.push({ dir: path.dirname(filePath), patterns });
171
+ }
172
+ }
173
+ catch {
174
+ // Ignore unreadable .gitignore files
175
+ }
176
+ }
177
+ // Ensure deterministic parent-before-child ordering
178
+ return sets.sort((a, b) => a.dir.localeCompare(b.dir));
179
+ }
180
+ function isGitignored(filePath, sets) {
181
+ for (const { dir, patterns } of sets) {
182
+ if (!filePath.startsWith(dir)) {
183
+ continue;
184
+ }
185
+ const relative = path.relative(dir, filePath) || path.basename(filePath);
186
+ if (matchesAny(relative, patterns)) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
193
+ const whitelist = new Set();
194
+ for (const filePath of filePaths) {
195
+ const absolute = path.resolve(filePath);
196
+ const rel = path.relative(cwd, absolute);
197
+ const parts = rel.split(path.sep).filter(Boolean);
198
+ for (let i = 0; i < parts.length - 1; i += 1) {
199
+ const part = parts[i];
200
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
201
+ continue;
202
+ }
203
+ const dirPath = path.resolve(cwd, ...parts.slice(0, i + 1));
204
+ if (whitelist.has(dirPath)) {
205
+ continue;
206
+ }
207
+ try {
208
+ const stats = await fsModule.stat(path.join(dirPath, '.gitignore'));
209
+ if (stats.isFile()) {
210
+ whitelist.add(dirPath);
211
+ }
212
+ }
213
+ catch {
214
+ // no .gitignore at this level; keep ignored
215
+ }
216
+ }
217
+ }
218
+ return whitelist;
219
+ }
220
+ function findIgnoredAncestor(filePath, cwd, _literalDirs, allowedPaths, ignoredWhitelist) {
221
+ const absolute = path.resolve(filePath);
222
+ if (Array.from(allowedPaths).some((allowed) => absolute === allowed || absolute.startsWith(`${allowed}${path.sep}`))) {
223
+ return null; // explicitly requested path overrides default ignore when the ignored dir itself was passed
224
+ }
225
+ const rel = path.relative(cwd, absolute);
226
+ const parts = rel.split(path.sep);
227
+ for (let idx = 0; idx < parts.length; idx += 1) {
228
+ const part = parts[idx];
229
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
230
+ continue;
231
+ }
232
+ const ignoredDir = path.resolve(cwd, parts.slice(0, idx + 1).join(path.sep));
233
+ if (ignoredWhitelist.has(ignoredDir)) {
234
+ continue;
235
+ }
236
+ return part;
237
+ }
238
+ return null;
239
+ }
240
+ function matchesAny(relativePath, patterns) {
241
+ return patterns.some((pattern) => matchesPattern(relativePath, pattern));
242
+ }
243
+ function matchesPattern(relativePath, pattern) {
244
+ if (!pattern) {
245
+ return false;
246
+ }
247
+ const normalized = pattern.replace(/\\+/g, '/');
248
+ // Directory rule
249
+ if (normalized.endsWith('/')) {
250
+ const dir = normalized.slice(0, -1);
251
+ return relativePath === dir || relativePath.startsWith(`${dir}/`);
252
+ }
253
+ // Simple glob support (* and **)
254
+ const regex = globToRegex(normalized);
255
+ return regex.test(relativePath);
256
+ }
257
+ function globToRegex(pattern) {
258
+ const withMarkers = pattern.replace(/\*\*/g, '§§DOUBLESTAR§§').replace(/\*/g, '§§SINGLESTAR§§');
259
+ const escaped = withMarkers.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
260
+ const restored = escaped
261
+ .replace(/§§DOUBLESTAR§§/g, '.*')
262
+ .replace(/§§SINGLESTAR§§/g, '[^/]*');
263
+ return new RegExp(`^${restored}$`);
264
+ }
265
+ function includesDotfileSegment(pattern) {
266
+ const segments = pattern.split('/');
267
+ return segments.some((segment) => segment.startsWith('.') && segment.length > 1);
268
+ }
269
+ async function expandWithCustomFs(partitioned, fsModule) {
270
+ const paths = new Set();
271
+ partitioned.literalFiles.forEach((file) => {
272
+ paths.add(file);
273
+ });
274
+ for (const directory of partitioned.literalDirectories) {
275
+ const nested = await expandDirectoryRecursive(directory, fsModule);
276
+ nested.forEach((entry) => {
277
+ paths.add(entry);
278
+ });
279
+ }
280
+ return Array.from(paths);
281
+ }
282
+ async function expandDirectoryRecursive(directory, fsModule) {
283
+ const entries = await fsModule.readdir(directory);
284
+ const results = [];
285
+ for (const entry of entries) {
286
+ const childPath = path.join(directory, entry);
287
+ const stats = await fsModule.stat(childPath);
288
+ if (stats.isDirectory()) {
289
+ results.push(...(await expandDirectoryRecursive(childPath, fsModule)));
290
+ }
291
+ else if (stats.isFile()) {
292
+ results.push(childPath);
293
+ }
294
+ }
295
+ return results;
296
+ }
297
+ function makeDirectoryPattern(relative) {
298
+ if (relative === '.' || relative === '') {
299
+ return '**/*';
300
+ }
301
+ return `${stripTrailingSlashes(relative)}/**/*`;
302
+ }
303
+ function isNativeFsModule(fsModule) {
304
+ return ((fsModule.__nativeFs === true ||
305
+ (fsModule.readFile === DEFAULT_FS.readFile &&
306
+ fsModule.stat === DEFAULT_FS.stat &&
307
+ fsModule.readdir === DEFAULT_FS.readdir)));
308
+ }
309
+ function normalizeGlob(pattern, cwd) {
310
+ if (!pattern) {
311
+ return '';
312
+ }
313
+ let normalized = pattern;
314
+ if (path.isAbsolute(normalized)) {
315
+ normalized = path.relative(cwd, normalized);
316
+ }
317
+ normalized = toPosix(normalized);
318
+ if (normalized.startsWith('./')) {
319
+ normalized = normalized.slice(2);
320
+ }
321
+ return normalized;
322
+ }
323
+ function toPosix(value) {
324
+ return value.replace(/\\/g, '/');
325
+ }
326
+ function toPosixRelative(absPath, cwd) {
327
+ const relative = path.relative(cwd, absPath);
328
+ if (!relative) {
329
+ return '.';
330
+ }
331
+ return toPosix(relative);
332
+ }
333
+ function toPosixRelativeOrBasename(absPath, cwd) {
334
+ const relative = path.relative(cwd, absPath);
335
+ return toPosix(relative || path.basename(absPath));
336
+ }
337
+ function stripTrailingSlashes(value) {
338
+ const normalized = toPosix(value);
339
+ return normalized.replace(/\/+$/g, '');
340
+ }
341
+ function formatBytes(size) {
342
+ if (size >= 1024 * 1024) {
343
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
344
+ }
345
+ if (size >= 1024) {
346
+ return `${(size / 1024).toFixed(1)} KB`;
347
+ }
348
+ return `${size} B`;
349
+ }
350
+ function relativePath(targetPath, cwd) {
351
+ const relative = path.relative(cwd, targetPath);
352
+ return relative || targetPath;
353
+ }
354
+ export function createFileSections(files, cwd = process.cwd()) {
355
+ return files.map((file, index) => {
356
+ const relative = toPosix(path.relative(cwd, file.path) || file.path);
357
+ const sectionText = [
358
+ `### File ${index + 1}: ${relative}`,
359
+ '```',
360
+ file.content.trimEnd(),
361
+ '```',
362
+ ].join('\n');
363
+ return {
364
+ index: index + 1,
365
+ absolutePath: file.path,
366
+ displayPath: relative,
367
+ sectionText,
368
+ content: file.content,
369
+ };
370
+ });
371
+ }
@@ -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
+ }