@thegitai/cli 1.0.0-preview.27 → 1.0.0-preview.29
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 +2 -2
- package/dist/bin/ai.js +0 -15
- package/dist/src/agent-mode.js +0 -5
- package/dist/src/api/browser-login.js +2 -2
- package/dist/src/api/chat.js +4 -7
- package/dist/src/help-text.js +4 -4
- package/dist/src/session-safety.js +0 -12
- package/dist/src/signin.js +5 -3
- package/dist/src/tool-executor.js +9 -44
- package/dist/src/tools/delete-file.js +1 -3
- package/dist/src/tools/index.js +0 -10
- package/dist/src/tools/patch-file.js +1 -11
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +2 -45
- package/dist/src/tools/run-node-script.js +1 -55
- package/dist/src/tools/str-replace.js +1 -11
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +1 -11
- package/dist/src/ui/repl.js +1 -10
- package/dist/src/ui/tui/build-frame.js +1 -4
- package/package.json +6 -6
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
3
|
import { ensurePermission } from '../permissions.js';
|
|
4
|
-
import { syncIndexFromDisk } from '../project-index.js';
|
|
5
4
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
5
|
import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
|
|
7
6
|
import { ensureSessionScratchDir } from '../scratch-dir.js';
|
|
@@ -57,29 +56,6 @@ function buildOutput(stdout, stderr, exitCode) {
|
|
|
57
56
|
return trimOutput(stdout.trim());
|
|
58
57
|
return trimOutput([stdout, stderr].filter(Boolean).join('\n').trim());
|
|
59
58
|
}
|
|
60
|
-
function emptyRepoSync(reason) {
|
|
61
|
-
return {
|
|
62
|
-
added: 0,
|
|
63
|
-
modified: 0,
|
|
64
|
-
removed: 0,
|
|
65
|
-
indexedChunks: 0,
|
|
66
|
-
retrievalTokensUsed: 0,
|
|
67
|
-
skipped: true,
|
|
68
|
-
...(reason ? { reason } : {}),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function readGitStatusSignature(rootDir) {
|
|
72
|
-
try {
|
|
73
|
-
return execFileSync('git', ['-C', rootDir, 'status', '--porcelain=v1', '--untracked-files=all'], {
|
|
74
|
-
encoding: 'utf-8',
|
|
75
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
76
|
-
timeout: 10_000,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
59
|
function executeNodeScript(rootDir, script, timeout) {
|
|
84
60
|
const effectiveTimeout = typeof timeout === 'number' && timeout > 0
|
|
85
61
|
? timeout
|
|
@@ -170,38 +146,10 @@ export async function runNodeScript(context, args) {
|
|
|
170
146
|
}
|
|
171
147
|
return denied;
|
|
172
148
|
}
|
|
173
|
-
const beforeGitStatus = readGitStatusSignature(rootDir);
|
|
174
149
|
const result = await executeNodeScript(rootDir, script, typeof args.timeout_ms === 'number' && args.timeout_ms > 0
|
|
175
150
|
? args.timeout_ms
|
|
176
151
|
: undefined);
|
|
177
|
-
const afterGitStatus = readGitStatusSignature(rootDir);
|
|
178
|
-
const gitStatusCleanBeforeAndAfter = beforeGitStatus === '' && afterGitStatus === '';
|
|
179
|
-
const shouldSync = context.projectIndex.initialized && !gitStatusCleanBeforeAndAfter;
|
|
180
|
-
let repoSync;
|
|
181
|
-
if (shouldSync) {
|
|
182
|
-
try {
|
|
183
|
-
repoSync = await syncIndexFromDisk(context.projectIndex);
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
-
context.onStatus(`Node script completed, but local index sync failed: ${message}`);
|
|
188
|
-
repoSync = {
|
|
189
|
-
...emptyRepoSync('local index sync failed after Node script execution'),
|
|
190
|
-
error: message,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
repoSync = emptyRepoSync(!context.projectIndex.initialized
|
|
196
|
-
? 'project index not initialized'
|
|
197
|
-
: gitStatusCleanBeforeAndAfter
|
|
198
|
-
? 'git status clean before and after'
|
|
199
|
-
: 'sync not needed');
|
|
200
|
-
}
|
|
201
152
|
invalidateShellDiagnosticsCache(rootDir);
|
|
202
|
-
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
203
|
-
context.onStatus(`Synced repo state after Node script (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
204
|
-
}
|
|
205
153
|
return {
|
|
206
154
|
ok: result.exitCode === 0,
|
|
207
155
|
command: COMMAND_LABEL,
|
|
@@ -210,8 +158,6 @@ export async function runNodeScript(context, args) {
|
|
|
210
158
|
stdout: result.stdout,
|
|
211
159
|
stderr: result.stderr,
|
|
212
160
|
output: result.output,
|
|
213
|
-
repoSync,
|
|
214
|
-
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
215
161
|
diagnostics: buildDeferredShellDiagnostics('run_node_script'),
|
|
216
162
|
};
|
|
217
163
|
}
|
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { classifyProjectPath, readProjectFile, writeProjectFile, } from '../patcher.js';
|
|
5
5
|
import { repairFilePath } from './path-suggest.js';
|
|
6
|
-
import { upsertIndexFile } from '../project-index.js';
|
|
7
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
8
7
|
import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
|
|
9
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -77,7 +76,7 @@ function buildStrReplacePreview(oldString, newString) {
|
|
|
77
76
|
return `@@ str_replace @@\n${minus}\n${plus}`;
|
|
78
77
|
}
|
|
79
78
|
export async function strReplace(context, args) {
|
|
80
|
-
const { rootDir
|
|
79
|
+
const { rootDir } = context;
|
|
81
80
|
const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
|
|
82
81
|
let oldString = typeof args.old_string === 'string'
|
|
83
82
|
? args.old_string
|
|
@@ -193,13 +192,6 @@ export async function strReplace(context, args) {
|
|
|
193
192
|
const nextContent = originalContent.split(oldString).join(newString);
|
|
194
193
|
const { changed } = writeProjectFile(rootDir, filePath, nextContent);
|
|
195
194
|
const replacements = changed ? (replaceAll ? n : 1) : 0;
|
|
196
|
-
let indexedChunks = 0;
|
|
197
|
-
let retrievalTokensUsed = 0;
|
|
198
|
-
if (changed && !scratchPath) {
|
|
199
|
-
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
200
|
-
indexedChunks = indexResult.indexedChunks;
|
|
201
|
-
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
202
|
-
}
|
|
203
195
|
let diagnostics;
|
|
204
196
|
if (!scratchPath) {
|
|
205
197
|
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
@@ -219,8 +211,6 @@ export async function strReplace(context, args) {
|
|
|
219
211
|
operation: 'str_replace',
|
|
220
212
|
...(scratchPath ? { scratch: true } : {}),
|
|
221
213
|
replacements,
|
|
222
|
-
indexedChunks,
|
|
223
|
-
retrievalTokensUsed,
|
|
224
214
|
bytesWritten: Buffer.byteLength(nextContent, 'utf-8'),
|
|
225
215
|
diagnostics,
|
|
226
216
|
message: changed ? undefined : 'The provided replacement resulted in no changes to the file content.',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
2
|
import { hashStoredContent, readFileEditSnapshot, storedContentBuffer, } from '../edit-journal.js';
|
|
3
3
|
import { deleteProjectFile, writeProjectFile, writeProjectFileBuffer, } from '../patcher.js';
|
|
4
|
-
import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
|
|
5
4
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
5
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
7
6
|
function normalizeTarget(value) {
|
|
@@ -101,10 +100,9 @@ function validateUndoPlan(rootDir, records) {
|
|
|
101
100
|
return { ok: true };
|
|
102
101
|
}
|
|
103
102
|
async function applyUndo(context, record) {
|
|
104
|
-
const { rootDir
|
|
103
|
+
const { rootDir } = context;
|
|
105
104
|
if (record.operation === 'create') {
|
|
106
105
|
deleteProjectFile(rootDir, record.filePath);
|
|
107
|
-
await removeIndexFile(projectIndex, record.filePath);
|
|
108
106
|
return { changed: true };
|
|
109
107
|
}
|
|
110
108
|
if (record.beforeContent === null) {
|
|
@@ -113,9 +111,6 @@ async function applyUndo(context, record) {
|
|
|
113
111
|
const { changed } = record.beforeContentEncoding === 'base64'
|
|
114
112
|
? writeProjectFileBuffer(rootDir, record.filePath, storedContentBuffer(record.beforeContent, record.beforeContentEncoding))
|
|
115
113
|
: writeProjectFile(rootDir, record.filePath, record.beforeContent);
|
|
116
|
-
if (changed) {
|
|
117
|
-
await upsertIndexFile(projectIndex, record.filePath);
|
|
118
|
-
}
|
|
119
114
|
return { changed };
|
|
120
115
|
}
|
|
121
116
|
function summarizeUndoRecord(record) {
|
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { classifyProjectPath, writeProjectFile } from '../patcher.js';
|
|
5
5
|
import { readFileEditSnapshot } from '../edit-journal.js';
|
|
6
|
-
import { upsertIndexFile } from '../project-index.js';
|
|
7
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
8
7
|
import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
|
|
9
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -44,7 +43,7 @@ function buildWritePreview(previous, next) {
|
|
|
44
43
|
return rows.join('\n');
|
|
45
44
|
}
|
|
46
45
|
export async function writeFile(context, args) {
|
|
47
|
-
const { rootDir
|
|
46
|
+
const { rootDir } = context;
|
|
48
47
|
const filePath = String(args.filePath ?? '').trim();
|
|
49
48
|
let content = typeof args.content === 'string' ? args.content : '';
|
|
50
49
|
if (!filePath) {
|
|
@@ -127,13 +126,6 @@ export async function writeFile(context, args) {
|
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
const { changed } = writeProjectFile(rootDir, filePath, content);
|
|
130
|
-
let indexedChunks = 0;
|
|
131
|
-
let retrievalTokensUsed = 0;
|
|
132
|
-
if (changed && !scratchPath) {
|
|
133
|
-
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
134
|
-
indexedChunks = indexResult.indexedChunks;
|
|
135
|
-
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
136
|
-
}
|
|
137
129
|
let diagnostics;
|
|
138
130
|
if (!scratchPath) {
|
|
139
131
|
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
@@ -150,8 +142,6 @@ export async function writeFile(context, args) {
|
|
|
150
142
|
changed,
|
|
151
143
|
operation: 'write',
|
|
152
144
|
...(scratchPath ? { scratch: true } : {}),
|
|
153
|
-
indexedChunks,
|
|
154
|
-
retrievalTokensUsed,
|
|
155
145
|
bytesWritten: Buffer.byteLength(content, 'utf-8'),
|
|
156
146
|
diagnostics,
|
|
157
147
|
};
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1049,7 +1049,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1049
1049
|
const reasoningTokens = usageSummary?.reasoningTokens ?? 0;
|
|
1050
1050
|
const cacheTokens = usageSummary?.cacheTokens ?? 0;
|
|
1051
1051
|
const cacheWriteTokens = usageSummary?.cacheWriteTokens ?? 0;
|
|
1052
|
-
const indexTokens = usageSummary?.indexTokens ?? 0;
|
|
1053
1052
|
return [
|
|
1054
1053
|
'Session tokens',
|
|
1055
1054
|
`in ${formatTokenCount(inputTokens)}`,
|
|
@@ -1057,7 +1056,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1057
1056
|
...(reasoningTokens > 0 ? [`think ${formatTokenCount(reasoningTokens)}`] : []),
|
|
1058
1057
|
`cache ${formatTokenCount(cacheTokens)}`,
|
|
1059
1058
|
`write ${formatTokenCount(cacheWriteTokens)}`,
|
|
1060
|
-
`index ${formatTokenCount(indexTokens)}`,
|
|
1061
1059
|
].join(' • ');
|
|
1062
1060
|
}
|
|
1063
1061
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
@@ -1344,7 +1342,7 @@ async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
|
1344
1342
|
saveSessionState(session);
|
|
1345
1343
|
await serverSessionClient.save(session);
|
|
1346
1344
|
}
|
|
1347
|
-
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi,
|
|
1345
|
+
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1348
1346
|
if (process.stdin.isTTY !== true) {
|
|
1349
1347
|
throw new Error('stdin is not a terminal');
|
|
1350
1348
|
}
|
|
@@ -2018,7 +2016,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2018
2016
|
const id = jobId.trim();
|
|
2019
2017
|
await collectBackgroundJobUiOutputMutations({
|
|
2020
2018
|
session,
|
|
2021
|
-
projectIndex,
|
|
2022
2019
|
jobId: id,
|
|
2023
2020
|
});
|
|
2024
2021
|
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
@@ -2050,7 +2047,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2050
2047
|
return;
|
|
2051
2048
|
await collectBackgroundJobUiOutputMutations({
|
|
2052
2049
|
session,
|
|
2053
|
-
projectIndex,
|
|
2054
2050
|
jobId,
|
|
2055
2051
|
});
|
|
2056
2052
|
syncBackgroundJobsState();
|
|
@@ -2066,7 +2062,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2066
2062
|
const killed = await killBackgroundJob(jobId);
|
|
2067
2063
|
await collectBackgroundJobUiKillMutations({
|
|
2068
2064
|
session,
|
|
2069
|
-
projectIndex,
|
|
2070
2065
|
jobId,
|
|
2071
2066
|
result: killed,
|
|
2072
2067
|
});
|
|
@@ -2454,7 +2449,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2454
2449
|
const killed = await killBackgroundJob(jobId);
|
|
2455
2450
|
await collectBackgroundJobUiKillMutations({
|
|
2456
2451
|
session,
|
|
2457
|
-
projectIndex,
|
|
2458
2452
|
jobId,
|
|
2459
2453
|
result: killed,
|
|
2460
2454
|
});
|
|
@@ -2662,7 +2656,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2662
2656
|
try {
|
|
2663
2657
|
const result = await chat.sendServerUserMessage({
|
|
2664
2658
|
config: authConfig,
|
|
2665
|
-
projectIndex,
|
|
2666
2659
|
session,
|
|
2667
2660
|
input,
|
|
2668
2661
|
imageAttachments,
|
|
@@ -2892,8 +2885,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2892
2885
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2893
2886
|
}
|
|
2894
2887
|
});
|
|
2895
|
-
projectIndex.onStatus = session.onStatus;
|
|
2896
|
-
projectIndex.onContextLog = session.onContextLog;
|
|
2897
2888
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2898
2889
|
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
2899
2890
|
session.requestPermission = async (request) => {
|
|
@@ -413,7 +413,7 @@ function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_P
|
|
|
413
413
|
];
|
|
414
414
|
}
|
|
415
415
|
function tokenUsageLines(usage) {
|
|
416
|
-
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))
|
|
416
|
+
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?$/);
|
|
417
417
|
if (!match) {
|
|
418
418
|
return [plainLine(usage, { color: 'cyan' })];
|
|
419
419
|
}
|
|
@@ -431,9 +431,6 @@ function tokenUsageLines(usage) {
|
|
|
431
431
|
if (match[5]) {
|
|
432
432
|
spans.push(span(' Write ', { color: 'gray' }), span(match[5].trim(), { color: 'yellow' }));
|
|
433
433
|
}
|
|
434
|
-
if (match[6]) {
|
|
435
|
-
spans.push(span(' Index ', { color: 'gray' }), span(match[6].trim(), { color: 'blue' }));
|
|
436
|
-
}
|
|
437
434
|
return [line(...spans)];
|
|
438
435
|
}
|
|
439
436
|
function footerTransientStatus(status) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
4
|
-
"description": "TheGitAI is an AI coding agent for your terminal. It
|
|
3
|
+
"version": "1.0.0-preview.29",
|
|
4
|
+
"description": "TheGitAI is an AI coding agent for your terminal. It reads and searches your repository, writes and edits files, runs commands, and builds features with you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"ai-coding-agent",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
38
38
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
39
39
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
40
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.29",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.29",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.29",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.29",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
@@ -1,233 +0,0 @@
|
|
|
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
|
-
function countIndexedChunks(index) {
|
|
43
|
-
return Array.from(index.chunksByFile.values()).reduce((sum, chunks) => sum + chunks.length, 0);
|
|
44
|
-
}
|
|
45
|
-
async function initializeIndex(index) {
|
|
46
|
-
if (index.initialized) {
|
|
47
|
-
return countIndexedChunks(index);
|
|
48
|
-
}
|
|
49
|
-
if (!index._initializing) {
|
|
50
|
-
index._initializing = scanProjectIntoIndex(index).finally(() => {
|
|
51
|
-
index._initializing = null;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return index._initializing;
|
|
55
|
-
}
|
|
56
|
-
async function scanProjectIntoIndex(index) {
|
|
57
|
-
const files = listProjectFiles(index.rootDir);
|
|
58
|
-
const chunks = await scanFiles(index.rootDir, files);
|
|
59
|
-
index.fileSignatures.clear();
|
|
60
|
-
index.chunksByFile.clear();
|
|
61
|
-
for (const filePath of files) {
|
|
62
|
-
const signature = getFileSignature(index.rootDir, filePath);
|
|
63
|
-
if (signature) {
|
|
64
|
-
index.fileSignatures.set(filePath, signature);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const chunk of chunks) {
|
|
68
|
-
const current = index.chunksByFile.get(chunk.filePath) ?? [];
|
|
69
|
-
current.push(chunk);
|
|
70
|
-
index.chunksByFile.set(chunk.filePath, current);
|
|
71
|
-
}
|
|
72
|
-
index.initialized = true;
|
|
73
|
-
index.onStatus?.(`Local code index ready: ${index.fileSignatures.size.toLocaleString()} files.`);
|
|
74
|
-
return chunks.length;
|
|
75
|
-
}
|
|
76
|
-
function queryTerms(query) {
|
|
77
|
-
const parts = query
|
|
78
|
-
.toLowerCase()
|
|
79
|
-
.split(/[^a-z0-9_./-]+/i)
|
|
80
|
-
.map((part) => part.trim())
|
|
81
|
-
.filter(Boolean);
|
|
82
|
-
return [...new Set(parts)];
|
|
83
|
-
}
|
|
84
|
-
function countOccurrences(haystack, needle) {
|
|
85
|
-
if (!needle)
|
|
86
|
-
return 0;
|
|
87
|
-
let count = 0;
|
|
88
|
-
let cursor = 0;
|
|
89
|
-
while (cursor < haystack.length) {
|
|
90
|
-
const index = haystack.indexOf(needle, cursor);
|
|
91
|
-
if (index === -1)
|
|
92
|
-
break;
|
|
93
|
-
count += 1;
|
|
94
|
-
cursor = index + needle.length;
|
|
95
|
-
}
|
|
96
|
-
return count;
|
|
97
|
-
}
|
|
98
|
-
function scoreChunk(terms, chunk) {
|
|
99
|
-
const filePath = chunk.filePath.toLowerCase();
|
|
100
|
-
const content = chunk.content.toLowerCase();
|
|
101
|
-
let score = 0;
|
|
102
|
-
for (const term of terms) {
|
|
103
|
-
if (term.length < 2)
|
|
104
|
-
continue;
|
|
105
|
-
score += countOccurrences(filePath, term) * 3;
|
|
106
|
-
score += countOccurrences(content, term);
|
|
107
|
-
if (chunk.label?.toLowerCase().includes(term)) {
|
|
108
|
-
score += 2;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return score;
|
|
112
|
-
}
|
|
113
|
-
function flattenChunks(index) {
|
|
114
|
-
return Array.from(index.chunksByFile.values()).flat();
|
|
115
|
-
}
|
|
116
|
-
export function createIndex({ rootDir, onStatus = null, onContextLog = null, }) {
|
|
117
|
-
return {
|
|
118
|
-
rootDir: path.resolve(rootDir),
|
|
119
|
-
initialized: false,
|
|
120
|
-
_initializing: null,
|
|
121
|
-
fileSignatures: new Map(),
|
|
122
|
-
chunksByFile: new Map(),
|
|
123
|
-
onStatus,
|
|
124
|
-
onContextLog,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
export async function searchIndex(index, query, limit = 10) {
|
|
128
|
-
await initializeIndex(index);
|
|
129
|
-
const terms = queryTerms(query);
|
|
130
|
-
if (terms.length === 0) {
|
|
131
|
-
return { results: [], retrievalTokensUsed: 0 };
|
|
132
|
-
}
|
|
133
|
-
const scored = flattenChunks(index)
|
|
134
|
-
.map((chunk) => ({
|
|
135
|
-
...chunk,
|
|
136
|
-
score: scoreChunk(terms, chunk),
|
|
137
|
-
}))
|
|
138
|
-
.filter((chunk) => chunk.score > 0)
|
|
139
|
-
.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
|
|
140
|
-
.slice(0, Math.max(1, Math.min(limit, 20)));
|
|
141
|
-
if (scored.length) {
|
|
142
|
-
const visible = scored
|
|
143
|
-
.slice(0, 6)
|
|
144
|
-
.map((chunk) => `${chunk.filePath}:${chunk.startLine}-${chunk.endLine}`)
|
|
145
|
-
.join(', ');
|
|
146
|
-
index.onContextLog?.(`Local search hit: ${visible}`);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
results: scored,
|
|
150
|
-
retrievalTokensUsed: 0,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
export function formatIndexResults(results) {
|
|
154
|
-
return results.map((chunk) => ({
|
|
155
|
-
...chunk,
|
|
156
|
-
content: truncate(chunk.content, 1200),
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
export function listIndexFiles(index) {
|
|
160
|
-
return [...index.fileSignatures.keys()].sort();
|
|
161
|
-
}
|
|
162
|
-
export async function upsertIndexFile(index, filePath) {
|
|
163
|
-
if (!index.initialized) {
|
|
164
|
-
return { indexedChunks: 0, retrievalTokensUsed: 0 };
|
|
165
|
-
}
|
|
166
|
-
const relPath = normalizeProjectFilePath(index.rootDir, filePath);
|
|
167
|
-
if (!relPath || shouldIgnorePath(relPath)) {
|
|
168
|
-
return { indexedChunks: 0, retrievalTokensUsed: 0 };
|
|
169
|
-
}
|
|
170
|
-
const chunks = existsSync(path.join(index.rootDir, relPath))
|
|
171
|
-
? await scanFiles(index.rootDir, [relPath])
|
|
172
|
-
: [];
|
|
173
|
-
setChunksForFile(index, relPath, chunks);
|
|
174
|
-
return {
|
|
175
|
-
indexedChunks: chunks.length,
|
|
176
|
-
retrievalTokensUsed: 0,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
export async function removeIndexFile(index, filePath) {
|
|
180
|
-
const relPath = normalizeProjectFilePath(index.rootDir, filePath);
|
|
181
|
-
if (!relPath)
|
|
182
|
-
return;
|
|
183
|
-
removeFile(index, relPath);
|
|
184
|
-
}
|
|
185
|
-
export async function syncIndexFromDisk(index) {
|
|
186
|
-
if (!index.initialized) {
|
|
187
|
-
const indexedChunks = await initializeIndex(index);
|
|
188
|
-
return {
|
|
189
|
-
added: index.fileSignatures.size,
|
|
190
|
-
modified: 0,
|
|
191
|
-
removed: 0,
|
|
192
|
-
indexedChunks,
|
|
193
|
-
retrievalTokensUsed: 0,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const currentFiles = new Set(listProjectFiles(index.rootDir));
|
|
197
|
-
let added = 0;
|
|
198
|
-
let modified = 0;
|
|
199
|
-
let removed = 0;
|
|
200
|
-
let indexedChunks = 0;
|
|
201
|
-
for (const existing of [...index.fileSignatures.keys()]) {
|
|
202
|
-
if (!currentFiles.has(existing)) {
|
|
203
|
-
removeFile(index, existing);
|
|
204
|
-
removed += 1;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
for (const relPath of currentFiles) {
|
|
208
|
-
const nextSignature = getFileSignature(index.rootDir, relPath);
|
|
209
|
-
if (!nextSignature)
|
|
210
|
-
continue;
|
|
211
|
-
const previousSignature = index.fileSignatures.get(relPath);
|
|
212
|
-
if (!previousSignature) {
|
|
213
|
-
const chunks = await scanFiles(index.rootDir, [relPath]);
|
|
214
|
-
setChunksForFile(index, relPath, chunks);
|
|
215
|
-
added += 1;
|
|
216
|
-
indexedChunks += chunks.length;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (previousSignature !== nextSignature) {
|
|
220
|
-
const chunks = await scanFiles(index.rootDir, [relPath]);
|
|
221
|
-
setChunksForFile(index, relPath, chunks);
|
|
222
|
-
modified += 1;
|
|
223
|
-
indexedChunks += chunks.length;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
added,
|
|
228
|
-
modified,
|
|
229
|
-
removed,
|
|
230
|
-
indexedChunks,
|
|
231
|
-
retrievalTokensUsed: 0,
|
|
232
|
-
};
|
|
233
|
-
}
|