@steipete/oracle 1.0.8 → 1.2.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 (106) hide show
  1. package/README.md +32 -4
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/bin/oracle-cli.js +178 -21
  4. package/dist/bin/oracle-mcp.js +6 -0
  5. package/dist/markdansi/types/index.js +4 -0
  6. package/dist/oracle/bin/oracle-cli.js +472 -0
  7. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  8. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  9. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  10. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  11. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  12. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  13. package/dist/oracle/src/browser/config.js +33 -0
  14. package/dist/oracle/src/browser/constants.js +40 -0
  15. package/dist/oracle/src/browser/cookies.js +210 -0
  16. package/dist/oracle/src/browser/domDebug.js +36 -0
  17. package/dist/oracle/src/browser/index.js +331 -0
  18. package/dist/oracle/src/browser/pageActions.js +5 -0
  19. package/dist/oracle/src/browser/prompt.js +88 -0
  20. package/dist/oracle/src/browser/promptSummary.js +20 -0
  21. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  22. package/dist/oracle/src/browser/types.js +1 -0
  23. package/dist/oracle/src/browser/utils.js +62 -0
  24. package/dist/oracle/src/browserMode.js +1 -0
  25. package/dist/oracle/src/cli/browserConfig.js +44 -0
  26. package/dist/oracle/src/cli/dryRun.js +59 -0
  27. package/dist/oracle/src/cli/engine.js +17 -0
  28. package/dist/oracle/src/cli/errorUtils.js +9 -0
  29. package/dist/oracle/src/cli/help.js +70 -0
  30. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  31. package/dist/oracle/src/cli/options.js +103 -0
  32. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  33. package/dist/oracle/src/cli/rootAlias.js +30 -0
  34. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  35. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  36. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  37. package/dist/oracle/src/heartbeat.js +43 -0
  38. package/dist/oracle/src/oracle/client.js +48 -0
  39. package/dist/oracle/src/oracle/config.js +29 -0
  40. package/dist/oracle/src/oracle/errors.js +101 -0
  41. package/dist/oracle/src/oracle/files.js +220 -0
  42. package/dist/oracle/src/oracle/format.js +33 -0
  43. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  44. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  45. package/dist/oracle/src/oracle/request.js +48 -0
  46. package/dist/oracle/src/oracle/run.js +444 -0
  47. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  48. package/dist/oracle/src/oracle/types.js +1 -0
  49. package/dist/oracle/src/oracle.js +9 -0
  50. package/dist/oracle/src/sessionManager.js +205 -0
  51. package/dist/oracle/src/version.js +39 -0
  52. package/dist/src/browser/actions/modelSelection.js +117 -29
  53. package/dist/src/browser/cookies.js +1 -1
  54. package/dist/src/browser/index.js +2 -1
  55. package/dist/src/browser/prompt.js +6 -5
  56. package/dist/src/browser/sessionRunner.js +4 -2
  57. package/dist/src/cli/dryRun.js +41 -5
  58. package/dist/src/cli/engine.js +7 -0
  59. package/dist/src/cli/help.js +1 -1
  60. package/dist/src/cli/hiddenAliases.js +17 -0
  61. package/dist/src/cli/markdownRenderer.js +97 -0
  62. package/dist/src/cli/notifier.js +223 -0
  63. package/dist/src/cli/promptRequirement.js +3 -0
  64. package/dist/src/cli/rootAlias.js +14 -0
  65. package/dist/src/cli/runOptions.js +29 -0
  66. package/dist/src/cli/sessionCommand.js +60 -2
  67. package/dist/src/cli/sessionDisplay.js +222 -10
  68. package/dist/src/cli/sessionRunner.js +21 -2
  69. package/dist/src/cli/tui/index.js +436 -0
  70. package/dist/src/config.js +27 -0
  71. package/dist/src/mcp/server.js +36 -0
  72. package/dist/src/mcp/tools/consult.js +158 -0
  73. package/dist/src/mcp/tools/sessionResources.js +64 -0
  74. package/dist/src/mcp/tools/sessions.js +106 -0
  75. package/dist/src/mcp/types.js +17 -0
  76. package/dist/src/mcp/utils.js +24 -0
  77. package/dist/src/oracle/files.js +143 -6
  78. package/dist/src/oracle/oscProgress.js +60 -0
  79. package/dist/src/oracle/run.js +104 -71
  80. package/dist/src/oracle/tokenEstimate.js +34 -0
  81. package/dist/src/sessionManager.js +65 -3
  82. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  83. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  84. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  85. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  86. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  87. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  88. package/dist/vendor/oracle-notifier/README.md +24 -0
  89. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  90. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  91. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  92. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  93. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  94. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  95. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  96. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  97. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  98. package/package.json +27 -9
  99. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  100. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  101. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  102. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  103. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  104. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  105. package/vendor/oracle-notifier/README.md +24 -0
  106. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,64 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import fs from 'node:fs/promises';
