@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.21
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 +37 -2
- package/dist/bin/ai.js +148 -75
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +7 -41
- package/dist/src/api/chat.js +77 -20
- package/dist/src/api/http.js +81 -4
- package/dist/src/api/models.js +26 -18
- package/dist/src/artifact-policy.js +12 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +60 -0
- package/dist/src/client-environment.js +129 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +75 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +24 -5
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +17 -2
- package/dist/src/scanner.js +58 -17
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +64 -31
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +164 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +8 -0
- package/dist/src/tools/patch-file.js +16 -2
- package/dist/src/tools/path-suggest.js +139 -0
- package/dist/src/tools/read-document.js +15 -4
- package/dist/src/tools/read-file.js +23 -7
- package/dist/src/tools/replace-document-text.js +234 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +83 -16
- package/dist/src/tools/run-node-script.js +3 -1
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +16 -2
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +315 -24
- package/dist/src/ui/tui/bridge.js +2 -6
- package/dist/src/ui/tui/build-frame.js +225 -25
- package/dist/src/ui/tui/shell-input.js +42 -5
- package/dist/src/version.js +29 -0
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +14 -15
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { drainBackgroundJobNotifications, getBackgroundJob, } from './background-jobs.js';
|
|
1
2
|
import { canStoreEditSnapshot, isEditToolName, isGitWorkTree, MAX_EDIT_JOURNAL_RECORDS, operationFromSnapshots, readFileEditSnapshot, } from './edit-journal.js';
|
|
2
3
|
import { clearEditFailure, collectCommandMutations, captureMutationBaseline, ensureActiveCheckpoint, recordEditFailure, recordSessionEdit, rememberCheckpointFiles, } from './session-safety.js';
|
|
3
4
|
import { buildAgentModeToolBlockedResult, } from './agent-mode.js';
|
|
4
5
|
import { dispatchTool } from './tools/index.js';
|
|
6
|
+
import { syncIndexFromDisk } from './project-index.js';
|
|
7
|
+
import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
|
|
5
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
|
|
9
|
+
import { extractTodosArg } from './todo-list.js';
|
|
6
10
|
const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
7
11
|
'filePath',
|
|
8
12
|
'file_path',
|
|
@@ -11,6 +15,7 @@ const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
|
11
15
|
'file',
|
|
12
16
|
'filename',
|
|
13
17
|
];
|
|
18
|
+
const backgroundCommandTrackers = new Map();
|
|
14
19
|
function toolCallSummary(call) {
|
|
15
20
|
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
16
21
|
if (call.name === 'run_command') {
|
|
@@ -19,6 +24,17 @@ function toolCallSummary(call) {
|
|
|
19
24
|
if (call.name === 'run_node_script') {
|
|
20
25
|
return String(args.script ?? '').trim().slice(0, 120);
|
|
21
26
|
}
|
|
27
|
+
if (call.name === 'shell_job_output' ||
|
|
28
|
+
call.name === 'shell_job_kill') {
|
|
29
|
+
return String(args.job_id ?? '').trim();
|
|
30
|
+
}
|
|
31
|
+
if (call.name === 'update_todos') {
|
|
32
|
+
const raw = extractTodosArg(args);
|
|
33
|
+
const todos = Array.isArray(raw) ? raw : [];
|
|
34
|
+
if (todos.length === 0)
|
|
35
|
+
return '';
|
|
36
|
+
return `${todos.length} item${todos.length === 1 ? '' : 's'}`;
|
|
37
|
+
}
|
|
22
38
|
const filePath = getEditToolFilePath(call);
|
|
23
39
|
if (filePath)
|
|
24
40
|
return filePath;
|
|
@@ -32,6 +48,12 @@ export function formatToolCallForStatus(call) {
|
|
|
32
48
|
}
|
|
33
49
|
function getEditToolFilePath(call) {
|
|
34
50
|
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
51
|
+
if (call.name === 'replace_document_text') {
|
|
52
|
+
const outputPath = args.outputPath ?? args.output_path;
|
|
53
|
+
if (typeof outputPath === 'string' && outputPath.trim()) {
|
|
54
|
+
return outputPath.trim();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
35
57
|
for (const key of EDIT_FILE_PATH_ARG_ALIASES) {
|
|
36
58
|
const value = args[key];
|
|
37
59
|
if (typeof value === 'string' && value.trim())
|
|
@@ -39,6 +61,91 @@ function getEditToolFilePath(call) {
|
|
|
39
61
|
}
|
|
40
62
|
return '';
|
|
41
63
|
}
|
|
64
|
+
function editToolWritesSeparateOutput(call) {
|
|
65
|
+
if (call.name !== 'replace_document_text')
|
|
66
|
+
return false;
|
|
67
|
+
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
68
|
+
const output = args.outputPath ?? args.output_path;
|
|
69
|
+
return typeof output === 'string' && output.trim().length > 0;
|
|
70
|
+
}
|
|
71
|
+
async function collectTrackedCommandMutations({ session, projectIndex, result, tracker, toolName, toolCallId, turnId, }) {
|
|
72
|
+
const checkpoint = ensureActiveCheckpoint(session.clientState.safety, turnId);
|
|
73
|
+
const records = collectCommandMutations({
|
|
74
|
+
state: session.clientState.safety,
|
|
75
|
+
rootDir: session.rootDir,
|
|
76
|
+
tracker,
|
|
77
|
+
toolName,
|
|
78
|
+
toolCallId,
|
|
79
|
+
turnId,
|
|
80
|
+
checkpointId: checkpoint.id,
|
|
81
|
+
});
|
|
82
|
+
if (!records.length)
|
|
83
|
+
return;
|
|
84
|
+
invalidateShellDiagnosticsCache(session.rootDir);
|
|
85
|
+
rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), turnId);
|
|
86
|
+
const repoSync = await syncIndexFromDisk(projectIndex);
|
|
87
|
+
result.repoSync = repoSync;
|
|
88
|
+
result.sessionEdits = records.map((record) => ({
|
|
89
|
+
id: record.id,
|
|
90
|
+
filePath: record.filePath,
|
|
91
|
+
operation: record.operation,
|
|
92
|
+
beforeHash: record.beforeHash,
|
|
93
|
+
afterHash: record.afterHash,
|
|
94
|
+
}));
|
|
95
|
+
result.diagnostics = runShellDiagnostics(session.rootDir);
|
|
96
|
+
}
|
|
97
|
+
export async function collectBackgroundJobUiKillMutations({ session, projectIndex, jobId, result, }) {
|
|
98
|
+
const normalizedJobId = String(jobId ?? result.snapshot?.id ?? '').trim();
|
|
99
|
+
if (!normalizedJobId || !result.ok)
|
|
100
|
+
return;
|
|
101
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
102
|
+
if (!tracked)
|
|
103
|
+
return;
|
|
104
|
+
const mutationResult = result;
|
|
105
|
+
await collectTrackedCommandMutations({
|
|
106
|
+
session,
|
|
107
|
+
projectIndex,
|
|
108
|
+
result: mutationResult,
|
|
109
|
+
tracker: tracked.tracker,
|
|
110
|
+
toolName: tracked.toolName,
|
|
111
|
+
toolCallId: tracked.toolCallId,
|
|
112
|
+
turnId: tracked.turnId,
|
|
113
|
+
});
|
|
114
|
+
if (result.snapshot?.status === 'running') {
|
|
115
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function collectBackgroundJobUiOutputMutations({ session, projectIndex, jobId, }) {
|
|
122
|
+
const normalizedJobId = String(jobId ?? '').trim();
|
|
123
|
+
if (!normalizedJobId)
|
|
124
|
+
return;
|
|
125
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
126
|
+
if (!tracked)
|
|
127
|
+
return;
|
|
128
|
+
const snapshot = getBackgroundJob(normalizedJobId, {
|
|
129
|
+
sessionId: session.sessionId,
|
|
130
|
+
});
|
|
131
|
+
if (!snapshot)
|
|
132
|
+
return;
|
|
133
|
+
await collectTrackedCommandMutations({
|
|
134
|
+
session,
|
|
135
|
+
projectIndex,
|
|
136
|
+
result: {},
|
|
137
|
+
tracker: tracked.tracker,
|
|
138
|
+
toolName: tracked.toolName,
|
|
139
|
+
toolCallId: tracked.toolCallId,
|
|
140
|
+
turnId: tracked.turnId,
|
|
141
|
+
});
|
|
142
|
+
if (snapshot.status === 'running') {
|
|
143
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
42
149
|
function recordAssistantEdit(session, call, result, before) {
|
|
43
150
|
if (!before || !isEditToolName(call.name))
|
|
44
151
|
return;
|
|
@@ -79,6 +186,7 @@ function recordAssistantEdit(session, call, result, before) {
|
|
|
79
186
|
beforeHash: before.hash,
|
|
80
187
|
afterHash: after.hash,
|
|
81
188
|
beforeContent: operation === 'create' ? null : before.content,
|
|
189
|
+
beforeContentEncoding: before.contentEncoding,
|
|
82
190
|
createdAt: new Date().toISOString(),
|
|
83
191
|
revertedAt: null,
|
|
84
192
|
revertedByToolCallId: null,
|
|
@@ -98,6 +206,7 @@ function recordAssistantEdit(session, call, result, before) {
|
|
|
98
206
|
beforeHash: before.hash,
|
|
99
207
|
afterHash: after.hash,
|
|
100
208
|
beforeContent: operation === 'create' ? null : before.content,
|
|
209
|
+
beforeContentEncoding: before.contentEncoding,
|
|
101
210
|
checkpointId: checkpoint.id,
|
|
102
211
|
});
|
|
103
212
|
clearEditFailure(session.clientState.safety, filePath);
|
|
@@ -111,7 +220,14 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
111
220
|
session.onToolEvent?.({ call, result });
|
|
112
221
|
return result;
|
|
113
222
|
}
|
|
114
|
-
const
|
|
223
|
+
const rawEditFilePath = isEditToolName(call.name)
|
|
224
|
+
? getEditToolFilePath(call)
|
|
225
|
+
: '';
|
|
226
|
+
const filePathBeforeEdit = rawEditFilePath &&
|
|
227
|
+
PATH_REPAIRING_EDIT_TOOLS.has(call.name) &&
|
|
228
|
+
!editToolWritesSeparateOutput(call)
|
|
229
|
+
? repairFilePath(session.rootDir, rawEditFilePath)
|
|
230
|
+
: rawEditFilePath;
|
|
115
231
|
if (filePathBeforeEdit) {
|
|
116
232
|
rememberCheckpointFiles(session.clientState.safety, session.rootDir, [filePathBeforeEdit], session.turnState.id);
|
|
117
233
|
}
|
|
@@ -123,6 +239,7 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
123
239
|
: null;
|
|
124
240
|
const context = {
|
|
125
241
|
rootDir: session.rootDir,
|
|
242
|
+
sessionId: session.sessionId,
|
|
126
243
|
projectIndex: toolContext.projectIndex,
|
|
127
244
|
autoYes: session.autoYes,
|
|
128
245
|
confirmCommand: session.confirmCommand,
|
|
@@ -144,28 +261,57 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
144
261
|
};
|
|
145
262
|
const result = await dispatchTool(context, call);
|
|
146
263
|
recordAssistantEdit(session, call, result, beforeEditSnapshot);
|
|
147
|
-
if (result &&
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
264
|
+
if (result && typeof result === 'object' && commandTracker) {
|
|
265
|
+
await collectTrackedCommandMutations({
|
|
266
|
+
session,
|
|
267
|
+
projectIndex: toolContext.projectIndex,
|
|
268
|
+
result,
|
|
152
269
|
tracker: commandTracker,
|
|
153
270
|
toolName: call.name,
|
|
154
271
|
toolCallId: call.id,
|
|
155
272
|
turnId: session.turnState.id,
|
|
156
|
-
checkpointId: checkpoint.id,
|
|
157
273
|
});
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
result.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
|
|
274
|
+
if (call.name === 'run_command' &&
|
|
275
|
+
result.backgrounded === true &&
|
|
276
|
+
result.status === 'running' &&
|
|
277
|
+
result.jobId) {
|
|
278
|
+
backgroundCommandTrackers.set(String(result.jobId), {
|
|
279
|
+
tracker: captureMutationBaseline(session.rootDir),
|
|
280
|
+
toolName: call.name,
|
|
281
|
+
toolCallId: call.id,
|
|
282
|
+
turnId: session.turnState.id,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (result &&
|
|
287
|
+
typeof result === 'object' &&
|
|
288
|
+
(call.name === 'shell_job_output' || call.name === 'shell_job_kill')) {
|
|
289
|
+
const jobId = String(result.jobId ?? call.args?.job_id ?? '').trim();
|
|
290
|
+
const tracked = backgroundCommandTrackers.get(jobId);
|
|
291
|
+
if (tracked) {
|
|
292
|
+
await collectTrackedCommandMutations({
|
|
293
|
+
session,
|
|
294
|
+
projectIndex: toolContext.projectIndex,
|
|
295
|
+
result,
|
|
296
|
+
tracker: tracked.tracker,
|
|
297
|
+
toolName: tracked.toolName,
|
|
298
|
+
toolCallId: tracked.toolCallId,
|
|
299
|
+
turnId: tracked.turnId,
|
|
300
|
+
});
|
|
301
|
+
if (result.status === 'running') {
|
|
302
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
backgroundCommandTrackers.delete(jobId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (result && typeof result === 'object') {
|
|
310
|
+
const backgroundJobUpdate = drainBackgroundJobNotifications({
|
|
311
|
+
sessionId: session.sessionId,
|
|
312
|
+
});
|
|
313
|
+
if (backgroundJobUpdate) {
|
|
314
|
+
result.backgroundJobUpdate = backgroundJobUpdate;
|
|
169
315
|
}
|
|
170
316
|
}
|
|
171
317
|
session.onToolEvent?.({ call, result });
|
package/dist/src/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shellJobOutput } from './shell-job-output.js';
|
|
1
2
|
import { deleteFile } from './delete-file.js';
|
|
2
3
|
import { findSymbol } from './find-symbol.js';
|
|
3
4
|
import { getDiagnostics } from './get-diagnostics.js';
|
|
@@ -11,13 +12,16 @@ import { listSymbols } from './list-symbols.js';
|
|
|
11
12
|
import { patchFile } from './patch-file.js';
|
|
12
13
|
import { readDocument } from './read-document.js';
|
|
13
14
|
import { readFile } from './read-file.js';
|
|
15
|
+
import { replaceDocumentText } from './replace-document-text.js';
|
|
14
16
|
import { runShellCommand } from './run-command.js';
|
|
15
17
|
import { runNodeScript } from './run-node-script.js';
|
|
16
18
|
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
17
19
|
import { searchCode } from './search-code.js';
|
|
18
20
|
import { getSignatureHelp } from './signature-help.js';
|
|
21
|
+
import { shellJobKill } from './shell-job-kill.js';
|
|
19
22
|
import { strReplace } from './str-replace.js';
|
|
20
23
|
import { undoEdit } from './undo-edit.js';
|
|
24
|
+
import { updateTodos } from './update-todos.js';
|
|
21
25
|
import { writeFile } from './write-file.js';
|
|
22
26
|
export const TOOL_MAP = {
|
|
23
27
|
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
@@ -25,6 +29,7 @@ export const TOOL_MAP = {
|
|
|
25
29
|
list_directories: (context, args) => listDirectories(context, args),
|
|
26
30
|
read_file: (context, args) => readFile(context, args),
|
|
27
31
|
read_document: (context, args) => readDocument(context.rootDir, args, context.env),
|
|
32
|
+
replace_document_text: replaceDocumentText,
|
|
28
33
|
grep_code: (context, args) => grepCode(context.rootDir, args),
|
|
29
34
|
find_symbol: (context, args) => findSymbol(context, args),
|
|
30
35
|
list_symbols: (context, args) => listSymbols(context, args),
|
|
@@ -42,6 +47,9 @@ export const TOOL_MAP = {
|
|
|
42
47
|
undo_edit: undoEdit,
|
|
43
48
|
run_command: runShellCommand,
|
|
44
49
|
run_node_script: runNodeScript,
|
|
50
|
+
shell_job_output: shellJobOutput,
|
|
51
|
+
shell_job_kill: shellJobKill,
|
|
52
|
+
update_todos: updateTodos,
|
|
45
53
|
};
|
|
46
54
|
function invalidToolCall(error) {
|
|
47
55
|
return {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
3
4
|
import { applyUnifiedPatch, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
|
|
4
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
6
|
+
import { repairFilePath } from './path-suggest.js';
|
|
5
7
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
8
|
import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
|
|
7
9
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
10
|
+
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
8
11
|
export async function patchFile(context, args) {
|
|
9
12
|
const { rootDir, projectIndex, autoYes, confirmPatch } = context;
|
|
10
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
13
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
11
14
|
let patch = typeof args.patch === 'string' ? args.patch : '';
|
|
12
15
|
if (!filePath) {
|
|
13
16
|
return { ok: false, error: 'filePath is required' };
|
|
@@ -15,6 +18,17 @@ export async function patchFile(context, args) {
|
|
|
15
18
|
if (!patch.trim()) {
|
|
16
19
|
return { ok: false, error: 'patch is required' };
|
|
17
20
|
}
|
|
21
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
22
|
+
if (DOCUMENT_EXTENSIONS.has(ext)) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
filePath,
|
|
26
|
+
error: ext === '.docx'
|
|
27
|
+
? 'Use replace_document_text for .docx files.'
|
|
28
|
+
: `Use read_document for ${ext} files; patching is not supported.`,
|
|
29
|
+
failureCategory: 'invalid_argument',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
18
32
|
let originalContent;
|
|
19
33
|
try {
|
|
20
34
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isSensitiveProjectPath, shouldIgnoreArtifactPath, } from '../artifact-policy.js';
|
|
4
|
+
function foldName(name) {
|
|
5
|
+
return name
|
|
6
|
+
.normalize('NFC')
|
|
7
|
+
.replace(/[‘’ʼ]/g, "'")
|
|
8
|
+
.replace(/[“”]/g, '"')
|
|
9
|
+
.replace(/ /g, ' ')
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function levenshtein(a, b) {
|
|
13
|
+
if (a === b)
|
|
14
|
+
return 0;
|
|
15
|
+
const rows = a.length + 1;
|
|
16
|
+
const cols = b.length + 1;
|
|
17
|
+
let prev = Array.from({ length: cols }, (_, j) => j);
|
|
18
|
+
for (let i = 1; i < rows; i++) {
|
|
19
|
+
const current = [i, ...new Array(cols - 1).fill(0)];
|
|
20
|
+
for (let j = 1; j < cols; j++) {
|
|
21
|
+
current[j] = Math.min(prev[j] + 1, current[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
22
|
+
}
|
|
23
|
+
prev = current;
|
|
24
|
+
}
|
|
25
|
+
return prev[cols - 1];
|
|
26
|
+
}
|
|
27
|
+
export function suggestClosestPath(rootDir, missingPath) {
|
|
28
|
+
const resolved = path.isAbsolute(missingPath)
|
|
29
|
+
? missingPath
|
|
30
|
+
: path.resolve(rootDir, missingPath);
|
|
31
|
+
const directory = path.dirname(resolved);
|
|
32
|
+
const wantedBase = foldName(path.basename(resolved));
|
|
33
|
+
if (!wantedBase)
|
|
34
|
+
return null;
|
|
35
|
+
let candidates;
|
|
36
|
+
try {
|
|
37
|
+
candidates = readdirSync(directory);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const threshold = Math.max(2, Math.floor(wantedBase.length * 0.25));
|
|
43
|
+
let best = null;
|
|
44
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
const candidateRelative = path.relative(rootDir, path.join(directory, candidate));
|
|
47
|
+
if (isSensitiveProjectPath(candidateRelative))
|
|
48
|
+
continue;
|
|
49
|
+
const distance = levenshtein(wantedBase, foldName(candidate));
|
|
50
|
+
if (distance < bestDistance) {
|
|
51
|
+
bestDistance = distance;
|
|
52
|
+
best = candidate;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!best || bestDistance > threshold)
|
|
56
|
+
return null;
|
|
57
|
+
const suggested = path.join(directory, best);
|
|
58
|
+
const relative = path.relative(rootDir, suggested);
|
|
59
|
+
return relative && !relative.startsWith('..') ? relative : suggested;
|
|
60
|
+
}
|
|
61
|
+
function foldPunctuation(name) {
|
|
62
|
+
return name
|
|
63
|
+
.normalize('NFC')
|
|
64
|
+
.replace(/[‘’ʼ]/g, "'")
|
|
65
|
+
.replace(/[“”]/g, '"')
|
|
66
|
+
.replace(/ /g, ' ');
|
|
67
|
+
}
|
|
68
|
+
function stripSurroundingQuotes(p) {
|
|
69
|
+
if (p.length >= 2) {
|
|
70
|
+
const first = p[0];
|
|
71
|
+
const last = p[p.length - 1];
|
|
72
|
+
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
|
|
73
|
+
return p.slice(1, -1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
function collapseDoubledBackslashes(p) {
|
|
79
|
+
return p.replace(/\\\\/g, '\\');
|
|
80
|
+
}
|
|
81
|
+
function resolveAgainst(rootDir, p) {
|
|
82
|
+
return path.isAbsolute(p) ? p : path.resolve(rootDir, p);
|
|
83
|
+
}
|
|
84
|
+
function existsAgainst(rootDir, p) {
|
|
85
|
+
try {
|
|
86
|
+
return existsSync(resolveAgainst(rootDir, p));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isProtectedRepairTarget(rootDir, candidate) {
|
|
93
|
+
const rel = path.relative(rootDir, resolveAgainst(rootDir, candidate));
|
|
94
|
+
const projectPath = rel && !rel.startsWith('..') ? rel : candidate;
|
|
95
|
+
return (isSensitiveProjectPath(projectPath) ||
|
|
96
|
+
(rel !== '' && !rel.startsWith('..') && shouldIgnoreArtifactPath(rel)));
|
|
97
|
+
}
|
|
98
|
+
export const PATH_REPAIRING_EDIT_TOOLS = new Set([
|
|
99
|
+
'str_replace',
|
|
100
|
+
'patch_file',
|
|
101
|
+
'replace_document_text',
|
|
102
|
+
]);
|
|
103
|
+
export function repairFilePath(rootDir, raw) {
|
|
104
|
+
if (!raw || existsAgainst(rootDir, raw))
|
|
105
|
+
return raw;
|
|
106
|
+
const dequoted = stripSurroundingQuotes(raw);
|
|
107
|
+
for (const candidate of [
|
|
108
|
+
dequoted,
|
|
109
|
+
collapseDoubledBackslashes(raw),
|
|
110
|
+
collapseDoubledBackslashes(dequoted),
|
|
111
|
+
]) {
|
|
112
|
+
if (candidate !== raw &&
|
|
113
|
+
existsAgainst(rootDir, candidate) &&
|
|
114
|
+
!isProtectedRepairTarget(rootDir, candidate)) {
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const probe = resolveAgainst(rootDir, dequoted);
|
|
119
|
+
const directory = path.dirname(probe);
|
|
120
|
+
const wanted = foldPunctuation(path.basename(probe));
|
|
121
|
+
if (!wanted)
|
|
122
|
+
return raw;
|
|
123
|
+
let entries;
|
|
124
|
+
try {
|
|
125
|
+
entries = readdirSync(directory);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
const matches = entries.filter((entry) => foldPunctuation(entry) === wanted);
|
|
131
|
+
if (matches.length !== 1)
|
|
132
|
+
return raw;
|
|
133
|
+
const matchedAbs = path.join(directory, matches[0]);
|
|
134
|
+
const matchedRel = path.relative(rootDir, matchedAbs);
|
|
135
|
+
const matched = path.isAbsolute(dequoted) ? matchedAbs : matchedRel || matchedAbs;
|
|
136
|
+
if (isProtectedRepairTarget(rootDir, matched))
|
|
137
|
+
return raw;
|
|
138
|
+
return matched;
|
|
139
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { isSensitiveProjectPath, normalizeProjectRelativePath, } from '../artifact-policy.js';
|
|
4
|
+
import { repairFilePath, suggestClosestPath } from './path-suggest.js';
|
|
4
5
|
import { readCliAuthConfig } from '../api/auth.js';
|
|
5
6
|
export function normalizeDocumentText(raw) {
|
|
6
7
|
const text = String(raw ?? '').replace(/\r\n?/g, '\n');
|
|
@@ -36,6 +37,7 @@ export function normalizeDocumentText(raw) {
|
|
|
36
37
|
}
|
|
37
38
|
async function parseDocumentOnServer(config, fileName, fileData, ext, args) {
|
|
38
39
|
const includePageArgs = ext === '.pdf';
|
|
40
|
+
const includeParagraphArgs = ext === '.docx';
|
|
39
41
|
const response = await globalThis.fetch(`${config.serverUrl.replace(/\/+$/, '')}/v1/document/parse`, {
|
|
40
42
|
method: 'POST',
|
|
41
43
|
headers: {
|
|
@@ -51,6 +53,12 @@ async function parseDocumentOnServer(config, fileName, fileData, ext, args) {
|
|
|
51
53
|
...(includePageArgs && args.lastPage !== undefined
|
|
52
54
|
? { lastPage: args.lastPage }
|
|
53
55
|
: {}),
|
|
56
|
+
...(includeParagraphArgs && args.firstParagraph !== undefined
|
|
57
|
+
? { firstParagraph: args.firstParagraph }
|
|
58
|
+
: {}),
|
|
59
|
+
...(includeParagraphArgs && args.lastParagraph !== undefined
|
|
60
|
+
? { lastParagraph: args.lastParagraph }
|
|
61
|
+
: {}),
|
|
54
62
|
}),
|
|
55
63
|
});
|
|
56
64
|
const data = await response.json().catch(() => null);
|
|
@@ -63,7 +71,7 @@ async function parseDocumentOnServer(config, fileName, fileData, ext, args) {
|
|
|
63
71
|
return data;
|
|
64
72
|
}
|
|
65
73
|
export async function readDocument(rootDir, args, env) {
|
|
66
|
-
const raw = String(args.filePath ?? '').trim();
|
|
74
|
+
const raw = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
67
75
|
if (!raw) {
|
|
68
76
|
return { ok: false, error: 'filePath is required' };
|
|
69
77
|
}
|
|
@@ -76,17 +84,20 @@ export async function readDocument(rootDir, args, env) {
|
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
if (!existsSync(resolvedPath)) {
|
|
87
|
+
const suggestion = suggestClosestPath(rootDir, resolvedPath);
|
|
79
88
|
return {
|
|
80
89
|
ok: false,
|
|
81
|
-
error:
|
|
90
|
+
error: suggestion
|
|
91
|
+
? `File not found: ${resolvedPath}. Did you mean "${suggestion}"? Note the exact punctuation (e.g. curly apostrophe ’ vs straight ').`
|
|
92
|
+
: `File not found: ${resolvedPath}`,
|
|
82
93
|
failureCategory: 'not_found',
|
|
83
94
|
};
|
|
84
95
|
}
|
|
85
96
|
const ext = path.extname(resolvedPath).toLowerCase();
|
|
86
|
-
if (ext !== '.pdf' && ext !== '.xlsx') {
|
|
97
|
+
if (ext !== '.pdf' && ext !== '.xlsx' && ext !== '.docx') {
|
|
87
98
|
return {
|
|
88
99
|
ok: false,
|
|
89
|
-
error: `Unsupported file type: "${ext}". read_document only supports .pdf and .
|
|
100
|
+
error: `Unsupported file type: "${ext}". read_document only supports .pdf, .xlsx, and .docx.`,
|
|
90
101
|
};
|
|
91
102
|
}
|
|
92
103
|
const authConfig = readCliAuthConfig(env);
|
|
@@ -3,13 +3,15 @@ import path from 'path';
|
|
|
3
3
|
import { normalizeProjectRelativePath, shouldIgnoreArtifactPath, } from '../artifact-policy.js';
|
|
4
4
|
import { buildSecretFilePreview, isDotenvLikePath, looksLikeEditableDotenv, shouldUseSecretFilePreview, } from '../secret-preview.js';
|
|
5
5
|
import { readProjectFile } from '../patcher.js';
|
|
6
|
+
import { repairFilePath } from './path-suggest.js';
|
|
6
7
|
import { dotenvFitsRedactionBudget, getCurrentFileHash, recordReadCoverage, redactContentWithStableTokens, redactDotenvWithStableTokens, } from '../session-safety.js';
|
|
7
8
|
import { readFileRange, truncate } from '../utils.js';
|
|
8
9
|
const MAX_FILE_READ_CHARS = 12000;
|
|
10
|
+
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
9
11
|
export async function readFile(context, args) {
|
|
10
12
|
const rootDir = typeof context === 'string' ? context : context.rootDir;
|
|
11
13
|
const safety = typeof context === 'string' ? undefined : context.safety;
|
|
12
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
14
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
13
15
|
if (!filePath) {
|
|
14
16
|
return { ok: false, error: 'filePath is required' };
|
|
15
17
|
}
|
|
@@ -26,19 +28,37 @@ export async function readFile(context, args) {
|
|
|
26
28
|
error: 'This path is not permitted.',
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
const documentExt = path.extname(projectPath ?? filePath).toLowerCase();
|
|
32
|
+
if (DOCUMENT_EXTENSIONS.has(documentExt)) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: `Use read_document for ${documentExt} files.`,
|
|
36
|
+
failureCategory: 'invalid_argument',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
29
39
|
let content;
|
|
30
40
|
if (projectPath) {
|
|
31
41
|
try {
|
|
32
42
|
content = readProjectFile(rootDir, filePath);
|
|
33
43
|
}
|
|
34
44
|
catch (err) {
|
|
35
|
-
|
|
45
|
+
const message = String(err?.message ?? err);
|
|
46
|
+
const notFound = /^File does not exist:/.test(message);
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: message,
|
|
50
|
+
...(notFound ? { failureCategory: 'not_found' } : {}),
|
|
51
|
+
};
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
else {
|
|
39
55
|
const absPath = path.resolve(filePath);
|
|
40
56
|
if (!existsSync(absPath)) {
|
|
41
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `File does not exist: ${filePath}`,
|
|
60
|
+
failureCategory: 'not_found',
|
|
61
|
+
};
|
|
42
62
|
}
|
|
43
63
|
try {
|
|
44
64
|
content = readFileSync(absPath, 'utf-8');
|
|
@@ -48,10 +68,6 @@ export async function readFile(context, args) {
|
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
const previewPath = projectPath ?? filePath;
|
|
51
|
-
// A clean dotenv file is shown with keys visible and values tokenized so the
|
|
52
|
-
// agent can still edit it (str_replace/write_file round-trip the tokens) and
|
|
53
|
-
// read coverage is recorded. Any other secret file — PEM, JSON credentials,
|
|
54
|
-
// or a dotenv with a stray non-assignment line — keeps the opaque blackout.
|
|
55
71
|
const editableDotenv = Boolean(projectPath) &&
|
|
56
72
|
Boolean(safety) &&
|
|
57
73
|
isDotenvLikePath(previewPath) &&
|