@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.51
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 +93 -4
- package/dist/bin/ai.js +142 -112
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +32 -7
- package/dist/src/api/auth.js +10 -4
- package/dist/src/api/browser-login.js +149 -37
- package/dist/src/api/chat.js +680 -166
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +151 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +262 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +219 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +130 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +112 -21
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +13 -46
- package/dist/src/tools/browser.js +641 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-file.js +5 -0
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +949 -279
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +38 -5
- package/dist/src/ui/tui/build-frame.js +686 -223
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +256 -73
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/user-input.js +593 -0
- package/dist/src/update-check.js +205 -0
- package/dist/src/usage.js +5 -2
- package/dist/src/utils.js +13 -0
- package/dist/src/version.js +52 -2
- package/package.json +22 -9
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- 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
|
|
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 (!
|
|
179
|
-
const
|
|
180
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
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);
|