@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.11
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 +16 -4
- package/dist/bin/ai.js +57 -287
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +147 -30
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/help-text.js +19 -7
- package/dist/src/patcher.js +96 -9
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scratch-dir.js +51 -33
- package/dist/src/session-store.js +52 -20
- package/dist/src/session.js +8 -0
- package/dist/src/tool-executor.js +38 -6
- package/dist/src/tools/delete-file.js +22 -4
- package/dist/src/tools/patch-file.js +30 -5
- package/dist/src/tools/read-file.js +3 -1
- package/dist/src/tools/replace-document-text.js +7 -1
- package/dist/src/tools/run-command.js +37 -19
- package/dist/src/tools/run-node-script.js +24 -4
- package/dist/src/tools/str-replace.js +30 -5
- package/dist/src/tools/write-file.js +25 -5
- 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 +197 -51
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +179 -82
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +42 -13
- package/dist/src/ui/tui/terminal-title.js +3 -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/package.json +18 -6
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -49,7 +49,13 @@ export async function runShellCommand(context, args) {
|
|
|
49
49
|
ok: false,
|
|
50
50
|
skipped: true,
|
|
51
51
|
command,
|
|
52
|
-
|
|
52
|
+
failureCategory: 'user_declined',
|
|
53
|
+
failureDetails: {
|
|
54
|
+
category: 'user_declined',
|
|
55
|
+
tool: 'run_command',
|
|
56
|
+
action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
|
|
57
|
+
},
|
|
58
|
+
error: 'The real user rejected this proposed command. Nothing was executed; this was not a tool failure or an automated system skip.',
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -60,15 +66,7 @@ export async function runShellCommand(context, args) {
|
|
|
60
66
|
requestSudoPassword,
|
|
61
67
|
timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
|
|
62
68
|
});
|
|
63
|
-
const repoSync = projectIndex
|
|
64
|
-
? await syncIndexFromDisk(projectIndex)
|
|
65
|
-
: {
|
|
66
|
-
added: 0,
|
|
67
|
-
modified: 0,
|
|
68
|
-
removed: 0,
|
|
69
|
-
indexedChunks: 0,
|
|
70
|
-
retrievalTokensUsed: 0,
|
|
71
|
-
};
|
|
69
|
+
const repoSync = await syncRepoIndex(projectIndex, onStatus);
|
|
72
70
|
invalidateShellDiagnosticsCache(rootDir);
|
|
73
71
|
const diagnostics = buildDeferredShellDiagnostics('run_command');
|
|
74
72
|
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
@@ -98,6 +96,34 @@ export function boundCommandOutput(output) {
|
|
|
98
96
|
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
99
97
|
output.slice(-tailSize));
|
|
100
98
|
}
|
|
99
|
+
async function syncRepoIndex(projectIndex, onStatus) {
|
|
100
|
+
if (!projectIndex.initialized) {
|
|
101
|
+
return {
|
|
102
|
+
added: 0,
|
|
103
|
+
modified: 0,
|
|
104
|
+
removed: 0,
|
|
105
|
+
indexedChunks: 0,
|
|
106
|
+
retrievalTokensUsed: 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return await syncIndexFromDisk(projectIndex);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
onStatus(`Command completed, but local index sync failed: ${message}`);
|
|
115
|
+
return {
|
|
116
|
+
added: 0,
|
|
117
|
+
modified: 0,
|
|
118
|
+
removed: 0,
|
|
119
|
+
indexedChunks: 0,
|
|
120
|
+
retrievalTokensUsed: 0,
|
|
121
|
+
skipped: true,
|
|
122
|
+
reason: 'local index sync failed after command execution',
|
|
123
|
+
error: message,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
101
127
|
async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
102
128
|
const { rootDir, projectIndex, onStatus } = context;
|
|
103
129
|
const started = await startBackgroundJob(command, rootDir, {
|
|
@@ -116,15 +142,7 @@ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
|
116
142
|
};
|
|
117
143
|
}
|
|
118
144
|
const snapshot = started.snapshot;
|
|
119
|
-
const repoSync = projectIndex
|
|
120
|
-
? await syncIndexFromDisk(projectIndex)
|
|
121
|
-
: {
|
|
122
|
-
added: 0,
|
|
123
|
-
modified: 0,
|
|
124
|
-
removed: 0,
|
|
125
|
-
indexedChunks: 0,
|
|
126
|
-
retrievalTokensUsed: 0,
|
|
127
|
-
};
|
|
145
|
+
const repoSync = await syncRepoIndex(projectIndex, onStatus);
|
|
128
146
|
invalidateShellDiagnosticsCache(rootDir);
|
|
129
147
|
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
130
148
|
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
@@ -175,7 +175,13 @@ export async function runNodeScript(context, args) {
|
|
|
175
175
|
ok: false,
|
|
176
176
|
skipped: true,
|
|
177
177
|
command: COMMAND_LABEL,
|
|
178
|
-
|
|
178
|
+
failureCategory: 'user_declined',
|
|
179
|
+
failureDetails: {
|
|
180
|
+
category: 'user_declined',
|
|
181
|
+
tool: 'run_node_script',
|
|
182
|
+
action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
|
|
183
|
+
},
|
|
184
|
+
error: 'The real user rejected this proposed script. Nothing was executed; this was not a tool failure or an automated system skip.',
|
|
179
185
|
};
|
|
180
186
|
}
|
|
181
187
|
}
|
|
@@ -186,13 +192,27 @@ export async function runNodeScript(context, args) {
|
|
|
186
192
|
const afterGitStatus = readGitStatusSignature(rootDir);
|
|
187
193
|
const gitStatusCleanBeforeAndAfter = beforeGitStatus === '' && afterGitStatus === '';
|
|
188
194
|
const shouldSync = context.projectIndex.initialized && !gitStatusCleanBeforeAndAfter;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
let repoSync;
|
|
196
|
+
if (shouldSync) {
|
|
197
|
+
try {
|
|
198
|
+
repoSync = await syncIndexFromDisk(context.projectIndex);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
+
context.onStatus(`Node script completed, but local index sync failed: ${message}`);
|
|
203
|
+
repoSync = {
|
|
204
|
+
...emptyRepoSync('local index sync failed after Node script execution'),
|
|
205
|
+
error: message,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
repoSync = emptyRepoSync(!context.projectIndex.initialized
|
|
192
211
|
? 'project index not initialized'
|
|
193
212
|
: gitStatusCleanBeforeAndAfter
|
|
194
213
|
? 'git status clean before and after'
|
|
195
214
|
: 'sync not needed');
|
|
215
|
+
}
|
|
196
216
|
invalidateShellDiagnosticsCache(rootDir);
|
|
197
217
|
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
198
218
|
context.onStatus(`Synced repo state after Node script (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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
5
|
import { repairFilePath } from './path-suggest.js';
|
|
6
6
|
import { upsertIndexFile } from '../project-index.js';
|
|
7
7
|
import { isTuiMode } from '../runtime-mode.js';
|
|
@@ -106,6 +106,21 @@ export async function strReplace(context, args) {
|
|
|
106
106
|
failureCategory: 'invalid_argument',
|
|
107
107
|
};
|
|
108
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';
|
|
109
124
|
let originalContent;
|
|
110
125
|
try {
|
|
111
126
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -169,7 +184,13 @@ export async function strReplace(context, args) {
|
|
|
169
184
|
ok: false,
|
|
170
185
|
skipped: true,
|
|
171
186
|
filePath,
|
|
172
|
-
|
|
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.',
|
|
173
194
|
};
|
|
174
195
|
}
|
|
175
196
|
}
|
|
@@ -178,13 +199,16 @@ export async function strReplace(context, args) {
|
|
|
178
199
|
const replacements = changed ? (replaceAll ? n : 1) : 0;
|
|
179
200
|
let indexedChunks = 0;
|
|
180
201
|
let retrievalTokensUsed = 0;
|
|
181
|
-
if (changed) {
|
|
202
|
+
if (changed && !scratchPath) {
|
|
182
203
|
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
183
204
|
indexedChunks = indexResult.indexedChunks;
|
|
184
205
|
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
185
206
|
}
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
let diagnostics;
|
|
208
|
+
if (!scratchPath) {
|
|
209
|
+
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
210
|
+
diagnostics = runShellDiagnostics(rootDir, filePath);
|
|
211
|
+
}
|
|
188
212
|
const originalLines = originalContent.split('\n').length;
|
|
189
213
|
const nextLines = nextContent.split('\n').length;
|
|
190
214
|
if (!isTuiMode()) {
|
|
@@ -197,6 +221,7 @@ export async function strReplace(context, args) {
|
|
|
197
221
|
filePath,
|
|
198
222
|
changed,
|
|
199
223
|
operation: 'str_replace',
|
|
224
|
+
...(scratchPath ? { scratch: true } : {}),
|
|
200
225
|
replacements,
|
|
201
226
|
indexedChunks,
|
|
202
227
|
retrievalTokensUsed,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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'),
|
|
@@ -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);
|