@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.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.
Files changed (38) hide show
  1. package/README.md +32 -4
  2. package/dist/bin/ai.js +57 -291
  3. package/dist/src/agent-mode.js +1 -1
  4. package/dist/src/api/auth.js +2 -2
  5. package/dist/src/api/browser-login.js +72 -3
  6. package/dist/src/api/chat.js +232 -33
  7. package/dist/src/api/contracts.js +55 -1
  8. package/dist/src/api/http.js +16 -3
  9. package/dist/src/api/models.js +9 -4
  10. package/dist/src/executor.js +1 -1
  11. package/dist/src/help-text.js +51 -11
  12. package/dist/src/permissions.js +243 -0
  13. package/dist/src/project-index.js +13 -1
  14. package/dist/src/session-store.js +57 -20
  15. package/dist/src/session.js +14 -3
  16. package/dist/src/tool-executor.js +2 -2
  17. package/dist/src/tools/delete-file.js +14 -0
  18. package/dist/src/tools/patch-file.js +12 -16
  19. package/dist/src/tools/replace-document-text.js +28 -18
  20. package/dist/src/tools/run-command.js +13 -27
  21. package/dist/src/tools/run-node-script.js +11 -26
  22. package/dist/src/tools/str-replace.js +12 -16
  23. package/dist/src/tools/write-file.js +66 -0
  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 +569 -151
  27. package/dist/src/ui/tui/bridge.js +10 -0
  28. package/dist/src/ui/tui/build-frame.js +535 -159
  29. package/dist/src/ui/tui/markdown-render.js +81 -73
  30. package/dist/src/ui/tui/shell-input.js +155 -45
  31. package/dist/src/ui/tui/terminal-theme.js +28 -0
  32. package/dist/src/ui/tui/terminal-title.js +3 -0
  33. package/dist/src/ui/tui/terminal-writes.js +48 -0
  34. package/dist/src/ui/tui/text.js +158 -4
  35. package/dist/src/ui/tui/user-input.js +568 -0
  36. package/dist/src/utils.js +9 -0
  37. package/package.json +18 -6
  38. package/dist/src/markdown-renderer.js +0 -112
@@ -2,13 +2,14 @@ import chalk from '../colors.js';
2
2
  import { startBackgroundJob } from '../background-jobs.js';
3
3
  import { getBlockedCommandReason, runCommand, } from '../executor.js';
4
4
  import { syncIndexFromDisk } from '../project-index.js';
5
+ import { ensurePermission } from '../permissions.js';
5
6
  import { isTuiMode } from '../runtime-mode.js';
6
7
  import { redactConnectionStringCredentials } from '../secret-preview.js';
7
8
  import { buildNestedGitHint } from '../session-safety.js';
8
9
  import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
9
10
  const MAX_OUTPUT_CHARS = 4000;
