@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,279 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { runCommand } from './executor.js';
|
|
6
|
+
import { isTuiMode } from './runtime-mode.js';
|
|
7
|
+
import { truncate } from './utils.js';
|
|
8
|
+
function parseUnifiedDiff(patchText) {
|
|
9
|
+
const lines = patchText.split('\n');
|
|
10
|
+
const hunks = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < lines.length) {
|
|
13
|
+
const line = lines[i];
|
|
14
|
+
if (line === undefined)
|
|
15
|
+
break;
|
|
16
|
+
if (line.startsWith('diff ') ||
|
|
17
|
+
line.startsWith('--- ') ||
|
|
18
|
+
line.startsWith('+++ ')) {
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
while (i < lines.length) {
|
|
25
|
+
const line = lines[i];
|
|
26
|
+
if (line === undefined)
|
|
27
|
+
break;
|
|
28
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
29
|
+
if (!hunkMatch) {
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const oldStart = parseInt(hunkMatch[1] || '0', 10);
|
|
34
|
+
const oldCount = hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1;
|
|
35
|
+
const newStart = parseInt(hunkMatch[3] || '0', 10);
|
|
36
|
+
const newCount = hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1;
|
|
37
|
+
i++;
|
|
38
|
+
const hunkLines = [];
|
|
39
|
+
while (i < lines.length) {
|
|
40
|
+
const nextLine = lines[i];
|
|
41
|
+
if (nextLine === undefined || nextLine.startsWith('@@'))
|
|
42
|
+
break;
|
|
43
|
+
hunkLines.push(nextLine);
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
hunks.push({ oldStart, oldCount, newStart, newCount, lines: hunkLines });
|
|
47
|
+
}
|
|
48
|
+
return hunks;
|
|
49
|
+
}
|
|
50
|
+
function formatActualFileSnippet(lines, lineIndex) {
|
|
51
|
+
const before = Math.max(0, lineIndex - 3);
|
|
52
|
+
const after = Math.min(lines.length, lineIndex + 4);
|
|
53
|
+
const width = String(after).length;
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (let i = before; i < after; i++) {
|
|
56
|
+
const marker = i === lineIndex ? '→' : ' ';
|
|
57
|
+
const line = i < lines.length ? lines[i] : '';
|
|
58
|
+
rows.push(`${marker} ${String(i + 1).padStart(width)}: ${line}`);
|
|
59
|
+
}
|
|
60
|
+
return rows.join('\n');
|
|
61
|
+
}
|
|
62
|
+
export function applyUnifiedPatch(originalContent, patchText) {
|
|
63
|
+
const hunks = parseUnifiedDiff(patchText);
|
|
64
|
+
if (!hunks.length) {
|
|
65
|
+
throw new Error('No valid hunks found in patch. Ensure the patch uses standard unified diff format with @@ hunk headers.');
|
|
66
|
+
}
|
|
67
|
+
const endsWithNewline = originalContent.endsWith('\n');
|
|
68
|
+
const originalLines = endsWithNewline
|
|
69
|
+
? originalContent.slice(0, -1).split('\n')
|
|
70
|
+
: originalContent.split('\n');
|
|
71
|
+
const result = [...originalLines];
|
|
72
|
+
let offset = 0;
|
|
73
|
+
for (const hunk of hunks) {
|
|
74
|
+
const startIdx = hunk.oldStart - 1 + offset;
|
|
75
|
+
let oldIdx = startIdx;
|
|
76
|
+
const newChunk = [];
|
|
77
|
+
if (hunk.oldCount === 0 && startIdx > result.length) {
|
|
78
|
+
throw new Error(`Patch hunk claims insertion at line ${hunk.oldStart} but the file only has ${result.length} line(s). ` +
|
|
79
|
+
'Re-read the file to get current line numbers before patching.');
|
|
80
|
+
}
|
|
81
|
+
for (const line of hunk.lines) {
|
|
82
|
+
if (line.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
if (line === '\')
|
|
85
|
+
continue;
|
|
86
|
+
const prefix = line[0];
|
|
87
|
+
const content = line.slice(1);
|
|
88
|
+
if (prefix === ' ') {
|
|
89
|
+
if (result[oldIdx] !== content) {
|
|
90
|
+
throw new Error(`Patch context mismatch at line ${oldIdx + 1}:\n` +
|
|
91
|
+
` Expected: ${JSON.stringify(content)}\n` +
|
|
92
|
+
` Got: ${JSON.stringify(result[oldIdx])}\n\n` +
|
|
93
|
+
`Actual file around line ${oldIdx + 1}:\n${formatActualFileSnippet(result, oldIdx)}\n\n` +
|
|
94
|
+
`Re-read the file to get the current lines and rebuild the hunk before retrying.`);
|
|
95
|
+
}
|
|
96
|
+
newChunk.push(content);
|
|
97
|
+
oldIdx++;
|
|
98
|
+
}
|
|
99
|
+
else if (prefix === '-') {
|
|
100
|
+
if (result[oldIdx] !== content) {
|
|
101
|
+
throw new Error(`Patch removal mismatch at line ${oldIdx + 1}:\n` +
|
|
102
|
+
` Expected: ${JSON.stringify(content)}\n` +
|
|
103
|
+
` Got: ${JSON.stringify(result[oldIdx])}\n\n` +
|
|
104
|
+
`Actual file around line ${oldIdx + 1}:\n${formatActualFileSnippet(result, oldIdx)}\n\n` +
|
|
105
|
+
`Re-read the file to get the current lines and rebuild the hunk before retrying.`);
|
|
106
|
+
}
|
|
107
|
+
oldIdx++;
|
|
108
|
+
}
|
|
109
|
+
else if (prefix === '+') {
|
|
110
|
+
newChunk.push(content);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const removedCount = oldIdx - startIdx;
|
|
114
|
+
result.splice(startIdx, removedCount, ...newChunk);
|
|
115
|
+
offset += newChunk.length - removedCount;
|
|
116
|
+
}
|
|
117
|
+
const joined = result.join('\n');
|
|
118
|
+
return endsWithNewline ? `${joined}\n` : joined;
|
|
119
|
+
}
|
|
120
|
+
export function renderDiffPreview(filePath, patchText) {
|
|
121
|
+
if (isTuiMode())
|
|
122
|
+
return;
|
|
123
|
+
console.log(chalk.bold(`\n 📋 Patch preview for ${filePath}:`));
|
|
124
|
+
for (const line of patchText.split('\n')) {
|
|
125
|
+
if (line.startsWith('+++ ') || line.startsWith('--- ')) {
|
|
126
|
+
console.log(chalk.bold(line));
|
|
127
|
+
}
|
|
128
|
+
else if (line.startsWith('@@')) {
|
|
129
|
+
console.log(chalk.cyan(line));
|
|
130
|
+
}
|
|
131
|
+
else if (line.startsWith('+')) {
|
|
132
|
+
console.log(chalk.green(line));
|
|
133
|
+
}
|
|
134
|
+
else if (line.startsWith('-')) {
|
|
135
|
+
console.log(chalk.red(line));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(chalk.dim(line));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
function normalizeRoot(rootDir) {
|
|
144
|
+
return path.resolve(rootDir);
|
|
145
|
+
}
|
|
146
|
+
export function resolveProjectPath(rootDir, filePath) {
|
|
147
|
+
const absRoot = normalizeRoot(rootDir);
|
|
148
|
+
const absPath = path.resolve(absRoot, filePath);
|
|
149
|
+
const relative = path.relative(absRoot, absPath);
|
|
150
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
151
|
+
throw new Error(`Refusing to access path outside the project root: ${filePath}`);
|
|
152
|
+
}
|
|
153
|
+
return absPath;
|
|
154
|
+
}
|
|
155
|
+
export function writeProjectFile(rootDir, filePath, content) {
|
|
156
|
+
const absPath = resolveProjectPath(rootDir, filePath);
|
|
157
|
+
if (existsSync(absPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const existingContent = readFileSync(absPath, 'utf-8');
|
|
160
|
+
if (existingContent === content) {
|
|
161
|
+
return { absPath, changed: false };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// If we can't read it for some reason, proceed with write
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
mkdirSync(path.dirname(absPath), { recursive: true });
|
|
169
|
+
writeFileSync(absPath, content, 'utf-8');
|
|
170
|
+
return { absPath, changed: true };
|
|
171
|
+
}
|
|
172
|
+
export function deleteProjectFile(rootDir, filePath) {
|
|
173
|
+
const absPath = resolveProjectPath(rootDir, filePath);
|
|
174
|
+
if (!existsSync(absPath)) {
|
|
175
|
+
return { deleted: false, absPath };
|
|
176
|
+
}
|
|
177
|
+
let content;
|
|
178
|
+
try {
|
|
179
|
+
if (!lstatSync(absPath).isSymbolicLink()) {
|
|
180
|
+
content = readFileSync(absPath, 'utf-8');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
content = undefined;
|
|
185
|
+
}
|
|
186
|
+
unlinkSync(absPath);
|
|
187
|
+
return { deleted: true, absPath, content };
|
|
188
|
+
}
|
|
189
|
+
export function readProjectFile(rootDir, filePath) {
|
|
190
|
+
const absPath = resolveProjectPath(rootDir, filePath);
|
|
191
|
+
if (!existsSync(absPath)) {
|
|
192
|
+
throw new Error(`File does not exist: ${filePath}`);
|
|
193
|
+
}
|
|
194
|
+
return readFileSync(absPath, 'utf-8');
|
|
195
|
+
}
|
|
196
|
+
function confirm(question) {
|
|
197
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
rl.question(question, (answer) => {
|
|
200
|
+
rl.close();
|
|
201
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
export async function applyChanges(operations, rootDir, { autoYes = false, } = {}) {
|
|
206
|
+
const silent = isTuiMode();
|
|
207
|
+
if (!operations.length) {
|
|
208
|
+
if (!silent)
|
|
209
|
+
console.log(chalk.yellow('\n⚠ No changes to apply.'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const fileOps = operations.filter((op) => op.action !== 'run');
|
|
213
|
+
const cmdOps = operations.filter((op) => op.action === 'run');
|
|
214
|
+
if (!silent)
|
|
215
|
+
console.log(chalk.bold(`\n📝 Applying ${fileOps.length} file change(s) and ${cmdOps.length} command(s):\n`));
|
|
216
|
+
for (const op of operations) {
|
|
217
|
+
switch (op.action) {
|
|
218
|
+
case 'create':
|
|
219
|
+
case 'update': {
|
|
220
|
+
if (op.filePath && op.content !== undefined) {
|
|
221
|
+
writeProjectFile(rootDir, op.filePath, op.content);
|
|
222
|
+
const icon = op.action === 'create' ? '✨' : '✏️ ';
|
|
223
|
+
const label = op.action === 'create' ? 'Created' : 'Updated';
|
|
224
|
+
if (!silent)
|
|
225
|
+
console.log(chalk.green(` ${icon} ${label}: ${op.filePath}`));
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'delete': {
|
|
230
|
+
if (op.filePath) {
|
|
231
|
+
try {
|
|
232
|
+
const result = deleteProjectFile(rootDir, op.filePath);
|
|
233
|
+
if (result.deleted) {
|
|
234
|
+
if (!silent)
|
|
235
|
+
console.log(chalk.red(` 🗑️ Deleted: ${op.filePath}`));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
if (!silent)
|
|
239
|
+
console.log(chalk.yellow(` ⚠ Could not delete ${op.filePath}: file not found`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (!silent)
|
|
244
|
+
console.log(chalk.yellow(` ⚠ Could not delete ${op.filePath}: ${err.message}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case 'run': {
|
|
250
|
+
if (op.command) {
|
|
251
|
+
if (!silent)
|
|
252
|
+
console.log(chalk.bold.yellow(`\n ⚡ Command: ${op.command}`));
|
|
253
|
+
if (!autoYes) {
|
|
254
|
+
const proceed = await confirm(chalk.yellow(` ⚠ Run this command? [y/N] `));
|
|
255
|
+
if (!proceed) {
|
|
256
|
+
if (!silent)
|
|
257
|
+
console.log(chalk.dim(` ⏭ Skipped: ${op.command}`));
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const result = await runCommand(op.command, rootDir);
|
|
262
|
+
if (result.exitCode !== 0) {
|
|
263
|
+
if (!silent)
|
|
264
|
+
console.log(chalk.red(` ⚠ Command failed (exit ${result.exitCode}). Continuing…`));
|
|
265
|
+
if (!silent && result.output) {
|
|
266
|
+
console.log(chalk.dim(truncate(result.output, 1000)));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
default:
|
|
273
|
+
if (!silent)
|
|
274
|
+
console.log(chalk.yellow(` ⚠ Unknown action "${op.action}"`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!silent)
|
|
278
|
+
console.log(chalk.bold.green('\n✅ All operations complete.\n'));
|
|
279
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { listProjectFiles, scanFiles, shouldIgnorePath, } from './scanner.js';
|
|
4
|
+
import { truncate } from './utils.js';
|
|
5
|
+
function normalizeProjectFilePath(rootDir, filePath) {
|
|
6
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
7
|
+
const resolvedFile = path.resolve(resolvedRoot, filePath);
|
|
8
|
+
const relative = path.relative(resolvedRoot, resolvedFile);
|
|
9
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return relative.replace(/\\/g, '/');
|
|
13
|
+
}
|
|
14
|
+
function getFileSignature(rootDir, relPath) {
|
|
15
|
+
try {
|
|
16
|
+
const stat = statSync(path.join(rootDir, relPath));
|
|
17
|
+
if (!stat.isFile()) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function setChunksForFile(index, relPath, chunks) {
|
|
27
|
+
if (chunks.length === 0) {
|
|
28
|
+
index.chunksByFile.delete(relPath);
|
|
29
|
+
index.fileSignatures.delete(relPath);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
index.chunksByFile.set(relPath, chunks);
|
|
33
|
+
const signature = getFileSignature(index.rootDir, relPath);
|
|
34
|
+
if (signature) {
|
|
35
|
+
index.fileSignatures.set(relPath, signature);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function removeFile(index, relPath) {
|
|
39
|
+
index.chunksByFile.delete(relPath);
|
|
40
|
+
index.fileSignatures.delete(relPath);
|
|
41
|
+
}
|
|
42
|
+
async function initializeIndex(index) {
|
|
43
|
+
if (index.initialized) {
|
|
44
|
+
return Array.from(index.chunksByFile.values()).reduce((sum, chunks) => sum + chunks.length, 0);
|
|
45
|
+
}
|
|
46
|
+
const files = listProjectFiles(index.rootDir);
|
|
47
|
+
const chunks = await scanFiles(index.rootDir, files);
|
|
48
|
+
index.fileSignatures.clear();
|
|
49
|
+
index.chunksByFile.clear();
|
|
50
|
+
for (const filePath of files) {
|
|
51
|
+
const signature = getFileSignature(index.rootDir, filePath);
|
|
52
|
+
if (signature) {
|
|
53
|
+
index.fileSignatures.set(filePath, signature);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
const current = index.chunksByFile.get(chunk.filePath) ?? [];
|
|
58
|
+
current.push(chunk);
|
|
59
|
+
index.chunksByFile.set(chunk.filePath, current);
|
|
60
|
+
}
|
|
61
|
+
index.initialized = true;
|
|
62
|
+
index.onStatus?.(`Local code index ready: ${index.fileSignatures.size.toLocaleString()} files.`);
|
|
63
|
+
return chunks.length;
|
|
64
|
+
}
|
|
65
|
+
function queryTerms(query) {
|
|
66
|
+
const parts = query
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.split(/[^a-z0-9_./-]+/i)
|
|
69
|
+
.map((part) => part.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
return [...new Set(parts)];
|
|
72
|
+
}
|
|
73
|
+
function countOccurrences(haystack, needle) {
|
|
74
|
+
if (!needle)
|
|
75
|
+
return 0;
|
|
76
|
+
let count = 0;
|
|
77
|
+
let cursor = 0;
|
|
78
|
+
while (cursor < haystack.length) {
|
|
79
|
+
const index = haystack.indexOf(needle, cursor);
|
|
80
|
+
if (index === -1)
|
|
81
|
+
break;
|
|
82
|
+
count += 1;
|
|
83
|
+
cursor = index + needle.length;
|
|
84
|
+
}
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
function scoreChunk(terms, chunk) {
|
|
88
|
+
const filePath = chunk.filePath.toLowerCase();
|
|
89
|
+
const content = chunk.content.toLowerCase();
|
|
90
|
+
let score = 0;
|
|
91
|
+
for (const term of terms) {
|
|
92
|
+
if (term.length < 2)
|
|
93
|
+
continue;
|
|
94
|
+
score += countOccurrences(filePath, term) * 3;
|
|
95
|
+
score += countOccurrences(content, term);
|
|
96
|
+
if (chunk.label?.toLowerCase().includes(term)) {
|
|
97
|
+
score += 2;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return score;
|
|
101
|
+
}
|
|
102
|
+
function flattenChunks(index) {
|
|
103
|
+
return Array.from(index.chunksByFile.values()).flat();
|
|
104
|
+
}
|
|
105
|
+
export function createIndex({ rootDir, onStatus = null, onContextLog = null, }) {
|
|
106
|
+
return {
|
|
107
|
+
rootDir: path.resolve(rootDir),
|
|
108
|
+
initialized: false,
|
|
109
|
+
fileSignatures: new Map(),
|
|
110
|
+
chunksByFile: new Map(),
|
|
111
|
+
onStatus,
|
|
112
|
+
onContextLog,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export async function searchIndex(index, query, limit = 10) {
|
|
116
|
+
await initializeIndex(index);
|
|
117
|
+
const terms = queryTerms(query);
|
|
118
|
+
if (terms.length === 0) {
|
|
119
|
+
return { results: [], retrievalTokensUsed: 0 };
|
|
120
|
+
}
|
|
121
|
+
const scored = flattenChunks(index)
|
|
122
|
+
.map((chunk) => ({
|
|
123
|
+
...chunk,
|
|
124
|
+
score: scoreChunk(terms, chunk),
|
|
125
|
+
}))
|
|
126
|
+
.filter((chunk) => chunk.score > 0)
|
|
127
|
+
.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
|
|
128
|
+
.slice(0, Math.max(1, Math.min(limit, 20)));
|
|
129
|
+
if (scored.length) {
|
|
130
|
+
const visible = scored
|
|
131
|
+
.slice(0, 6)
|
|
132
|
+
.map((chunk) => `${chunk.filePath}:${chunk.startLine}-${chunk.endLine}`)
|
|
133
|
+
.join(', ');
|
|
134
|
+
index.onContextLog?.(`Local search hit: ${visible}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
results: scored,
|
|
138
|
+
retrievalTokensUsed: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export function formatIndexResults(results) {
|
|
142
|
+
return results.map((chunk) => ({
|
|
143
|
+
...chunk,
|
|
144
|
+
content: truncate(chunk.content, 1200),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
export function listIndexFiles(index) {
|
|
148
|
+
return [...index.fileSignatures.keys()].sort();
|
|
149
|
+
}
|
|
150
|
+
export async function upsertIndexFile(index, filePath) {
|
|
151
|
+
if (!index.initialized) {
|
|
152
|
+
return { indexedChunks: 0, retrievalTokensUsed: 0 };
|
|
153
|
+
}
|
|
154
|
+
const relPath = normalizeProjectFilePath(index.rootDir, filePath);
|
|
155
|
+
if (!relPath || shouldIgnorePath(relPath)) {
|
|
156
|
+
return { indexedChunks: 0, retrievalTokensUsed: 0 };
|
|
157
|
+
}
|
|
158
|
+
const chunks = existsSync(path.join(index.rootDir, relPath))
|
|
159
|
+
? await scanFiles(index.rootDir, [relPath])
|
|
160
|
+
: [];
|
|
161
|
+
setChunksForFile(index, relPath, chunks);
|
|
162
|
+
return {
|
|
163
|
+
indexedChunks: chunks.length,
|
|
164
|
+
retrievalTokensUsed: 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export async function removeIndexFile(index, filePath) {
|
|
168
|
+
const relPath = normalizeProjectFilePath(index.rootDir, filePath);
|
|
169
|
+
if (!relPath)
|
|
170
|
+
return;
|
|
171
|
+
removeFile(index, relPath);
|
|
172
|
+
}
|
|
173
|
+
export async function syncIndexFromDisk(index) {
|
|
174
|
+
if (!index.initialized) {
|
|
175
|
+
const indexedChunks = await initializeIndex(index);
|
|
176
|
+
return {
|
|
177
|
+
added: index.fileSignatures.size,
|
|
178
|
+
modified: 0,
|
|
179
|
+
removed: 0,
|
|
180
|
+
indexedChunks,
|
|
181
|
+
retrievalTokensUsed: 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const currentFiles = new Set(listProjectFiles(index.rootDir));
|
|
185
|
+
let added = 0;
|
|
186
|
+
let modified = 0;
|
|
187
|
+
let removed = 0;
|
|
188
|
+
let indexedChunks = 0;
|
|
189
|
+
for (const existing of [...index.fileSignatures.keys()]) {
|
|
190
|
+
if (!currentFiles.has(existing)) {
|
|
191
|
+
removeFile(index, existing);
|
|
192
|
+
removed += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const relPath of currentFiles) {
|
|
196
|
+
const nextSignature = getFileSignature(index.rootDir, relPath);
|
|
197
|
+
if (!nextSignature)
|
|
198
|
+
continue;
|
|
199
|
+
const previousSignature = index.fileSignatures.get(relPath);
|
|
200
|
+
if (!previousSignature) {
|
|
201
|
+
const chunks = await scanFiles(index.rootDir, [relPath]);
|
|
202
|
+
setChunksForFile(index, relPath, chunks);
|
|
203
|
+
added += 1;
|
|
204
|
+
indexedChunks += chunks.length;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (previousSignature !== nextSignature) {
|
|
208
|
+
const chunks = await scanFiles(index.rootDir, [relPath]);
|
|
209
|
+
setChunksForFile(index, relPath, chunks);
|
|
210
|
+
modified += 1;
|
|
211
|
+
indexedChunks += chunks.length;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
added,
|
|
216
|
+
modified,
|
|
217
|
+
removed,
|
|
218
|
+
indexedChunks,
|
|
219
|
+
retrievalTokensUsed: 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const REPO_MAP_LANGUAGE_CONFIGS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'javascript',
|
|
4
|
+
displayName: 'JavaScript',
|
|
5
|
+
wasmFile: 'tree-sitter-javascript.wasm',
|
|
6
|
+
extensions: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'typescript',
|
|
10
|
+
displayName: 'TypeScript',
|
|
11
|
+
wasmFile: 'tree-sitter-typescript.wasm',
|
|
12
|
+
extensions: ['.ts', '.mts', '.cts'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'tsx',
|
|
16
|
+
displayName: 'TSX',
|
|
17
|
+
wasmFile: 'tree-sitter-tsx.wasm',
|
|
18
|
+
extensions: ['.tsx'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'python',
|
|
22
|
+
displayName: 'Python',
|
|
23
|
+
wasmFile: 'tree-sitter-python.wasm',
|
|
24
|
+
extensions: ['.py'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'go',
|
|
28
|
+
displayName: 'Go',
|
|
29
|
+
wasmFile: 'tree-sitter-go.wasm',
|
|
30
|
+
extensions: ['.go'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'rust',
|
|
34
|
+
displayName: 'Rust',
|
|
35
|
+
wasmFile: 'tree-sitter-rust.wasm',
|
|
36
|
+
extensions: ['.rs'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'php',
|
|
40
|
+
displayName: 'PHP',
|
|
41
|
+
wasmFile: 'tree-sitter-php.wasm',
|
|
42
|
+
extensions: ['.php'],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'java',
|
|
46
|
+
displayName: 'Java',
|
|
47
|
+
wasmFile: 'tree-sitter-java.wasm',
|
|
48
|
+
extensions: ['.java'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'c',
|
|
52
|
+
displayName: 'C',
|
|
53
|
+
wasmFile: 'tree-sitter-c.wasm',
|
|
54
|
+
extensions: ['.c'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'cpp',
|
|
58
|
+
displayName: 'C++',
|
|
59
|
+
wasmFile: 'tree-sitter-cpp.wasm',
|
|
60
|
+
extensions: ['.cpp', '.hpp', '.cc', '.h'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'csharp',
|
|
64
|
+
displayName: 'C#',
|
|
65
|
+
wasmFile: 'tree-sitter-c-sharp.wasm',
|
|
66
|
+
extensions: ['.cs'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'objc',
|
|
70
|
+
displayName: 'Objective-C',
|
|
71
|
+
wasmFile: 'tree-sitter-objc.wasm',
|
|
72
|
+
extensions: ['.m'],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'html',
|
|
76
|
+
displayName: 'HTML',
|
|
77
|
+
wasmFile: 'tree-sitter-html.wasm',
|
|
78
|
+
extensions: ['.html', '.htm'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'css',
|
|
82
|
+
displayName: 'CSS',
|
|
83
|
+
wasmFile: 'tree-sitter-css.wasm',
|
|
84
|
+
extensions: ['.css', '.scss'],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'ruby',
|
|
88
|
+
displayName: 'Ruby',
|
|
89
|
+
wasmFile: 'tree-sitter-ruby.wasm',
|
|
90
|
+
extensions: ['.rb'],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
export const REPO_MAP_LANGUAGE_BY_EXTENSION = new Map(REPO_MAP_LANGUAGE_CONFIGS.flatMap((config) => config.extensions.map((extension) => [extension, config])));
|
|
94
|
+
export function getRepoMapLanguageForFile(filePath, pathModule) {
|
|
95
|
+
const extension = pathModule.extname(filePath).toLowerCase();
|
|
96
|
+
return REPO_MAP_LANGUAGE_BY_EXTENSION.get(extension) ?? null;
|
|
97
|
+
}
|
|
98
|
+
export function listRepoMapWasmFiles() {
|
|
99
|
+
return REPO_MAP_LANGUAGE_CONFIGS.map((config) => config.wasmFile);
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isTuiMode(env = process.env) {
|
|
2
|
+
return env.THEGITAI_TUI === '1';
|
|
3
|
+
}
|
|
4
|
+
let _commandOutputHook = null;
|
|
5
|
+
export function setCommandOutputHook(hook) {
|
|
6
|
+
_commandOutputHook = hook;
|
|
7
|
+
}
|
|
8
|
+
export function emitCommandOutput(chunk) {
|
|
9
|
+
_commandOutputHook?.(chunk);
|
|
10
|
+
}
|
|
11
|
+
const tuiEnvStacks = new WeakMap();
|
|
12
|
+
export async function withTuiMode(run, env = process.env) {
|
|
13
|
+
let stack = tuiEnvStacks.get(env);
|
|
14
|
+
if (!stack) {
|
|
15
|
+
stack = { depth: 0, original: env.THEGITAI_TUI };
|
|
16
|
+
tuiEnvStacks.set(env, stack);
|
|
17
|
+
}
|
|
18
|
+
stack.depth += 1;
|
|
19
|
+
env.THEGITAI_TUI = '1';
|
|
20
|
+
try {
|
|
21
|
+
return await run();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
stack.depth -= 1;
|
|
25
|
+
if (stack.depth <= 0) {
|
|
26
|
+
tuiEnvStacks.delete(env);
|
|
27
|
+
if (stack.original === undefined) {
|
|
28
|
+
delete env.THEGITAI_TUI;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
env.THEGITAI_TUI = stack.original;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|