@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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 +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- 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 +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- 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 +52 -21
- package/dist/src/session.js +8 -0
- 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/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -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 +27 -16
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -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,7 +1,8 @@
|
|
|
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 { readProjectFile, writeProjectFile } from '../patcher.js';
|
|
4
|
+
import { classifyProjectPath, readProjectFile, writeProjectFile, } from '../patcher.js';
|
|
5
|
+
import { repairFilePath } from './path-suggest.js';
|
|
5
6
|
import { upsertIndexFile } from '../project-index.js';
|
|
6
7
|
import { isTuiMode } from '../runtime-mode.js';
|
|
7
8
|
import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
|
|
@@ -76,7 +77,7 @@ function buildStrReplacePreview(oldString, newString) {
|
|
|
76
77
|
}
|
|
77
78
|
export async function strReplace(context, args) {
|
|
78
79
|
const { rootDir, projectIndex, autoYes, confirmPatch } = context;
|
|
79
|
-
const filePath = String(args.filePath ?? args.file_path ?? '').trim();
|
|
80
|
+
const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
|
|
80
81
|
let oldString = typeof args.old_string === 'string'
|
|
81
82
|
? args.old_string
|
|
82
83
|
: typeof args.oldString === 'string'
|
|
@@ -105,6 +106,21 @@ export async function strReplace(context, args) {
|
|
|
105
106
|
failureCategory: 'invalid_argument',
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
const pathKind = classifyProjectPath(rootDir, filePath);
|
|
110
|
+
if (pathKind === 'outside') {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
filePath,
|
|
114
|
+
error: `Refusing to edit outside the project root: ${filePath}. Editable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
|
|
115
|
+
failureCategory: 'invalid_argument',
|
|
116
|
+
failureDetails: {
|
|
117
|
+
category: 'invalid_argument',
|
|
118
|
+
tool: 'str_replace',
|
|
119
|
+
action: 'Edit files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const scratchPath = pathKind === 'scratch';
|
|
108
124
|
let originalContent;
|
|
109
125
|
try {
|
|
110
126
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -168,7 +184,13 @@ export async function strReplace(context, args) {
|
|
|
168
184
|
ok: false,
|
|
169
185
|
skipped: true,
|
|
170
186
|
filePath,
|
|
171
|
-
|
|
187
|
+
failureCategory: 'user_declined',
|
|
188
|
+
failureDetails: {
|
|
189
|
+
category: 'user_declined',
|
|
190
|
+
tool: 'str_replace',
|
|
191
|
+
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.',
|
|
192
|
+
},
|
|
193
|
+
error: 'The real user rejected this proposed edit. Nothing was changed; this was not a tool failure or an automated system skip.',
|
|
172
194
|
};
|
|
173
195
|
}
|
|
174
196
|
}
|
|
@@ -177,13 +199,16 @@ export async function strReplace(context, args) {
|
|
|
177
199
|
const replacements = changed ? (replaceAll ? n : 1) : 0;
|
|
178
200
|
let indexedChunks = 0;
|
|
179
201
|
let retrievalTokensUsed = 0;
|
|
180
|
-
if (changed) {
|
|
202
|
+
if (changed && !scratchPath) {
|
|
181
203
|
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
182
204
|
indexedChunks = indexResult.indexedChunks;
|
|
183
205
|
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
184
206
|
}
|
|
185
|
-
|
|
186
|
-
|
|
207
|
+
let diagnostics;
|
|
208
|
+
if (!scratchPath) {
|
|
209
|
+
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
210
|
+
diagnostics = runShellDiagnostics(rootDir, filePath);
|
|
211
|
+
}
|
|
187
212
|
const originalLines = originalContent.split('\n').length;
|
|
188
213
|
const nextLines = nextContent.split('\n').length;
|
|
189
214
|
if (!isTuiMode()) {
|
|
@@ -196,6 +221,7 @@ export async function strReplace(context, args) {
|
|
|
196
221
|
filePath,
|
|
197
222
|
changed,
|
|
198
223
|
operation: 'str_replace',
|
|
224
|
+
...(scratchPath ? { scratch: true } : {}),
|
|
199
225
|
replacements,
|
|
200
226
|
indexedChunks,
|
|
201
227
|
retrievalTokensUsed,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import { hashStoredContent, readFileEditSnapshot, storedContentBuffer, } from '../edit-journal.js';
|
|
3
3
|
import { deleteProjectFile, writeProjectFile, writeProjectFileBuffer, } from '../patcher.js';
|
|
4
4
|
import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
|
|
@@ -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,7 +1,7 @@
|
|
|
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 { writeProjectFile } from '../patcher.js';
|
|
4
|
+
import { classifyProjectPath, writeProjectFile } from '../patcher.js';
|
|
5
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
6
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
7
7
|
import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
|
|
@@ -25,9 +25,25 @@ export async function writeFile(context, args) {
|
|
|
25
25
|
failureCategory: 'invalid_argument',
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
const pathKind = classifyProjectPath(rootDir, filePath);
|
|
29
|
+
if (pathKind === 'outside') {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
filePath,
|
|
33
|
+
error: `Refusing to write outside the project root: ${filePath}. Writable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
|
|
34
|
+
failureCategory: 'invalid_argument',
|
|
35
|
+
failureDetails: {
|
|
36
|
+
category: 'invalid_argument',
|
|
37
|
+
tool: 'write_file',
|
|
38
|
+
action: 'Write inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const scratchPath = pathKind === 'scratch';
|
|
28
43
|
const coveragePath = normalizeProjectRelativePath(rootDir, filePath) ?? filePath;
|
|
29
44
|
const currentHash = getCurrentFileHash(rootDir, filePath);
|
|
30
|
-
if (
|
|
45
|
+
if (!scratchPath &&
|
|
46
|
+
currentHash !== null &&
|
|
31
47
|
context.safety &&
|
|
32
48
|
!hasFreshFullReadCoverage(context.safety, coveragePath, currentHash)) {
|
|
33
49
|
return {
|
|
@@ -47,13 +63,16 @@ export async function writeFile(context, args) {
|
|
|
47
63
|
const { changed } = writeProjectFile(rootDir, filePath, content);
|
|
48
64
|
let indexedChunks = 0;
|
|
49
65
|
let retrievalTokensUsed = 0;
|
|
50
|
-
if (changed) {
|
|
66
|
+
if (changed && !scratchPath) {
|
|
51
67
|
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
52
68
|
indexedChunks = indexResult.indexedChunks;
|
|
53
69
|
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
54
70
|
}
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
let diagnostics;
|
|
72
|
+
if (!scratchPath) {
|
|
73
|
+
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
74
|
+
diagnostics = runShellDiagnostics(rootDir, filePath);
|
|
75
|
+
}
|
|
57
76
|
if (!isTuiMode()) {
|
|
58
77
|
const icon = changed ? '✨' : '📝';
|
|
59
78
|
const label = changed ? 'Created/Updated' : 'Created/Updated (no change)';
|
|
@@ -64,6 +83,7 @@ export async function writeFile(context, args) {
|
|
|
64
83
|
filePath,
|
|
65
84
|
changed,
|
|
66
85
|
operation: 'write',
|
|
86
|
+
...(scratchPath ? { scratch: true } : {}),
|
|
67
87
|
indexedChunks,
|
|
68
88
|
retrievalTokensUsed,
|
|
69
89
|
bytesWritten: Buffer.byteLength(content, 'utf-8'),
|
|
@@ -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);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const TURN_FAILURE_MARKER_PATTERN = /^Turn failed before completion: ([a-z][a-z0-9_-]*)\.?$/i;
|
|
2
|
+
export function formatTurnFailureMarker(category) {
|
|
3
|
+
const normalized = category.trim().toLowerCase();
|
|
4
|
+
const safeCategory = /^[a-z][a-z0-9_-]*$/.test(normalized)
|
|
5
|
+
? normalized
|
|
6
|
+
: 'unknown_error';
|
|
7
|
+
return `Turn failed before completion: ${safeCategory}.`;
|
|
8
|
+
}
|
|
9
|
+
export function isTurnFailureMarker(text) {
|
|
10
|
+
return TURN_FAILURE_MARKER_PATTERN.test(text.trim());
|
|
11
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
4
|
const HISTORY_FILE = 'prompt-history.json';
|
|
5
5
|
const LEGACY_HISTORY_FILE = 'prompt-history.txt';
|
|
6
|
-
export const MAX_PROMPT_HISTORY_ENTRIES =
|
|
6
|
+
export const MAX_PROMPT_HISTORY_ENTRIES = 20;
|
|
7
7
|
const ENTRY_SEPARATOR = '\x1e';
|
|
8
8
|
function getHistoryFilePath(env = process.env) {
|
|
9
9
|
return path.join(getClientStateDir(env), HISTORY_FILE);
|