3
+ import { getSessionPaths, readSessionLog, readSessionMetadata } from '../../sessionManager.js';
4
+ // URIs:
5
+ // - oracle-session://<id>/metadata
6
+ // - oracle-session://<id>/log
7
+ // - oracle-session://<id>/request
8
+ export function registerSessionResources(server) {
9
+ const template = new ResourceTemplate('oracle-session://{id}/{kind}', { list: undefined });
10
+ server.registerResource('oracle-session', template, {
11
+ title: 'oracle session resources',
12
+ description: 'Read stored session metadata, log, or request payload.',
13
+ }, async (uri, variables) => {
14
+ const idRaw = variables?.id;
15
+ const kindRaw = variables?.kind;
16
+ // uri-template variables arrive as string | string[]; collapse to first value.
17
+ const id = Array.isArray(idRaw) ? idRaw[0] : idRaw;
18
+ const kind = Array.isArray(kindRaw) ? kindRaw[0] : kindRaw;
19
+ if (!id || !kind) {
20
+ throw new Error('Missing id or kind');
21
+ }
22
+ const paths = await getSessionPaths(id);
23
+ switch (kind) {
24
+ case 'metadata': {
25
+ const metadata = await readSessionMetadata(id);
26
+ if (!metadata) {
27
+ throw new Error(`Session "${id}" not found.`);
28
+ }
29
+ return {
30
+ contents: [
31
+ {
32
+ uri: uri.href,
33
+ text: JSON.stringify(metadata, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ case 'log': {
39
+ const log = await readSessionLog(id);
40
+ return {
41
+ contents: [
42
+ {
43
+ uri: uri.href,
44
+ text: log,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ case 'request': {
50
+ const raw = await fs.readFile(paths.request, 'utf8');
51
+ return {
52
+ contents: [
53
+ {
54
+ uri: uri.href,
55
+ text: raw,
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ default:
61
+ throw new Error(`Unsupported resource kind: ${kind}`);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { filterSessionsByRange, getSessionPaths, listSessionsMetadata, readSessionLog, readSessionMetadata, } from '../../sessionManager.js';
4
+ import { sessionsInputSchema } from '../types.js';
5
+ const sessionsInputShape = {
6
+ id: z.string().optional(),
7
+ hours: z.number().optional(),
8
+ limit: z.number().optional(),
9
+ includeAll: z.boolean().optional(),
10
+ detail: z.boolean().optional(),
11
+ };
12
+ const sessionsOutputShape = {
13
+ entries: z
14
+ .array(z.object({
15
+ id: z.string(),
16
+ createdAt: z.string(),
17
+ status: z.string(),
18
+ model: z.string().optional(),
19
+ mode: z.string().optional(),
20
+ }))
21
+ .optional(),
22
+ total: z.number().optional(),
23
+ truncated: z.boolean().optional(),
24
+ session: z
25
+ .object({
26
+ metadata: z.record(z.string(), z.any()),
27
+ log: z.string(),
28
+ request: z.record(z.string(), z.any()).optional(),
29
+ })
30
+ .optional(),
31
+ };
32
+ export function registerSessionsTool(server) {
33
+ server.registerTool('sessions', {
34
+ title: 'List or fetch oracle sessions',
35
+ description: 'List stored sessions (same defaults as `oracle status`) or, with id/slug, return a summary row. Pass detail:true to include metadata, log, and stored request for that session.',
36
+ inputSchema: sessionsInputShape,
37
+ outputSchema: sessionsOutputShape,
38
+ }, async (input) => {
39
+ const textContent = (text) => [{ type: 'text', text }];
40
+ const { id, hours = 24, limit = 100, includeAll = false, detail = false } = sessionsInputSchema.parse(input);
41
+ if (id) {
42
+ if (!detail) {
43
+ const metadata = await readSessionMetadata(id);
44
+ if (!metadata) {
45
+ throw new Error(`Session "${id}" not found.`);
46
+ }
47
+ return {
48
+ content: textContent(`${metadata.createdAt} | ${metadata.status} | ${metadata.model ?? 'n/a'} | ${metadata.id}`),
49
+ structuredContent: {
50
+ entries: [
51
+ {
52
+ id: metadata.id,
53
+ createdAt: metadata.createdAt,
54
+ status: metadata.status,
55
+ model: metadata.model,
56
+ mode: metadata.mode,
57
+ },
58
+ ],
59
+ total: 1,
60
+ truncated: false,
61
+ },
62
+ };
63
+ }
64
+ const metadata = await readSessionMetadata(id);
65
+ if (!metadata) {
66
+ throw new Error(`Session "${id}" not found.`);
67
+ }
68
+ const log = await readSessionLog(id);
69
+ let request;
70
+ try {
71
+ const paths = await getSessionPaths(id);
72
+ const raw = await fs.readFile(paths.request, 'utf8');
73
+ // Old sessions may lack a request payload; treat it as best-effort metadata.
74
+ request = JSON.parse(raw);
75
+ }
76
+ catch {
77
+ request = undefined;
78
+ }
79
+ return {
80
+ content: textContent(log),
81
+ structuredContent: { session: { metadata, log, request } },
82
+ };
83
+ }
84
+ const metas = await listSessionsMetadata();
85
+ const { entries, truncated, total } = filterSessionsByRange(metas, { hours, includeAll, limit });
86
+ return {
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: entries.map((entry) => `${entry.createdAt} | ${entry.status} | ${entry.model ?? 'n/a'} | ${entry.id}`).join('\n'),
91
+ },
92
+ ],
93
+ structuredContent: {
94
+ entries: entries.map((entry) => ({
95
+ id: entry.id,
96
+ createdAt: entry.createdAt,
97
+ status: entry.status,
98
+ model: entry.model,
99
+ mode: entry.mode,
100
+ })),
101
+ total,
102
+ truncated,
103
+ },
104
+ };
105
+ });
106
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const consultInputSchema = z.object({
3
+ prompt: z.string().min(1, 'Prompt is required.'),
4
+ files: z.array(z.string()).default([]),
5
+ model: z.string().optional(),
6
+ engine: z.enum(['api', 'browser']).optional(),
7
+ browserModelLabel: z.string().optional(),
8
+ search: z.boolean().optional(),
9
+ slug: z.string().optional(),
10
+ });
11
+ export const sessionsInputSchema = z.object({
12
+ id: z.string().optional(),
13
+ hours: z.number().optional(),
14
+ limit: z.number().optional(),
15
+ includeAll: z.boolean().optional(),
16
+ detail: z.boolean().optional(),
17
+ });
@@ -0,0 +1,24 @@
1
+ import { resolveRunOptionsFromConfig } from '../cli/runOptions.js';
2
+ import { Launcher } from 'chrome-launcher';
3
+ export function mapConsultToRunOptions({ prompt, files, model, engine, search, userConfig, env = process.env, }) {
4
+ // Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
5
+ // then overlay MCP-only overrides such as explicit search toggles.
6
+ const result = resolveRunOptionsFromConfig({ prompt, files, model, engine, userConfig, env });
7
+ if (typeof search === 'boolean') {
8
+ result.runOptions.search = search;
9
+ }
10
+ return result;
11
+ }
12
+ export function ensureBrowserAvailable(engine) {
13
+ if (engine !== 'browser') {
14
+ return null;
15
+ }
16
+ if (process.env.CHROME_PATH) {
17
+ return null;
18
+ }
19
+ const found = Launcher.getFirstInstallation();
20
+ if (!found) {
21
+ return 'Browser engine unavailable: no Chrome installation found and CHROME_PATH is unset.';
22
+ }
23
+ return null;
24
+ }
@@ -4,6 +4,7 @@ import fg from 'fast-glob';
4
4
  import { FileValidationError } from './errors.js';
5
5
  const 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'];
7
8
  export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = MAX_FILE_SIZE_BYTES } = {}) {
8
9
  if (!filePaths || filePaths.length === 0) {
9
10
  return [];
@@ -20,7 +21,28 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
20
21
  }
21
22
  candidatePaths = await expandWithCustomFs(partitioned, fsModule);
22
23
  }
23
- if (candidatePaths.length === 0) {
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) {
24
46
  throw new FileValidationError('No files matched the provided --file patterns.', {
25
47
  patterns: partitioned.globPatterns,
26
48
  excludes: partitioned.excludePatterns,
@@ -28,7 +50,7 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
28
50
  }
29
51
  const oversized = [];
30
52
  const accepted = [];
31
- for (const filePath of candidatePaths) {
53
+ for (const filePath of filteredCandidates) {
32
54
  let stats;
33
55
  try {
34
56
  stats = await fsModule.stat(filePath);
@@ -111,15 +133,130 @@ async function expandWithNativeGlob(partitioned, cwd) {
111
133
  if (patterns.length === 0) {
112
134
  return [];
113
135
  }
114
- const matches = await fg(patterns, {
136
+ const dotfileOptIn = patterns.some((pattern) => includesDotfileSegment(pattern));
137
+ const gitignoreSets = await loadGitignoreSets(cwd);
138
+ const matches = (await fg(patterns, {
115
139
  cwd,
116
- absolute: true,
140
+ absolute: false,
117
141
  dot: true,
118
142
  ignore: partitioned.excludePatterns,
119
143
  onlyFiles: true,
120
144
  followSymbolicLinks: false,
121
- });
122
- return Array.from(new Set(matches.map((match) => path.resolve(match))));
145
+ }));
146
+ const resolved = matches.map((match) => path.resolve(cwd, match));
147
+ const filtered = resolved.filter((filePath) => !isGitignored(filePath, gitignoreSets));
148
+ const finalFiles = dotfileOptIn ? filtered : filtered.filter((filePath) => !path.basename(filePath).startsWith('.'));
149
+ return Array.from(new Set(finalFiles));
150
+ }
151
+ async function loadGitignoreSets(cwd) {
152
+ const gitignorePaths = await fg('**/.gitignore', { cwd, dot: true, absolute: true, onlyFiles: true, followSymbolicLinks: false });
153
+ const sets = [];
154
+ for (const filePath of gitignorePaths) {
155
+ try {
156
+ const raw = await fs.readFile(filePath, 'utf8');
157
+ const patterns = raw
158
+ .split('\n')
159
+ .map((line) => line.trim())
160
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
161
+ if (patterns.length > 0) {
162
+ sets.push({ dir: path.dirname(filePath), patterns });
163
+ }
164
+ }
165
+ catch {
166
+ // Ignore unreadable .gitignore files
167
+ }
168
+ }
169
+ // Ensure deterministic parent-before-child ordering
170
+ return sets.sort((a, b) => a.dir.localeCompare(b.dir));
171
+ }
172
+ function isGitignored(filePath, sets) {
173
+ for (const { dir, patterns } of sets) {
174
+ if (!filePath.startsWith(dir)) {
175
+ continue;
176
+ }
177
+ const relative = path.relative(dir, filePath) || path.basename(filePath);
178
+ if (matchesAny(relative, patterns)) {
179
+ return true;
180
+ }
181
+ }
182
+ return false;
183
+ }
184
+ async function buildIgnoredWhitelist(filePaths, cwd, fsModule) {
185
+ const whitelist = new Set();
186
+ for (const filePath of filePaths) {
187
+ const absolute = path.resolve(filePath);
188
+ const rel = path.relative(cwd, absolute);
189
+ const parts = rel.split(path.sep).filter(Boolean);
190
+ for (let i = 0; i < parts.length - 1; i += 1) {
191
+ const part = parts[i];
192
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
193
+ continue;
194
+ }
195
+ const dirPath = path.resolve(cwd, ...parts.slice(0, i + 1));
196
+ if (whitelist.has(dirPath)) {
197
+ continue;
198
+ }
199
+ try {
200
+ const stats = await fsModule.stat(path.join(dirPath, '.gitignore'));
201
+ if (stats.isFile()) {
202
+ whitelist.add(dirPath);
203
+ }
204
+ }
205
+ catch {
206
+ // no .gitignore at this level; keep ignored
207
+ }
208
+ }
209
+ }
210
+ return whitelist;
211
+ }
212
+ function findIgnoredAncestor(filePath, cwd, _literalDirs, allowedPaths, ignoredWhitelist) {
213
+ const absolute = path.resolve(filePath);
214
+ if (Array.from(allowedPaths).some((allowed) => absolute === allowed || absolute.startsWith(`${allowed}${path.sep}`))) {
215
+ return null; // explicitly requested path overrides default ignore when the ignored dir itself was passed
216
+ }
217
+ const rel = path.relative(cwd, absolute);
218
+ const parts = rel.split(path.sep);
219
+ for (let idx = 0; idx < parts.length; idx += 1) {
220
+ const part = parts[idx];
221
+ if (!DEFAULT_IGNORED_DIRS.includes(part)) {
222
+ continue;
223
+ }
224
+ const ignoredDir = path.resolve(cwd, parts.slice(0, idx + 1).join(path.sep));
225
+ if (ignoredWhitelist.has(ignoredDir)) {
226
+ continue;
227
+ }
228
+ return part;
229
+ }
230
+ return null;
231
+ }
232
+ function matchesAny(relativePath, patterns) {
233
+ return patterns.some((pattern) => matchesPattern(relativePath, pattern));
234
+ }
235
+ function matchesPattern(relativePath, pattern) {
236
+ if (!pattern) {
237
+ return false;
238
+ }
239
+ const normalized = pattern.replace(/\\+/g, '/');
240
+ // Directory rule
241
+ if (normalized.endsWith('/')) {
242
+ const dir = normalized.slice(0, -1);
243
+ return relativePath === dir || relativePath.startsWith(`${dir}/`);
244
+ }
245
+ // Simple glob support (* and **)
246
+ const regex = globToRegex(normalized);
247
+ return regex.test(relativePath);
248
+ }
249
+ function globToRegex(pattern) {
250
+ const withMarkers = pattern.replace(/\*\*/g, '§§DOUBLESTAR§§').replace(/\*/g, '§§SINGLESTAR§§');
251
+ const escaped = withMarkers.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
252
+ const restored = escaped
253
+ .replace(/§§DOUBLESTAR§§/g, '.*')
254
+ .replace(/§§SINGLESTAR§§/g, '[^/]*');
255
+ return new RegExp(`^${restored}$`);
256
+ }
257
+ function includesDotfileSegment(pattern) {
258
+ const segments = pattern.split('/');
259
+ return segments.some((segment) => segment.startsWith('.') && segment.length > 1);
123
260
  }
124
261
  async function expandWithCustomFs(partitioned, fsModule) {
125
262
  const paths = new Set();
@@ -0,0 +1,60 @@
1
+ import process from 'node:process';
2
+ const OSC = '\u001b]9;4;';
3
+ const ST = '\u001b\\';
4
+ function sanitizeLabel(label) {
5
+ const withoutEscape = label.split('\u001b').join('');
6
+ const withoutBellAndSt = withoutEscape.replaceAll('\u0007', '').replaceAll('\u009c', '');
7
+ return withoutBellAndSt.replaceAll(']', '').trim();
8
+ }
9
+ export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
10
+ if (!isTty) {
11
+ return false;
12
+ }
13
+ if (env.ORACLE_NO_OSC_PROGRESS === '1') {
14
+ return false;
15
+ }
16
+ if (env.ORACLE_FORCE_OSC_PROGRESS === '1') {
17
+ return true;
18
+ }
19
+ const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
20
+ if (termProgram.includes('ghostty')) {
21
+ return true;
22
+ }
23
+ if (termProgram.includes('wezterm')) {
24
+ return true;
25
+ }
26
+ if (env.WT_SESSION) {
27
+ return true; // Windows Terminal exposes this
28
+ }
29
+ return false;
30
+ }
31
+ export function startOscProgress(options = {}) {
32
+ const { label = 'Waiting for OpenAI', targetMs = 10 * 60_000, write = (text) => process.stdout.write(text) } = options;
33
+ if (!supportsOscProgress(options.env, options.isTty)) {
34
+ return () => { };
35
+ }
36
+ const cleanLabel = sanitizeLabel(label);
37
+ const target = Math.max(targetMs, 1_000);
38
+ const send = (state, percent) => {
39
+ const clamped = Math.max(0, Math.min(100, Math.round(percent)));
40
+ write(`${OSC}${state};${clamped};${cleanLabel}${ST}`);
41
+ };
42
+ const startedAt = Date.now();
43
+ send(1, 0); // activate progress bar
44
+ const timer = setInterval(() => {
45
+ const elapsed = Date.now() - startedAt;
46
+ const percent = Math.min(99, (elapsed / target) * 100);
47
+ send(1, percent);
48
+ }, 900);
49
+ timer.unref?.();
50
+ let stopped = false;
51
+ return () => {
52
+ // biome-ignore lint/nursery/noUnnecessaryConditions: multiple callers may try to stop
53
+ if (stopped) {
54
+ return;
55
+ }
56
+ stopped = true;
57
+ clearInterval(timer);
58
+ send(0, 0); // clear the progress bar
59
+ };
60
+ }