@thegitai/cli 1.0.0-beta.8 → 1.0.0-preview.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/README.md +36 -2
- package/dist/bin/ai.js +135 -26
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +3 -3
- package/dist/src/api/browser-login.js +3 -38
- package/dist/src/api/chat.js +57 -11
- package/dist/src/api/http.js +49 -1
- package/dist/src/api/models.js +26 -20
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +11 -6
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +1 -3
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +159 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +3 -2
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +14 -7
- package/dist/src/tools/replace-document-text.js +3 -11
- 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 +3 -2
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +1 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +313 -23
- package/dist/src/ui/tui/bridge.js +0 -4
- package/dist/src/ui/tui/build-frame.js +220 -24
- package/dist/src/ui/tui/shell-input.js +33 -4
- package/dist/src/ui/tui/terminal-title.js +81 -0
- package/dist/src/version.js +0 -6
- 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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const MAX_TODO_ITEMS = 20;
|
|
2
|
+
export const MAX_TODO_TEXT_CHARS = 160;
|
|
3
|
+
let items = [];
|
|
4
|
+
let activeSessionId = null;
|
|
5
|
+
const STATUS_ALIASES = {
|
|
6
|
+
pending: 'pending',
|
|
7
|
+
todo: 'pending',
|
|
8
|
+
not_started: 'pending',
|
|
9
|
+
in_progress: 'in_progress',
|
|
10
|
+
active: 'in_progress',
|
|
11
|
+
doing: 'in_progress',
|
|
12
|
+
completed: 'completed',
|
|
13
|
+
complete: 'completed',
|
|
14
|
+
done: 'completed',
|
|
15
|
+
};
|
|
16
|
+
function normalizeStatus(raw) {
|
|
17
|
+
const key = String(raw ?? '')
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[-\s]+/g, '_');
|
|
21
|
+
return STATUS_ALIASES[key] ?? null;
|
|
22
|
+
}
|
|
23
|
+
export function isCompletedStatus(raw) {
|
|
24
|
+
return normalizeStatus(raw) === 'completed';
|
|
25
|
+
}
|
|
26
|
+
const TODOS_ARG_ALIASES = ['items', 'todo_list', 'todoList', 'list', 'tasks'];
|
|
27
|
+
export function extractTodosArg(args) {
|
|
28
|
+
if (!args || typeof args !== 'object')
|
|
29
|
+
return undefined;
|
|
30
|
+
const record = args;
|
|
31
|
+
if (record.todos !== undefined)
|
|
32
|
+
return record.todos;
|
|
33
|
+
for (const key of TODOS_ARG_ALIASES) {
|
|
34
|
+
if (record[key] !== undefined)
|
|
35
|
+
return record[key];
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function setTodoSession(sessionId) {
|
|
40
|
+
const next = String(sessionId ?? '').trim() || null;
|
|
41
|
+
if (activeSessionId !== next) {
|
|
42
|
+
items = [];
|
|
43
|
+
}
|
|
44
|
+
activeSessionId = next;
|
|
45
|
+
}
|
|
46
|
+
export function listTodos() {
|
|
47
|
+
return items.map((item) => ({ ...item }));
|
|
48
|
+
}
|
|
49
|
+
export function getTodoSnapshot() {
|
|
50
|
+
return {
|
|
51
|
+
items: listTodos(),
|
|
52
|
+
completedCount: items.filter((item) => item.status === 'completed').length,
|
|
53
|
+
totalCount: items.length,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function clearTodos() {
|
|
57
|
+
items = [];
|
|
58
|
+
}
|
|
59
|
+
export function replaceTodos(raw) {
|
|
60
|
+
if (!Array.isArray(raw)) {
|
|
61
|
+
return { ok: false, error: 'todos must be an array of { text, status } items.' };
|
|
62
|
+
}
|
|
63
|
+
const normalizations = [];
|
|
64
|
+
if (raw.length > MAX_TODO_ITEMS) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: `todos supports at most ${MAX_TODO_ITEMS} items; got ${raw.length}. Use fewer, broader steps.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const next = [];
|
|
71
|
+
let sawInProgress = false;
|
|
72
|
+
for (const [index, entry] of raw.entries()) {
|
|
73
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
74
|
+
return { ok: false, error: `todos[${index}] must be an object with text and status.` };
|
|
75
|
+
}
|
|
76
|
+
const text = String(entry.text ?? '')
|
|
77
|
+
.replace(/\s+/g, ' ')
|
|
78
|
+
.trim();
|
|
79
|
+
if (!text) {
|
|
80
|
+
return { ok: false, error: `todos[${index}].text must be a non-empty string.` };
|
|
81
|
+
}
|
|
82
|
+
const status = normalizeStatus(entry.status);
|
|
83
|
+
if (!status) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: `todos[${index}].status must be one of: pending, in_progress, completed.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
let boundedText = text;
|
|
90
|
+
if (boundedText.length > MAX_TODO_TEXT_CHARS) {
|
|
91
|
+
boundedText = `${boundedText.slice(0, MAX_TODO_TEXT_CHARS - 1)}…`;
|
|
92
|
+
normalizations.push(`todos[${index}].text truncated to ${MAX_TODO_TEXT_CHARS} chars`);
|
|
93
|
+
}
|
|
94
|
+
let finalStatus = status;
|
|
95
|
+
if (status === 'in_progress') {
|
|
96
|
+
if (sawInProgress) {
|
|
97
|
+
finalStatus = 'pending';
|
|
98
|
+
normalizations.push(`todos[${index}] demoted to pending: only one item can be in_progress`);
|
|
99
|
+
}
|
|
100
|
+
sawInProgress = true;
|
|
101
|
+
}
|
|
102
|
+
next.push({ text: boundedText, status: finalStatus });
|
|
103
|
+
}
|
|
104
|
+
items = next;
|
|
105
|
+
return { ok: true, snapshot: getTodoSnapshot(), normalizations };
|
|
106
|
+
}
|
|
@@ -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;
|
|
@@ -45,6 +61,94 @@ function getEditToolFilePath(call) {
|
|
|
45
61
|
}
|
|
46
62
|
return '';
|
|
47
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 priorSync = result.repoSync;
|
|
87
|
+
if (!(priorSync &&
|
|
88
|
+
(priorSync.added || priorSync.modified || priorSync.removed))) {
|
|
89
|
+
result.repoSync = await syncIndexFromDisk(projectIndex);
|
|
90
|
+
}
|
|
91
|
+
result.sessionEdits = records.map((record) => ({
|
|
92
|
+
id: record.id,
|
|
93
|
+
filePath: record.filePath,
|
|
94
|
+
operation: record.operation,
|
|
95
|
+
beforeHash: record.beforeHash,
|
|
96
|
+
afterHash: record.afterHash,
|
|
97
|
+
}));
|
|
98
|
+
result.diagnostics = runShellDiagnostics(session.rootDir);
|
|
99
|
+
}
|
|
100
|
+
export async function collectBackgroundJobUiKillMutations({ session, projectIndex, jobId, result, }) {
|
|
101
|
+
const normalizedJobId = String(jobId ?? result.snapshot?.id ?? '').trim();
|
|
102
|
+
if (!normalizedJobId || !result.ok)
|
|
103
|
+
return;
|
|
104
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
105
|
+
if (!tracked)
|
|
106
|
+
return;
|
|
107
|
+
const mutationResult = result;
|
|
108
|
+
await collectTrackedCommandMutations({
|
|
109
|
+
session,
|
|
110
|
+
projectIndex,
|
|
111
|
+
result: mutationResult,
|
|
112
|
+
tracker: tracked.tracker,
|
|
113
|
+
toolName: tracked.toolName,
|
|
114
|
+
toolCallId: tracked.toolCallId,
|
|
115
|
+
turnId: tracked.turnId,
|
|
116
|
+
});
|
|
117
|
+
if (result.snapshot?.status === 'running') {
|
|
118
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function collectBackgroundJobUiOutputMutations({ session, projectIndex, jobId, }) {
|
|
125
|
+
const normalizedJobId = String(jobId ?? '').trim();
|
|
126
|
+
if (!normalizedJobId)
|
|
127
|
+
return;
|
|
128
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
129
|
+
if (!tracked)
|
|
130
|
+
return;
|
|
131
|
+
const snapshot = getBackgroundJob(normalizedJobId, {
|
|
132
|
+
sessionId: session.sessionId,
|
|
133
|
+
});
|
|
134
|
+
if (!snapshot)
|
|
135
|
+
return;
|
|
136
|
+
await collectTrackedCommandMutations({
|
|
137
|
+
session,
|
|
138
|
+
projectIndex,
|
|
139
|
+
result: {},
|
|
140
|
+
tracker: tracked.tracker,
|
|
141
|
+
toolName: tracked.toolName,
|
|
142
|
+
toolCallId: tracked.toolCallId,
|
|
143
|
+
turnId: tracked.turnId,
|
|
144
|
+
});
|
|
145
|
+
if (snapshot.status === 'running') {
|
|
146
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
48
152
|
function recordAssistantEdit(session, call, result, before) {
|
|
49
153
|
if (!before || !isEditToolName(call.name))
|
|
50
154
|
return;
|
|
@@ -119,7 +223,14 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
119
223
|
session.onToolEvent?.({ call, result });
|
|
120
224
|
return result;
|
|
121
225
|
}
|
|
122
|
-
const
|
|
226
|
+
const rawEditFilePath = isEditToolName(call.name)
|
|
227
|
+
? getEditToolFilePath(call)
|
|
228
|
+
: '';
|
|
229
|
+
const filePathBeforeEdit = rawEditFilePath &&
|
|
230
|
+
PATH_REPAIRING_EDIT_TOOLS.has(call.name) &&
|
|
231
|
+
!editToolWritesSeparateOutput(call)
|
|
232
|
+
? repairFilePath(session.rootDir, rawEditFilePath)
|
|
233
|
+
: rawEditFilePath;
|
|
123
234
|
if (filePathBeforeEdit) {
|
|
124
235
|
rememberCheckpointFiles(session.clientState.safety, session.rootDir, [filePathBeforeEdit], session.turnState.id);
|
|
125
236
|
}
|
|
@@ -131,6 +242,7 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
131
242
|
: null;
|
|
132
243
|
const context = {
|
|
133
244
|
rootDir: session.rootDir,
|
|
245
|
+
sessionId: session.sessionId,
|
|
134
246
|
projectIndex: toolContext.projectIndex,
|
|
135
247
|
autoYes: session.autoYes,
|
|
136
248
|
confirmCommand: session.confirmCommand,
|
|
@@ -152,28 +264,57 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
152
264
|
};
|
|
153
265
|
const result = await dispatchTool(context, call);
|
|
154
266
|
recordAssistantEdit(session, call, result, beforeEditSnapshot);
|
|
155
|
-
if (result &&
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
267
|
+
if (result && typeof result === 'object' && commandTracker) {
|
|
268
|
+
await collectTrackedCommandMutations({
|
|
269
|
+
session,
|
|
270
|
+
projectIndex: toolContext.projectIndex,
|
|
271
|
+
result,
|
|
160
272
|
tracker: commandTracker,
|
|
161
273
|
toolName: call.name,
|
|
162
274
|
toolCallId: call.id,
|
|
163
275
|
turnId: session.turnState.id,
|
|
164
|
-
checkpointId: checkpoint.id,
|
|
165
276
|
});
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
result.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
277
|
+
if (call.name === 'run_command' &&
|
|
278
|
+
result.backgrounded === true &&
|
|
279
|
+
result.status === 'running' &&
|
|
280
|
+
result.jobId) {
|
|
281
|
+
backgroundCommandTrackers.set(String(result.jobId), {
|
|
282
|
+
tracker: captureMutationBaseline(session.rootDir),
|
|
283
|
+
toolName: call.name,
|
|
284
|
+
toolCallId: call.id,
|
|
285
|
+
turnId: session.turnState.id,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (result &&
|
|
290
|
+
typeof result === 'object' &&
|
|
291
|
+
(call.name === 'shell_job_output' || call.name === 'shell_job_kill')) {
|
|
292
|
+
const jobId = String(result.jobId ?? call.args?.job_id ?? '').trim();
|
|
293
|
+
const tracked = backgroundCommandTrackers.get(jobId);
|
|
294
|
+
if (tracked) {
|
|
295
|
+
await collectTrackedCommandMutations({
|
|
296
|
+
session,
|
|
297
|
+
projectIndex: toolContext.projectIndex,
|
|
298
|
+
result,
|
|
299
|
+
tracker: tracked.tracker,
|
|
300
|
+
toolName: tracked.toolName,
|
|
301
|
+
toolCallId: tracked.toolCallId,
|
|
302
|
+
turnId: tracked.turnId,
|
|
303
|
+
});
|
|
304
|
+
if (result.status === 'running') {
|
|
305
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
backgroundCommandTrackers.delete(jobId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (result && typeof result === 'object') {
|
|
313
|
+
const backgroundJobUpdate = drainBackgroundJobNotifications({
|
|
314
|
+
sessionId: session.sessionId,
|
|
315
|
+
});
|
|
316
|
+
if (backgroundJobUpdate) {
|
|
317
|
+
result.backgroundJobUpdate = backgroundJobUpdate;
|
|
177
318
|
}
|
|
178
319
|
}
|
|
179
320
|
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';
|
|
@@ -17,8 +18,10 @@ import { runNodeScript } from './run-node-script.js';
|
|
|
17
18
|
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
18
19
|
import { searchCode } from './search-code.js';
|
|
19
20
|
import { getSignatureHelp } from './signature-help.js';
|
|
21
|
+
import { shellJobKill } from './shell-job-kill.js';
|
|
20
22
|
import { strReplace } from './str-replace.js';
|
|
21
23
|
import { undoEdit } from './undo-edit.js';
|
|
24
|
+
import { updateTodos } from './update-todos.js';
|
|
22
25
|
import { writeFile } from './write-file.js';
|
|
23
26
|
export const TOOL_MAP = {
|
|
24
27
|
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
@@ -44,6 +47,9 @@ export const TOOL_MAP = {
|
|
|
44
47
|
undo_edit: undoEdit,
|
|
45
48
|
run_command: runShellCommand,
|
|
46
49
|
run_node_script: runNodeScript,
|
|
50
|
+
shell_job_output: shellJobOutput,
|
|
51
|
+
shell_job_kill: shellJobKill,
|
|
52
|
+
update_todos: updateTodos,
|
|
47
53
|
};
|
|
48
54
|
function invalidToolCall(error) {
|
|
49
55
|
return {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { applyUnifiedPatch, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
|
|
5
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
6
|
+
import { repairFilePath } from './path-suggest.js';
|
|
6
7
|
import { isTuiMode } from '../runtime-mode.js';
|
|
7
8
|
import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
|
|
8
9
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
9
10
|
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
10
11
|
export async function patchFile(context, args) {
|
|
11
12
|
const { rootDir, projectIndex, autoYes, confirmPatch } = context;
|
|
12
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
13
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
13
14
|
let patch = typeof args.patch === 'string' ? args.patch : '';
|
|
14
15
|
if (!filePath) {
|
|
15
16
|
return { ok: false, error: 'filePath is required' };
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { readdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { isSensitiveProjectPath } from '../artifact-policy.js';
|
|
4
|
-
// "File not found" recovery hint: when a model mistypes a filename (most
|
|
5
|
-
// often Unicode punctuation — a straight ' for a curly ’ — or a small typo),
|
|
6
|
-
// suggest the closest real file from the same directory.
|
|
3
|
+
import { isSensitiveProjectPath, shouldIgnoreArtifactPath, } from '../artifact-policy.js';
|
|
7
4
|
function foldName(name) {
|
|
8
5
|
return name
|
|
9
6
|
.normalize('NFC')
|
|
@@ -46,9 +43,6 @@ export function suggestClosestPath(rootDir, missingPath) {
|
|
|
46
43
|
let best = null;
|
|
47
44
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
48
45
|
for (const candidate of candidates) {
|
|
49
|
-
// Never suggest a file the caller would refuse to read/write directly:
|
|
50
|
-
// probing a near-miss like `.enx` or `credential.docx` must not leak the
|
|
51
|
-
// existence of `.env`/credentials through the recovery hint.
|
|
52
46
|
const candidateRelative = path.relative(rootDir, path.join(directory, candidate));
|
|
53
47
|
if (isSensitiveProjectPath(candidateRelative))
|
|
54
48
|
continue;
|
|
@@ -64,3 +58,82 @@ export function suggestClosestPath(rootDir, missingPath) {
|
|
|
64
58
|
const relative = path.relative(rootDir, suggested);
|
|
65
59
|
return relative && !relative.startsWith('..') ? relative : suggested;
|
|
66
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,7 +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 { suggestClosestPath } from './path-suggest.js';
|
|
4
|
+
import { repairFilePath, suggestClosestPath } from './path-suggest.js';
|
|
5
5
|
import { readCliAuthConfig } from '../api/auth.js';
|
|
6
6
|
export function normalizeDocumentText(raw) {
|
|
7
7
|
const text = String(raw ?? '').replace(/\r\n?/g, '\n');
|
|
@@ -71,7 +71,7 @@ async function parseDocumentOnServer(config, fileName, fileData, ext, args) {
|
|
|
71
71
|
return data;
|
|
72
72
|
}
|
|
73
73
|
export async function readDocument(rootDir, args, env) {
|
|
74
|
-
const raw = String(args.filePath ?? '').trim();
|
|
74
|
+
const raw = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
75
75
|
if (!raw) {
|
|
76
76
|
return { ok: false, error: 'filePath is required' };
|
|
77
77
|
}
|
|
@@ -3,6 +3,7 @@ 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,7 +11,7 @@ const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
|
10
11
|
export async function readFile(context, args) {
|
|
11
12
|
const rootDir = typeof context === 'string' ? context : context.rootDir;
|
|
12
13
|
const safety = typeof context === 'string' ? undefined : context.safety;
|
|
13
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
14
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
14
15
|
if (!filePath) {
|
|
15
16
|
return { ok: false, error: 'filePath is required' };
|
|
16
17
|
}
|
|
@@ -41,13 +42,23 @@ export async function readFile(context, args) {
|
|
|
41
42
|
content = readProjectFile(rootDir, filePath);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
|
|
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
|
+
};
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
else {
|
|
48
55
|
const absPath = path.resolve(filePath);
|
|
49
56
|
if (!existsSync(absPath)) {
|
|
50
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `File does not exist: ${filePath}`,
|
|
60
|
+
failureCategory: 'not_found',
|
|
61
|
+
};
|
|
51
62
|
}
|
|
52
63
|
try {
|
|
53
64
|
content = readFileSync(absPath, 'utf-8');
|
|
@@ -57,10 +68,6 @@ export async function readFile(context, args) {
|
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
const previewPath = projectPath ?? filePath;
|
|
60
|
-
// A clean dotenv file is shown with keys visible and values tokenized so the
|
|
61
|
-
// agent can still edit it (str_replace/write_file round-trip the tokens) and
|
|
62
|
-
// read coverage is recorded. Any other secret file — PEM, JSON credentials,
|
|
63
|
-
// or a dotenv with a stray non-assignment line — keeps the opaque blackout.
|
|
64
71
|
const editableDotenv = Boolean(projectPath) &&
|
|
65
72
|
Boolean(safety) &&
|
|
66
73
|
isDotenvLikePath(previewPath) &&
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isSensitiveProjectPath, normalizeProjectRelativePath, } from '../artifact-policy.js';
|
|
5
5
|
import { readCliAuthConfig } from '../api/auth.js';
|
|
6
6
|
import { resolveProjectPath, writeProjectFileBuffer } from '../patcher.js';
|
|
7
|
-
import { suggestClosestPath } from './path-suggest.js';
|
|
7
|
+
import { repairFilePath, suggestClosestPath } from './path-suggest.js';
|
|
8
8
|
import { isTuiMode } from '../runtime-mode.js';
|
|
9
9
|
function normalizeReplacements(value) {
|
|
10
10
|
if (!Array.isArray(value))
|
|
@@ -76,7 +76,7 @@ async function replaceDocumentTextOnServer(config, fileName, fileData, replaceme
|
|
|
76
76
|
return data;
|
|
77
77
|
}
|
|
78
78
|
export async function replaceDocumentText(context, args) {
|
|
79
|
-
const sourceRaw = String(args.filePath ?? args.file_path ?? '').trim();
|
|
79
|
+
const sourceRaw = repairFilePath(context.rootDir, String(args.filePath ?? args.file_path ?? '').trim());
|
|
80
80
|
if (!sourceRaw) {
|
|
81
81
|
return { ok: false, error: 'filePath is required' };
|
|
82
82
|
}
|
|
@@ -149,9 +149,6 @@ export async function replaceDocumentText(context, args) {
|
|
|
149
149
|
failureCategory: serverResult.failureCategory ?? 'external_service',
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
// Validate-only: report per-replacement match info without touching the file.
|
|
153
|
-
// changed:false marks it non-mutating so the agent loop does not count a
|
|
154
|
-
// dry-run as an applied edit.
|
|
155
152
|
if (validateOnly) {
|
|
156
153
|
return {
|
|
157
154
|
ok: true,
|
|
@@ -162,8 +159,6 @@ export async function replaceDocumentText(context, args) {
|
|
|
162
159
|
results: serverResult.results,
|
|
163
160
|
};
|
|
164
161
|
}
|
|
165
|
-
// No replacement matched: nothing was written. Surface per-item reasons so
|
|
166
|
-
// the model can correct and resend only the failing entries.
|
|
167
162
|
const replacementCount = Number(serverResult.replacementCount ?? 0);
|
|
168
163
|
if (replacementCount === 0) {
|
|
169
164
|
const failures = Array.isArray(serverResult.replacements)
|
|
@@ -220,9 +215,6 @@ export async function replaceDocumentText(context, args) {
|
|
|
220
215
|
failedCount: serverResult.failedCount,
|
|
221
216
|
replacements: serverResult.replacements,
|
|
222
217
|
bytesWritten: nextData.length,
|
|
223
|
-
// A partial batch still wrote the matched entries (changed:true above), but
|
|
224
|
-
// the loop must reflect and repair the missed entries — needsRepair forces
|
|
225
|
-
// that without losing credit for the applied edits.
|
|
226
218
|
...(failedCount > 0
|
|
227
219
|
? {
|
|
228
220
|
needsRepair: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import { isTuiMode } from '../runtime-mode.js';
|
|
3
3
|
import { createPromptCheckpoint, restoreCheckpointFiles, } from '../session-safety.js';
|
|
4
4
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|