@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.
- package/README.md +32 -4
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +178 -21
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/cookies.js +1 -1
- package/dist/src/browser/index.js +2 -1
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +4 -2
- package/dist/src/cli/dryRun.js +41 -5
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/runOptions.js +29 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +222 -10
- package/dist/src/cli/sessionRunner.js +21 -2
- package/dist/src/cli/tui/index.js +436 -0
- package/dist/src/config.js +27 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +158 -0
- package/dist/src/mcp/tools/sessionResources.js +64 -0
- package/dist/src/mcp/tools/sessions.js +106 -0
- package/dist/src/mcp/types.js +17 -0
- package/dist/src/mcp/utils.js +24 -0
- package/dist/src/oracle/files.js +143 -6
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +104 -71
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/sessionManager.js +65 -3
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +27 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- 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
|
+
}
|
package/dist/src/oracle/files.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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:
|
|
140
|
+
absolute: false,
|
|
117
141
|
dot: true,
|
|
118
142
|
ignore: partitioned.excludePatterns,
|
|
119
143
|
onlyFiles: true,
|
|
120
144
|
followSymbolicLinks: false,
|
|
121
|
-
});
|
|
122
|
-
|
|
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
|
+
}
|