@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.50

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 (75) hide show
  1. package/README.md +93 -4
  2. package/dist/bin/ai.js +142 -112
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +32 -7
  5. package/dist/src/api/auth.js +10 -4
  6. package/dist/src/api/browser-login.js +149 -37
  7. package/dist/src/api/chat.js +680 -166
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +151 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/api/startup-handshake.js +36 -0
  13. package/dist/src/api/turn-recovery.js +189 -0
  14. package/dist/src/background-jobs.js +4 -2
  15. package/dist/src/browser/bridge.js +262 -0
  16. package/dist/src/browser/framing.js +42 -0
  17. package/dist/src/browser/native-host.js +219 -0
  18. package/dist/src/browser/protocol.js +46 -0
  19. package/dist/src/browser/session-bridge.js +130 -0
  20. package/dist/src/cli-args.js +19 -5
  21. package/dist/src/client-environment.js +2 -0
  22. package/dist/src/core/clipboard.js +7 -13
  23. package/dist/src/core/image-limits.js +56 -0
  24. package/dist/src/core/image-path-extractor.js +70 -3
  25. package/dist/src/core/session-image-store.js +246 -0
  26. package/dist/src/edit-journal.js +33 -2
  27. package/dist/src/executor.js +25 -3
  28. package/dist/src/help-text.js +112 -21
  29. package/dist/src/patcher.js +43 -4
  30. package/dist/src/permissions.js +288 -0
  31. package/dist/src/preferences.js +39 -0
  32. package/dist/src/quota-notice.js +35 -0
  33. package/dist/src/session-safety.js +0 -12
  34. package/dist/src/session-store.js +121 -20
  35. package/dist/src/session.js +19 -4
  36. package/dist/src/signin.js +58 -0
  37. package/dist/src/tool-executor.js +13 -46
  38. package/dist/src/tools/browser.js +628 -0
  39. package/dist/src/tools/delete-file.js +20 -4
  40. package/dist/src/tools/index.js +32 -10
  41. package/dist/src/tools/patch-file.js +12 -26
  42. package/dist/src/tools/read-image-file.js +85 -0
  43. package/dist/src/tools/replace-document-text.js +28 -18
  44. package/dist/src/tools/restore-checkpoint.js +0 -1
  45. package/dist/src/tools/run-command.js +14 -71
  46. package/dist/src/tools/run-node-script.js +12 -81
  47. package/dist/src/tools/save-generated-image.js +120 -0
  48. package/dist/src/tools/str-replace.js +12 -26
  49. package/dist/src/tools/undo-edit.js +1 -6
  50. package/dist/src/tools/write-file.js +67 -11
  51. package/dist/src/ui/prompt-history-store.js +1 -1
  52. package/dist/src/ui/repl.js +949 -279
  53. package/dist/src/ui/slash-commands.js +121 -0
  54. package/dist/src/ui/tui/attention.js +25 -0
  55. package/dist/src/ui/tui/bridge.js +38 -5
  56. package/dist/src/ui/tui/build-frame.js +686 -223
  57. package/dist/src/ui/tui/composer-layout.js +76 -0
  58. package/dist/src/ui/tui/desktop-notification.js +118 -0
  59. package/dist/src/ui/tui/markdown-render.js +11 -2
  60. package/dist/src/ui/tui/shell-input.js +256 -73
  61. package/dist/src/ui/tui/shortcuts.js +61 -0
  62. package/dist/src/ui/tui/terminal-theme.js +28 -0
  63. package/dist/src/ui/tui/user-input.js +593 -0
  64. package/dist/src/update-check.js +205 -0
  65. package/dist/src/usage.js +5 -2
  66. package/dist/src/utils.js +13 -0
  67. package/dist/src/version.js +52 -2
  68. package/package.json +22 -9
  69. package/dist/src/project-index.js +0 -233
  70. package/dist/src/tools/code-intel.js +0 -472
  71. package/dist/src/tools/find-symbol.js +0 -70
  72. package/dist/src/tools/hover-symbol.js +0 -95
  73. package/dist/src/tools/list-symbols.js +0 -55
  74. package/dist/src/tools/search-code.js +0 -37
  75. package/dist/src/tools/signature-help.js +0 -118
