@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.
- 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 +628 -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-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
package/dist/src/tools/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { browserBatch, browserComputer, browserConsole, browserDownloads, browserEvaluate, browserFileUpload, browserFind, browserFormInput, browserGateway, browserGetPageText, browserNavigate, browserNetwork, browserReadPage, browserResize, browserTabClose, browserTabNew, browserTabs, } from './browser.js';
|
|
1
2
|
import { shellJobOutput } from './shell-job-output.js';
|
|
2
3
|
import { deleteFile } from './delete-file.js';
|
|
3
|
-
import { findSymbol } from './find-symbol.js';
|
|
4
4
|
import { getDiagnostics } from './get-diagnostics.js';
|
|
5
5
|
import { grepCode } from './grep-code.js';
|
|
6
|
-
import { hoverSymbol } from './hover-symbol.js';
|
|
7
6
|
import { listDirectories } from './list-directories.js';
|
|
8
7
|
import { listCheckpoints } from './list-checkpoints.js';
|
|
9
8
|
import { listFiles } from './list-files.js';
|
|
10
9
|
import { listSessionEdits } from './list-session-edits.js';
|
|
11
|
-
import { listSymbols } from './list-symbols.js';
|
|
12
10
|
import { patchFile } from './patch-file.js';
|
|
13
11
|
import { readDocument } from './read-document.js';
|
|
14
12
|
import { readFile } from './read-file.js';
|
|
@@ -16,25 +14,20 @@ import { replaceDocumentText } from './replace-document-text.js';
|
|
|
16
14
|
import { runShellCommand } from './run-command.js';
|
|
17
15
|
import { runNodeScript } from './run-node-script.js';
|
|
18
16
|
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
19
|
-
import { searchCode } from './search-code.js';
|
|
20
|
-
import { getSignatureHelp } from './signature-help.js';
|
|
21
17
|
import { shellJobKill } from './shell-job-kill.js';
|
|
22
18
|
import { strReplace } from './str-replace.js';
|
|
23
19
|
import { undoEdit } from './undo-edit.js';
|
|
24
20
|
import { updateTodos } from './update-todos.js';
|
|
21
|
+
import { readImageFile } from './read-image-file.js';
|
|
22
|
+
import { saveGeneratedImage } from './save-generated-image.js';
|
|
25
23
|
import { writeFile } from './write-file.js';
|
|
26
24
|
export const TOOL_MAP = {
|
|
27
|
-
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
28
25
|
list_files: (context, args) => listFiles(context, args),
|
|
29
26
|
list_directories: (context, args) => listDirectories(context, args),
|
|
30
27
|
read_file: (context, args) => readFile(context, args),
|
|
31
28
|
read_document: (context, args) => readDocument(context.rootDir, args, context.env),
|
|
32
29
|
replace_document_text: replaceDocumentText,
|
|
33
30
|
grep_code: (context, args) => grepCode(context.rootDir, args),
|
|
34
|
-
find_symbol: (context, args) => findSymbol(context, args),
|
|
35
|
-
list_symbols: (context, args) => listSymbols(context, args),
|
|
36
|
-
hover_symbol: (context, args) => hoverSymbol(context, args),
|
|
37
|
-
signature_help: (context, args) => getSignatureHelp(context, args),
|
|
38
31
|
get_diagnostics: (context, args) => getDiagnostics(context, args),
|
|
39
32
|
list_checkpoints: (context) => listCheckpoints(context),
|
|
40
33
|
list_session_edits: (context) => listSessionEdits(context),
|
|
@@ -50,6 +43,35 @@ export const TOOL_MAP = {
|
|
|
50
43
|
shell_job_output: shellJobOutput,
|
|
51
44
|
shell_job_kill: shellJobKill,
|
|
52
45
|
update_todos: updateTodos,
|
|
46
|
+
analyze_image: (context, args) => readImageFile(context, args),
|
|
47
|
+
browser: (context) => browserGateway(context),
|
|
48
|
+
browser_tabs: browserTabs,
|
|
49
|
+
browser_tab_new: (context) => browserTabNew(context),
|
|
50
|
+
browser_tab_close: browserTabClose,
|
|
51
|
+
browser_navigate: browserNavigate,
|
|
52
|
+
browser_read_page: browserReadPage,
|
|
53
|
+
browser_get_page_text: browserGetPageText,
|
|
54
|
+
browser_find: browserFind,
|
|
55
|
+
browser_computer: browserComputer,
|
|
56
|
+
browser_screenshot: ((context, args) => browserComputer(context, { ...args, action: 'screenshot' })),
|
|
57
|
+
browser_form_input: browserFormInput,
|
|
58
|
+
browser_evaluate: browserEvaluate,
|
|
59
|
+
browser_console: browserConsole,
|
|
60
|
+
browser_network: browserNetwork,
|
|
61
|
+
browser_resize: browserResize,
|
|
62
|
+
browser_file_upload: browserFileUpload,
|
|
63
|
+
browser_downloads: browserDownloads,
|
|
64
|
+
browser_batch: browserBatch,
|
|
65
|
+
generate_image: (_context, args) => {
|
|
66
|
+
if (!String(args.base64Data ?? '').trim()) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: 'This CLI build cannot generate images without server-supplied image bytes. Update TheGitAI CLI.',
|
|
70
|
+
failureCategory: 'tool_exception',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return saveGeneratedImage(args);
|
|
74
|
+
},
|
|
53
75
|
};
|
|
54
76
|
function invalidToolCall(error) {
|
|
55
77
|
return {
|
|
@@ -2,14 +2,14 @@ import chalk from '../colors.js';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { applyUnifiedPatch, classifyProjectPath, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
|
|
5
|
-
import { upsertIndexFile } from '../project-index.js';
|
|
6
5
|
import { repairFilePath } from './path-suggest.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
|
export async function patchFile(context, args) {
|
|
12
|
-
const { rootDir
|
|
12
|
+
const { rootDir } = context;
|
|
13
13
|
const filePath = repairFilePath(rootDir, String(args.filePath ?? '').trim());
|
|
14
14
|
let patch = typeof args.patch === 'string' ? args.patch : '';
|
|
15
15
|
if (!filePath) {
|
|
@@ -74,33 +74,21 @@ export async function patchFile(context, args) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
renderDiffPreview(filePath, patch);
|
|
77
|
-
if (!
|
|
78
|
-
const
|
|
79
|
-
|
|
77
|
+
if (!scratchPath) {
|
|
78
|
+
const denied = await ensurePermission(context, {
|
|
79
|
+
bucket: getCurrentFileHash(rootDir, filePath) === null ? 'create' : 'edit',
|
|
80
|
+
title: 'Approve patch?',
|
|
81
|
+
body: 'Review changes before applying.',
|
|
82
|
+
filePath,
|
|
83
|
+
diff: patch,
|
|
84
|
+
}, 'patch_file', { filePath });
|
|
85
|
+
if (denied) {
|
|
80
86
|
if (!isTuiMode())
|
|
81
87
|
console.log(chalk.dim(` ⏭ Patch skipped: ${filePath}`));
|
|
82
|
-
return
|
|
83
|
-
ok: false,
|
|
84
|
-
skipped: true,
|
|
85
|
-
filePath,
|
|
86
|
-
failureCategory: 'user_declined',
|
|
87
|
-
failureDetails: {
|
|
88
|
-
category: 'user_declined',
|
|
89
|
-
tool: 'patch_file',
|
|
90
|
-
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.',
|
|
91
|
-
},
|
|
92
|
-
error: 'The real user rejected this proposed patch. Nothing was changed; this was not a tool failure or an automated system skip.',
|
|
93
|
-
};
|
|
88
|
+
return denied;
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
91
|
const { changed } = writeProjectFile(rootDir, filePath, patchedContent);
|
|
97
|
-
let indexedChunks = 0;
|
|
98
|
-
let retrievalTokensUsed = 0;
|
|
99
|
-
if (changed && !scratchPath) {
|
|
100
|
-
const indexResult = await upsertIndexFile(projectIndex, filePath);
|
|
101
|
-
indexedChunks = indexResult.indexedChunks;
|
|
102
|
-
retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
|
|
103
|
-
}
|
|
104
92
|
let diagnostics;
|
|
105
93
|
if (!scratchPath) {
|
|
106
94
|
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
@@ -119,8 +107,6 @@ export async function patchFile(context, args) {
|
|
|
119
107
|
changed,
|
|
120
108
|
operation: 'patch',
|
|
121
109
|
...(scratchPath ? { scratch: true } : {}),
|
|
122
|
-
indexedChunks,
|
|
123
|
-
retrievalTokensUsed,
|
|
124
110
|
bytesWritten: Buffer.byteLength(patchedContent, 'utf-8'),
|
|
125
111
|
diagnostics,
|
|
126
112
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
3
|
+
import { SessionImageError, getImageStoreSession, isSessionStorePath, readSessionImage, readSessionImageByIndex, storeSessionImageFromPath, } from '../core/session-image-store.js';
|
|
4
|
+
export async function readImageFile(context, args) {
|
|
5
|
+
const rawPath = String(args.path ?? args.filePath ?? args.file_path ?? '').trim();
|
|
6
|
+
if (!rawPath) {
|
|
7
|
+
const requested = Number(args.imageIndex ?? args.image_index ?? args.index);
|
|
8
|
+
const stored = Number.isInteger(requested)
|
|
9
|
+
? readSessionImageByIndex(requested)
|
|
10
|
+
: null;
|
|
11
|
+
if (!stored) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: Number.isInteger(requested)
|
|
15
|
+
? `Image #${requested} is not in this session's image store.`
|
|
16
|
+
: 'path is required and must name an image file on this machine.',
|
|
17
|
+
failureCategory: Number.isInteger(requested)
|
|
18
|
+
? 'not_found'
|
|
19
|
+
: 'missing_required_argument',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
imageBytes: {
|
|
25
|
+
base64Data: stored.base64Data,
|
|
26
|
+
mimeType: stored.mimeType,
|
|
27
|
+
cachePath: stored.cachePath,
|
|
28
|
+
index: stored.index,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let resolved;
|
|
33
|
+
if (path.isAbsolute(rawPath)) {
|
|
34
|
+
resolved = rawPath;
|
|
35
|
+
}
|
|
36
|
+
else if (normalizeProjectRelativePath(context.rootDir, rawPath)) {
|
|
37
|
+
resolved = path.resolve(context.rootDir, rawPath);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: `Refusing to access path outside the project root: ${rawPath}`,
|
|
43
|
+
failureCategory: 'permission_denied',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const stored = isSessionStorePath(resolved)
|
|
48
|
+
? readSessionImage(resolved)
|
|
49
|
+
: storeSessionImageFromPath({
|
|
50
|
+
sessionId: getImageStoreSession() ?? 'unbound',
|
|
51
|
+
sourcePath: resolved,
|
|
52
|
+
});
|
|
53
|
+
if (!stored) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `Not a readable image: ${resolved}`,
|
|
57
|
+
failureCategory: 'not_found',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
imageBytes: {
|
|
63
|
+
base64Data: stored.base64Data,
|
|
64
|
+
mimeType: stored.mimeType,
|
|
65
|
+
filePath: resolved,
|
|
66
|
+
cachePath: stored.cachePath,
|
|
67
|
+
index: stored.index,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof SessionImageError) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: err.message,
|
|
76
|
+
failureCategory: err.code === 'NOT_FOUND' ? 'not_found' : 'invalid_argument',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `Could not read image: ${err?.message ?? err}`,
|
|
82
|
+
failureCategory: 'tool_exception',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -6,6 +6,8 @@ import { readCliAuthConfig } from '../api/auth.js';
|
|
|
6
6
|
import { resolveProjectPath, writeProjectFileBuffer } from '../patcher.js';
|
|
7
7
|
import { repairFilePath, suggestClosestPath } from './path-suggest.js';
|
|
8
8
|
import { isTuiMode } from '../runtime-mode.js';
|
|
9
|
+
import { ensurePermission } from '../permissions.js';
|
|
10
|
+
import { getCurrentFileHash } from '../session-safety.js';
|
|
9
11
|
function normalizeReplacements(value) {
|
|
10
12
|
if (!Array.isArray(value))
|
|
11
13
|
return [];
|
|
@@ -178,27 +180,35 @@ export async function replaceDocumentText(context, args) {
|
|
|
178
180
|
}
|
|
179
181
|
const preview = String(serverResult.preview ?? '');
|
|
180
182
|
renderPreview(targetPath, preview);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
category: 'user_declined',
|
|
194
|
-
tool: 'replace_document_text',
|
|
195
|
-
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.',
|
|
196
|
-
},
|
|
197
|
-
error: 'The real user rejected this proposed document edit. Nothing was changed; this was not a tool failure or an automated system skip.',
|
|
198
|
-
};
|
|
183
|
+
const targetHashBeforeApproval = getCurrentFileHash(context.rootDir, targetPath);
|
|
184
|
+
const documentIsNew = targetHashBeforeApproval === null;
|
|
185
|
+
const denied = await ensurePermission(context, {
|
|
186
|
+
bucket: documentIsNew ? 'create' : 'edit',
|
|
187
|
+
title: documentIsNew ? 'Approve new document?' : 'Approve document edit?',
|
|
188
|
+
body: 'Review changes before applying.',
|
|
189
|
+
filePath: targetPath,
|
|
190
|
+
diff: preview,
|
|
191
|
+
}, 'replace_document_text', { filePath: targetPath });
|
|
192
|
+
if (denied) {
|
|
193
|
+
if (!isTuiMode()) {
|
|
194
|
+
console.log(chalk.dim(` replace_document_text skipped: ${targetPath}`));
|
|
199
195
|
}
|
|
196
|
+
return denied;
|
|
200
197
|
}
|
|
201
198
|
const fileData = String(serverResult.fileData ?? '');
|
|
199
|
+
if (getCurrentFileHash(context.rootDir, targetPath) !== targetHashBeforeApproval) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
filePath: targetPath,
|
|
203
|
+
failureCategory: 'conflict',
|
|
204
|
+
error: `replace_document_text refused: ${targetPath} changed on disk while the approval prompt was open.`,
|
|
205
|
+
failureDetails: {
|
|
206
|
+
category: 'conflict',
|
|
207
|
+
tool: 'replace_document_text',
|
|
208
|
+
action: 'Re-read the document to see its current contents, then rebuild the replacements against it and retry.',
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
202
212
|
const nextData = Buffer.from(fileData, 'base64');
|
|
203
213
|
const write = writeProjectFileBuffer(context.rootDir, targetPath, nextData);
|
|
204
214
|
const failedCount = Number(serverResult.failedCount ?? 0);
|
|
@@ -54,7 +54,6 @@ export async function restoreToCheckpoint(context, args) {
|
|
|
54
54
|
const result = await restoreCheckpointFiles({
|
|
55
55
|
state: context.safety,
|
|
56
56
|
rootDir: context.rootDir,
|
|
57
|
-
projectIndex: context.projectIndex,
|
|
58
57
|
checkpointId,
|
|
59
58
|
filePaths,
|
|
60
59
|
currentTurnId: context.currentTurnId ?? null,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
2
|
import { startBackgroundJob } from '../background-jobs.js';
|
|
3
3
|
import { getBlockedCommandReason, runCommand, } from '../executor.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensurePermission } from '../permissions.js';
|
|
5
5
|
import { isTuiMode } from '../runtime-mode.js';
|
|
6
6
|
import { redactConnectionStringCredentials } from '../secret-preview.js';
|
|
7
7
|
import { buildNestedGitHint } from '../session-safety.js';
|
|
8
8
|
import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
|
|
9
9
|
const MAX_OUTPUT_CHARS = 4000;
|
|
10
10
|
export async function runShellCommand(context, args) {
|
|
11
|
-
const { rootDir,
|
|
11
|
+
const { rootDir, requestSudoPassword, onStatus, } = context;
|
|
12
12
|
const command = String(args.command ?? '').trim();
|
|
13
13
|
if (!command) {
|
|
14
14
|
return { ok: false, error: 'command is required' };
|
|
@@ -31,33 +31,18 @@ export async function runShellCommand(context, args) {
|
|
|
31
31
|
}
|
|
32
32
|
if (!isTuiMode())
|
|
33
33
|
console.log(chalk.bold.yellow(`\n ⚡ Command: ${command}`));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
command,
|
|
39
|
-
error: 'confirmCommand is required when autoYes is false',
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
const approved = await confirmCommand(runInBackground
|
|
34
|
+
const denied = await ensurePermission(context, {
|
|
35
|
+
bucket: 'run',
|
|
36
|
+
title: 'Approve command?',
|
|
37
|
+
body: runInBackground
|
|
43
38
|
? `${command}\n\nRuns as a managed background job until it exits or is killed.`
|
|
44
|
-
: command
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
command,
|
|
52
|
-
failureCategory: 'user_declined',
|
|
53
|
-
failureDetails: {
|
|
54
|
-
category: 'user_declined',
|
|
55
|
-
tool: 'run_command',
|
|
56
|
-
action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
|
|
57
|
-
},
|
|
58
|
-
error: 'The real user rejected this proposed command. Nothing was executed; this was not a tool failure or an automated system skip.',
|
|
59
|
-
};
|
|
60
|
-
}
|
|
39
|
+
: command,
|
|
40
|
+
command,
|
|
41
|
+
}, 'run_command', { command });
|
|
42
|
+
if (denied) {
|
|
43
|
+
if (!isTuiMode())
|
|
44
|
+
console.log(chalk.dim(` ⏭ Skipped: ${command}`));
|
|
45
|
+
return denied;
|
|
61
46
|
}
|
|
62
47
|
if (runInBackground) {
|
|
63
48
|
return runBackgroundCommand(context, command, args.timeout_ms, repoHint);
|
|
@@ -66,12 +51,8 @@ export async function runShellCommand(context, args) {
|
|
|
66
51
|
requestSudoPassword,
|
|
67
52
|
timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
|
|
68
53
|
});
|
|
69
|
-
const repoSync = await syncRepoIndex(projectIndex, onStatus);
|
|
70
54
|
invalidateShellDiagnosticsCache(rootDir);
|
|
71
55
|
const diagnostics = buildDeferredShellDiagnostics('run_command');
|
|
72
|
-
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
73
|
-
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
74
|
-
}
|
|
75
56
|
const output = typeof result.output === 'string'
|
|
76
57
|
? boundCommandOutput(redactConnectionStringCredentials(result.output))
|
|
77
58
|
: result.output;
|
|
@@ -81,8 +62,6 @@ export async function runShellCommand(context, args) {
|
|
|
81
62
|
exitCode: result.exitCode,
|
|
82
63
|
timedOut: result.timedOut,
|
|
83
64
|
output,
|
|
84
|
-
repoSync,
|
|
85
|
-
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
86
65
|
diagnostics,
|
|
87
66
|
repoHint,
|
|
88
67
|
};
|
|
@@ -96,36 +75,8 @@ export function boundCommandOutput(output) {
|
|
|
96
75
|
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
97
76
|
output.slice(-tailSize));
|
|
98
77
|
}
|
|
99
|
-
async function syncRepoIndex(projectIndex, onStatus) {
|
|
100
|
-
if (!projectIndex.initialized) {
|
|
101
|
-
return {
|
|
102
|
-
added: 0,
|
|
103
|
-
modified: 0,
|
|
104
|
-
removed: 0,
|
|
105
|
-
indexedChunks: 0,
|
|
106
|
-
retrievalTokensUsed: 0,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
return await syncIndexFromDisk(projectIndex);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
-
onStatus(`Command completed, but local index sync failed: ${message}`);
|
|
115
|
-
return {
|
|
116
|
-
added: 0,
|
|
117
|
-
modified: 0,
|
|
118
|
-
removed: 0,
|
|
119
|
-
indexedChunks: 0,
|
|
120
|
-
retrievalTokensUsed: 0,
|
|
121
|
-
skipped: true,
|
|
122
|
-
reason: 'local index sync failed after command execution',
|
|
123
|
-
error: message,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
78
|
async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
128
|
-
const { rootDir,
|
|
79
|
+
const { rootDir, onStatus } = context;
|
|
129
80
|
const started = await startBackgroundJob(command, rootDir, {
|
|
130
81
|
startupWaitMs: typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined,
|
|
131
82
|
sessionId: context.sessionId,
|
|
@@ -142,11 +93,7 @@ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
|
142
93
|
};
|
|
143
94
|
}
|
|
144
95
|
const snapshot = started.snapshot;
|
|
145
|
-
const repoSync = await syncRepoIndex(projectIndex, onStatus);
|
|
146
96
|
invalidateShellDiagnosticsCache(rootDir);
|
|
147
|
-
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
148
|
-
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
149
|
-
}
|
|
150
97
|
const output = boundCommandOutput(redactConnectionStringCredentials(started.startupOutput ?? '').trim());
|
|
151
98
|
if (snapshot.status === 'running') {
|
|
152
99
|
return {
|
|
@@ -158,8 +105,6 @@ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
|
158
105
|
pid: snapshot.pid,
|
|
159
106
|
output,
|
|
160
107
|
note: `Background job ${snapshot.id} is running. Use shell_job_output to poll status and new output, and shell_job_kill to stop it.`,
|
|
161
|
-
repoSync,
|
|
162
|
-
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
163
108
|
repoHint,
|
|
164
109
|
};
|
|
165
110
|
}
|
|
@@ -172,8 +117,6 @@ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
|
172
117
|
exitCode: snapshot.exitCode,
|
|
173
118
|
output,
|
|
174
119
|
note: `Background job ${snapshot.id} finished during the startup window.`,
|
|
175
|
-
repoSync,
|
|
176
|
-
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
177
120
|
repoHint,
|
|
178
121
|
};
|
|
179
122
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { ensurePermission } from '../permissions.js';
|
|
4
4
|
import { isTuiMode } from '../runtime-mode.js';
|
|
5
5
|
import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
|
|
6
6
|
import { ensureSessionScratchDir } from '../scratch-dir.js';
|
|
@@ -56,29 +56,6 @@ function buildOutput(stdout, stderr, exitCode) {
|
|
|
56
56
|
return trimOutput(stdout.trim());
|
|
57
57
|
return trimOutput([stdout, stderr].filter(Boolean).join('\n').trim());
|
|
58
58
|
}
|
|
59
|
-
function emptyRepoSync(reason) {
|
|
60
|
-
return {
|
|
61
|
-
added: 0,
|
|
62
|
-
modified: 0,
|
|
63
|
-
removed: 0,
|
|
64
|
-
indexedChunks: 0,
|
|
65
|
-
retrievalTokensUsed: 0,
|
|
66
|
-
skipped: true,
|
|
67
|
-
...(reason ? { reason } : {}),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function readGitStatusSignature(rootDir) {
|
|
71
|
-
try {
|
|
72
|
-
return execFileSync('git', ['-C', rootDir, 'status', '--porcelain=v1', '--untracked-files=all'], {
|
|
73
|
-
encoding: 'utf-8',
|
|
74
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
75
|
-
timeout: 10_000,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
59
|
function executeNodeScript(rootDir, script, timeout) {
|
|
83
60
|
const effectiveTimeout = typeof timeout === 'number' && timeout > 0
|
|
84
61
|
? timeout
|
|
@@ -148,7 +125,7 @@ function executeNodeScript(rootDir, script, timeout) {
|
|
|
148
125
|
});
|
|
149
126
|
}
|
|
150
127
|
export async function runNodeScript(context, args) {
|
|
151
|
-
const { rootDir
|
|
128
|
+
const { rootDir } = context;
|
|
152
129
|
const script = typeof args.script === 'string' ? args.script : '';
|
|
153
130
|
if (!script.trim()) {
|
|
154
131
|
return { ok: false, error: 'script is required' };
|
|
@@ -158,65 +135,21 @@ export async function runNodeScript(context, args) {
|
|
|
158
135
|
console.log(chalk.bold.yellow(`\n ⚡ Node script:\n${commandForApproval}\n`));
|
|
159
136
|
console.log(chalk.dim(` in: ${rootDir}\n`));
|
|
160
137
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const approved = await confirmCommand(commandForApproval);
|
|
170
|
-
if (!approved) {
|
|
171
|
-
if (!isTuiMode()) {
|
|
172
|
-
console.log(chalk.dim(` ⏭ Skipped: ${COMMAND_LABEL}`));
|
|
173
|
-
}
|
|
174
|
-
return {
|
|
175
|
-
ok: false,
|
|
176
|
-
skipped: true,
|
|
177
|
-
command: COMMAND_LABEL,
|
|
178
|
-
failureCategory: 'user_declined',
|
|
179
|
-
failureDetails: {
|
|
180
|
-
category: 'user_declined',
|
|
181
|
-
tool: 'run_node_script',
|
|
182
|
-
action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
|
|
183
|
-
},
|
|
184
|
-
error: 'The real user rejected this proposed script. Nothing was executed; this was not a tool failure or an automated system skip.',
|
|
185
|
-
};
|
|
138
|
+
const denied = await ensurePermission(context, {
|
|
139
|
+
bucket: 'run',
|
|
140
|
+
title: 'Approve node script?',
|
|
141
|
+
body: commandForApproval,
|
|
142
|
+
}, 'run_node_script', { command: COMMAND_LABEL });
|
|
143
|
+
if (denied) {
|
|
144
|
+
if (!isTuiMode()) {
|
|
145
|
+
console.log(chalk.dim(` ⏭ Skipped: ${COMMAND_LABEL}`));
|
|
186
146
|
}
|
|
147
|
+
return denied;
|
|
187
148
|
}
|
|
188
|
-
const beforeGitStatus = readGitStatusSignature(rootDir);
|
|
189
149
|
const result = await executeNodeScript(rootDir, script, typeof args.timeout_ms === 'number' && args.timeout_ms > 0
|
|
190
150
|
? args.timeout_ms
|
|
191
151
|
: undefined);
|
|
192
|
-
const afterGitStatus = readGitStatusSignature(rootDir);
|
|
193
|
-
const gitStatusCleanBeforeAndAfter = beforeGitStatus === '' && afterGitStatus === '';
|
|
194
|
-
const shouldSync = context.projectIndex.initialized && !gitStatusCleanBeforeAndAfter;
|
|
195
|
-
let repoSync;
|
|
196
|
-
if (shouldSync) {
|
|
197
|
-
try {
|
|
198
|
-
repoSync = await syncIndexFromDisk(context.projectIndex);
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
-
context.onStatus(`Node script completed, but local index sync failed: ${message}`);
|
|
203
|
-
repoSync = {
|
|
204
|
-
...emptyRepoSync('local index sync failed after Node script execution'),
|
|
205
|
-
error: message,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
repoSync = emptyRepoSync(!context.projectIndex.initialized
|
|
211
|
-
? 'project index not initialized'
|
|
212
|
-
: gitStatusCleanBeforeAndAfter
|
|
213
|
-
? 'git status clean before and after'
|
|
214
|
-
: 'sync not needed');
|
|
215
|
-
}
|
|
216
152
|
invalidateShellDiagnosticsCache(rootDir);
|
|
217
|
-
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
218
|
-
context.onStatus(`Synced repo state after Node script (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
219
|
-
}
|
|
220
153
|
return {
|
|
221
154
|
ok: result.exitCode === 0,
|
|
222
155
|
command: COMMAND_LABEL,
|
|
@@ -225,8 +158,6 @@ export async function runNodeScript(context, args) {
|
|
|
225
158
|
stdout: result.stdout,
|
|
226
159
|
stderr: result.stderr,
|
|
227
160
|
output: result.output,
|
|
228
|
-
repoSync,
|
|
229
|
-
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
230
161
|
diagnostics: buildDeferredShellDiagnostics('run_node_script'),
|
|
231
162
|
};
|
|
232
163
|
}
|