@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.20
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 +224 -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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isSensitiveProjectPath, normalizeProjectRelativePath, } from '../artifact-policy.js';
|
|
5
|
+
import { readCliAuthConfig } from '../api/auth.js';
|
|
6
|
+
import { resolveProjectPath, writeProjectFileBuffer } from '../patcher.js';
|
|
7
|
+
import { repairFilePath, suggestClosestPath } from './path-suggest.js';
|
|
8
|
+
import { isTuiMode } from '../runtime-mode.js';
|
|
9
|
+
function normalizeReplacements(value) {
|
|
10
|
+
if (!Array.isArray(value))
|
|
11
|
+
return [];
|
|
12
|
+
return value
|
|
13
|
+
.map((item) => {
|
|
14
|
+
if (!item || typeof item !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const entry = item;
|
|
17
|
+
const oldText = String(entry.oldText ?? entry.old_text ?? '');
|
|
18
|
+
const newText = String(entry.newText ?? entry.new_text ?? '');
|
|
19
|
+
if (!oldText)
|
|
20
|
+
return null;
|
|
21
|
+
return { oldText, newText };
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function relativeEditablePath(rootDir, rawPath) {
|
|
26
|
+
const resolvedPath = path.isAbsolute(rawPath)
|
|
27
|
+
? rawPath
|
|
28
|
+
: path.resolve(rootDir, rawPath);
|
|
29
|
+
const projectPath = normalizeProjectRelativePath(rootDir, resolvedPath);
|
|
30
|
+
if (!projectPath || isSensitiveProjectPath(projectPath))
|
|
31
|
+
return null;
|
|
32
|
+
return projectPath.split(path.sep).join('/');
|
|
33
|
+
}
|
|
34
|
+
function renderPreview(filePath, preview) {
|
|
35
|
+
if (isTuiMode())
|
|
36
|
+
return;
|
|
37
|
+
console.log(chalk.bold(`\n Document text replacement preview for ${filePath}:`));
|
|
38
|
+
for (const line of preview.split('\n')) {
|
|
39
|
+
if (line.startsWith('+')) {
|
|
40
|
+
console.log(chalk.green(line));
|
|
41
|
+
}
|
|
42
|
+
else if (line.startsWith('-')) {
|
|
43
|
+
console.log(chalk.red(line));
|
|
44
|
+
}
|
|
45
|
+
else if (line.startsWith('@@')) {
|
|
46
|
+
console.log(chalk.cyan(line));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(chalk.dim(line));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
async function replaceDocumentTextOnServer(config, fileName, fileData, replacements, replaceAll, validate) {
|
|
55
|
+
const response = await globalThis.fetch(`${config.serverUrl.replace(/\/+$/, '')}/v1/document/replace-text`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
authorization: `Bearer ${config.token}`,
|
|
59
|
+
'content-type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
fileName,
|
|
63
|
+
fileData: fileData.toString('base64'),
|
|
64
|
+
replacements,
|
|
65
|
+
replaceAll,
|
|
66
|
+
validate,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const data = await response.json().catch(() => null);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `Server document edit failed: ${data?.error?.message ?? response.status}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
export async function replaceDocumentText(context, args) {
|
|
79
|
+
const sourceRaw = repairFilePath(context.rootDir, String(args.filePath ?? args.file_path ?? '').trim());
|
|
80
|
+
if (!sourceRaw) {
|
|
81
|
+
return { ok: false, error: 'filePath is required' };
|
|
82
|
+
}
|
|
83
|
+
const sourcePath = relativeEditablePath(context.rootDir, sourceRaw);
|
|
84
|
+
if (!sourcePath) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: 'replace_document_text can only edit permitted files inside the project root.',
|
|
88
|
+
failureCategory: 'permission_denied',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (path.extname(sourcePath).toLowerCase() !== '.docx') {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: 'replace_document_text only supports .docx files.',
|
|
95
|
+
failureCategory: 'invalid_argument',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const outputRaw = String(args.outputPath ?? args.output_path ?? '').trim();
|
|
99
|
+
const targetPath = outputRaw
|
|
100
|
+
? relativeEditablePath(context.rootDir, outputRaw)
|
|
101
|
+
: sourcePath;
|
|
102
|
+
if (!targetPath) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: 'outputPath must be inside the project root.',
|
|
106
|
+
failureCategory: 'permission_denied',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (path.extname(targetPath).toLowerCase() !== '.docx') {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: 'outputPath must end with .docx.',
|
|
113
|
+
failureCategory: 'invalid_argument',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const replacements = normalizeReplacements(args.replacements);
|
|
117
|
+
if (!replacements.length) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: 'replacements must include at least one { oldText, newText } item.',
|
|
121
|
+
failureCategory: 'missing_required_argument',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const authConfig = readCliAuthConfig(context.env);
|
|
125
|
+
if (!authConfig) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: 'Document editing requires a server connection. Please log in first.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const sourceAbsPath = resolveProjectPath(context.rootDir, sourcePath);
|
|
132
|
+
if (!existsSync(sourceAbsPath)) {
|
|
133
|
+
const suggestion = suggestClosestPath(context.rootDir, sourceAbsPath);
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
filePath: sourcePath,
|
|
137
|
+
error: suggestion
|
|
138
|
+
? `File does not exist: ${sourcePath}. Did you mean "${suggestion}"? Note the exact punctuation (e.g. curly apostrophe ’ vs straight ').`
|
|
139
|
+
: `File does not exist: ${sourcePath}`,
|
|
140
|
+
failureCategory: 'not_found',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const validateOnly = args.validate === true || args.dryRun === true;
|
|
144
|
+
const serverResult = await replaceDocumentTextOnServer(authConfig, path.basename(sourcePath), readFileSync(sourceAbsPath), replacements, args.replaceAll === true || args.replace_all === true, validateOnly);
|
|
145
|
+
if (!serverResult.ok) {
|
|
146
|
+
return {
|
|
147
|
+
...serverResult,
|
|
148
|
+
filePath: sourcePath,
|
|
149
|
+
failureCategory: serverResult.failureCategory ?? 'external_service',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (validateOnly) {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
validate: true,
|
|
156
|
+
changed: false,
|
|
157
|
+
filePath: sourcePath,
|
|
158
|
+
operation: 'replace_document_text',
|
|
159
|
+
results: serverResult.results,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const replacementCount = Number(serverResult.replacementCount ?? 0);
|
|
163
|
+
if (replacementCount === 0) {
|
|
164
|
+
const failures = Array.isArray(serverResult.replacements)
|
|
165
|
+
? serverResult.replacements
|
|
166
|
+
.filter((item) => item && item.ok === false)
|
|
167
|
+
.map((item) => `- ${item.error ?? 'no match'}`)
|
|
168
|
+
: [];
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
filePath: sourcePath,
|
|
172
|
+
operation: 'replace_document_text',
|
|
173
|
+
failureCategory: 'conflict',
|
|
174
|
+
error: `0 of ${serverResult.requestedCount ?? replacements.length} replacements applied; file unchanged.` +
|
|
175
|
+
(failures.length ? `\n${failures.join('\n')}` : ''),
|
|
176
|
+
replacements: serverResult.replacements,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const preview = String(serverResult.preview ?? '');
|
|
180
|
+
renderPreview(targetPath, preview);
|
|
181
|
+
if (!context.autoYes && context.confirmPatch) {
|
|
182
|
+
const confirmed = await context.confirmPatch(targetPath, preview);
|
|
183
|
+
if (!confirmed) {
|
|
184
|
+
if (!isTuiMode()) {
|
|
185
|
+
console.log(chalk.dim(` replace_document_text skipped: ${targetPath}`));
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
skipped: true,
|
|
190
|
+
filePath: targetPath,
|
|
191
|
+
error: 'User declined replace_document_text',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const fileData = String(serverResult.fileData ?? '');
|
|
196
|
+
const nextData = Buffer.from(fileData, 'base64');
|
|
197
|
+
const write = writeProjectFileBuffer(context.rootDir, targetPath, nextData);
|
|
198
|
+
const failedCount = Number(serverResult.failedCount ?? 0);
|
|
199
|
+
const appliedCount = Number(serverResult.appliedCount ?? replacementCount);
|
|
200
|
+
const requestedCount = Number(serverResult.requestedCount ?? replacements.length);
|
|
201
|
+
const partialFailures = Array.isArray(serverResult.replacements)
|
|
202
|
+
? serverResult.replacements
|
|
203
|
+
.filter((item) => item && item.ok === false)
|
|
204
|
+
.map((item) => `- ${item.error ?? 'no match'}`)
|
|
205
|
+
: [];
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
filePath: targetPath,
|
|
209
|
+
sourceFilePath: sourcePath,
|
|
210
|
+
changed: write.changed,
|
|
211
|
+
operation: 'replace_document_text',
|
|
212
|
+
replacementCount: serverResult.replacementCount,
|
|
213
|
+
requestedCount: serverResult.requestedCount,
|
|
214
|
+
appliedCount: serverResult.appliedCount,
|
|
215
|
+
failedCount: serverResult.failedCount,
|
|
216
|
+
replacements: serverResult.replacements,
|
|
217
|
+
bytesWritten: nextData.length,
|
|
218
|
+
...(failedCount > 0
|
|
219
|
+
? {
|
|
220
|
+
needsRepair: true,
|
|
221
|
+
failureCategory: 'conflict',
|
|
222
|
+
error: `${appliedCount} of ${requestedCount} replacements applied; ` +
|
|
223
|
+
`${failedCount} missed and still need to be fixed:\n` +
|
|
224
|
+
partialFailures.join('\n'),
|
|
225
|
+
failureDetails: {
|
|
226
|
+
category: 'conflict',
|
|
227
|
+
tool: 'replace_document_text',
|
|
228
|
+
action: 'Re-issue replace_document_text for the missed entries only, with corrected oldText. ' +
|
|
229
|
+
'Copy the exact text from read_document (mind curly vs straight quotes), or pass validate:true to test a match first.',
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -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';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { startBackgroundJob } from '../background-jobs.js';
|
|
2
3
|
import { getBlockedCommandReason, runCommand, } from '../executor.js';
|
|
3
4
|
import { syncIndexFromDisk } from '../project-index.js';
|
|
4
5
|
import { isTuiMode } from '../runtime-mode.js';
|
|
@@ -12,9 +13,10 @@ export async function runShellCommand(context, args) {
|
|
|
12
13
|
if (!command) {
|
|
13
14
|
return { ok: false, error: 'command is required' };
|
|
14
15
|
}
|
|
16
|
+
const runInBackground = args.background === true;
|
|
15
17
|
const hasTimeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0;
|
|
16
18
|
const repoHint = buildNestedGitHint(rootDir, command);
|
|
17
|
-
const blockedReason = getBlockedCommandReason(command, hasTimeout, rootDir);
|
|
19
|
+
const blockedReason = getBlockedCommandReason(command, hasTimeout || runInBackground, rootDir);
|
|
18
20
|
if (blockedReason) {
|
|
19
21
|
const error = blockedReason;
|
|
20
22
|
if (!isTuiMode())
|
|
@@ -37,7 +39,9 @@ export async function runShellCommand(context, args) {
|
|
|
37
39
|
error: 'confirmCommand is required when autoYes is false',
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
|
-
const approved = await confirmCommand(
|
|
42
|
+
const approved = await confirmCommand(runInBackground
|
|
43
|
+
? `${command}\n\nRuns as a managed background job until it exits or is killed.`
|
|
44
|
+
: command);
|
|
41
45
|
if (!approved) {
|
|
42
46
|
if (!isTuiMode())
|
|
43
47
|
console.log(chalk.dim(` ⏭ Skipped: ${command}`));
|
|
@@ -45,10 +49,13 @@ export async function runShellCommand(context, args) {
|
|
|
45
49
|
ok: false,
|
|
46
50
|
skipped: true,
|
|
47
51
|
command,
|
|
48
|
-
error: 'User declined command execution',
|
|
52
|
+
error: 'User declined command execution. Do not rerun this command or try a broader variant of it; either continue without it or ask the user one specific question about how to proceed.',
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
}
|
|
56
|
+
if (runInBackground) {
|
|
57
|
+
return runBackgroundCommand(context, command, args.timeout_ms, repoHint);
|
|
58
|
+
}
|
|
52
59
|
const result = await runCommand(command, rootDir, {
|
|
53
60
|
requestSudoPassword,
|
|
54
61
|
timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
|
|
@@ -67,19 +74,9 @@ export async function runShellCommand(context, args) {
|
|
|
67
74
|
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
68
75
|
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
69
76
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let output = typeof result.output === 'string'
|
|
73
|
-
? redactConnectionStringCredentials(result.output)
|
|
77
|
+
const output = typeof result.output === 'string'
|
|
78
|
+
? boundCommandOutput(redactConnectionStringCredentials(result.output))
|
|
74
79
|
: result.output;
|
|
75
|
-
if (typeof output === 'string' && output.length > MAX_OUTPUT_CHARS) {
|
|
76
|
-
const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
|
|
77
|
-
const tailSize = MAX_OUTPUT_CHARS - headSize;
|
|
78
|
-
output =
|
|
79
|
-
output.slice(0, headSize) +
|
|
80
|
-
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
81
|
-
output.slice(-tailSize);
|
|
82
|
-
}
|
|
83
80
|
return {
|
|
84
81
|
ok: result.exitCode === 0,
|
|
85
82
|
command,
|
|
@@ -92,3 +89,73 @@ export async function runShellCommand(context, args) {
|
|
|
92
89
|
repoHint,
|
|
93
90
|
};
|
|
94
91
|
}
|
|
92
|
+
export function boundCommandOutput(output) {
|
|
93
|
+
if (output.length <= MAX_OUTPUT_CHARS)
|
|
94
|
+
return output;
|
|
95
|
+
const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
|
|
96
|
+
const tailSize = MAX_OUTPUT_CHARS - headSize;
|
|
97
|
+
return (output.slice(0, headSize) +
|
|
98
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
99
|
+
output.slice(-tailSize));
|
|
100
|
+
}
|
|
101
|
+
async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
102
|
+
const { rootDir, projectIndex, onStatus } = context;
|
|
103
|
+
const started = await startBackgroundJob(command, rootDir, {
|
|
104
|
+
startupWaitMs: typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined,
|
|
105
|
+
sessionId: context.sessionId,
|
|
106
|
+
});
|
|
107
|
+
if (!started.ok || !started.snapshot) {
|
|
108
|
+
if (!isTuiMode())
|
|
109
|
+
console.log(chalk.red(`\n ✖ ${started.error}`));
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
blocked: true,
|
|
113
|
+
command,
|
|
114
|
+
error: started.error ?? 'Background job failed to start.',
|
|
115
|
+
repoHint,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const snapshot = started.snapshot;
|
|
119
|
+
const repoSync = projectIndex.initialized
|
|
120
|
+
? await syncIndexFromDisk(projectIndex)
|
|
121
|
+
: {
|
|
122
|
+
added: 0,
|
|
123
|
+
modified: 0,
|
|
124
|
+
removed: 0,
|
|
125
|
+
indexedChunks: 0,
|
|
126
|
+
retrievalTokensUsed: 0,
|
|
127
|
+
};
|
|
128
|
+
invalidateShellDiagnosticsCache(rootDir);
|
|
129
|
+
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
130
|
+
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
131
|
+
}
|
|
132
|
+
const output = boundCommandOutput(redactConnectionStringCredentials(started.startupOutput ?? '').trim());
|
|
133
|
+
if (snapshot.status === 'running') {
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
backgrounded: true,
|
|
137
|
+
command,
|
|
138
|
+
jobId: snapshot.id,
|
|
139
|
+
status: 'running',
|
|
140
|
+
pid: snapshot.pid,
|
|
141
|
+
output,
|
|
142
|
+
note: `Background job ${snapshot.id} is running. Use shell_job_output to poll status and new output, and shell_job_kill to stop it.`,
|
|
143
|
+
repoSync,
|
|
144
|
+
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
145
|
+
repoHint,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: snapshot.exitCode === 0,
|
|
150
|
+
backgrounded: true,
|
|
151
|
+
command,
|
|
152
|
+
jobId: snapshot.id,
|
|
153
|
+
status: snapshot.status,
|
|
154
|
+
exitCode: snapshot.exitCode,
|
|
155
|
+
output,
|
|
156
|
+
note: `Background job ${snapshot.id} finished during the startup window.`,
|
|
157
|
+
repoSync,
|
|
158
|
+
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
159
|
+
repoHint,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
import { syncIndexFromDisk } from '../project-index.js';
|
|
4
4
|
import { isTuiMode } from '../runtime-mode.js';
|
|
5
5
|
import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
|
|
6
|
+
import { ensureSessionScratchDir } from '../scratch-dir.js';
|
|
6
7
|
const DEFAULT_TIMEOUT = 5 * 60 * 1000;
|
|
7
8
|
const MAX_OUTPUT_CHARS = 4000;
|
|
8
9
|
const MAX_CAPTURE_CHARS = 1024 * 1024;
|
|
@@ -99,6 +100,7 @@ function executeNodeScript(rootDir, script, timeout) {
|
|
|
99
100
|
npm_config_progress: 'false',
|
|
100
101
|
npm_config_fund: 'false',
|
|
101
102
|
npm_config_audit: 'false',
|
|
103
|
+
THEGITAI_SCRATCH_DIR: ensureSessionScratchDir(),
|
|
102
104
|
},
|
|
103
105
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
106
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { killBackgroundJob } from '../background-jobs.js';
|
|
2
|
+
import { redactConnectionStringCredentials } from '../secret-preview.js';
|
|
3
|
+
const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
|
|
4
|
+
function boundJobToolOutput(output) {
|
|
5
|
+
if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
|
|
6
|
+
return output;
|
|
7
|
+
const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
|
|
8
|
+
const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
|
|
9
|
+
return (output.slice(0, headSize) +
|
|
10
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
11
|
+
output.slice(-tailSize));
|
|
12
|
+
}
|
|
13
|
+
export async function shellJobKill(context, args) {
|
|
14
|
+
const jobId = String(args.job_id ?? '').trim();
|
|
15
|
+
if (!jobId) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: 'job_id is required',
|
|
19
|
+
failureCategory: 'missing_required_argument',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const result = await killBackgroundJob(jobId, {
|
|
23
|
+
sessionId: context.sessionId,
|
|
24
|
+
});
|
|
25
|
+
if (!result.ok || !result.snapshot) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: result.error ?? 'Background job lookup failed.',
|
|
29
|
+
failureCategory: 'not_found',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const snapshot = result.snapshot;
|
|
33
|
+
let finalOutput = redactConnectionStringCredentials(result.finalOutput ?? '');
|
|
34
|
+
if (result.droppedChars) {
|
|
35
|
+
finalOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${finalOutput}`;
|
|
36
|
+
}
|
|
37
|
+
finalOutput = boundJobToolOutput(finalOutput);
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
jobId: snapshot.id,
|
|
41
|
+
command: snapshot.command,
|
|
42
|
+
status: snapshot.status,
|
|
43
|
+
exitCode: snapshot.exitCode,
|
|
44
|
+
alreadyFinished: result.alreadyFinished === true,
|
|
45
|
+
ranMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
|
|
46
|
+
finalOutput,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MAX_JOB_WAIT_MS, readBackgroundJobOutput, } from '../background-jobs.js';
|
|
2
|
+
import { redactConnectionStringCredentials } from '../secret-preview.js';
|
|
3
|
+
const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
|
|
4
|
+
function boundJobToolOutput(output) {
|
|
5
|
+
if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
|
|
6
|
+
return output;
|
|
7
|
+
const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
|
|
8
|
+
const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
|
|
9
|
+
return (output.slice(0, headSize) +
|
|
10
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
11
|
+
output.slice(-tailSize));
|
|
12
|
+
}
|
|
13
|
+
export async function shellJobOutput(context, args) {
|
|
14
|
+
const jobId = String(args.job_id ?? '').trim();
|
|
15
|
+
if (!jobId) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: 'job_id is required',
|
|
19
|
+
failureCategory: 'missing_required_argument',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const waitMs = typeof args.wait_ms === 'number' && args.wait_ms > 0
|
|
23
|
+
? Math.min(args.wait_ms, MAX_JOB_WAIT_MS)
|
|
24
|
+
: 0;
|
|
25
|
+
const result = await readBackgroundJobOutput(jobId, {
|
|
26
|
+
waitMs,
|
|
27
|
+
sessionId: context.sessionId,
|
|
28
|
+
});
|
|
29
|
+
if (!result.ok || !result.snapshot) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: result.error ?? 'Background job lookup failed.',
|
|
33
|
+
failureCategory: 'not_found',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const snapshot = result.snapshot;
|
|
37
|
+
let newOutput = redactConnectionStringCredentials(result.newOutput ?? '');
|
|
38
|
+
if (result.droppedChars) {
|
|
39
|
+
newOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${newOutput}`;
|
|
40
|
+
}
|
|
41
|
+
newOutput = boundJobToolOutput(newOutput);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
jobId: snapshot.id,
|
|
45
|
+
command: snapshot.command,
|
|
46
|
+
status: snapshot.status,
|
|
47
|
+
exitCode: snapshot.exitCode,
|
|
48
|
+
elapsedMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
|
|
49
|
+
newOutput,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
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 { readProjectFile, writeProjectFile } from '../patcher.js';
|
|
5
|
+
import { repairFilePath } from './path-suggest.js';
|
|
4
6
|
import { upsertIndexFile } from '../project-index.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
|
function countOccurrences(haystack, needle) {
|
|
9
12
|
if (needle.length === 0)
|
|
10
13
|
return 0;
|
|
@@ -74,7 +77,7 @@ function buildStrReplacePreview(oldString, newString) {
|
|
|
74
77
|
}
|
|
75
78
|
export async function strReplace(context, args) {
|
|
76
79
|
const { rootDir, projectIndex, autoYes, confirmPatch } = context;
|
|
77
|
-
const filePath = String(args.filePath ?? args.file_path ?? '').trim();
|
|
80
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
|
|
78
81
|
let oldString = typeof args.old_string === 'string'
|
|
79
82
|
? args.old_string
|
|
80
83
|
: typeof args.oldString === 'string'
|
|
@@ -92,6 +95,17 @@ export async function strReplace(context, args) {
|
|
|
92
95
|
if (oldString.length === 0) {
|
|
93
96
|
return { ok: false, error: 'old_string is required and must be non-empty' };
|
|
94
97
|
}
|
|
98
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
99
|
+
if (DOCUMENT_EXTENSIONS.has(ext)) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
filePath,
|
|
103
|
+
error: ext === '.docx'
|
|
104
|
+
? 'Use replace_document_text for .docx files.'
|
|
105
|
+
: `Use read_document for ${ext} files; text replacement is not supported.`,
|
|
106
|
+
failureCategory: 'invalid_argument',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
95
109
|
let originalContent;
|
|
96
110
|
try {
|
|
97
111
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
import {
|
|
3
|
-
import { deleteProjectFile, writeProjectFile, } from '../patcher.js';
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { hashStoredContent, readFileEditSnapshot, storedContentBuffer, } from '../edit-journal.js';
|
|
3
|
+
import { deleteProjectFile, writeProjectFile, writeProjectFileBuffer, } from '../patcher.js';
|
|
4
4
|
import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
|
|
5
5
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
6
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -84,7 +84,7 @@ function validateUndoPlan(rootDir, records) {
|
|
|
84
84
|
currentHash,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
const beforeContentHash =
|
|
87
|
+
const beforeContentHash = hashStoredContent(record.beforeContent, record.beforeContentEncoding);
|
|
88
88
|
if (beforeContentHash !== record.beforeHash) {
|
|
89
89
|
return {
|
|
90
90
|
ok: false,
|
|
@@ -110,7 +110,9 @@ async function applyUndo(context, record) {
|
|
|
110
110
|
if (record.beforeContent === null) {
|
|
111
111
|
throw new Error(`Cannot undo ${record.id}: the stored pre-edit snapshot is incomplete.`);
|
|
112
112
|
}
|
|
113
|
-
const { changed } =
|
|
113
|
+
const { changed } = record.beforeContentEncoding === 'base64'
|
|
114
|
+
? writeProjectFileBuffer(rootDir, record.filePath, storedContentBuffer(record.beforeContent, record.beforeContentEncoding))
|
|
115
|
+
: writeProjectFile(rootDir, record.filePath, record.beforeContent);
|
|
114
116
|
if (changed) {
|
|
115
117
|
await upsertIndexFile(projectIndex, record.filePath);
|
|
116
118
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { extractTodosArg, replaceTodos } from '../todo-list.js';
|
|
2
|
+
export function updateTodos(_context, args) {
|
|
3
|
+
const rawTodos = extractTodosArg(args);
|
|
4
|
+
if (rawTodos === undefined) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
error: 'todos is required (pass [] to clear the list).',
|
|
8
|
+
failureCategory: 'missing_required_argument',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const result = replaceTodos(rawTodos);
|
|
12
|
+
if (!result.ok) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: result.error,
|
|
16
|
+
failureCategory: 'invalid_argument',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const { snapshot, normalizations } = result;
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
todos: snapshot.items,
|
|
23
|
+
completedCount: snapshot.completedCount,
|
|
24
|
+
totalCount: snapshot.totalCount,
|
|
25
|
+
...(normalizations.length ? { normalizations } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
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 { writeProjectFile } from '../patcher.js';
|
|
4
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
5
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
7
|
import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
|
|
7
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
9
|
+
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
8
10
|
export async function writeFile(context, args) {
|
|
9
11
|
const { rootDir, projectIndex } = context;
|
|
10
12
|
const filePath = String(args.filePath ?? '').trim();
|
|
@@ -12,6 +14,17 @@ export async function writeFile(context, args) {
|
|
|
12
14
|
if (!filePath) {
|
|
13
15
|
return { ok: false, error: 'filePath is required' };
|
|
14
16
|
}
|
|
17
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
18
|
+
if (DOCUMENT_EXTENSIONS.has(ext)) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
filePath,
|
|
22
|
+
error: ext === '.docx'
|
|
23
|
+
? 'Use replace_document_text for .docx files.'
|
|
24
|
+
: `Use read_document for ${ext} files; write_file is not supported.`,
|
|
25
|
+
failureCategory: 'invalid_argument',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
const coveragePath = normalizeProjectRelativePath(rootDir, filePath) ?? filePath;
|
|
16
29
|
const currentHash = getCurrentFileHash(rootDir, filePath);
|
|
17
30
|
if (currentHash !== null &&
|
|
@@ -5,8 +5,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { addSignatureForNode } from './extractors/index.js';
|
|
6
6
|
import { getRepoMapLanguageForFile, } from './repo-map-languages.js';
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const TreeSitter = require('web-tree-sitter');
|
|
9
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
function resolveVendoredTreeSitter() {
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.resolve(__dirname, '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
|
|
12
|
+
path.resolve(__dirname, '..', '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
|
|
13
|
+
];
|
|
14
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
15
|
+
}
|
|
16
|
+
const TreeSitter = require(resolveVendoredTreeSitter());
|
|
10
17
|
let parserInitPromise = null;
|
|
11
18
|
const parserCache = Object.create(null);
|
|
12
19
|
const languageCache = Object.create(null);
|