@@ -0,0 +1,120 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getClientStateDir } from '../client-state.js';
4
+ import { MAX_IMAGE_SIZE_BYTES, sniffImageMimeType, } from '../core/image-limits.js';
5
+ const IMAGE_FILE_MODE = 0o600;
6
+ const IMAGE_DIR_MODE = 0o700;
7
+ export const GENERATED_IMAGES_SUBDIR = 'generated_images';
8
+ export const GENERATED_IMAGE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
9
+ export function getGeneratedImagesDir(env = process.env) {
10
+ return path.join(getClientStateDir(env), GENERATED_IMAGES_SUBDIR);
11
+ }
12
+ export function sanitizeGeneratedImageBasename(raw) {
13
+ const trimmed = String(raw ?? '').trim();
14
+ const base = trimmed
15
+ .replace(/\\/g, '/')
16
+ .split('/')
17
+ .pop()
18
+ ?.replace(/^\.+/, '')
19
+ .replace(/[^\w.\-]+/g, '-')
20
+ .replace(/-+/g, '-')
21
+ .replace(/^-+|-+$/g, '')
22
+ .replace(/-\.|\.-/g, '.')
23
+ .replace(/^-+|-+$/g, '')
24
+ .slice(0, 80);
25
+ if (!base || base === '.' || base === '..') {
26
+ return `generated-${Date.now()}.png`;
27
+ }
28
+ if (/\.png$/i.test(base)) {
29
+ return base.replace(/\.png$/i, '.png');
30
+ }
31
+ const withoutExt = base.replace(/\.[^.]+$/, '').replace(/-+$/g, '');
32
+ return `${withoutExt || 'generated'}.png`;
33
+ }
34
+ function uniquePath(dir, basename) {
35
+ const candidate = path.join(dir, basename);
36
+ if (!existsSync(candidate)) {
37
+ return candidate;
38
+ }
39
+ const ext = path.extname(basename) || '.png';
40
+ const stem = path.basename(basename, ext);
41
+ for (let i = 2; i < 10_000; i += 1) {
42
+ const next = path.join(dir, `${stem}-${i}${ext}`);
43
+ if (!existsSync(next)) {
44
+ return next;
45
+ }
46
+ }
47
+ return path.join(dir, `${stem}-${Date.now()}${ext}`);
48
+ }
49
+ export function sweepGeneratedImages({ maxAgeMs = GENERATED_IMAGE_MAX_AGE_MS, env = process.env, now = Date.now(), } = {}) {
50
+ const dir = getGeneratedImagesDir(env);
51
+ if (!existsSync(dir))
52
+ return 0;
53
+ let removed = 0;
54
+ let entries;
55
+ try {
56
+ entries = readdirSync(dir);
57
+ }
58
+ catch {
59
+ return 0;
60
+ }
61
+ for (const entry of entries) {
62
+ const filePath = path.join(dir, entry);
63
+ try {
64
+ if (now - statSync(filePath).mtimeMs < maxAgeMs)
65
+ continue;
66
+ rmSync(filePath, { force: true });
67
+ removed += 1;
68
+ }
69
+ catch {
70
+ }
71
+ }
72
+ return removed;
73
+ }
74
+ export function saveGeneratedImage(args) {
75
+ const base64Data = String(args.base64Data ?? '').trim();
76
+ if (!base64Data) {
77
+ return {
78
+ ok: false,
79
+ error: 'This CLI build cannot generate images without server-supplied image bytes. Update TheGitAI CLI.',
80
+ failureCategory: 'tool_exception',
81
+ };
82
+ }
83
+ let bytes;
84
+ try {
85
+ bytes = Buffer.from(base64Data, 'base64');
86
+ }
87
+ catch {
88
+ return {
89
+ ok: false,
90
+ error: 'Generated image bytes are invalid.',
91
+ failureCategory: 'invalid_argument',
92
+ };
93
+ }
94
+ if (bytes.length === 0 || bytes.length > MAX_IMAGE_SIZE_BYTES) {
95
+ return {
96
+ ok: false,
97
+ error: 'Generated image exceeded the allowed size limit.',
98
+ failureCategory: 'invalid_argument',
99
+ };
100
+ }
101
+ const sniffed = sniffImageMimeType(bytes);
102
+ if (sniffed !== 'image/png') {
103
+ return {
104
+ ok: false,
105
+ error: 'Generated image must be a PNG.',
106
+ failureCategory: 'invalid_argument',
107
+ };
108
+ }
109
+ const env = args.env ?? process.env;
110
+ const dir = getGeneratedImagesDir(env);
111
+ mkdirSync(dir, { recursive: true, mode: IMAGE_DIR_MODE });
112
+ const basename = sanitizeGeneratedImageBasename(args.suggestedFilename ?? args.filename);
113
+ const target = uniquePath(dir, basename);
114
+ writeFileSync(target, bytes, { mode: IMAGE_FILE_MODE });
115
+ return {
116
+ ok: true,
117
+ path: target,
118
+ message: `Image saved to ${target}`,
119
+ };
120
+ }
@@ -3,10 +3,10 @@ import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
4
  import { classifyProjectPath, readProjectFile, writeProjectFile, } from '../patcher.js';
