@thegitai/cli 1.0.0-beta.1 → 1.0.0-beta.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 +4 -3
- package/dist/bin/ai.js +18 -61
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/api/auth.js +4 -2
- package/dist/src/api/browser-login.js +10 -28
- package/dist/src/api/chat.js +20 -9
- package/dist/src/api/http.js +32 -3
- package/dist/src/api/models.js +4 -2
- package/dist/src/artifact-policy.js +9 -0
- package/dist/src/cli-args.js +65 -0
- package/dist/src/client-environment.js +127 -0
- package/dist/src/colors.js +59 -0
- package/dist/src/core/clipboard.js +56 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +28 -5
- package/dist/src/help-text.js +15 -1
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +18 -1
- package/dist/src/scanner.js +67 -17
- package/dist/src/session-safety.js +64 -12
- package/dist/src/tool-executor.js +8 -0
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +14 -1
- package/dist/src/tools/path-suggest.js +66 -0
- package/dist/src/tools/read-document.js +14 -3
- package/dist/src/tools/read-file.js +9 -0
- package/dist/src/tools/replace-document-text.js +242 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +1 -1
- package/dist/src/tools/run-node-script.js +1 -1
- package/dist/src/tools/str-replace.js +14 -1
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +14 -1
- package/dist/src/ui/repl.js +13 -1
- package/dist/src/ui/tui/bridge.js +2 -2
- package/dist/src/ui/tui/build-frame.js +18 -2
- package/dist/src/ui/tui/shell-input.js +9 -1
- package/dist/src/version.js +35 -0
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +15 -16
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isSensitiveProjectPath, normalizeProjectRelativePath, } from '../artifact-policy.js';
|
|
5
|
+
import { readCliAuthConfig } from '../api/auth.js';
|
|
6
|
+
import { resolveProjectPath, writeProjectFileBuffer } from '../patcher.js';
|
|
7
|
+
import { suggestClosestPath } from './path-suggest.js';
|
|
8
|
+
import { isTuiMode } from '../runtime-mode.js';
|
|
9
|
+
function normalizeReplacements(value) {
|
|
10
|
+
if (!Array.isArray(value))
|
|
11
|
+
return [];
|
|
12
|
+
return value
|
|
13
|
+
.map((item) => {
|
|
14
|
+
if (!item || typeof item !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const entry = item;
|
|
17
|
+
const oldText = String(entry.oldText ?? entry.old_text ?? '');
|
|
18
|
+
const newText = String(entry.newText ?? entry.new_text ?? '');
|
|
19
|
+
if (!oldText)
|
|
20
|
+
return null;
|
|
21
|
+
return { oldText, newText };
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function relativeEditablePath(rootDir, rawPath) {
|
|
26
|
+
const resolvedPath = path.isAbsolute(rawPath)
|
|
27
|
+
? rawPath
|
|
28
|
+
: path.resolve(rootDir, rawPath);
|
|
29
|
+
const projectPath = normalizeProjectRelativePath(rootDir, resolvedPath);
|
|
30
|
+
if (!projectPath || isSensitiveProjectPath(projectPath))
|
|
31
|
+
return null;
|
|
32
|
+
return projectPath.split(path.sep).join('/');
|
|
33
|
+
}
|
|
34
|
+
function renderPreview(filePath, preview) {
|
|
35
|
+
if (isTuiMode())
|
|
36
|
+
return;
|
|
37
|
+
console.log(chalk.bold(`\n Document text replacement preview for ${filePath}:`));
|
|
38
|
+
for (const line of preview.split('\n')) {
|
|
39
|
+
if (line.startsWith('+')) {
|
|
40
|
+
console.log(chalk.green(line));
|
|
41
|
+
}
|
|
42
|
+
else if (line.startsWith('-')) {
|
|
43
|
+
console.log(chalk.red(line));
|
|
44
|
+
}
|
|
45
|
+
else if (line.startsWith('@@')) {
|
|
46
|
+
console.log(chalk.cyan(line));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(chalk.dim(line));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
async function replaceDocumentTextOnServer(config, fileName, fileData, replacements, replaceAll, validate) {
|
|
55
|
+
const response = await globalThis.fetch(`${config.serverUrl.replace(/\/+$/, '')}/v1/document/replace-text`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
authorization: `Bearer ${config.token}`,
|
|
59
|
+
'content-type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
fileName,
|
|
63
|
+
fileData: fileData.toString('base64'),
|
|
64
|
+
replacements,
|
|
65
|
+
replaceAll,
|
|
66
|
+
validate,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const data = await response.json().catch(() => null);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `Server document edit failed: ${data?.error?.message ?? response.status}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
export async function replaceDocumentText(context, args) {
|
|
79
|
+
const sourceRaw = String(args.filePath ?? args.file_path ?? '').trim();
|
|
80
|
+
if (!sourceRaw) {
|
|
81
|
+
return { ok: false, error: 'filePath is required' };
|
|
82
|
+
}
|
|
83
|
+
const sourcePath = relativeEditablePath(context.rootDir, sourceRaw);
|
|
84
|
+
if (!sourcePath) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: 'replace_document_text can only edit permitted files inside the project root.',
|
|
88
|
+
failureCategory: 'permission_denied',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (path.extname(sourcePath).toLowerCase() !== '.docx') {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: 'replace_document_text only supports .docx files.',
|
|
95
|
+
failureCategory: 'invalid_argument',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const outputRaw = String(args.outputPath ?? args.output_path ?? '').trim();
|
|
99
|
+
const targetPath = outputRaw
|
|
100
|
+
? relativeEditablePath(context.rootDir, outputRaw)
|
|
101
|
+
: sourcePath;
|
|
102
|
+
if (!targetPath) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: 'outputPath must be inside the project root.',
|
|
106
|
+
failureCategory: 'permission_denied',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (path.extname(targetPath).toLowerCase() !== '.docx') {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: 'outputPath must end with .docx.',
|
|
113
|
+
failureCategory: 'invalid_argument',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const replacements = normalizeReplacements(args.replacements);
|
|
117
|
+
if (!replacements.length) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: 'replacements must include at least one { oldText, newText } item.',
|
|
121
|
+
failureCategory: 'missing_required_argument',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const authConfig = readCliAuthConfig(context.env);
|
|
125
|
+
if (!authConfig) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: 'Document editing requires a server connection. Please log in first.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const sourceAbsPath = resolveProjectPath(context.rootDir, sourcePath);
|
|
132
|
+
if (!existsSync(sourceAbsPath)) {
|
|
133
|
+
const suggestion = suggestClosestPath(context.rootDir, sourceAbsPath);
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
filePath: sourcePath,
|
|
137
|
+
error: suggestion
|
|
138
|
+
? `File does not exist: ${sourcePath}. Did you mean "${suggestion}"? Note the exact punctuation (e.g. curly apostrophe ’ vs straight ').`
|
|
139
|
+
: `File does not exist: ${sourcePath}`,
|
|
140
|
+
failureCategory: 'not_found',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const validateOnly = args.validate === true || args.dryRun === true;
|
|
144
|
+
const serverResult = await replaceDocumentTextOnServer(authConfig, path.basename(sourcePath), readFileSync(sourceAbsPath), replacements, args.replaceAll === true || args.replace_all === true, validateOnly);
|
|
145
|
+
if (!serverResult.ok) {
|
|
146
|
+
return {
|
|
147
|
+
...serverResult,
|
|
148
|
+
filePath: sourcePath,
|
|
149
|
+
failureCategory: serverResult.failureCategory ?? 'external_service',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Validate-only: report per-replacement match info without touching the file.
|
|
153
|
+
// changed:false marks it non-mutating so the agent loop does not count a
|
|
154
|
+
// dry-run as an applied edit.
|
|
155
|
+
if (validateOnly) {
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
validate: true,
|
|
159
|
+
changed: false,
|
|
160
|
+
filePath: sourcePath,
|
|
161
|
+
operation: 'replace_document_text',
|
|
162
|
+
results: serverResult.results,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// No replacement matched: nothing was written. Surface per-item reasons so
|
|
166
|
+
// the model can correct and resend only the failing entries.
|
|
167
|
+
const replacementCount = Number(serverResult.replacementCount ?? 0);
|
|
168
|
+
if (replacementCount === 0) {
|
|
169
|
+
const failures = Array.isArray(serverResult.replacements)
|
|
170
|
+
? serverResult.replacements
|
|
171
|
+
.filter((item) => item && item.ok === false)
|
|
172
|
+
.map((item) => `- ${item.error ?? 'no match'}`)
|
|
173
|
+
: [];
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
filePath: sourcePath,
|
|
177
|
+
operation: 'replace_document_text',
|
|
178
|
+
failureCategory: 'conflict',
|
|
179
|
+
error: `0 of ${serverResult.requestedCount ?? replacements.length} replacements applied; file unchanged.` +
|
|
180
|
+
(failures.length ? `\n${failures.join('\n')}` : ''),
|
|
181
|
+
replacements: serverResult.replacements,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const preview = String(serverResult.preview ?? '');
|
|
185
|
+
renderPreview(targetPath, preview);
|
|
186
|
+
if (!context.autoYes && context.confirmPatch) {
|
|
187
|
+
const confirmed = await context.confirmPatch(targetPath, preview);
|
|
188
|
+
if (!confirmed) {
|
|
189
|
+
if (!isTuiMode()) {
|
|
190
|
+
console.log(chalk.dim(` replace_document_text skipped: ${targetPath}`));
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
skipped: true,
|
|
195
|
+
filePath: targetPath,
|
|
196
|
+
error: 'User declined replace_document_text',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const fileData = String(serverResult.fileData ?? '');
|
|
201
|
+
const nextData = Buffer.from(fileData, 'base64');
|
|
202
|
+
const write = writeProjectFileBuffer(context.rootDir, targetPath, nextData);
|
|
203
|
+
const failedCount = Number(serverResult.failedCount ?? 0);
|
|
204
|
+
const appliedCount = Number(serverResult.appliedCount ?? replacementCount);
|
|
205
|
+
const requestedCount = Number(serverResult.requestedCount ?? replacements.length);
|
|
206
|
+
const partialFailures = Array.isArray(serverResult.replacements)
|
|
207
|
+
? serverResult.replacements
|
|
208
|
+
.filter((item) => item && item.ok === false)
|
|
209
|
+
.map((item) => `- ${item.error ?? 'no match'}`)
|
|
210
|
+
: [];
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
filePath: targetPath,
|
|
214
|
+
sourceFilePath: sourcePath,
|
|
215
|
+
changed: write.changed,
|
|
216
|
+
operation: 'replace_document_text',
|
|
217
|
+
replacementCount: serverResult.replacementCount,
|
|
218
|
+
requestedCount: serverResult.requestedCount,
|
|
219
|
+
appliedCount: serverResult.appliedCount,
|
|
220
|
+
failedCount: serverResult.failedCount,
|
|
221
|
+
replacements: serverResult.replacements,
|
|
222
|
+
bytesWritten: nextData.length,
|
|
223
|
+
// A partial batch still wrote the matched entries (changed:true above), but
|
|
224
|
+
// the loop must reflect and repair the missed entries — needsRepair forces
|
|
225
|
+
// that without losing credit for the applied edits.
|
|
226
|
+
...(failedCount > 0
|
|
227
|
+
? {
|
|
228
|
+
needsRepair: true,
|
|
229
|
+
failureCategory: 'conflict',
|
|
230
|
+
error: `${appliedCount} of ${requestedCount} replacements applied; ` +
|
|
231
|
+
`${failedCount} missed and still need to be fixed:\n` +
|
|
232
|
+
partialFailures.join('\n'),
|
|
233
|
+
failureDetails: {
|
|
234
|
+
category: 'conflict',
|
|
235
|
+
tool: 'replace_document_text',
|
|
236
|
+
action: 'Re-issue replace_document_text for the missed entries only, with corrected oldText. ' +
|
|
237
|
+
'Copy the exact text from read_document (mind curly vs straight quotes), or pass validate:true to test a match first.',
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
2
|
import { isTuiMode } from '../runtime-mode.js';
|
|
3
3
|
import { createPromptCheckpoint, restoreCheckpointFiles, } from '../session-safety.js';
|
|
4
4
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
3
4
|
import { readProjectFile, writeProjectFile } from '../patcher.js';
|
|
4
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
5
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
7
|
import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
|
|
7
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
9
|
+
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
8
10
|
function countOccurrences(haystack, needle) {
|
|
9
11
|
if (needle.length === 0)
|
|
10
12
|
return 0;
|
|
@@ -92,6 +94,17 @@ export async function strReplace(context, args) {
|
|
|
92
94
|
if (oldString.length === 0) {
|
|
93
95
|
return { ok: false, error: 'old_string is required and must be non-empty' };
|
|
94
96
|
}
|
|
97
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
98
|
+
if (DOCUMENT_EXTENSIONS.has(ext)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
filePath,
|
|
102
|
+
error: ext === '.docx'
|
|
103
|
+
? 'Use replace_document_text for .docx files.'
|
|
104
|
+
: `Use read_document for ${ext} files; text replacement is not supported.`,
|
|
105
|
+
failureCategory: 'invalid_argument',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
95
108
|
let originalContent;
|
|
96
109
|
try {
|
|
97
110
|
originalContent = readProjectFile(rootDir, filePath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
import {
|
|
3
|
-
import { deleteProjectFile, writeProjectFile, } from '../patcher.js';
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import { hashStoredContent, readFileEditSnapshot, storedContentBuffer, } from '../edit-journal.js';
|
|
3
|
+
import { deleteProjectFile, writeProjectFile, writeProjectFileBuffer, } from '../patcher.js';
|
|
4
4
|
import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
|
|
5
5
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
6
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
@@ -84,7 +84,7 @@ function validateUndoPlan(rootDir, records) {
|
|
|
84
84
|
currentHash,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
const beforeContentHash =
|
|
87
|
+
const beforeContentHash = hashStoredContent(record.beforeContent, record.beforeContentEncoding);
|
|
88
88
|
if (beforeContentHash !== record.beforeHash) {
|
|
89
89
|
return {
|
|
90
90
|
ok: false,
|
|
@@ -110,7 +110,9 @@ async function applyUndo(context, record) {
|
|
|
110
110
|
if (record.beforeContent === null) {
|
|
111
111
|
throw new Error(`Cannot undo ${record.id}: the stored pre-edit snapshot is incomplete.`);
|
|
112
112
|
}
|
|
113
|
-
const { changed } =
|
|
113
|
+
const { changed } = record.beforeContentEncoding === 'base64'
|
|
114
|
+
? writeProjectFileBuffer(rootDir, record.filePath, storedContentBuffer(record.beforeContent, record.beforeContentEncoding))
|
|
115
|
+
: writeProjectFile(rootDir, record.filePath, record.beforeContent);
|
|
114
116
|
if (changed) {
|
|
115
117
|
await upsertIndexFile(projectIndex, record.filePath);
|
|
116
118
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from '../colors.js';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
3
4
|
import { writeProjectFile } from '../patcher.js';
|
|
4
5
|
import { upsertIndexFile } from '../project-index.js';
|
|
5
6
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
7
|
import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
|
|
7
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
9
|
+
const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
|
|
8
10
|
export async function writeFile(context, args) {
|
|
9
11
|
const { rootDir, projectIndex } = context;
|
|
10
12
|
const filePath = String(args.filePath ?? '').trim();
|
|
@@ -12,6 +14,17 @@ export async function writeFile(context, args) {
|
|
|
12
14
|
if (!filePath) {
|
|
13
15
|
return { ok: false, error: 'filePath is required' };
|
|
14
16
|
}
|
|
17
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
18
|
+
if (DOCUMENT_EXTENSIONS.has(ext)) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
filePath,
|
|
22
|
+
error: ext === '.docx'
|
|
23
|
+
? 'Use replace_document_text for .docx files.'
|
|
24
|
+
: `Use read_document for ${ext} files; write_file is not supported.`,
|
|
25
|
+
failureCategory: 'invalid_argument',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
const coveragePath = normalizeProjectRelativePath(rootDir, filePath) ?? filePath;
|
|
16
29
|
const currentHash = getCurrentFileHash(rootDir, filePath);
|
|
17
30
|
if (currentHash !== null &&
|
|
@@ -5,8 +5,21 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { addSignatureForNode } from './extractors/index.js';
|
|
6
6
|
import { getRepoMapLanguageForFile, } from './repo-map-languages.js';
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const TreeSitter = require('web-tree-sitter');
|
|
9
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
// web-tree-sitter is vendored (see vendor/web-tree-sitter/NOTICE) so the
|
|
10
|
+
// published package has zero runtime dependencies. Resolve the vendored CommonJS
|
|
11
|
+
// runtime relative to this compiled file: dist/src/ -> dist/vendor in the
|
|
12
|
+
// published layout, with the source tree as a dev fallback. The .cjs locates its
|
|
13
|
+
// own web-tree-sitter.wasm next to itself via __dirname, so no locateFile
|
|
14
|
+
// override is needed.
|
|
15
|
+
function resolveVendoredTreeSitter() {
|
|
16
|
+
const candidates = [
|
|
17
|
+
path.resolve(__dirname, '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
|
|
18
|
+
path.resolve(__dirname, '..', '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
|
|
19
|
+
];
|
|
20
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
21
|
+
}
|
|
22
|
+
const TreeSitter = require(resolveVendoredTreeSitter());
|
|
10
23
|
let parserInitPromise = null;
|
|
11
24
|
const parserCache = Object.create(null);
|
|
12
25
|
const languageCache = Object.create(null);
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -11,7 +11,7 @@ import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSes
|
|
|
11
11
|
import { truncate } from '../utils.js';
|
|
12
12
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
13
13
|
import { openUrl } from '../core/open-url.js';
|
|
14
|
-
import { formatInteractiveHelpText } from '../help-text.js';
|
|
14
|
+
import { formatAboutCard, formatInteractiveHelpText } from '../help-text.js';
|
|
15
15
|
import { expandPastedChunks, } from './paste-collapse.js';
|
|
16
16
|
import { loadPromptHistory, MAX_PROMPT_HISTORY_ENTRIES, } from './prompt-history-store.js';
|
|
17
17
|
const RESPONSE_STREAM_CHUNK_SIZE = 4;
|
|
@@ -86,6 +86,10 @@ export const SLASH_COMMANDS = [
|
|
|
86
86
|
command: '/help',
|
|
87
87
|
description: 'Show available chat commands',
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
command: '/about',
|
|
91
|
+
description: 'Show version and platform info',
|
|
92
|
+
},
|
|
89
93
|
{
|
|
90
94
|
command: '/usage',
|
|
91
95
|
description: 'Show account usage percentage and reset times',
|
|
@@ -1875,6 +1879,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1875
1879
|
});
|
|
1876
1880
|
return;
|
|
1877
1881
|
}
|
|
1882
|
+
if (input === '/about') {
|
|
1883
|
+
appendStaticEntry({
|
|
1884
|
+
body: formatAboutCard(),
|
|
1885
|
+
kind: 'system',
|
|
1886
|
+
title: 'About',
|
|
1887
|
+
});
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1878
1890
|
if (input === '/usage') {
|
|
1879
1891
|
store.update((current) => ({
|
|
1880
1892
|
...current,
|
|
@@ -114,13 +114,13 @@ export function resolveTuiBinaryPath() {
|
|
|
114
114
|
export function spawnTuiProcess(options = {}) {
|
|
115
115
|
const binaryPath = resolveTuiBinaryPath();
|
|
116
116
|
return spawn(binaryPath, [], {
|
|
117
|
-
stdio: ['pipe', '
|
|
117
|
+
stdio: ['pipe', 'inherit', 'pipe'],
|
|
118
118
|
env: { ...process.env, ...options.env },
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
export function createRatatuiBridge() {
|
|
122
122
|
const child = spawnTuiProcess();
|
|
123
|
-
const rl = createInterface({ input: child.
|
|
123
|
+
const rl = createInterface({ input: child.stderr });
|
|
124
124
|
let eventHandler = null;
|
|
125
125
|
let closed = false;
|
|
126
126
|
rl.on('line', (line) => {
|
|
@@ -131,6 +131,18 @@ function diffLinePrefix(kind) {
|
|
|
131
131
|
return ' ';
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
// Fit a single diff line to the available terminal width with a clean ellipsis.
|
|
135
|
+
// Unlike the general-purpose truncate(), this never appends a word like
|
|
136
|
+
// "(truncated)" — that wording belongs on tool output, not on-screen diff rows.
|
|
137
|
+
function fitDiffLine(content, maxWidth) {
|
|
138
|
+
if (maxWidth <= 0)
|
|
139
|
+
return '';
|
|
140
|
+
if (content.length <= maxWidth)
|
|
141
|
+
return content;
|
|
142
|
+
if (maxWidth === 1)
|
|
143
|
+
return '…';
|
|
144
|
+
return `${content.slice(0, maxWidth - 1)}…`;
|
|
145
|
+
}
|
|
134
146
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
135
147
|
const trimmed = String(projectRoot ?? '').trim();
|
|
136
148
|
if (!trimmed)
|
|
@@ -228,7 +240,7 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
228
240
|
if (entry.diffPreview) {
|
|
229
241
|
lines.push(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
|
|
230
242
|
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
231
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(
|
|
243
|
+
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitDiffLine(diffLine.content || ' ', width - 4), {
|
|
232
244
|
color: diffLineColor(diffLine.kind),
|
|
233
245
|
})));
|
|
234
246
|
}
|
|
@@ -311,7 +323,11 @@ function composerFooterLines(state) {
|
|
|
311
323
|
? state.queuedMessage
|
|
312
324
|
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
313
325
|
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
314
|
-
:
|
|
326
|
+
: process.platform === 'win32'
|
|
327
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
|
|
328
|
+
: process.platform === 'darwin'
|
|
329
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
|
|
330
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
|
|
315
331
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
316
332
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
317
333
|
const footerSpans = [
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
2
2
|
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, resolveApprovalChoiceFromInput, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
|
|
3
3
|
import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
|
|
4
|
+
function isClipboardImagePasteKey(key) {
|
|
5
|
+
if (process.platform === 'win32') {
|
|
6
|
+
return ((key.ctrl || key.meta) &&
|
|
7
|
+
key.input.toLowerCase() === 'v' &&
|
|
8
|
+
!key.shift);
|
|
9
|
+
}
|
|
10
|
+
return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
|
|
11
|
+
}
|
|
4
12
|
function shouldShowCommandPalette(state) {
|
|
5
13
|
if (state.busy ||
|
|
6
14
|
state.exiting ||
|
|
@@ -428,7 +436,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
428
436
|
});
|
|
429
437
|
return;
|
|
430
438
|
}
|
|
431
|
-
if (key
|
|
439
|
+
if (isClipboardImagePasteKey(key)) {
|
|
432
440
|
const current = store.getState();
|
|
433
441
|
if (current.busy)
|
|
434
442
|
return;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PACKAGE_NAME = '@thegitai/cli';
|
|
5
|
+
const UNKNOWN_VERSION = '0.0.0';
|
|
6
|
+
// Resolve the package version at runtime by walking up from this module to the
|
|
7
|
+
// nearest package.json named @thegitai/cli. This works in both layouts: the
|
|
8
|
+
// compiled binary (dist/bin/ai.js → ../../package.json) and the source tree run
|
|
9
|
+
// under tsx in tests (src/version.ts → ../package.json). The name guard avoids
|
|
10
|
+
// picking up an unrelated manifest if the file is ever nested elsewhere.
|
|
11
|
+
export function getCliVersion() {
|
|
12
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
16
|
+
if (pkg.name === PACKAGE_NAME && typeof pkg.version === 'string') {
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// No package.json at this level (or unreadable); keep walking up.
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir)
|
|
25
|
+
break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
return UNKNOWN_VERSION;
|
|
29
|
+
}
|
|
30
|
+
export function getPlatformTag() {
|
|
31
|
+
return `${process.platform}-${process.arch}`;
|
|
32
|
+
}
|
|
33
|
+
export function formatVersionLine() {
|
|
34
|
+
return `ai ${getCliVersion()} (${getPlatformTag()}, node ${process.version})`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Max Brunsfeld
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
web-tree-sitter
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
The files `web-tree-sitter.cjs` and `web-tree-sitter.wasm` in this directory are
|
|
5
|
+
vendored, unmodified, from the `web-tree-sitter` npm package, version 0.26.6.
|
|
6
|
+
|
|
7
|
+
They are bundled here (rather than installed as a runtime dependency) so the
|
|
8
|
+
published `@thegitai/cli` package declares zero runtime dependencies while still
|
|
9
|
+
shipping local tree-sitter code intelligence. They are used only as a local
|
|
10
|
+
parsing runtime; no part of web-tree-sitter is modified.
|
|
11
|
+
|
|
12
|
+
Upstream: https://github.com/tree-sitter/tree-sitter
|
|
13
|
+
License: MIT (see the adjacent LICENSE file)
|