@thegitai/cli 1.0.0-beta.1
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/LICENSE +20 -0
- package/README.md +30 -0
- package/dist/bin/ai.js +438 -0
- package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
- package/dist/parsers/tree-sitter-c.wasm +0 -0
- package/dist/parsers/tree-sitter-cpp.wasm +0 -0
- package/dist/parsers/tree-sitter-css.wasm +0 -0
- package/dist/parsers/tree-sitter-go.wasm +0 -0
- package/dist/parsers/tree-sitter-html.wasm +0 -0
- package/dist/parsers/tree-sitter-java.wasm +0 -0
- package/dist/parsers/tree-sitter-javascript.wasm +0 -0
- package/dist/parsers/tree-sitter-objc.wasm +0 -0
- package/dist/parsers/tree-sitter-php.wasm +0 -0
- package/dist/parsers/tree-sitter-python.wasm +0 -0
- package/dist/parsers/tree-sitter-ruby.wasm +0 -0
- package/dist/parsers/tree-sitter-rust.wasm +0 -0
- package/dist/parsers/tree-sitter-tsx.wasm +0 -0
- package/dist/parsers/tree-sitter-typescript.wasm +0 -0
- package/dist/src/agent-mode.js +142 -0
- package/dist/src/api/auth.js +81 -0
- package/dist/src/api/browser-login.js +184 -0
- package/dist/src/api/chat.js +346 -0
- package/dist/src/api/contracts.js +1 -0
- package/dist/src/api/http.js +44 -0
- package/dist/src/api/index.js +11 -0
- package/dist/src/api/models.js +110 -0
- package/dist/src/api/sessions.js +72 -0
- package/dist/src/artifact-policy.js +207 -0
- package/dist/src/client-state.js +14 -0
- package/dist/src/core/clipboard.js +208 -0
- package/dist/src/core/open-url.js +32 -0
- package/dist/src/edit-journal.js +133 -0
- package/dist/src/executor.js +924 -0
- package/dist/src/extractors/cpp.js +18 -0
- package/dist/src/extractors/csharp.js +16 -0
- package/dist/src/extractors/css.js +12 -0
- package/dist/src/extractors/go.js +27 -0
- package/dist/src/extractors/index.js +52 -0
- package/dist/src/extractors/java.js +14 -0
- package/dist/src/extractors/javascript.js +33 -0
- package/dist/src/extractors/objc.js +14 -0
- package/dist/src/extractors/php.js +20 -0
- package/dist/src/extractors/python.js +11 -0
- package/dist/src/extractors/ruby.js +13 -0
- package/dist/src/extractors/rust.js +17 -0
- package/dist/src/extractors/utils.js +58 -0
- package/dist/src/help-text.js +125 -0
- package/dist/src/markdown-renderer.js +112 -0
- package/dist/src/patcher.js +279 -0
- package/dist/src/project-index.js +221 -0
- package/dist/src/repo-map-languages.js +100 -0
- package/dist/src/runtime-mode.js +35 -0
- package/dist/src/scanner.js +362 -0
- package/dist/src/secret-preview.js +137 -0
- package/dist/src/session-exit.js +17 -0
- package/dist/src/session-safety.js +1012 -0
- package/dist/src/session-store.js +266 -0
- package/dist/src/session.js +93 -0
- package/dist/src/tool-executor.js +188 -0
- package/dist/src/tools/code-intel.js +472 -0
- package/dist/src/tools/delete-file.js +27 -0
- package/dist/src/tools/exec-utils.js +17 -0
- package/dist/src/tools/find-symbol.js +70 -0
- package/dist/src/tools/get-diagnostics.js +22 -0
- package/dist/src/tools/grep-code.js +331 -0
- package/dist/src/tools/hover-symbol.js +95 -0
- package/dist/src/tools/index.js +73 -0
- package/dist/src/tools/list-checkpoints.js +11 -0
- package/dist/src/tools/list-directories.js +16 -0
- package/dist/src/tools/list-files.js +13 -0
- package/dist/src/tools/list-session-edits.js +9 -0
- package/dist/src/tools/list-symbols.js +55 -0
- package/dist/src/tools/patch-file.js +88 -0
- package/dist/src/tools/path-listing.js +83 -0
- package/dist/src/tools/read-document.js +111 -0
- package/dist/src/tools/read-file.js +109 -0
- package/dist/src/tools/restore-checkpoint.js +100 -0
- package/dist/src/tools/ripgrep.js +29 -0
- package/dist/src/tools/run-command.js +94 -0
- package/dist/src/tools/run-node-script.js +210 -0
- package/dist/src/tools/search-code.js +37 -0
- package/dist/src/tools/shell-diagnostics.js +707 -0
- package/dist/src/tools/signature-help.js +118 -0
- package/dist/src/tools/str-replace.js +193 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/undo-edit.js +202 -0
- package/dist/src/tools/write-file.js +59 -0
- package/dist/src/tree-sitter-runtime.js +135 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ui/paste-collapse.js +22 -0
- package/dist/src/ui/prompt-history-store.js +96 -0
- package/dist/src/ui/repl.js +2238 -0
- package/dist/src/ui/tui/bridge.js +175 -0
- package/dist/src/ui/tui/build-frame.js +718 -0
- package/dist/src/ui/tui/markdown-render.js +455 -0
- package/dist/src/ui/tui/shell-input.js +488 -0
- package/dist/src/ui/tui/text.js +30 -0
- package/dist/src/ui/tui/types.js +1 -0
- package/dist/src/usage.js +47 -0
- package/dist/src/utils.js +38 -0
- package/package.json +38 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ARTIFACT_IGNORE_FILES, BINARY_ARTIFACT_EXTENSIONS, } from '../artifact-policy.js';
|
|
5
|
+
import { ALWAYS_IGNORE_DIRS, listProjectFiles, shouldIgnorePath, } from '../scanner.js';
|
|
6
|
+
import { clampInteger } from '../utils.js';
|
|
7
|
+
import { getRipgrepPathCandidates, isRipgrepExecutionUnavailable, } from './ripgrep.js';
|
|
8
|
+
import { getCommandErrorText, getCommandExitCode, } from './exec-utils.js';
|
|
9
|
+
const MAX_RESULTS = 200;
|
|
10
|
+
const MAX_OUTPUT_CHARS = 8000;
|
|
11
|
+
const MAX_FALLBACK_FILE_BYTES = 512 * 1024;
|
|
12
|
+
const MAX_FALLBACK_FILES_TO_SCAN = 5000;
|
|
13
|
+
function buildRgIgnoreArgs() {
|
|
14
|
+
const args = [];
|
|
15
|
+
for (const dir of ALWAYS_IGNORE_DIRS) {
|
|
16
|
+
args.push('--glob', `!${dir}`);
|
|
17
|
+
args.push('--glob', `!**/${dir}/**`);
|
|
18
|
+
}
|
|
19
|
+
args.push('--glob', '!*.min.js');
|
|
20
|
+
args.push('--glob', '!*.min.css');
|
|
21
|
+
args.push('--glob', '!*.map');
|
|
22
|
+
args.push('--glob', '!package-lock.json');
|
|
23
|
+
args.push('--glob', '!yarn.lock');
|
|
24
|
+
args.push('--glob', '!pnpm-lock.yaml');
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
function isRegexParseFailure(error) {
|
|
28
|
+
if (getCommandExitCode(error) !== 2)
|
|
29
|
+
return false;
|
|
30
|
+
return /regex parse error|repetition quantifier|unclosed group|invalid regex/i.test(getCommandErrorText(error));
|
|
31
|
+
}
|
|
32
|
+
function escapeRegExp(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
}
|
|
35
|
+
function uniqueNonEmptyStrings(values) {
|
|
36
|
+
const unique = [];
|
|
37
|
+
for (const value of values) {
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (trimmed && !unique.includes(trimmed)) {
|
|
40
|
+
unique.push(trimmed);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return unique;
|
|
44
|
+
}
|
|
45
|
+
function literalPatternCandidates(pattern) {
|
|
46
|
+
const trimmed = pattern.trim();
|
|
47
|
+
const candidates = [trimmed];
|
|
48
|
+
if (trimmed.startsWith('(?:')) {
|
|
49
|
+
const inner = trimmed.slice(3).trim();
|
|
50
|
+
candidates.push(inner.endsWith(')') ? inner.slice(0, -1).trim() : inner);
|
|
51
|
+
}
|
|
52
|
+
else if (trimmed.startsWith('(')) {
|
|
53
|
+
const inner = trimmed.slice(1).trim();
|
|
54
|
+
candidates.push(inner.endsWith(')') ? inner.slice(0, -1).trim() : inner);
|
|
55
|
+
}
|
|
56
|
+
return uniqueNonEmptyStrings(candidates);
|
|
57
|
+
}
|
|
58
|
+
function globToRegExp(glob) {
|
|
59
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
60
|
+
let pattern = '^';
|
|
61
|
+
for (let index = 0; index < normalized.length;) {
|
|
62
|
+
const char = normalized[index] ?? '';
|
|
63
|
+
const next = normalized[index + 1] ?? '';
|
|
64
|
+
const afterNext = normalized[index + 2] ?? '';
|
|
65
|
+
if (char === '*' && next === '*' && afterNext === '/') {
|
|
66
|
+
pattern += '(?:.*/)?';
|
|
67
|
+
index += 3;
|
|
68
|
+
}
|
|
69
|
+
else if (char === '*' && next === '*') {
|
|
70
|
+
pattern += '.*';
|
|
71
|
+
index += 2;
|
|
72
|
+
}
|
|
73
|
+
else if (char === '*') {
|
|
74
|
+
pattern += '[^/]*';
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
else if (char === '?') {
|
|
78
|
+
pattern += '[^/]';
|
|
79
|
+
index += 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
pattern += escapeRegExp(char);
|
|
83
|
+
index += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return RegExp(`${pattern}$`);
|
|
87
|
+
}
|
|
88
|
+
function matchesFilePattern(filePath, filePattern) {
|
|
89
|
+
if (!filePattern)
|
|
90
|
+
return true;
|
|
91
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
92
|
+
const pattern = filePattern.replace(/\\/g, '/');
|
|
93
|
+
const negated = pattern.startsWith('!');
|
|
94
|
+
const effectivePattern = negated ? pattern.slice(1) : pattern;
|
|
95
|
+
const regex = globToRegExp(effectivePattern);
|
|
96
|
+
const matches = regex.test(normalized) ||
|
|
97
|
+
(!effectivePattern.includes('/') && regex.test(path.basename(normalized)));
|
|
98
|
+
return negated ? !matches : matches;
|
|
99
|
+
}
|
|
100
|
+
function isBinaryText(content) {
|
|
101
|
+
return content.slice(0, 8192).includes('\0');
|
|
102
|
+
}
|
|
103
|
+
function shouldSkipFallbackFile(rootDir, filePath) {
|
|
104
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
105
|
+
if (shouldIgnorePath(normalized))
|
|
106
|
+
return true;
|
|
107
|
+
if (ARTIFACT_IGNORE_FILES.has(path.basename(normalized)))
|
|
108
|
+
return true;
|
|
109
|
+
if (normalized.endsWith('.min.js') ||
|
|
110
|
+
normalized.endsWith('.min.css') ||
|
|
111
|
+
normalized.endsWith('.map')) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (BINARY_ARTIFACT_EXTENSIONS.has(path.extname(normalized).toLowerCase())) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const stat = statSync(path.join(rootDir, normalized));
|
|
119
|
+
return !stat.isFile() || stat.size > MAX_FALLBACK_FILE_BYTES;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function normalizeMatches(raw, limit) {
|
|
126
|
+
const limitedLines = raw.split('\n').filter(Boolean).slice(0, limit);
|
|
127
|
+
const limited = limitedLines.join('\n');
|
|
128
|
+
const matchCount = limitedLines.length;
|
|
129
|
+
const matches = limited.length > MAX_OUTPUT_CHARS
|
|
130
|
+
? `${limited.slice(0, MAX_OUTPUT_CHARS)}\n... (truncated)`
|
|
131
|
+
: limited;
|
|
132
|
+
return { matchCount, matches };
|
|
133
|
+
}
|
|
134
|
+
function grepCodeWithoutRipgrep(rootDir, pattern, options) {
|
|
135
|
+
let regexes;
|
|
136
|
+
let literalMode = false;
|
|
137
|
+
try {
|
|
138
|
+
regexes = [RegExp(pattern, options.caseSensitive ? '' : 'i')];
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
literalMode = true;
|
|
142
|
+
regexes = literalPatternCandidates(pattern).map((candidate) => RegExp(escapeRegExp(candidate), options.caseSensitive ? '' : 'i'));
|
|
143
|
+
}
|
|
144
|
+
const allFiles = listProjectFiles(rootDir)
|
|
145
|
+
.filter((filePath) => matchesFilePattern(filePath, options.filePattern))
|
|
146
|
+
.filter((filePath) => !shouldSkipFallbackFile(rootDir, filePath));
|
|
147
|
+
const files = allFiles.slice(0, MAX_FALLBACK_FILES_TO_SCAN);
|
|
148
|
+
const matches = [];
|
|
149
|
+
for (const filePath of files) {
|
|
150
|
+
if (matches.length >= options.limit)
|
|
151
|
+
break;
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = readFileSync(path.join(rootDir, filePath), 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (isBinaryText(content))
|
|
160
|
+
continue;
|
|
161
|
+
const lines = content.split(/\r?\n/);
|
|
162
|
+
for (let index = 0; index < lines.length; index++) {
|
|
163
|
+
if (matches.length >= options.limit)
|
|
164
|
+
break;
|
|
165
|
+
const line = lines[index] ?? '';
|
|
166
|
+
if (regexes.some((regex) => regex.test(line))) {
|
|
167
|
+
matches.push(`${filePath}:${index + 1}:${line}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const normalized = normalizeMatches(matches.join('\n'), options.limit);
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
pattern,
|
|
175
|
+
matchCount: normalized.matchCount,
|
|
176
|
+
matches: normalized.matches,
|
|
177
|
+
searchEngine: 'node_fallback',
|
|
178
|
+
searchMode: literalMode ? 'literal' : 'regex',
|
|
179
|
+
warning: allFiles.length > files.length
|
|
180
|
+
? `ripgrep is unavailable; searched the first ${files.length} matching files with the built-in fallback.`
|
|
181
|
+
: 'ripgrep is unavailable; searched with the built-in fallback.',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function buildRipgrepArgs(options) {
|
|
185
|
+
const rgArgs = [
|
|
186
|
+
'-n',
|
|
187
|
+
'--no-heading',
|
|
188
|
+
'--color',
|
|
189
|
+
'never',
|
|
190
|
+
...buildRgIgnoreArgs(),
|
|
191
|
+
];
|
|
192
|
+
if (options.fixedStrings) {
|
|
193
|
+
rgArgs.push('--fixed-strings');
|
|
194
|
+
}
|
|
195
|
+
if (!options.caseSensitive) {
|
|
196
|
+
rgArgs.push('-i');
|
|
197
|
+
}
|
|
198
|
+
if (options.filePattern) {
|
|
199
|
+
rgArgs.push('--glob', options.filePattern);
|
|
200
|
+
}
|
|
201
|
+
rgArgs.push('-m', String(options.limit));
|
|
202
|
+
rgArgs.push('--', options.pattern, '.');
|
|
203
|
+
return rgArgs;
|
|
204
|
+
}
|
|
205
|
+
function runRipgrep(rootDir, options) {
|
|
206
|
+
const args = buildRipgrepArgs(options);
|
|
207
|
+
let unavailableError = null;
|
|
208
|
+
for (const candidate of getRipgrepPathCandidates()) {
|
|
209
|
+
try {
|
|
210
|
+
return execFileSync(candidate, args, {
|
|
211
|
+
cwd: rootDir,
|
|
212
|
+
encoding: 'utf-8',
|
|
213
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
214
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
215
|
+
timeout: 15000,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (!isRipgrepExecutionUnavailable(error)) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
unavailableError = error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw unavailableError;
|
|
226
|
+
}
|
|
227
|
+
function runRipgrepLiteralSearch(rootDir, options) {
|
|
228
|
+
for (const literalPattern of literalPatternCandidates(options.pattern)) {
|
|
229
|
+
try {
|
|
230
|
+
return runRipgrep(rootDir, {
|
|
231
|
+
...options,
|
|
232
|
+
fixedStrings: true,
|
|
233
|
+
pattern: literalPattern,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (literalErr) {
|
|
237
|
+
if (getCommandExitCode(literalErr) === 1) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
throw literalErr;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
export function grepCode(rootDir, args) {
|
|
246
|
+
const pattern = String(args.pattern ?? '').trim();
|
|
247
|
+
if (!pattern) {
|
|
248
|
+
return { ok: false, error: 'pattern is required' };
|
|
249
|
+
}
|
|
250
|
+
const limit = clampInteger(args.limit, 50, MAX_RESULTS);
|
|
251
|
+
const filePattern = args.filePattern ? String(args.filePattern).trim() : '';
|
|
252
|
+
const caseSensitive = args.caseSensitive !== false;
|
|
253
|
+
try {
|
|
254
|
+
const raw = runRipgrep(rootDir, {
|
|
255
|
+
caseSensitive,
|
|
256
|
+
filePattern,
|
|
257
|
+
fixedStrings: false,
|
|
258
|
+
limit,
|
|
259
|
+
pattern,
|
|
260
|
+
});
|
|
261
|
+
const { matchCount, matches } = normalizeMatches(raw, limit);
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
pattern,
|
|
265
|
+
matchCount,
|
|
266
|
+
matches,
|
|
267
|
+
searchEngine: 'ripgrep',
|
|
268
|
+
searchMode: 'regex',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (getCommandExitCode(err) === 1) {
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
pattern,
|
|
276
|
+
matchCount: 0,
|
|
277
|
+
matches: '',
|
|
278
|
+
searchEngine: 'ripgrep',
|
|
279
|
+
searchMode: 'regex',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (isRegexParseFailure(err)) {
|
|
283
|
+
try {
|
|
284
|
+
const raw = runRipgrepLiteralSearch(rootDir, {
|
|
285
|
+
caseSensitive,
|
|
286
|
+
filePattern,
|
|
287
|
+
limit,
|
|
288
|
+
pattern,
|
|
289
|
+
});
|
|
290
|
+
if (raw === null) {
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
pattern,
|
|
294
|
+
matchCount: 0,
|
|
295
|
+
matches: '',
|
|
296
|
+
searchEngine: 'ripgrep',
|
|
297
|
+
searchMode: 'literal',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const { matchCount, matches } = normalizeMatches(raw, limit);
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
pattern,
|
|
304
|
+
matchCount,
|
|
305
|
+
matches,
|
|
306
|
+
searchEngine: 'ripgrep',
|
|
307
|
+
searchMode: 'literal',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (literalErr) {
|
|
311
|
+
if (!isRipgrepExecutionUnavailable(literalErr)) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: 'ripgrep literal search failed.',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (isRipgrepExecutionUnavailable(err)) {
|
|
320
|
+
return grepCodeWithoutRipgrep(rootDir, pattern, {
|
|
321
|
+
limit,
|
|
322
|
+
filePattern,
|
|
323
|
+
caseSensitive,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
error: 'ripgrep search failed.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { extractIdentifierAtPosition, isDefinitionLine, readProjectText, searchTextMatches, } from './code-intel.js';
|
|
2
|
+
const MAX_CONTEXT_LINES = 15;
|
|
3
|
+
const MAX_OUTPUT_CHARS = 4000;
|
|
4
|
+
async function findDefinitionContext(rootDir, symbol) {
|
|
5
|
+
const matches = await searchTextMatches(rootDir, symbol, { limit: 40 });
|
|
6
|
+
for (const match of matches) {
|
|
7
|
+
if (!isDefinitionLine(match.content, symbol))
|
|
8
|
+
continue;
|
|
9
|
+
return extractDefinitionBlock(rootDir, match.filePath, match.line, symbol);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function extractDefinitionBlock(rootDir, filePath, lineNum, symbol) {
|
|
14
|
+
let fileContent;
|
|
15
|
+
try {
|
|
16
|
+
fileContent = readProjectText(rootDir, filePath);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const lines = fileContent.split('\n');
|
|
22
|
+
const defIdx = lineNum - 1;
|
|
23
|
+
let start = defIdx;
|
|
24
|
+
while (start > 0) {
|
|
25
|
+
const prev = lines[start - 1].trim();
|
|
26
|
+
if (prev.startsWith('///') ||
|
|
27
|
+
prev.startsWith('//!') ||
|
|
28
|
+
prev.startsWith('#[') ||
|
|
29
|
+
prev.startsWith('#![') ||
|
|
30
|
+
prev.startsWith('#') ||
|
|
31
|
+
prev.startsWith('@') ||
|
|
32
|
+
prev.startsWith('/**') ||
|
|
33
|
+
prev.startsWith('*') ||
|
|
34
|
+
prev.startsWith('//') ||
|
|
35
|
+
prev === '') {
|
|
36
|
+
if (prev === '' && start > 1) {
|
|
37
|
+
const prevPrev = lines[start - 2].trim();
|
|
38
|
+
if (!prevPrev.startsWith('//') &&
|
|
39
|
+
!prevPrev.startsWith('*') &&
|
|
40
|
+
!prevPrev.startsWith('#') &&
|
|
41
|
+
!prevPrev.startsWith('@'))
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
start--;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const end = Math.min(lines.length, defIdx + MAX_CONTEXT_LINES);
|
|
51
|
+
const block = lines.slice(start, end);
|
|
52
|
+
return `${symbol} — ${filePath}:${lineNum}\n\`\`\`\n${block.join('\n')}\n\`\`\``;
|
|
53
|
+
}
|
|
54
|
+
export async function hoverSymbol(context, args) {
|
|
55
|
+
const filePath = String(args.filePath ?? '').trim();
|
|
56
|
+
const line = Number(args.line ?? 0);
|
|
57
|
+
const column = Number(args.column ?? 0);
|
|
58
|
+
if (!filePath || !line || !column) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: 'hover_symbol requires filePath, line, and column.',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
let content;
|
|
65
|
+
try {
|
|
66
|
+
content = readProjectText(context.rootDir, filePath);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ok: false, error: `Could not read ${filePath}.` };
|
|
70
|
+
}
|
|
71
|
+
const symbol = extractIdentifierAtPosition(content, line, column);
|
|
72
|
+
if (!symbol) {
|
|
73
|
+
return { ok: false, error: 'No identifier found at the given position.' };
|
|
74
|
+
}
|
|
75
|
+
const hoverContent = await findDefinitionContext(context.rootDir, symbol);
|
|
76
|
+
if (!hoverContent) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: `Could not find definition for "${symbol}". Use find_symbol or grep_code.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const output = hoverContent.length > MAX_OUTPUT_CHARS
|
|
83
|
+
? `${hoverContent.slice(0, MAX_OUTPUT_CHARS)}\n...(truncated)`
|
|
84
|
+
: hoverContent;
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
provider: 'text_search',
|
|
88
|
+
available: true,
|
|
89
|
+
filePath,
|
|
90
|
+
line,
|
|
91
|
+
column,
|
|
92
|
+
contents: output,
|
|
93
|
+
output,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { deleteFile } from './delete-file.js';
|
|
2
|
+
import { findSymbol } from './find-symbol.js';
|
|
3
|
+
import { getDiagnostics } from './get-diagnostics.js';
|
|
4
|
+
import { grepCode } from './grep-code.js';
|
|
5
|
+
import { hoverSymbol } from './hover-symbol.js';
|
|
6
|
+
import { listDirectories } from './list-directories.js';
|
|
7
|
+
import { listCheckpoints } from './list-checkpoints.js';
|
|
8
|
+
import { listFiles } from './list-files.js';
|
|
9
|
+
import { listSessionEdits } from './list-session-edits.js';
|
|
10
|
+
import { listSymbols } from './list-symbols.js';
|
|
11
|
+
import { patchFile } from './patch-file.js';
|
|
12
|
+
import { readDocument } from './read-document.js';
|
|
13
|
+
import { readFile } from './read-file.js';
|
|
14
|
+
import { runShellCommand } from './run-command.js';
|
|
15
|
+
import { runNodeScript } from './run-node-script.js';
|
|
16
|
+
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
17
|
+
import { searchCode } from './search-code.js';
|
|
18
|
+
import { getSignatureHelp } from './signature-help.js';
|
|
19
|
+
import { strReplace } from './str-replace.js';
|
|
20
|
+
import { undoEdit } from './undo-edit.js';
|
|
21
|
+
import { writeFile } from './write-file.js';
|
|
22
|
+
export const TOOL_MAP = {
|
|
23
|
+
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
24
|
+
list_files: (context, args) => listFiles(context, args),
|
|
25
|
+
list_directories: (context, args) => listDirectories(context, args),
|
|
26
|
+
read_file: (context, args) => readFile(context, args),
|
|
27
|
+
read_document: (context, args) => readDocument(context.rootDir, args, context.env),
|
|
28
|
+
grep_code: (context, args) => grepCode(context.rootDir, args),
|
|
29
|
+
find_symbol: (context, args) => findSymbol(context, args),
|
|
30
|
+
list_symbols: (context, args) => listSymbols(context, args),
|
|
31
|
+
hover_symbol: (context, args) => hoverSymbol(context, args),
|
|
32
|
+
signature_help: (context, args) => getSignatureHelp(context, args),
|
|
33
|
+
get_diagnostics: (context, args) => getDiagnostics(context, args),
|
|
34
|
+
list_checkpoints: (context) => listCheckpoints(context),
|
|
35
|
+
list_session_edits: (context) => listSessionEdits(context),
|
|
36
|
+
restore_to_checkpoint: restoreToCheckpoint,
|
|
37
|
+
restore_files_to_checkpoint: restoreFilesToCheckpoint,
|
|
38
|
+
patch_file: patchFile,
|
|
39
|
+
str_replace: strReplace,
|
|
40
|
+
write_file: writeFile,
|
|
41
|
+
delete_file: deleteFile,
|
|
42
|
+
undo_edit: undoEdit,
|
|
43
|
+
run_command: runShellCommand,
|
|
44
|
+
run_node_script: runNodeScript,
|
|
45
|
+
};
|
|
46
|
+
function invalidToolCall(error) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error,
|
|
50
|
+
failureCategory: 'malformed_arguments',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function normalizeArgs(value) {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
export async function dispatchTool(context, call) {
|
|
60
|
+
const toolName = String(call?.name ?? '').trim();
|
|
61
|
+
if (!toolName) {
|
|
62
|
+
return invalidToolCall('Tool call name is required.');
|
|
63
|
+
}
|
|
64
|
+
const toolFn = TOOL_MAP[toolName];
|
|
65
|
+
if (!toolFn) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: `Unknown tool: ${toolName}`,
|
|
69
|
+
failureCategory: 'unknown_tool',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return await toolFn(context, normalizeArgs(call.args));
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { listCheckpointSummaries } from '../session-safety.js';
|
|
2
|
+
export async function listCheckpoints(context) {
|
|
3
|
+
const checkpoints = context.safety
|
|
4
|
+
? listCheckpointSummaries(context.safety)
|
|
5
|
+
: [];
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
checkpoints,
|
|
9
|
+
total: checkpoints.length,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { clampInteger } from '../utils.js';
|
|
2
|
+
import { listFilesystemDirectories } from './path-listing.js';
|
|
3
|
+
export async function listDirectories(context, args) {
|
|
4
|
+
const pattern = String(args.pattern ?? '').trim();
|
|
5
|
+
const limit = clampInteger(args.limit, 200, 500);
|
|
6
|
+
const directories = listFilesystemDirectories(context.rootDir, {
|
|
7
|
+
pattern,
|
|
8
|
+
limit,
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
pattern,
|
|
13
|
+
directories,
|
|
14
|
+
total: directories.length,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { clampInteger } from '../utils.js';
|
|
2
|
+
import { listFilesystemFiles } from './path-listing.js';
|
|
3
|
+
export async function listFiles(context, args) {
|
|
4
|
+
const pattern = String(args.pattern ?? '').trim();
|
|
5
|
+
const limit = clampInteger(args.limit, 200, 500);
|
|
6
|
+
const files = listFilesystemFiles(context.rootDir, { pattern, limit });
|
|
7
|
+
return {
|
|
8
|
+
ok: true,
|
|
9
|
+
pattern,
|
|
10
|
+
files,
|
|
11
|
+
total: files.length,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { shouldIgnoreArtifactPath } from '../artifact-policy.js';
|
|
2
|
+
import { extractSymbolsFromFile, listProjectFiles, normalizeProjectFilePath, } from './code-intel.js';
|
|
3
|
+
function formatRegexSymbolsOutput(symbols) {
|
|
4
|
+
if (!symbols.length)
|
|
5
|
+
return 'No symbols found.';
|
|
6
|
+
const lines = symbols.map((s) => `- ${s.kind} ${s.name} @ ${s.filePath}:${s.selectionLine}:${s.selectionColumn}`);
|
|
7
|
+
return lines.join('\n');
|
|
8
|
+
}
|
|
9
|
+
export async function listSymbols(context, args) {
|
|
10
|
+
const scope = args.scope === 'workspace' ? 'workspace' : 'document';
|
|
11
|
+
const filePath = String(args.filePath ?? '').trim();
|
|
12
|
+
if (scope === 'document' && !filePath) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: 'filePath is required for document symbol queries.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (scope === 'document' &&
|
|
19
|
+
shouldIgnoreArtifactPath(normalizeProjectFilePath(context.rootDir, filePath))) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: 'This path is not permitted.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const limit = Math.max(1, Math.min(Number(args.limit ?? 40), 100));
|
|
26
|
+
const query = String(args.query ?? '').trim() || undefined;
|
|
27
|
+
const symbols = scope === 'workspace'
|
|
28
|
+
? await listWorkspaceSymbols(context.rootDir, query, limit)
|
|
29
|
+
: extractSymbolsFromFile(context.rootDir, filePath, query, limit);
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
provider: 'regex',
|
|
33
|
+
available: true,
|
|
34
|
+
scope,
|
|
35
|
+
filePath: scope === 'document' ? filePath : undefined,
|
|
36
|
+
total: symbols.length,
|
|
37
|
+
symbols,
|
|
38
|
+
output: formatRegexSymbolsOutput(symbols),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function listWorkspaceSymbols(rootDir, query, limit) {
|
|
42
|
+
const files = await listProjectFiles(rootDir, {
|
|
43
|
+
limit: 400,
|
|
44
|
+
sourceOnly: true,
|
|
45
|
+
});
|
|
46
|
+
const symbols = [];
|
|
47
|
+
for (const projectFile of files) {
|
|
48
|
+
const remaining = limit - symbols.length;
|
|
49
|
+
if (remaining <= 0)
|
|
50
|
+
break;
|
|
51
|
+
const fileSymbols = extractSymbolsFromFile(rootDir, projectFile, query, remaining);
|
|
52
|
+
symbols.push(...fileSymbols);
|
|
53
|
+
}
|
|
54
|
+
return symbols;
|
|
55
|
+
}
|