5
5
  import { repairFilePath } from './path-suggest.js';
6
- import { upsertIndexFile } from '../project-index.js';
7
6
  import { isTuiMode } from '../runtime-mode.js';
8
7
  import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
9
8
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
9
+ import { ensurePermission } from '../permissions.js';
10
10
  const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
11
11
  function countOccurrences(haystack, needle) {
12
12
  if (needle.length === 0)
@@ -76,7 +76,7 @@ function buildStrReplacePreview(oldString, newString) {
76
76
  return `@@ str_replace @@\n${minus}\n${plus}`;
77
77
  }
78
78
  export async function strReplace(context, args) {
79
- const { rootDir, projectIndex, autoYes, confirmPatch } = context;
79
+ const { rootDir } = context;
80
80
  const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
81
81
  let oldString = typeof args.old_string === 'string'
82
82
  ? args.old_string
@@ -175,35 +175,23 @@ export async function strReplace(context, args) {
175
175
  }
176
176
  console.log();
177
177
  }
178
- if (!autoYes && confirmPatch) {
179
- const confirmed = await confirmPatch(filePath, preview);
180
- if (!confirmed) {
178
+ if (!scratchPath) {
179
+ const denied = await ensurePermission(context, {
180
+ bucket: 'edit',
181
+ title: 'Approve patch?',
182
+ body: 'Review changes before applying.',
183
+ filePath,
184
+ diff: preview,
185
+ }, 'str_replace', { filePath });
186
+ if (denied) {
181
187
  if (!isTuiMode())
182
188
  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
- };
189
+ return denied;
195
190
  }
196
191
  }
197
192
  const nextContent = originalContent.split(oldString).join(newString);
198
193
  const { changed } = writeProjectFile(rootDir, filePath, nextContent);
199
194
  const replacements = changed ? (replaceAll ? n : 1) : 0;
200
- let indexedChunks = 0;
201
- let retrievalTokensUsed = 0;
202
- if (changed && !scratchPath) {
203
- const indexResult = await upsertIndexFile(projectIndex, filePath);
204
- indexedChunks = indexResult.indexedChunks;
205
- retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
206
- }
207
195
  let diagnostics;
208
196
  if (!scratchPath) {
209
197
  invalidateShellDiagnosticsCache(rootDir, filePath);
@@ -223,8 +211,6 @@ export async function strReplace(context, args) {
223
211
  operation: 'str_replace',
224
212
  ...(scratchPath ? { scratch: true } : {}),
225
213
  replacements,
226
- indexedChunks,
227
- retrievalTokensUsed,
228
214
  bytesWritten: Buffer.byteLength(nextContent, 'utf-8'),
229
215
  diagnostics,
230
216
  message: changed ? undefined : 'The provided replacement resulted in no changes to the file content.',
@@ -1,7 +1,6 @@
1
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
- import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
5
4
  import { isTuiMode } from '../runtime-mode.js';
6
5
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
7
6
  function normalizeTarget(value) {
@@ -101,10 +100,9 @@ function validateUndoPlan(rootDir, records) {
101
100
  return { ok: true };
102
101
  }
103
102
  async function applyUndo(context, record) {
104
- const { rootDir, projectIndex } = context;
103
+ const { rootDir } = context;
105
104
  if (record.operation === 'create') {
106
105
  deleteProjectFile(rootDir, record.filePath);
107
- await removeIndexFile(projectIndex, record.filePath);
108
106
  return { changed: true };
109
107
  }
110
108
  if (record.beforeContent === null) {
@@ -113,9 +111,6 @@ async function applyUndo(context, record) {
113
111
  const { changed } = record.beforeContentEncoding === 'base64'
114
112
  ? writeProjectFileBuffer(rootDir, record.filePath, storedContentBuffer(record.beforeContent, record.beforeContentEncoding))
115
113
  : writeProjectFile(rootDir, record.filePath, record.beforeContent);
116
- if (changed) {
117
- await upsertIndexFile(projectIndex, record.filePath);
118
- }
119
114
  return { changed };
120
115
  }
121
116
  function summarizeUndoRecord(record) {
@@ -2,13 +2,48 @@ 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 { upsertIndexFile } from '../project-index.js';
5
+ import { readFileEditSnapshot } from '../edit-journal.js';
6
6
  import { isTuiMode } from '../runtime-mode.js';
7
7
  import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
8
8
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
9
+ import { ensurePermission } from '../permissions.js';
9
10
  const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
11
+ const MAX_WRITE_PREVIEW_LINES = 400;
12
+ function boundedLines(text, limit) {
13
+ const lines = [];
14
+ let omitted = 0;
15
+ let start = 0;
16
+ for (;;) {
17
+ const newline = text.indexOf('\n', start);
18
+ if (lines.length < limit) {
19
+ lines.push(text.slice(start, newline === -1 ? undefined : newline));
20
+ }
21
+ else {
22
+ omitted += 1;
23
+ }
24
+ if (newline === -1)
25
+ break;
26
+ start = newline + 1;
27
+ }
28
+ return { lines, omitted };
29
+ }
30
+ function buildWritePreview(previous, next) {
31
+ const rows = ['@@ write_file @@'];
32
+ const append = (text, sign, marker) => {
33
+ const { lines, omitted } = boundedLines(text, MAX_WRITE_PREVIEW_LINES);
34
+ for (const line of lines)
35
+ rows.push(`${sign}${line}`);
36
+ if (omitted > 0) {
37
+ rows.push(`@@ ${omitted} more ${marker} line(s) not shown @@`);
38
+ }
39
+ };
40
+ if (previous !== null)
41
+ append(previous, '-', 'removed');
42
+ append(next, '+', 'added');
43
+ return rows.join('\n');
44
+ }
10
45
  export async function writeFile(context, args) {
11
- const { rootDir, projectIndex } = context;
46
+ const { rootDir } = context;
12
47
  const filePath = String(args.filePath ?? '').trim();
13
48
  let content = typeof args.content === 'string' ? args.content : '';
14
49
  if (!filePath) {
@@ -60,14 +95,37 @@ export async function writeFile(context, args) {
60
95
  };
61
96
  }
62
97
  content = resolveRedactionTokens(context.safety, content, coveragePath, currentHash);
63
- const { changed } = writeProjectFile(rootDir, filePath, content);
64
- let indexedChunks = 0;
65
- let retrievalTokensUsed = 0;
66
- if (changed && !scratchPath) {
67
- const indexResult = await upsertIndexFile(projectIndex, filePath);
68
- indexedChunks = indexResult.indexedChunks;
69
- retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
98
+ if (!scratchPath) {
99
+ const before = currentHash === null ? null : readFileEditSnapshot(rootDir, filePath);
100
+ const existing = before && before.contentEncoding === 'utf8' ? before.content : null;
101
+ const denied = await ensurePermission(context, {
102
+ bucket: currentHash === null ? 'create' : 'edit',
103
+ title: currentHash === null ? 'Approve new file?' : 'Approve patch?',
104
+ body: 'Review changes before applying.',
105
+ filePath,
106
+ diff: buildWritePreview(existing, content),
107
+ }, 'write_file', { filePath });
108
+ if (denied) {
109
+ if (!isTuiMode())
110
+ console.log(chalk.dim(` ⏭ write_file skipped: ${filePath}`));
111
+ return denied;
112
+ }
113
+ if (getCurrentFileHash(rootDir, filePath) !== currentHash) {
114
+ return {
115
+ ok: false,
116
+ filePath,
117
+ failureCategory: 'conflict',
118
+ error: `write_file refused: ${filePath} changed on disk while the approval prompt was open.`,
119
+ failureDetails: {
120
+ category: 'conflict',
121
+ tool: 'write_file',
122
+ action: 'Re-read the file to see its current contents, then decide whether the write is still correct and retry.',
123
+ },
124
+ currentHash,
125
+ };
126
+ }
70
127
  }
128
+ const { changed } = writeProjectFile(rootDir, filePath, content);
71
129
  let diagnostics;
72
130
  if (!scratchPath) {
73
131
  invalidateShellDiagnosticsCache(rootDir, filePath);
@@ -84,8 +142,6 @@ export async function writeFile(context, args) {
84
142
  changed,
85
143
  operation: 'write',
86
144
  ...(scratchPath ? { scratch: true } : {}),
87
- indexedChunks,
88
- retrievalTokensUsed,
89
145
  bytesWritten: Buffer.byteLength(content, 'utf-8'),
90
146
  diagnostics,
91
147
  };
@@ -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);