@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.2
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 +138 -18
- 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 +0 -16
- package/dist/src/api/chat.js +59 -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 +97 -12
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -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 +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- 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 +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- 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 +33 -7
- 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 +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +342 -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
|
@@ -454,17 +454,6 @@ export function resolveRedactionTokens(state, text, filePath, hash) {
|
|
|
454
454
|
}
|
|
455
455
|
const DOTENV_ASSIGNMENT_PATTERN = /^(\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=)(.*)$/;
|
|
456
456
|
const DOTENV_COMMENT_PATTERN = /^(\s*#\s*)(\S.*)$/;
|
|
457
|
-
/**
|
|
458
|
-
* Redact a dotenv file's values while leaving keys visible. Every assignment's
|
|
459
|
-
* value is replaced with a stable, reversible token so the agent can see the
|
|
460
|
-
* file's structure and edit it (remove or replace lines) without ever seeing a
|
|
461
|
-
* secret value; `resolveRedactionTokens` swaps the real values back on write.
|
|
462
|
-
* Comment bodies are tokenized too, because developers routinely leave
|
|
463
|
-
* commented-out credentials in dotenv files and those must not leak where the
|
|
464
|
-
* opaque preview would have hidden them. Callers must confirm the content is
|
|
465
|
-
* clean dotenv (`looksLikeEditableDotenv`) first so the only non-assignment
|
|
466
|
-
* lines reaching here are blanks and comments.
|
|
467
|
-
*/
|
|
468
457
|
export function redactDotenvWithStableTokens(state, content, filePath, hash) {
|
|
469
458
|
const tokens = [];
|
|
470
459
|
const redactedLines = content.split('\n').map((line) => {
|
|
@@ -490,14 +479,6 @@ export function redactDotenvWithStableTokens(state, content, filePath, hash) {
|
|
|
490
479
|
});
|
|
491
480
|
return { content: redactedLines.join('\n'), tokens };
|
|
492
481
|
}
|
|
493
|
-
/**
|
|
494
|
-
* The redaction-token registry is capped at `MAX_REDACTION_TOKENS`; a read that
|
|
495
|
-
* emits more tokens than that would evict its own oldest tokens, leaving
|
|
496
|
-
* `[REDACTED:n]` markers in the preview that `write_file`/`str_replace` can no
|
|
497
|
-
* longer resolve (silently writing the literal token back). So a dotenv file
|
|
498
|
-
* with more tokenizable lines than the budget must not use the editable preview
|
|
499
|
-
* — the caller falls back to the opaque blackout instead.
|
|
500
|
-
*/
|
|
501
482
|
export function dotenvFitsRedactionBudget(content) {
|
|
502
483
|
let count = 0;
|
|
503
484
|
for (const line of content.split('\n')) {
|
|
@@ -122,7 +122,6 @@ function loadAllSnapshots(rootDir, env = process.env) {
|
|
|
122
122
|
snapshots.push(loadSnapshotFile(filePath, rootDir));
|
|
123
123
|
}
|
|
124
124
|
catch {
|
|
125
|
-
// Skip corrupted snapshots silently — customers have no actionable debug path here.
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
return snapshots.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
@@ -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,13 @@
|
|
|
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';
|
|
5
|
+
import { classifyProjectPath } from './patcher.js';
|
|
4
6
|
import { dispatchTool } from './tools/index.js';
|
|
7
|
+
import { syncIndexFromDisk } from './project-index.js';
|
|
8
|
+
import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
|
|
5
9
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
|
|
10
|
+
import { extractTodosArg } from './todo-list.js';
|
|
6
11
|
const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
7
12
|
'filePath',
|
|
8
13
|
'file_path',
|
|
@@ -11,6 +16,7 @@ const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
|
11
16
|
'file',
|
|
12
17
|
'filename',
|
|
13
18
|
];
|
|
19
|
+
const backgroundCommandTrackers = new Map();
|
|
14
20
|
function toolCallSummary(call) {
|
|
15
21
|
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
16
22
|
if (call.name === 'run_command') {
|
|
@@ -19,6 +25,17 @@ function toolCallSummary(call) {
|
|
|
19
25
|
if (call.name === 'run_node_script') {
|
|
20
26
|
return String(args.script ?? '').trim().slice(0, 120);
|
|
21
27
|
}
|
|
28
|
+
if (call.name === 'shell_job_output' ||
|
|
29
|
+
call.name === 'shell_job_kill') {
|
|
30
|
+
return String(args.job_id ?? '').trim();
|
|
31
|
+
}
|
|
32
|
+
if (call.name === 'update_todos') {
|
|
33
|
+
const raw = extractTodosArg(args);
|
|
34
|
+
const todos = Array.isArray(raw) ? raw : [];
|
|
35
|
+
if (todos.length === 0)
|
|
36
|
+
return '';
|
|
37
|
+
return `${todos.length} item${todos.length === 1 ? '' : 's'}`;
|
|
38
|
+
}
|
|
22
39
|
const filePath = getEditToolFilePath(call);
|
|
23
40
|
if (filePath)
|
|
24
41
|
return filePath;
|
|
@@ -45,8 +62,125 @@ function getEditToolFilePath(call) {
|
|
|
45
62
|
}
|
|
46
63
|
return '';
|
|
47
64
|
}
|
|
65
|
+
function editToolWritesSeparateOutput(call) {
|
|
66
|
+
if (call.name !== 'replace_document_text')
|
|
67
|
+
return false;
|
|
68
|
+
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
69
|
+
const output = args.outputPath ?? args.output_path;
|
|
70
|
+
return typeof output === 'string' && output.trim().length > 0;
|
|
71
|
+
}
|
|
72
|
+
async function collectTrackedCommandMutations({ session, projectIndex, result, tracker, toolName, toolCallId, turnId, }) {
|
|
73
|
+
const checkpoint = ensureActiveCheckpoint(session.clientState.safety, turnId);
|
|
74
|
+
const records = collectCommandMutations({
|
|
75
|
+
state: session.clientState.safety,
|
|
76
|
+
rootDir: session.rootDir,
|
|
77
|
+
tracker,
|
|
78
|
+
toolName,
|
|
79
|
+
toolCallId,
|
|
80
|
+
turnId,
|
|
81
|
+
checkpointId: checkpoint.id,
|
|
82
|
+
});
|
|
83
|
+
if (!records.length)
|
|
84
|
+
return;
|
|
85
|
+
invalidateShellDiagnosticsCache(session.rootDir);
|
|
86
|
+
rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), turnId);
|
|
87
|
+
const priorSync = result.repoSync;
|
|
88
|
+
if (!(priorSync && (priorSync.added || priorSync.modified || priorSync.removed))) {
|
|
89
|
+
const mutationCounts = {
|
|
90
|
+
added: records.filter((record) => record.operation === 'create').length,
|
|
91
|
+
modified: records.filter((record) => record.operation === 'update').length,
|
|
92
|
+
removed: records.filter((record) => record.operation === 'delete').length,
|
|
93
|
+
indexedChunks: 0,
|
|
94
|
+
retrievalTokensUsed: 0,
|
|
95
|
+
};
|
|
96
|
+
if (priorSync?.error) {
|
|
97
|
+
result.repoSync = {
|
|
98
|
+
...mutationCounts,
|
|
99
|
+
indexSyncError: priorSync.error,
|
|
100
|
+
skipped: true,
|
|
101
|
+
reason: 'local index sync failed after command execution',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
try {
|
|
106
|
+
const repoSync = await syncIndexFromDisk(projectIndex);
|
|
107
|
+
result.repoSync = {
|
|
108
|
+
...repoSync,
|
|
109
|
+
added: Math.max(repoSync.added, mutationCounts.added),
|
|
110
|
+
modified: Math.max(repoSync.modified, mutationCounts.modified),
|
|
111
|
+
removed: Math.max(repoSync.removed, mutationCounts.removed),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const indexSyncError = error instanceof Error ? error.message : String(error);
|
|
116
|
+
result.repoSync = { ...mutationCounts, indexSyncError };
|
|
117
|
+
session.onStatus(`Repository changes were recorded, but local index sync failed: ${indexSyncError}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.sessionEdits = records.map((record) => ({
|
|
122
|
+
id: record.id,
|
|
123
|
+
filePath: record.filePath,
|
|
124
|
+
operation: record.operation,
|
|
125
|
+
beforeHash: record.beforeHash,
|
|
126
|
+
afterHash: record.afterHash,
|
|
127
|
+
}));
|
|
128
|
+
result.diagnostics = runShellDiagnostics(session.rootDir);
|
|
129
|
+
}
|
|
130
|
+
export async function collectBackgroundJobUiKillMutations({ session, projectIndex, jobId, result, }) {
|
|
131
|
+
const normalizedJobId = String(jobId ?? result.snapshot?.id ?? '').trim();
|
|
132
|
+
if (!normalizedJobId || !result.ok)
|
|
133
|
+
return;
|
|
134
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
135
|
+
if (!tracked)
|
|
136
|
+
return;
|
|
137
|
+
const mutationResult = result;
|
|
138
|
+
await collectTrackedCommandMutations({
|
|
139
|
+
session,
|
|
140
|
+
projectIndex,
|
|
141
|
+
result: mutationResult,
|
|
142
|
+
tracker: tracked.tracker,
|
|
143
|
+
toolName: tracked.toolName,
|
|
144
|
+
toolCallId: tracked.toolCallId,
|
|
145
|
+
turnId: tracked.turnId,
|
|
146
|
+
});
|
|
147
|
+
if (result.snapshot?.status === 'running') {
|
|
148
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function collectBackgroundJobUiOutputMutations({ session, projectIndex, jobId, }) {
|
|
155
|
+
const normalizedJobId = String(jobId ?? '').trim();
|
|
156
|
+
if (!normalizedJobId)
|
|
157
|
+
return;
|
|
158
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
159
|
+
if (!tracked)
|
|
160
|
+
return;
|
|
161
|
+
const snapshot = getBackgroundJob(normalizedJobId, {
|
|
162
|
+
sessionId: session.sessionId,
|
|
163
|
+
});
|
|
164
|
+
if (!snapshot)
|
|
165
|
+
return;
|
|
166
|
+
await collectTrackedCommandMutations({
|
|
167
|
+
session,
|
|
168
|
+
projectIndex,
|
|
169
|
+
result: {},
|
|
170
|
+
tracker: tracked.tracker,
|
|
171
|
+
toolName: tracked.toolName,
|
|
172
|
+
toolCallId: tracked.toolCallId,
|
|
173
|
+
turnId: tracked.turnId,
|
|
174
|
+
});
|
|
175
|
+
if (snapshot.status === 'running') {
|
|
176
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
48
182
|
function recordAssistantEdit(session, call, result, before) {
|
|
49
|
-
if (!before || !isEditToolName(call.name))
|
|
183
|
+
if (!before || !isEditToolName(call.name) || result?.scratch === true)
|
|
50
184
|
return;
|
|
51
185
|
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
52
186
|
const filePath = getEditToolFilePath(call);
|
|
@@ -119,11 +253,20 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
119
253
|
session.onToolEvent?.({ call, result });
|
|
120
254
|
return result;
|
|
121
255
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
256
|
+
const rawEditFilePath = isEditToolName(call.name)
|
|
257
|
+
? getEditToolFilePath(call)
|
|
258
|
+
: '';
|
|
259
|
+
const filePathBeforeEdit = rawEditFilePath &&
|
|
260
|
+
PATH_REPAIRING_EDIT_TOOLS.has(call.name) &&
|
|
261
|
+
!editToolWritesSeparateOutput(call)
|
|
262
|
+
? repairFilePath(session.rootDir, rawEditFilePath)
|
|
263
|
+
: rawEditFilePath;
|
|
264
|
+
const tracksRepositoryEdit = filePathBeforeEdit &&
|
|
265
|
+
classifyProjectPath(session.rootDir, filePathBeforeEdit) === 'project';
|
|
266
|
+
if (tracksRepositoryEdit) {
|
|
124
267
|
rememberCheckpointFiles(session.clientState.safety, session.rootDir, [filePathBeforeEdit], session.turnState.id);
|
|
125
268
|
}
|
|
126
|
-
const beforeEditSnapshot =
|
|
269
|
+
const beforeEditSnapshot = tracksRepositoryEdit
|
|
127
270
|
? readFileEditSnapshot(session.rootDir, filePathBeforeEdit)
|
|
128
271
|
: null;
|
|
129
272
|
const commandTracker = call.name === 'run_command' || call.name === 'run_node_script'
|
|
@@ -131,6 +274,7 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
131
274
|
: null;
|
|
132
275
|
const context = {
|
|
133
276
|
rootDir: session.rootDir,
|
|
277
|
+
sessionId: session.sessionId,
|
|
134
278
|
projectIndex: toolContext.projectIndex,
|
|
135
279
|
autoYes: session.autoYes,
|
|
136
280
|
confirmCommand: session.confirmCommand,
|
|
@@ -152,28 +296,57 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
152
296
|
};
|
|
153
297
|
const result = await dispatchTool(context, call);
|
|
154
298
|
recordAssistantEdit(session, call, result, beforeEditSnapshot);
|
|
155
|
-
if (result &&
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
299
|
+
if (result && typeof result === 'object' && commandTracker) {
|
|
300
|
+
await collectTrackedCommandMutations({
|
|
301
|
+
session,
|
|
302
|
+
projectIndex: toolContext.projectIndex,
|
|
303
|
+
result,
|
|
160
304
|
tracker: commandTracker,
|
|
161
305
|
toolName: call.name,
|
|
162
306
|
toolCallId: call.id,
|
|
163
307
|
turnId: session.turnState.id,
|
|
164
|
-
checkpointId: checkpoint.id,
|
|
165
308
|
});
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
result.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
309
|
+
if (call.name === 'run_command' &&
|
|
310
|
+
result.backgrounded === true &&
|
|
311
|
+
result.status === 'running' &&
|
|
312
|
+
result.jobId) {
|
|
313
|
+
backgroundCommandTrackers.set(String(result.jobId), {
|
|
314
|
+
tracker: captureMutationBaseline(session.rootDir),
|
|
315
|
+
toolName: call.name,
|
|
316
|
+
toolCallId: call.id,
|
|
317
|
+
turnId: session.turnState.id,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (result &&
|
|
322
|
+
typeof result === 'object' &&
|
|
323
|
+
(call.name === 'shell_job_output' || call.name === 'shell_job_kill')) {
|
|
324
|
+
const jobId = String(result.jobId ?? call.args?.job_id ?? '').trim();
|
|
325
|
+
const tracked = backgroundCommandTrackers.get(jobId);
|
|
326
|
+
if (tracked) {
|
|
327
|
+
await collectTrackedCommandMutations({
|
|
328
|
+
session,
|
|
329
|
+
projectIndex: toolContext.projectIndex,
|
|
330
|
+
result,
|
|
331
|
+
tracker: tracked.tracker,
|
|
332
|
+
toolName: tracked.toolName,
|
|
333
|
+
toolCallId: tracked.toolCallId,
|
|
334
|
+
turnId: tracked.turnId,
|
|
335
|
+
});
|
|
336
|
+
if (result.status === 'running') {
|
|
337
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
backgroundCommandTrackers.delete(jobId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (result && typeof result === 'object') {
|
|
345
|
+
const backgroundJobUpdate = drainBackgroundJobNotifications({
|
|
346
|
+
sessionId: session.sessionId,
|
|
347
|
+
});
|
|
348
|
+
if (backgroundJobUpdate) {
|
|
349
|
+
result.backgroundJobUpdate = backgroundJobUpdate;
|
|
177
350
|
}
|
|
178
351
|
}
|
|
179
352
|
session.onToolEvent?.({ call, result });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
import { deleteProjectFile } from '../patcher.js';
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { classifyProjectPath, deleteProjectFile } from '../patcher.js';
|
|
3
3
|
import { isTuiMode } from '../runtime-mode.js';
|
|
4
4
|
import { removeIndexFile } from '../project-index.js';
|
|
5
5
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -9,10 +9,27 @@ export async function deleteFile(context, args) {
|
|
|
9
9
|
if (!filePath) {
|
|
10
10
|
return { ok: false, error: 'filePath is required' };
|
|
11
11
|
}
|
|
12
|
+
const pathKind = classifyProjectPath(rootDir, filePath);
|
|
13
|
+
if (pathKind === 'outside') {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
filePath,
|
|
17
|
+
error: `Refusing to delete outside the project root: ${filePath}. Deletable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
|
|
18
|
+
failureCategory: 'invalid_argument',
|
|
19
|
+
failureDetails: {
|
|
20
|
+
category: 'invalid_argument',
|
|
21
|
+
tool: 'delete_file',
|
|
22
|
+
action: 'Delete files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR).',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const scratchPath = pathKind === 'scratch';
|
|
12
27
|
const result = deleteProjectFile(rootDir, filePath);
|
|
13
28
|
if (result.deleted) {
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
if (!scratchPath) {
|
|
30
|
+
await removeIndexFile(projectIndex, filePath);
|
|
31
|
+
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
32
|
+
}
|
|
16
33
|
if (!isTuiMode())
|
|
17
34
|
console.log(chalk.red(` 🗑️ Deleted: ${filePath}`));
|
|
18
35
|
}
|
|
@@ -21,7 +38,8 @@ export async function deleteFile(context, args) {
|
|
|
21
38
|
filePath,
|
|
22
39
|
changed: result.deleted,
|
|
23
40
|
deleted: result.deleted,
|
|
41
|
+
...(scratchPath ? { scratch: true } : {}),
|
|
24
42
|
content: result.content,
|
|
25
|
-
diagnostics: result.deleted ? runShellDiagnostics(rootDir) : undefined,
|
|
43
|
+
diagnostics: result.deleted && !scratchPath ? runShellDiagnostics(rootDir) : undefined,
|
|
26
44
|
};
|
|
27
45
|
}
|
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
|
-
import { applyUnifiedPatch, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
|
|
4
|
+
import { applyUnifiedPatch, classifyProjectPath, 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' };
|
|
@@ -28,6 +29,21 @@ export async function patchFile(context, args) {
|
|
|
28
29
|
failureCategory: 'invalid_argument',
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
const pathKind = classifyProjectPath(rootDir, filePath);
|
|
33
|
+
if (pathKind === 'outside') {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
filePath,
|
|
37
|
+
error: `Refusing to edit outside the project root: ${filePath}. Editable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
|
|
38
|
+
failureCategory: 'invalid_argument',
|
|
39
|
+
failureDetails: {
|
|
40
|
+
category: 'invalid_argument',
|
|
41
|
+
tool: 'patch_file',
|
|
42
|
+
action: 'Edit files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const scratchPath = pathKind === 'scratch';
|
|
31
47
|
let originalContent;
|
|
32
48
|
try {
|
|
33
49
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -67,20 +83,29 @@ export async function patchFile(context, args) {
|
|
|
67
83
|
ok: false,
|
|
68
84
|
skipped: true,
|
|
69
85
|
filePath,
|
|
70
|
-
|
|
86
|
+
failureCategory: 'user_declined',
|
|
87
|
+
failureDetails: {
|
|
88
|
+
category: 'user_declined',
|
|
89
|
+
tool: 'patch_file',
|
|
90
|
+
action: 'Respect the real user’s decision. Do not retry the same or an equivalent edit; reconsider the approach or ask one specific question if needed.',
|
|
91
|
+
},
|
|
92
|
+
error: 'The real user rejected this proposed patch. Nothing was changed; this was not a tool failure or an automated system skip.',
|
|
71
93
|
};
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
96
|
const { changed } = writeProjectFile(rootDir, filePath, patchedContent);
|
|
75
97
|
let indexedChunks = 0;
|
|
76
98
|
let retrievalTokensUsed = 0;
|
|
77
|
-
if (changed) {
|
|
99
|
+
if (changed && !scratchPath) {
|
|
78
100
|
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
79
101
|
indexedChunks = indexResult.indexedChunks;
|
|
80
102
|
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
81
103
|
}
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
let diagnostics;
|
|
105
|
+
if (!scratchPath) {
|
|
106
|
+
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
107
|
+
diagnostics = runShellDiagnostics(rootDir, filePath);
|
|
108
|
+
}
|
|
84
109
|
const originalLines = originalContent.split('\n').length;
|
|
85
110
|
const patchedLines = patchedContent.split('\n').length;
|
|
86
111
|
if (!isTuiMode()) {
|
|
@@ -93,6 +118,7 @@ export async function patchFile(context, args) {
|
|
|
93
118
|
filePath,
|
|
94
119
|
changed,
|
|
95
120
|
operation: 'patch',
|
|
121
|
+
...(scratchPath ? { scratch: true } : {}),
|
|
96
122
|
indexedChunks,
|
|
97
123
|
retrievalTokensUsed,
|
|
98
124
|
bytesWritten: Buffer.byteLength(patchedContent, 'utf-8'),
|