@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.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.
- package/README.md +37 -2
- package/dist/bin/ai.js +148 -75
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +7 -41
- package/dist/src/api/chat.js +77 -20
- package/dist/src/api/http.js +81 -4
- package/dist/src/api/models.js +26 -18
- package/dist/src/artifact-policy.js +12 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +60 -0
- package/dist/src/client-environment.js +129 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +75 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +24 -5
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +17 -2
- package/dist/src/scanner.js +58 -17
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +64 -31
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +164 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +8 -0
- package/dist/src/tools/patch-file.js +16 -2
- package/dist/src/tools/path-suggest.js +139 -0
- package/dist/src/tools/read-document.js +15 -4
- package/dist/src/tools/read-file.js +23 -7
- package/dist/src/tools/replace-document-text.js +234 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +83 -16
- package/dist/src/tools/run-node-script.js +3 -1
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +16 -2
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +315 -24
- package/dist/src/ui/tui/bridge.js +2 -6
- package/dist/src/ui/tui/build-frame.js +224 -25
- package/dist/src/ui/tui/shell-input.js +42 -5
- package/dist/src/version.js +29 -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 +14 -15
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
const MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
3
5
|
const MIME_BY_EXT = {
|
|
4
6
|
'.png': 'image/png',
|
|
@@ -21,6 +23,14 @@ export function isSupportedImageMimeType(mime) {
|
|
|
21
23
|
}
|
|
22
24
|
function whichSync(cmd) {
|
|
23
25
|
try {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
execFileSync('where.exe', [cmd], {
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
timeout: 2000,
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
24
34
|
execFileSync('which', [cmd], { stdio: 'ignore', timeout: 2000 });
|
|
25
35
|
return true;
|
|
26
36
|
}
|
|
@@ -93,6 +103,52 @@ function readClipboardLinux() {
|
|
|
93
103
|
}
|
|
94
104
|
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
95
105
|
}
|
|
106
|
+
const WINDOWS_CLIPBOARD_IMAGE_PS = [
|
|
107
|
+
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;',
|
|
108
|
+
'$ErrorActionPreference = "Stop";',
|
|
109
|
+
'Add-Type -AssemblyName System.Drawing;',
|
|
110
|
+
'$img = Get-Clipboard -Format Image;',
|
|
111
|
+
'if ($null -eq $img) { exit 2 }',
|
|
112
|
+
'$ms = New-Object System.IO.MemoryStream;',
|
|
113
|
+
'$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png);',
|
|
114
|
+
'[Convert]::ToBase64String($ms.ToArray())',
|
|
115
|
+
].join(' ');
|
|
116
|
+
function readClipboardWindows() {
|
|
117
|
+
try {
|
|
118
|
+
const b64 = execFileSync('powershell.exe', ['-NoProfile', '-Command', WINDOWS_CLIPBOARD_IMAGE_PS], {
|
|
119
|
+
encoding: 'utf-8',
|
|
120
|
+
timeout: 5000,
|
|
121
|
+
maxBuffer: MAX_IMAGE_SIZE_BYTES * 2,
|
|
122
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
}).trim();
|
|
125
|
+
if (!b64) {
|
|
126
|
+
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
127
|
+
}
|
|
128
|
+
const buf = Buffer.from(b64, 'base64');
|
|
129
|
+
if (!buf.length) {
|
|
130
|
+
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
131
|
+
}
|
|
132
|
+
if (buf.length > MAX_IMAGE_SIZE_BYTES) {
|
|
133
|
+
throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
|
|
134
|
+
}
|
|
135
|
+
return { base64Data: b64, mimeType: 'image/png' };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof ClipboardError)
|
|
139
|
+
throw err;
|
|
140
|
+
if (err?.status === 2) {
|
|
141
|
+
throw new ClipboardError('Clipboard contains no image data. Copy an image first (Win+Shift+S), then press Alt+V.', 'NO_IMAGE');
|
|
142
|
+
}
|
|
143
|
+
if (isMaxBufferError(err)) {
|
|
144
|
+
throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
|
|
145
|
+
}
|
|
146
|
+
const detail = [err?.message, err?.stderr?.toString?.()?.trim()]
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join(' — ');
|
|
149
|
+
throw new ClipboardError(`Failed to read clipboard image on Windows: ${detail || 'unknown error'}`, 'READ_FAILED');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
96
152
|
function readClipboardDarwin() {
|
|
97
153
|
if (whichSync('pngpaste')) {
|
|
98
154
|
try {
|
|
@@ -123,6 +179,8 @@ export function readClipboardImage(platform = process.platform) {
|
|
|
123
179
|
return readClipboardLinux();
|
|
124
180
|
case 'darwin':
|
|
125
181
|
return readClipboardDarwin();
|
|
182
|
+
case 'win32':
|
|
183
|
+
return readClipboardWindows();
|
|
126
184
|
default:
|
|
127
185
|
throw new ClipboardError(`Clipboard image paste is not supported on ${platform}.`, 'NO_TOOL');
|
|
128
186
|
}
|
|
@@ -206,3 +264,20 @@ export function writeClipboardText(text, platform = process.platform) {
|
|
|
206
264
|
}
|
|
207
265
|
throw new ClipboardError(`Clipboard text copy is not supported on ${platform}.`, 'NO_TOOL');
|
|
208
266
|
}
|
|
267
|
+
export function loadImageFromFile(filePath) {
|
|
268
|
+
const resolved = path.resolve(filePath);
|
|
269
|
+
if (!existsSync(resolved)) {
|
|
270
|
+
throw new ClipboardError(`Image file not found: ${resolved}`, 'READ_FAILED');
|
|
271
|
+
}
|
|
272
|
+
const stat = statSync(resolved);
|
|
273
|
+
if (stat.size > MAX_IMAGE_SIZE_BYTES) {
|
|
274
|
+
throw new ClipboardError(`Image file exceeds 10MB limit (${(stat.size / 1024 / 1024).toFixed(1)}MB): ${resolved}`, 'READ_FAILED');
|
|
275
|
+
}
|
|
276
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
277
|
+
const mimeType = MIME_BY_EXT[ext];
|
|
278
|
+
if (!mimeType) {
|
|
279
|
+
throw new ClipboardError(`Unsupported image format "${ext}". Supported: PNG, JPEG, GIF, WebP.`, 'READ_FAILED');
|
|
280
|
+
}
|
|
281
|
+
const buf = readFileSync(resolved);
|
|
282
|
+
return { base64Data: buf.toString('base64'), mimeType };
|
|
283
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { loadImageFromFile } from './clipboard.js';
|
|
5
|
+
const EXT = '(?:png|jpe?g|gif|webp)';
|
|
6
|
+
const BARE_CHAR = "[^\\s\"'<>,:;!?()\\[\\]{}]";
|
|
7
|
+
const BARE_PATH = `(?:[A-Za-z]:[\\\\/])?(?:\\\\ |${BARE_CHAR})+\\.${EXT}`;
|
|
8
|
+
const IMAGE_PATH_PATTERN = new RegExp(`"([^"]*\\.${EXT})"` +
|
|
9
|
+
`|'([^']*\\.${EXT})'` +
|
|
10
|
+
`|file://(\\S*\\.${EXT})` +
|
|
11
|
+
`|(${BARE_PATH})`, 'gi');
|
|
12
|
+
const EXTENSION_TOKEN = new RegExp(`^(?:[A-Za-z]:[\\\\/])?(?:\\\\ |${BARE_CHAR})+$`);
|
|
13
|
+
const MAX_EXTENSION_TOKENS = 8;
|
|
14
|
+
function isFile(p) {
|
|
15
|
+
try {
|
|
16
|
+
return statSync(p, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function extendBareMatchAcrossSpaces(input, matchStart, raw, cwd) {
|
|
23
|
+
const baseInner = raw.replace(/\\ /g, ' ');
|
|
24
|
+
const baseExists = isFile(path.isAbsolute(baseInner) ? baseInner : path.resolve(cwd, baseInner));
|
|
25
|
+
let best = null;
|
|
26
|
+
let candidate = raw;
|
|
27
|
+
let start = matchStart;
|
|
28
|
+
let addedSeparator = false;
|
|
29
|
+
for (let hops = 0; hops < MAX_EXTENSION_TOKENS; hops++) {
|
|
30
|
+
if (start < 2 || input[start - 1] !== ' ' || /\s/.test(input[start - 2]))
|
|
31
|
+
break;
|
|
32
|
+
let i = start - 2;
|
|
33
|
+
while (i >= 0 && !/\s/.test(input[i]))
|
|
34
|
+
i--;
|
|
35
|
+
const tokenStart = i + 1;
|
|
36
|
+
const token = input.slice(tokenStart, start - 1);
|
|
37
|
+
if (!EXTENSION_TOKEN.test(token))
|
|
38
|
+
break;
|
|
39
|
+
candidate = `${token} ${candidate}`;
|
|
40
|
+
start = tokenStart;
|
|
41
|
+
addedSeparator = addedSeparator || /[\\/]/.test(token);
|
|
42
|
+
if (baseExists && !addedSeparator)
|
|
43
|
+
continue;
|
|
44
|
+
const inner = candidate.replace(/\\ /g, ' ');
|
|
45
|
+
const resolved = path.isAbsolute(inner) ? inner : path.resolve(cwd, inner);
|
|
46
|
+
if (isFile(resolved))
|
|
47
|
+
best = candidate;
|
|
48
|
+
}
|
|
49
|
+
return best ?? raw;
|
|
50
|
+
}
|
|
51
|
+
function detectImagePaths(input, cwd) {
|
|
52
|
+
const regex = new RegExp(IMAGE_PATH_PATTERN.source, IMAGE_PATH_PATTERN.flags);
|
|
53
|
+
const detected = [];
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = regex.exec(input)) !== null) {
|
|
56
|
+
let raw = match[0];
|
|
57
|
+
if (match[4] != null && raw.includes('://'))
|
|
58
|
+
continue;
|
|
59
|
+
let inner;
|
|
60
|
+
if (match[1] != null || match[2] != null) {
|
|
61
|
+
const quoted = (match[1] ?? match[2]);
|
|
62
|
+
if (/^file:\/\//i.test(quoted)) {
|
|
63
|
+
try {
|
|
64
|
+
inner = fileURLToPath(quoted);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
inner = quoted;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (match[3] != null) {
|
|
75
|
+
try {
|
|
76
|
+
inner = fileURLToPath(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
try {
|
|
80
|
+
inner = decodeURIComponent(match[3]);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
raw = extendBareMatchAcrossSpaces(input, match.index, raw, cwd);
|
|
89
|
+
inner = raw.replace(/\\ /g, ' ');
|
|
90
|
+
}
|
|
91
|
+
const resolvedPath = path.isAbsolute(inner) ? inner : path.resolve(cwd, inner);
|
|
92
|
+
const start = match.index - (raw.length - match[0].length);
|
|
93
|
+
while (detected.length > 0) {
|
|
94
|
+
const prev = detected[detected.length - 1];
|
|
95
|
+
if (prev.start + prev.raw.length <= start)
|
|
96
|
+
break;
|
|
97
|
+
detected.pop();
|
|
98
|
+
}
|
|
99
|
+
detected.push({ resolvedPath, raw, start });
|
|
100
|
+
}
|
|
101
|
+
const rawsByPath = new Map();
|
|
102
|
+
for (const { resolvedPath, raw } of detected) {
|
|
103
|
+
const existing = rawsByPath.get(resolvedPath);
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.push(raw);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
rawsByPath.set(resolvedPath, [raw]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return rawsByPath;
|
|
112
|
+
}
|
|
113
|
+
export function autoAttachImages(input, cwd, existing = []) {
|
|
114
|
+
const max = 2;
|
|
115
|
+
const rawsByPath = detectImagePaths(input, cwd);
|
|
116
|
+
let sanitizedInput = input;
|
|
117
|
+
const attachments = [];
|
|
118
|
+
const errors = [];
|
|
119
|
+
const maxExistingIndex = existing.reduce((highest, a) => Math.max(highest, a.index ?? 0), 0);
|
|
120
|
+
for (const [resolvedPath, rawForms] of rawsByPath) {
|
|
121
|
+
if (existing.length + attachments.length >= max)
|
|
122
|
+
break;
|
|
123
|
+
if (!existsSync(resolvedPath))
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
const loaded = loadImageFromFile(resolvedPath);
|
|
127
|
+
const idx = maxExistingIndex + attachments.length + 1;
|
|
128
|
+
attachments.push({
|
|
129
|
+
index: idx,
|
|
130
|
+
mimeType: loaded.mimeType,
|
|
131
|
+
base64Data: loaded.base64Data,
|
|
132
|
+
source: 'file',
|
|
133
|
+
filePath: resolvedPath,
|
|
134
|
+
});
|
|
135
|
+
for (const raw of rawForms) {
|
|
136
|
+
sanitizedInput = sanitizedInput.replace(raw, `[Image #${idx}]`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
errors.push(err.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { sanitizedInput, attachments, errors };
|
|
144
|
+
}
|
package/dist/src/edit-journal.js
CHANGED
|
@@ -1,33 +1,64 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { BINARY_ARTIFACT_EXTENSIONS } from './artifact-policy.js';
|
|
4
6
|
import { resolveProjectPath } from './patcher.js';
|
|
5
7
|
export const MAX_EDIT_JOURNAL_RECORDS = 50;
|
|
6
|
-
const MAX_STORED_CONTENT_CHARS =
|
|
7
|
-
export function
|
|
8
|
+
const MAX_STORED_CONTENT_CHARS = 8_000_000;
|
|
9
|
+
export function hashBytes(content) {
|
|
8
10
|
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
9
11
|
}
|
|
12
|
+
export function hashContent(content) {
|
|
13
|
+
return hashBytes(Buffer.from(content, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
export function hashStoredContent(content, encoding = 'utf8') {
|
|
16
|
+
return encoding === 'base64'
|
|
17
|
+
? hashBytes(Buffer.from(content, 'base64'))
|
|
18
|
+
: hashContent(content);
|
|
19
|
+
}
|
|
20
|
+
export function storedContentBuffer(content, encoding = 'utf8') {
|
|
21
|
+
return encoding === 'base64'
|
|
22
|
+
? Buffer.from(content, 'base64')
|
|
23
|
+
: Buffer.from(content, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
function snapshotEncoding(filePath, content) {
|
|
26
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
27
|
+
if (BINARY_ARTIFACT_EXTENSIONS.has(ext))
|
|
28
|
+
return 'base64';
|
|
29
|
+
if (content.includes(0))
|
|
30
|
+
return 'base64';
|
|
31
|
+
return 'utf8';
|
|
32
|
+
}
|
|
10
33
|
export function readFileEditSnapshot(rootDir, filePath) {
|
|
11
34
|
try {
|
|
12
35
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
13
36
|
if (!existsSync(absPath)) {
|
|
14
|
-
return { exists: false, content: null, hash: null };
|
|
37
|
+
return { exists: false, content: null, contentEncoding: 'utf8', hash: null };
|
|
15
38
|
}
|
|
16
39
|
if (lstatSync(absPath).isSymbolicLink()) {
|
|
17
40
|
return {
|
|
18
41
|
exists: true,
|
|
19
42
|
content: null,
|
|
43
|
+
contentEncoding: 'utf8',
|
|
20
44
|
hash: null,
|
|
21
45
|
error: `Refusing to snapshot symbolic link: ${filePath}`,
|
|
22
46
|
};
|
|
23
47
|
}
|
|
24
|
-
const content = readFileSync(absPath
|
|
25
|
-
|
|
48
|
+
const content = readFileSync(absPath);
|
|
49
|
+
const encoding = snapshotEncoding(filePath, content);
|
|
50
|
+
return {
|
|
51
|
+
exists: true,
|
|
52
|
+
content: encoding === 'base64' ? content.toString('base64') : content.toString('utf8'),
|
|
53
|
+
contentEncoding: encoding,
|
|
54
|
+
hash: hashBytes(content),
|
|
55
|
+
};
|
|
26
56
|
}
|
|
27
57
|
catch (err) {
|
|
28
58
|
return {
|
|
29
59
|
exists: false,
|
|
30
60
|
content: null,
|
|
61
|
+
contentEncoding: 'utf8',
|
|
31
62
|
hash: null,
|
|
32
63
|
error: err?.message ? String(err.message) : String(err),
|
|
33
64
|
};
|
|
@@ -46,7 +77,8 @@ export function isEditToolName(toolName) {
|
|
|
46
77
|
return (toolName === 'write_file' ||
|
|
47
78
|
toolName === 'patch_file' ||
|
|
48
79
|
toolName === 'str_replace' ||
|
|
49
|
-
toolName === 'delete_file'
|
|
80
|
+
toolName === 'delete_file' ||
|
|
81
|
+
toolName === 'replace_document_text');
|
|
50
82
|
}
|
|
51
83
|
export function operationFromSnapshots(before, after) {
|
|
52
84
|
if (before.hash === after.hash)
|
|
@@ -102,6 +134,7 @@ export function normalizeAssistantEditJournal(value) {
|
|
|
102
134
|
? entry.afterHash
|
|
103
135
|
: null,
|
|
104
136
|
beforeContent: typeof entry.beforeContent === 'string' ? entry.beforeContent : null,
|
|
137
|
+
beforeContentEncoding: entry.beforeContentEncoding === 'base64' ? 'base64' : 'utf8',
|
|
105
138
|
createdAt: typeof entry.createdAt === 'string' && entry.createdAt
|
|
106
139
|
? entry.createdAt
|
|
107
140
|
: new Date().toISOString(),
|
package/dist/src/executor.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
|
|
1
|
+
import chalk from './colors.js';
|
|
3
2
|
import { execFileSync, spawn } from 'child_process';
|
|
4
3
|
import { existsSync, statSync } from 'fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { ARTIFACT_INSPECT_BLOCK_DIRS, getBlockedArtifactInspectDir, relativeProjectPath, } from './artifact-policy.js';
|
|
8
8
|
import { emitCommandOutput, isTuiMode } from './runtime-mode.js';
|
|
9
|
+
import { ensureSessionScratchDir, isInsideTheGitAiScratch, sessionScratchDir, } from './scratch-dir.js';
|
|
10
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
11
|
+
let nodePtyCache;
|
|
12
|
+
function loadNodePty() {
|
|
13
|
+
if (nodePtyCache !== undefined)
|
|
14
|
+
return nodePtyCache;
|
|
15
|
+
try {
|
|
16
|
+
nodePtyCache = requireFromHere('@lydell/node-pty');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
nodePtyCache = null;
|
|
20
|
+
}
|
|
21
|
+
return nodePtyCache;
|
|
22
|
+
}
|
|
9
23
|
const COMMON_TOOLCHAIN_BIN_DIRS = ['/usr/local/go/bin'];
|
|
10
24
|
function detectVenvBin(dir) {
|
|
11
25
|
for (const name of ['.venv', 'venv', 'env']) {
|
|
@@ -204,6 +218,7 @@ function isExistingDirectory(absPath) {
|
|
|
204
218
|
return false;
|
|
205
219
|
}
|
|
206
220
|
}
|
|
221
|
+
const OS_TEMP_BLOCK_MARKER = 'os-temp:';
|
|
207
222
|
function getBlockedOsTempInspection(rawToken, rootDir) {
|
|
208
223
|
const token = normalizeToken(rawToken);
|
|
209
224
|
if (!token || !path.isAbsolute(token))
|
|
@@ -211,6 +226,8 @@ function getBlockedOsTempInspection(rawToken, rootDir) {
|
|
|
211
226
|
const resolved = path.resolve(token);
|
|
212
227
|
if (!isInsideOsTemp(resolved))
|
|
213
228
|
return null;
|
|
229
|
+
if (isInsideTheGitAiScratch(resolved))
|
|
230
|
+
return null;
|
|
214
231
|
if (rootDir) {
|
|
215
232
|
const resolvedRoot = path.resolve(rootDir);
|
|
216
233
|
if (resolved === resolvedRoot ||
|
|
@@ -221,7 +238,7 @@ function getBlockedOsTempInspection(rawToken, rootDir) {
|
|
|
221
238
|
if (resolved === path.resolve(os.tmpdir()) ||
|
|
222
239
|
hasPathGlob(token) ||
|
|
223
240
|
isExistingDirectory(resolved)) {
|
|
224
|
-
return path.basename(os.tmpdir()) || os.tmpdir()
|
|
241
|
+
return `${OS_TEMP_BLOCK_MARKER}${path.basename(os.tmpdir()) || os.tmpdir()}`;
|
|
225
242
|
}
|
|
226
243
|
return null;
|
|
227
244
|
}
|
|
@@ -237,6 +254,9 @@ function findBlockedDirForToken(rawToken, rootDir, baseDir, allowBareDirMatch =
|
|
|
237
254
|
const resolved = path.isAbsolute(token)
|
|
238
255
|
? path.resolve(token)
|
|
239
256
|
: path.resolve(baseDir ?? rootDir, token);
|
|
257
|
+
const osTempResolved = getBlockedOsTempInspection(resolved, rootDir);
|
|
258
|
+
if (osTempResolved)
|
|
259
|
+
return osTempResolved;
|
|
240
260
|
return getBlockedProjectPathDir(resolved, rootDir);
|
|
241
261
|
}
|
|
242
262
|
if (!allowBareDirMatch || !BLOCKED_PATH_INSPECT_DIRS.has(token)) {
|
|
@@ -373,11 +393,16 @@ function findBlockedDirInCommandTokens(command, rootDir, baseDir) {
|
|
|
373
393
|
}
|
|
374
394
|
return null;
|
|
375
395
|
}
|
|
396
|
+
function expandTempEnvRefs(command) {
|
|
397
|
+
return command
|
|
398
|
+
.replace(/\$\{THEGITAI_SCRATCH_DIR[^}]*\}|\$THEGITAI_SCRATCH_DIR\b/g, sessionScratchDir())
|
|
399
|
+
.replace(/\$\{TMPDIR[^}]*\}|\$TMPDIR\b/g, process.env.TMPDIR || os.tmpdir());
|
|
400
|
+
}
|
|
376
401
|
function findIgnoredPathInspection(command, rootDir) {
|
|
377
402
|
if (!FILE_INSPECTION_COMMAND_PATTERN.test(command)) {
|
|
378
403
|
return null;
|
|
379
404
|
}
|
|
380
|
-
const haystack = maskNonPathIgnoreDirTokens(maskHereDocumentBodies(command));
|
|
405
|
+
const haystack = maskNonPathIgnoreDirTokens(maskHereDocumentBodies(expandTempEnvRefs(command)));
|
|
381
406
|
const baseDir = getCommandBaseDir(haystack, rootDir);
|
|
382
407
|
const blockedDir = findBlockedDirInCommandTokens(haystack, rootDir, baseDir);
|
|
383
408
|
if (blockedDir) {
|
|
@@ -434,7 +459,7 @@ function shouldDropOutputLine(line, rootDir, baseDir) {
|
|
|
434
459
|
function isLsDirectoryHeader(line) {
|
|
435
460
|
return /^\.?\/?.+:$/.test(line) && !line.includes(' ');
|
|
436
461
|
}
|
|
437
|
-
function sanitizeCommandText(command, text, rootDir) {
|
|
462
|
+
export function sanitizeCommandText(command, text, rootDir) {
|
|
438
463
|
if (!text)
|
|
439
464
|
return '';
|
|
440
465
|
const lines = text.split('\n');
|
|
@@ -509,7 +534,7 @@ export function cancelActiveCommand() {
|
|
|
509
534
|
cancelled: true,
|
|
510
535
|
});
|
|
511
536
|
}
|
|
512
|
-
function terminateChild(child, signal) {
|
|
537
|
+
export function terminateChild(child, signal) {
|
|
513
538
|
if (!child?.pid)
|
|
514
539
|
return;
|
|
515
540
|
if (process.platform === 'win32') {
|
|
@@ -537,10 +562,17 @@ function terminateChild(child, signal) {
|
|
|
537
562
|
}
|
|
538
563
|
}
|
|
539
564
|
export function getBlockedPathInspectDir(command, rootDir) {
|
|
540
|
-
|
|
565
|
+
const blocked = findIgnoredPathInspection(command, rootDir);
|
|
566
|
+
return blocked?.startsWith(OS_TEMP_BLOCK_MARKER)
|
|
567
|
+
? blocked.slice(OS_TEMP_BLOCK_MARKER.length)
|
|
568
|
+
: blocked;
|
|
541
569
|
}
|
|
542
570
|
export function getBlockedCommandReason(command, hasTimeout, rootDir) {
|
|
543
571
|
const ignoredDir = findIgnoredPathInspection(command, rootDir);
|
|
572
|
+
if (ignoredDir?.startsWith(OS_TEMP_BLOCK_MARKER)) {
|
|
573
|
+
const tempName = ignoredDir.slice(OS_TEMP_BLOCK_MARKER.length);
|
|
574
|
+
return `Listing or scanning the shared OS temp directory (${tempName}) is blocked because it can contain other users' and processes' files. Use the session scratch directory ${ensureSessionScratchDir()} for temporary scripts and files — creating, running, and listing are all allowed there — or reference an exact file path.`;
|
|
575
|
+
}
|
|
544
576
|
if (ignoredDir) {
|
|
545
577
|
return `Command inspects an off-limits generated or dependency directory (${ignoredDir}). Avoid that path.`;
|
|
546
578
|
}
|
|
@@ -559,7 +591,7 @@ export function getBlockedCommandReason(command, hasTimeout, rootDir) {
|
|
|
559
591
|
}
|
|
560
592
|
return null;
|
|
561
593
|
}
|
|
562
|
-
function commandUsesSudo(command) {
|
|
594
|
+
export function commandUsesSudo(command) {
|
|
563
595
|
return /\bsudo\b/.test(getUnquotedShellText(command));
|
|
564
596
|
}
|
|
565
597
|
export function sudoPromptFromTail(text) {
|
|
@@ -587,7 +619,7 @@ function redactSecrets(text, secrets) {
|
|
|
587
619
|
}
|
|
588
620
|
return redacted;
|
|
589
621
|
}
|
|
590
|
-
function buildCommandEnv(cwd) {
|
|
622
|
+
export function buildCommandEnv(cwd) {
|
|
591
623
|
const venvBin = detectVenvBin(cwd);
|
|
592
624
|
const envPath = buildCommandPath(process.env.PATH, venvBin ? [venvBin] : []);
|
|
593
625
|
return {
|
|
@@ -602,12 +634,13 @@ function buildCommandEnv(cwd) {
|
|
|
602
634
|
npm_config_fund: 'false',
|
|
603
635
|
npm_config_audit: 'false',
|
|
604
636
|
NUXI_INIT_SKIP_PROMPT: 'true',
|
|
637
|
+
THEGITAI_SCRATCH_DIR: ensureSessionScratchDir(),
|
|
605
638
|
};
|
|
606
639
|
}
|
|
607
640
|
function sanitizePtyOutput(command, output, cwd, secrets) {
|
|
608
641
|
return sanitizeCommandText(command, stripSudoPromptText(redactSecrets(output, secrets)), cwd);
|
|
609
642
|
}
|
|
610
|
-
async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword) {
|
|
643
|
+
async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword, nodePty) {
|
|
611
644
|
return new Promise((resolve) => {
|
|
612
645
|
let output = '';
|
|
613
646
|
let timedOut = false;
|
|
@@ -623,7 +656,7 @@ async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, reques
|
|
|
623
656
|
const args = process.platform === 'win32'
|
|
624
657
|
? ['/d', '/s', '/c', command]
|
|
625
658
|
: ['-lc', command];
|
|
626
|
-
const child =
|
|
659
|
+
const child = nodePty.spawn(shell, args, {
|
|
627
660
|
cols: 120,
|
|
628
661
|
rows: 30,
|
|
629
662
|
cwd,
|
|
@@ -776,7 +809,10 @@ export async function runCommand(command, cwd, { requestSudoPassword, timeout, }
|
|
|
776
809
|
}
|
|
777
810
|
const exploratory = isExploratoryCommand(command);
|
|
778
811
|
if (requestSudoPassword && commandUsesSudo(command)) {
|
|
779
|
-
|
|
812
|
+
const nodePty = loadNodePty();
|
|
813
|
+
if (nodePty) {
|
|
814
|
+
return runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword, nodePty);
|
|
815
|
+
}
|
|
780
816
|
}
|
|
781
817
|
return new Promise((resolve) => {
|
|
782
818
|
let stdout = '';
|
package/dist/src/help-text.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
|
|
3
|
-
// a terminal. The one thing that genuinely differs is the terminal's paste
|
|
4
|
-
// shortcut, so surface the one for the host OS (right-click paste works
|
|
5
|
-
// everywhere regardless).
|
|
1
|
+
import chalk from './colors.js';
|
|
2
|
+
import { getCliVersion, getPlatformTag } from './version.js';
|
|
6
3
|
function pasteShortcutForPlatform() {
|
|
7
4
|
switch (process.platform) {
|
|
8
5
|
case 'darwin':
|
|
@@ -45,6 +42,7 @@ const HELP_MARKDOWN = [
|
|
|
45
42
|
'',
|
|
46
43
|
'- `-y, --yes` — start in Auto-Accept mode',
|
|
47
44
|
'- `-h, --help` — show this help',
|
|
45
|
+
'- `-v, --version` — print the version and exit',
|
|
48
46
|
'',
|
|
49
47
|
'## Modes',
|
|
50
48
|
'',
|
|
@@ -70,10 +68,15 @@ const HELP_MARKDOWN = [
|
|
|
70
68
|
'## Chat commands',
|
|
71
69
|
'',
|
|
72
70
|
'- `/help` — show this help',
|
|
71
|
+
'- `/about` — show version and platform info',
|
|
73
72
|
'- `/usage` — show account usage percentage and reset times',
|
|
74
73
|
'- `/model` — list supported models and pick one',
|
|
75
74
|
'- `/model <id>` — switch the active model without clearing history',
|
|
76
75
|
'- `/resume` — resume a saved session for this repo',
|
|
76
|
+
'- `/jobs` — manage long-running commands like dev servers and watchers:',
|
|
77
|
+
' browse them, press Enter to expand one and read its output, k to stop it',
|
|
78
|
+
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
79
|
+
'- `/jobs kill <id>` — stop one background job',
|
|
77
80
|
'- `/clear` — clear the current conversation history',
|
|
78
81
|
'- `/exit` — quit the session',
|
|
79
82
|
'',
|
|
@@ -86,6 +89,12 @@ const HELP_MARKDOWN = [
|
|
|
86
89
|
' command and keeps the password masked and local.',
|
|
87
90
|
'- `-y` / `--yes` at startup auto-approves every shell command and file',
|
|
88
91
|
' edit for the whole session — use with care.',
|
|
92
|
+
'- Long-running commands (e.g. dev servers) can run as managed background',
|
|
93
|
+
' jobs after the same approval as any other command. Background output',
|
|
94
|
+
' stays quiet once the model has responded; the footer shows only a compact',
|
|
95
|
+
' shell-running indicator, `/jobs` lists, inspects, and kills jobs, and',
|
|
96
|
+
' killed jobs disappear immediately. Every job is killed when the session',
|
|
97
|
+
' ends.',
|
|
89
98
|
'- File and shell operations are confined to the target repo root.',
|
|
90
99
|
'- Sensitive directories (`.git`, `node_modules`, build output) are',
|
|
91
100
|
' never indexed.',
|
|
@@ -100,6 +109,16 @@ const HELP_MARKDOWN = [
|
|
|
100
109
|
'- For anything else, re-run the command and report the printed error',
|
|
101
110
|
' message — there is no client-side debug mode by design.',
|
|
102
111
|
].join('\n');
|
|
112
|
+
export function formatAboutCard() {
|
|
113
|
+
return [
|
|
114
|
+
'```',
|
|
115
|
+
'TheGitAI',
|
|
116
|
+
` Version ${getCliVersion()}`,
|
|
117
|
+
` Platform ${getPlatformTag()}`,
|
|
118
|
+
` Node ${process.version}`,
|
|
119
|
+
'```',
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
103
122
|
export function formatHelpMarkdown() {
|
|
104
123
|
return HELP_MARKDOWN;
|
|
105
124
|
}
|
package/dist/src/patcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from './colors.js';
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
@@ -162,13 +162,28 @@ export function writeProjectFile(rootDir, filePath, content) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
165
|
-
// If we can't read it for some reason, proceed with write
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
mkdirSync(path.dirname(absPath), { recursive: true });
|
|
169
168
|
writeFileSync(absPath, content, 'utf-8');
|
|
170
169
|
return { absPath, changed: true };
|
|
171
170
|
}
|
|
171
|
+
export function writeProjectFileBuffer(rootDir, filePath, content) {
|
|
172
|
+
const absPath = resolveProjectPath(rootDir, filePath);
|
|
173
|
+
if (existsSync(absPath)) {
|
|
174
|
+
try {
|
|
175
|
+
const existingContent = readFileSync(absPath);
|
|
176
|
+
if (existingContent.equals(content)) {
|
|
177
|
+
return { absPath, changed: false };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
mkdirSync(path.dirname(absPath), { recursive: true });
|
|
184
|
+
writeFileSync(absPath, content);
|
|
185
|
+
return { absPath, changed: true };
|
|
186
|
+
}
|
|
172
187
|
export function deleteProjectFile(rootDir, filePath) {
|
|
173
188
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
174
189
|
if (!existsSync(absPath)) {
|