10
11
  export async function runShellCommand(context, args) {
11
- const { rootDir, projectIndex, autoYes, confirmCommand, requestSudoPassword, onStatus, } = context;
12
+ const { rootDir, projectIndex, requestSudoPassword, onStatus, } = context;
12
13
  const command = String(args.command ?? '').trim();
13
14
  if (!command) {
14
15
  return { ok: false, error: 'command is required' };
@@ -31,33 +32,18 @@ export async function runShellCommand(context, args) {
31
32
  }
32
33
  if (!isTuiMode())
33
34
  console.log(chalk.bold.yellow(`\n ⚡ Command: ${command}`));
34
- if (!autoYes) {
35
- if (confirmCommand == null) {
36
- return {
37
- ok: false,
38
- command,
39
- error: 'confirmCommand is required when autoYes is false',
40
- };
41
- }
42
- const approved = await confirmCommand(runInBackground
35
+ const denied = await ensurePermission(context, {
36
+ bucket: 'run',
37
+ title: 'Approve command?',
38
+ body: runInBackground
43
39
  ? `${command}\n\nRuns as a managed background job until it exits or is killed.`
44
- : command);
45
- if (!approved) {
46
- if (!isTuiMode())
47
- console.log(chalk.dim(` ⏭ Skipped: ${command}`));
48
- return {
49
- ok: false,
50
- skipped: true,
51
- command,
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.',
59
- };
60
- }
40
+ : command,
41
+ command,
42
+ }, 'run_command', { command });
43
+ if (denied) {
44
+ if (!isTuiMode())
45
+ console.log(chalk.dim(` ⏭ Skipped: ${command}`));
46
+ return denied;
61
47
  }
62
48
  if (runInBackground) {
63
49
  return runBackgroundCommand(context, command, args.timeout_ms, repoHint);
@@ -1,5 +1,6 @@
1
1
  import chalk from '../colors.js';
2
2
  import { execFileSync, spawn } from 'node:child_process';
3
+ import { ensurePermission } from '../permissions.js';
3
4
  import { syncIndexFromDisk } from '../project-index.js';
4
5
  import { isTuiMode } from '../runtime-mode.js';
5
6
  import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
@@ -148,7 +149,7 @@ function executeNodeScript(rootDir, script, timeout) {
148
149
  });
149
150
  }
150
151
  export async function runNodeScript(context, args) {
151
- const { rootDir, autoYes, confirmCommand } = context;
152
+ const { rootDir } = context;
152
153
  const script = typeof args.script === 'string' ? args.script : '';
153
154
  if (!script.trim()) {
154
155
  return { ok: false, error: 'script is required' };
@@ -158,32 +159,16 @@ export async function runNodeScript(context, args) {
158
159
  console.log(chalk.bold.yellow(`\n ⚡ Node script:\n${commandForApproval}\n`));
159
160
  console.log(chalk.dim(` in: ${rootDir}\n`));
160
161
  }
161
- if (!autoYes) {
162
- if (confirmCommand == null) {
163
- return {
164
- ok: false,
165
- command: COMMAND_LABEL,
166
- error: 'confirmCommand is required when autoYes is false',
167
- };
168
- }
169
- const approved = await confirmCommand(commandForApproval);
170
- if (!approved) {
171
- if (!isTuiMode()) {
172
- console.log(chalk.dim(` ⏭ Skipped: ${COMMAND_LABEL}`));
173
- }
174
- return {
175
- ok: false,
176
- skipped: true,
177
- command: COMMAND_LABEL,
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.',
185
- };
162
+ const denied = await ensurePermission(context, {
163
+ bucket: 'run',
164
+ title: 'Approve node script?',
165
+ body: commandForApproval,
166
+ }, 'run_node_script', { command: COMMAND_LABEL });
167
+ if (denied) {
168
+ if (!isTuiMode()) {
169
+ console.log(chalk.dim(` ⏭ Skipped: ${COMMAND_LABEL}`));
186
170
  }
171
+ return denied;
187
172
  }
188
173
  const beforeGitStatus = readGitStatusSignature(rootDir);
189
174
  const result = await executeNodeScript(rootDir, script, typeof args.timeout_ms === 'number' && args.timeout_ms > 0
@@ -7,6 +7,7 @@ import { upsertIndexFile } from '../project-index.js';
7
7
  import { isTuiMode } from '../runtime-mode.js';
8
8
  import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
9
9
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
10
+ import { ensurePermission } from '../permissions.js';
10
11
  const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
11
12
  function countOccurrences(haystack, needle) {
12
13
  if (needle.length === 0)
@@ -76,7 +77,7 @@ function buildStrReplacePreview(oldString, newString) {
76
77
  return `@@ str_replace @@\n${minus}\n${plus}`;
77
78
  }
78
79
  export async function strReplace(context, args) {
79
- const { rootDir, projectIndex, autoYes, confirmPatch } = context;
80
+ const { rootDir, projectIndex } = context;
80
81
  const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
81
82
  let oldString = typeof args.old_string === 'string'
82
83
  ? args.old_string
@@ -175,23 +176,18 @@ export async function strReplace(context, args) {
175
176
  }
176
177
  console.log();
177
178
  }
178
- if (!autoYes && confirmPatch) {
179
- const confirmed = await confirmPatch(filePath, preview);
180
- if (!confirmed) {
179
+ if (!scratchPath) {
180
+ const denied = await ensurePermission(context, {
181
+ bucket: 'edit',
182
+ title: 'Approve patch?',
183
+ body: 'Review changes before applying.',
184
+ filePath,
185
+ diff: preview,
186
+ }, 'str_replace', { filePath });
187
+ if (denied) {
181
188
  if (!isTuiMode())
182
189
  console.log(chalk.dim(` ⏭ str_replace skipped: ${filePath}`));
183
- return {
184
- ok: false,
185
- skipped: true,
186
- filePath,
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.',
194
- };
190
+ return denied;
195
191
  }
196
192
  }
197
193
  const nextContent = originalContent.split(oldString).join(newString);
@@ -2,11 +2,47 @@ import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
4
  import { classifyProjectPath, writeProjectFile } from '../patcher.js';
5
+ import { readFileEditSnapshot } from '../edit-journal.js';
5
6
  import { upsertIndexFile } from '../project-index.js';
6
7
  import { isTuiMode } from '../runtime-mode.js';
7
8
  import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
8
9
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
10
+ import { ensurePermission } from '../permissions.js';
9
11
  const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
12
+ const MAX_WRITE_PREVIEW_LINES = 400;
13
+ function boundedLines(text, limit) {
14
+ const lines = [];
15
+ let omitted = 0;
16
+ let start = 0;
17
+ for (;;) {
18
+ const newline = text.indexOf('\n', start);
19
+ if (lines.length < limit) {
20
+ lines.push(text.slice(start, newline === -1 ? undefined : newline));
21
+ }
22
+ else {
23
+ omitted += 1;
24
+ }
25
+ if (newline === -1)
26
+ break;
27
+ start = newline + 1;
28
+ }
29
+ return { lines, omitted };
30
+ }
31
+ function buildWritePreview(previous, next) {
32
+ const rows = ['@@ write_file @@'];
33
+ const append = (text, sign, marker) => {
34
+ const { lines, omitted } = boundedLines(text, MAX_WRITE_PREVIEW_LINES);
35
+ for (const line of lines)
36
+ rows.push(`${sign}${line}`);
37
+ if (omitted > 0) {
38
+ rows.push(`@@ ${omitted} more ${marker} line(s) not shown @@`);
39
+ }
40
+ };
41
+ if (previous !== null)
42
+ append(previous, '-', 'removed');
43
+ append(next, '+', 'added');
44
+ return rows.join('\n');
45
+ }
10
46
  export async function writeFile(context, args) {
11
47
  const { rootDir, projectIndex } = context;
12
48
  const filePath = String(args.filePath ?? '').trim();
@@ -60,6 +96,36 @@ export async function writeFile(context, args) {
60
96
  };
61
97
  }
62
98
  content = resolveRedactionTokens(context.safety, content, coveragePath, currentHash);
99
+ if (!scratchPath) {
100
+ const before = currentHash === null ? null : readFileEditSnapshot(rootDir, filePath);
101
+ const existing = before && before.contentEncoding === 'utf8' ? before.content : null;
102
+ const denied = await ensurePermission(context, {
103
+ bucket: currentHash === null ? 'create' : 'edit',
104
+ title: currentHash === null ? 'Approve new file?' : 'Approve patch?',
105
+ body: 'Review changes before applying.',
106
+ filePath,
107
+ diff: buildWritePreview(existing, content),
108
+ }, 'write_file', { filePath });
109
+ if (denied) {
110
+ if (!isTuiMode())
111
+ console.log(chalk.dim(` ⏭ write_file skipped: ${filePath}`));
112
+ return denied;
113
+ }
114
+ if (getCurrentFileHash(rootDir, filePath) !== currentHash) {
115
+ return {
116
+ ok: false,
117
+ filePath,
118
+ failureCategory: 'conflict',
119
+ error: `write_file refused: ${filePath} changed on disk while the approval prompt was open.`,
120
+ failureDetails: {
121
+ category: 'conflict',
122
+ tool: 'write_file',
123
+ action: 'Re-read the file to see its current contents, then decide whether the write is still correct and retry.',
124
+ },
125
+ currentHash,
126
+ };
127
+ }
128
+ }
63
129
  const { changed } = writeProjectFile(rootDir, filePath, content);
64
130
  let indexedChunks = 0;
65
131
  let retrievalTokensUsed = 0;
@@ -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);