@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.
Files changed (36) hide show
  1. package/README.md +16 -4
  2. package/dist/bin/ai.js +57 -287
  3. package/dist/src/api/auth.js +2 -2
  4. package/dist/src/api/browser-login.js +72 -3
  5. package/dist/src/api/chat.js +147 -30
  6. package/dist/src/api/http.js +16 -3
  7. package/dist/src/api/models.js +9 -4
  8. package/dist/src/help-text.js +19 -7
  9. package/dist/src/patcher.js +96 -9
  10. package/dist/src/project-index.js +13 -1
  11. package/dist/src/project-orientation.js +99 -0
  12. package/dist/src/scratch-dir.js +51 -33
  13. package/dist/src/session-store.js +52 -20
  14. package/dist/src/session.js +8 -0
  15. package/dist/src/tool-executor.js +38 -6
  16. package/dist/src/tools/delete-file.js +22 -4
  17. package/dist/src/tools/patch-file.js +30 -5
  18. package/dist/src/tools/read-file.js +3 -1
  19. package/dist/src/tools/replace-document-text.js +7 -1
  20. package/dist/src/tools/run-command.js +37 -19
  21. package/dist/src/tools/run-node-script.js +24 -4
  22. package/dist/src/tools/str-replace.js +30 -5
  23. package/dist/src/tools/write-file.js +25 -5
  24. package/dist/src/turn-failure-marker.js +11 -0
  25. package/dist/src/ui/prompt-history-store.js +1 -1
  26. package/dist/src/ui/repl.js +197 -51
  27. package/dist/src/ui/tui/bridge.js +3 -0
  28. package/dist/src/ui/tui/build-frame.js +179 -82
  29. package/dist/src/ui/tui/markdown-render.js +72 -73
  30. package/dist/src/ui/tui/shell-input.js +42 -13
  31. package/dist/src/ui/tui/terminal-title.js +3 -0
  32. package/dist/src/ui/tui/terminal-writes.js +48 -0
  33. package/dist/src/ui/tui/text.js +158 -4
  34. package/dist/src/utils.js +9 -0
  35. package/package.json +18 -6
  36. 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
- 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.',
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.initialized
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.initialized
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
- error: 'User declined command execution',
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
- const repoSync = shouldSync
190
- ? await syncIndexFromDisk(context.projectIndex)
191
- : emptyRepoSync(!context.projectIndex.initialized
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
- error: 'User declined str_replace',
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
- invalidateShellDiagnosticsCache(rootDir, filePath);
187
- const diagnostics = runShellDiagnostics(rootDir, filePath);
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 (currentHash !== null &&
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
- invalidateShellDiagnosticsCache(rootDir, filePath);
56
- const diagnostics = runShellDiagnostics(rootDir, filePath);
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 = 15;
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);