@thegitai/cli 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +30 -0
  3. package/dist/bin/ai.js +438 -0
  4. package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
  5. package/dist/parsers/tree-sitter-c.wasm +0 -0
  6. package/dist/parsers/tree-sitter-cpp.wasm +0 -0
  7. package/dist/parsers/tree-sitter-css.wasm +0 -0
  8. package/dist/parsers/tree-sitter-go.wasm +0 -0
  9. package/dist/parsers/tree-sitter-html.wasm +0 -0
  10. package/dist/parsers/tree-sitter-java.wasm +0 -0
  11. package/dist/parsers/tree-sitter-javascript.wasm +0 -0
  12. package/dist/parsers/tree-sitter-objc.wasm +0 -0
  13. package/dist/parsers/tree-sitter-php.wasm +0 -0
  14. package/dist/parsers/tree-sitter-python.wasm +0 -0
  15. package/dist/parsers/tree-sitter-ruby.wasm +0 -0
  16. package/dist/parsers/tree-sitter-rust.wasm +0 -0
  17. package/dist/parsers/tree-sitter-tsx.wasm +0 -0
  18. package/dist/parsers/tree-sitter-typescript.wasm +0 -0
  19. package/dist/src/agent-mode.js +142 -0
  20. package/dist/src/api/auth.js +81 -0
  21. package/dist/src/api/browser-login.js +184 -0
  22. package/dist/src/api/chat.js +346 -0
  23. package/dist/src/api/contracts.js +1 -0
  24. package/dist/src/api/http.js +44 -0
  25. package/dist/src/api/index.js +11 -0
  26. package/dist/src/api/models.js +110 -0
  27. package/dist/src/api/sessions.js +72 -0
  28. package/dist/src/artifact-policy.js +207 -0
  29. package/dist/src/client-state.js +14 -0
  30. package/dist/src/core/clipboard.js +208 -0
  31. package/dist/src/core/open-url.js +32 -0
  32. package/dist/src/edit-journal.js +133 -0
  33. package/dist/src/executor.js +924 -0
  34. package/dist/src/extractors/cpp.js +18 -0
  35. package/dist/src/extractors/csharp.js +16 -0
  36. package/dist/src/extractors/css.js +12 -0
  37. package/dist/src/extractors/go.js +27 -0
  38. package/dist/src/extractors/index.js +52 -0
  39. package/dist/src/extractors/java.js +14 -0
  40. package/dist/src/extractors/javascript.js +33 -0
  41. package/dist/src/extractors/objc.js +14 -0
  42. package/dist/src/extractors/php.js +20 -0
  43. package/dist/src/extractors/python.js +11 -0
  44. package/dist/src/extractors/ruby.js +13 -0
  45. package/dist/src/extractors/rust.js +17 -0
  46. package/dist/src/extractors/utils.js +58 -0
  47. package/dist/src/help-text.js +125 -0
  48. package/dist/src/markdown-renderer.js +112 -0
  49. package/dist/src/patcher.js +279 -0
  50. package/dist/src/project-index.js +221 -0
  51. package/dist/src/repo-map-languages.js +100 -0
  52. package/dist/src/runtime-mode.js +35 -0
  53. package/dist/src/scanner.js +362 -0
  54. package/dist/src/secret-preview.js +137 -0
  55. package/dist/src/session-exit.js +17 -0
  56. package/dist/src/session-safety.js +1012 -0
  57. package/dist/src/session-store.js +266 -0
  58. package/dist/src/session.js +93 -0
  59. package/dist/src/tool-executor.js +188 -0
  60. package/dist/src/tools/code-intel.js +472 -0
  61. package/dist/src/tools/delete-file.js +27 -0
  62. package/dist/src/tools/exec-utils.js +17 -0
  63. package/dist/src/tools/find-symbol.js +70 -0
  64. package/dist/src/tools/get-diagnostics.js +22 -0
  65. package/dist/src/tools/grep-code.js +331 -0
  66. package/dist/src/tools/hover-symbol.js +95 -0
  67. package/dist/src/tools/index.js +73 -0
  68. package/dist/src/tools/list-checkpoints.js +11 -0
  69. package/dist/src/tools/list-directories.js +16 -0
  70. package/dist/src/tools/list-files.js +13 -0
  71. package/dist/src/tools/list-session-edits.js +9 -0
  72. package/dist/src/tools/list-symbols.js +55 -0
  73. package/dist/src/tools/patch-file.js +88 -0
  74. package/dist/src/tools/path-listing.js +83 -0
  75. package/dist/src/tools/read-document.js +111 -0
  76. package/dist/src/tools/read-file.js +109 -0
  77. package/dist/src/tools/restore-checkpoint.js +100 -0
  78. package/dist/src/tools/ripgrep.js +29 -0
  79. package/dist/src/tools/run-command.js +94 -0
  80. package/dist/src/tools/run-node-script.js +210 -0
  81. package/dist/src/tools/search-code.js +37 -0
  82. package/dist/src/tools/shell-diagnostics.js +707 -0
  83. package/dist/src/tools/signature-help.js +118 -0
  84. package/dist/src/tools/str-replace.js +193 -0
  85. package/dist/src/tools/types.js +1 -0
  86. package/dist/src/tools/undo-edit.js +202 -0
  87. package/dist/src/tools/write-file.js +59 -0
  88. package/dist/src/tree-sitter-runtime.js +135 -0
  89. package/dist/src/types.js +1 -0
  90. package/dist/src/ui/paste-collapse.js +22 -0
  91. package/dist/src/ui/prompt-history-store.js +96 -0
  92. package/dist/src/ui/repl.js +2238 -0
  93. package/dist/src/ui/tui/bridge.js +175 -0
  94. package/dist/src/ui/tui/build-frame.js +718 -0
  95. package/dist/src/ui/tui/markdown-render.js +455 -0
  96. package/dist/src/ui/tui/shell-input.js +488 -0
  97. package/dist/src/ui/tui/text.js +30 -0
  98. package/dist/src/ui/tui/types.js +1 -0
  99. package/dist/src/usage.js +47 -0
  100. package/dist/src/utils.js +38 -0
  101. package/package.json +38 -0
@@ -0,0 +1,88 @@
1
+ import chalk from 'chalk';
2
+ import { normalizeProjectRelativePath } from '../artifact-policy.js';
3
+ import { applyUnifiedPatch, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
4
+ import { upsertIndexFile } from '../project-index.js';
5
+ import { isTuiMode } from '../runtime-mode.js';
6
+ import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
7
+ import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
8
+ export async function patchFile(context, args) {
9
+ const { rootDir, projectIndex, autoYes, confirmPatch } = context;
10
+ const filePath = String(args.filePath ?? '').trim();
11
+ let patch = typeof args.patch === 'string' ? args.patch : '';
12
+ if (!filePath) {
13
+ return { ok: false, error: 'filePath is required' };
14
+ }
15
+ if (!patch.trim()) {
16
+ return { ok: false, error: 'patch is required' };
17
+ }
18
+ let originalContent;
19
+ try {
20
+ originalContent = readProjectFile(rootDir, filePath);
21
+ }
22
+ catch (err) {
23
+ return {
24
+ ok: false,
25
+ error: `Cannot read file for patching: ${err.message}. Use write_file to create new files.`,
26
+ };
27
+ }
28
+ const coveragePath = normalizeProjectRelativePath(rootDir, filePath) ?? filePath;
29
+ patch = resolveRedactionTokens(context.safety, patch, coveragePath, getCurrentFileHash(rootDir, filePath));
30
+ let patchedContent;
31
+ try {
32
+ patchedContent = applyUnifiedPatch(originalContent, patch);
33
+ }
34
+ catch (err) {
35
+ return {
36
+ ok: false,
37
+ filePath,
38
+ error: `Patch failed: ${err.message}`,
39
+ failureCategory: 'conflict',
40
+ failureDetails: {
41
+ category: 'conflict',
42
+ tool: 'patch_file',
43
+ action: 'Re-read the full file and rebuild the patch against the current exact file content.',
44
+ },
45
+ };
46
+ }
47
+ renderDiffPreview(filePath, patch);
48
+ if (!autoYes && confirmPatch) {
49
+ const confirmed = await confirmPatch(filePath, patch);
50
+ if (!confirmed) {
51
+ if (!isTuiMode())
52
+ console.log(chalk.dim(` ⏭ Patch skipped: ${filePath}`));
53
+ return {
54
+ ok: false,
55
+ skipped: true,
56
+ filePath,
57
+ error: 'User declined patch',
58
+ };
59
+ }
60
+ }
61
+ const { changed } = writeProjectFile(rootDir, filePath, patchedContent);
62
+ let indexedChunks = 0;
63
+ let retrievalTokensUsed = 0;
64
+ if (changed) {
65
+ const indexResult = await upsertIndexFile(projectIndex, filePath);
66
+ indexedChunks = indexResult.indexedChunks;
67
+ retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
68
+ }
69
+ invalidateShellDiagnosticsCache(rootDir, filePath);
70
+ const diagnostics = runShellDiagnostics(rootDir, filePath);
71
+ const originalLines = originalContent.split('\n').length;
72
+ const patchedLines = patchedContent.split('\n').length;
73
+ if (!isTuiMode()) {
74
+ const actionLabel = changed ? 'Patched' : 'Patched (no change)';
75
+ const color = changed ? chalk.green : chalk.yellow;
76
+ console.log(color(` ✏️ ${actionLabel}: ${filePath} (${originalLines} → ${patchedLines} lines)`));
77
+ }
78
+ return {
79
+ ok: true,
80
+ filePath,
81
+ changed,
82
+ operation: 'patch',
83
+ indexedChunks,
84
+ retrievalTokensUsed,
85
+ bytesWritten: Buffer.byteLength(patchedContent, 'utf-8'),
86
+ diagnostics,
87
+ };
88
+ }
@@ -0,0 +1,83 @@
1
+ import { listProjectFiles } from '../scanner.js';
2
+ const LISTING_SCAN_MULTIPLIER = 20;
3
+ const LISTING_MIN_SCAN_LIMIT = 1000;
4
+ const LISTING_MAX_SCAN_LIMIT = 10000;
5
+ export function listFilesystemFiles(rootDir, { pattern = '', limit = 200 } = {}) {
6
+ return listProjectFiles(rootDir, { limit: scanLimitForListing(limit) })
7
+ .filter((filePath) => matchesPathPattern(filePath, pattern))
8
+ .sort()
9
+ .slice(0, limit);
10
+ }
11
+ export function listFilesystemDirectories(rootDir, { pattern = '', limit = 200 } = {}) {
12
+ const directories = new Set();
13
+ for (const filePath of listProjectFiles(rootDir, {
14
+ limit: scanLimitForListing(limit),
15
+ })) {
16
+ const parts = filePath.split(/[\\/]+/).filter(Boolean);
17
+ for (let i = 1; i < parts.length; i++) {
18
+ directories.add(parts.slice(0, i).join('/'));
19
+ }
20
+ if (directories.size >= limit * LISTING_SCAN_MULTIPLIER)
21
+ break;
22
+ }
23
+ return [...directories]
24
+ .filter((dirPath) => matchesPathPattern(dirPath, pattern))
25
+ .sort()
26
+ .slice(0, limit);
27
+ }
28
+ function scanLimitForListing(limit) {
29
+ const requested = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 200;
30
+ return Math.min(LISTING_MAX_SCAN_LIMIT, Math.max(LISTING_MIN_SCAN_LIMIT, requested * LISTING_SCAN_MULTIPLIER));
31
+ }
32
+ function matchesPathPattern(filePath, pattern) {
33
+ if (!pattern || pattern === '*')
34
+ return true;
35
+ if (!/[*?{}]/.test(pattern)) {
36
+ return filePath.includes(pattern);
37
+ }
38
+ return globLikePatternToRegExp(pattern).test(filePath);
39
+ }
40
+ function globLikePatternToRegExp(pattern) {
41
+ let source = '';
42
+ for (let i = 0; i < pattern.length; i++) {
43
+ const char = pattern[i];
44
+ if (char === '*' && pattern[i + 1] === '*') {
45
+ i++;
46
+ if (pattern[i + 1] === '/') {
47
+ source += '(?:[^/]+/)*';
48
+ i++;
49
+ }
50
+ else {
51
+ source += '.*';
52
+ }
53
+ continue;
54
+ }
55
+ if (char === '*') {
56
+ source += '.*';
57
+ }
58
+ else if (char === '?') {
59
+ source += '.';
60
+ }
61
+ else if (char === '{') {
62
+ const end = pattern.indexOf('}', i + 1);
63
+ if (end === -1) {
64
+ source += escapeRegExp(char);
65
+ }
66
+ else {
67
+ const alternatives = pattern
68
+ .slice(i + 1, end)
69
+ .split(',')
70
+ .map((part) => escapeRegExp(part.trim()));
71
+ source += `(?:${alternatives.join('|')})`;
72
+ i = end;
73
+ }
74
+ }
75
+ else {
76
+ source += escapeRegExp(char);
77
+ }
78
+ }
79
+ return new RegExp(`^${source}$`);
80
+ }
81
+ function escapeRegExp(value) {
82
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
+ }
@@ -0,0 +1,111 @@
1
+ import path from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { isSensitiveProjectPath, normalizeProjectRelativePath, } from '../artifact-policy.js';
4
+ import { readCliAuthConfig } from '../api/auth.js';
5
+ export function normalizeDocumentText(raw) {
6
+ const text = String(raw ?? '').replace(/\r\n?/g, '\n');
7
+ let output = '';
8
+ for (let index = 0; index < text.length; index++) {
9
+ const code = text.charCodeAt(index);
10
+ if (code === 0) {
11
+ output += ' ';
12
+ continue;
13
+ }
14
+ if (code >= 0xd800 && code <= 0xdbff) {
15
+ const next = text.charCodeAt(index + 1);
16
+ if (next >= 0xdc00 && next <= 0xdfff) {
17
+ output += text.charAt(index) + text.charAt(index + 1);
18
+ index += 1;
19
+ }
20
+ else {
21
+ output += String.fromCharCode(0xfffd);
22
+ }
23
+ continue;
24
+ }
25
+ if (code >= 0xdc00 && code <= 0xdfff) {
26
+ output += String.fromCharCode(0xfffd);
27
+ continue;
28
+ }
29
+ if (code < 0x20 && code !== 0x09 && code !== 0x0a) {
30
+ output += ' ';
31
+ continue;
32
+ }
33
+ output += text.charAt(index);
34
+ }
35
+ return output;
36
+ }
37
+ async function parseDocumentOnServer(config, fileName, fileData, ext, args) {
38
+ const includePageArgs = ext === '.pdf';
39
+ const response = await globalThis.fetch(`${config.serverUrl.replace(/\/+$/, '')}/v1/document/parse`, {
40
+ method: 'POST',
41
+ headers: {
42
+ authorization: `Bearer ${config.token}`,
43
+ 'content-type': 'application/json',
44
+ },
45
+ body: JSON.stringify({
46
+ fileName,
47
+ fileData: fileData.toString('base64'),
48
+ ...(includePageArgs && args.firstPage !== undefined
49
+ ? { firstPage: args.firstPage }
50
+ : {}),
51
+ ...(includePageArgs && args.lastPage !== undefined
52
+ ? { lastPage: args.lastPage }
53
+ : {}),
54
+ }),
55
+ });
56
+ const data = await response.json().catch(() => null);
57
+ if (!response.ok) {
58
+ return {
59
+ ok: false,
60
+ error: `Server document parse failed: ${data?.error?.message ?? response.status}`,
61
+ };
62
+ }
63
+ return data;
64
+ }
65
+ export async function readDocument(rootDir, args, env) {
66
+ const raw = String(args.filePath ?? '').trim();
67
+ if (!raw) {
68
+ return { ok: false, error: 'filePath is required' };
69
+ }
70
+ const resolvedPath = path.isAbsolute(raw) ? raw : path.resolve(rootDir, raw);
71
+ const projectPath = normalizeProjectRelativePath(rootDir, resolvedPath);
72
+ if (projectPath && isSensitiveProjectPath(projectPath)) {
73
+ return {
74
+ ok: false,
75
+ error: 'This path is not permitted.',
76
+ };
77
+ }
78
+ if (!existsSync(resolvedPath)) {
79
+ return {
80
+ ok: false,
81
+ error: `File not found: ${resolvedPath}`,
82
+ failureCategory: 'not_found',
83
+ };
84
+ }
85
+ const ext = path.extname(resolvedPath).toLowerCase();
86
+ if (ext !== '.pdf' && ext !== '.xlsx') {
87
+ return {
88
+ ok: false,
89
+ error: `Unsupported file type: "${ext}". read_document only supports .pdf and .xlsx.`,
90
+ };
91
+ }
92
+ const authConfig = readCliAuthConfig(env);
93
+ if (!authConfig) {
94
+ return {
95
+ ok: false,
96
+ error: 'Document parsing requires a server connection. Please log in first.',
97
+ };
98
+ }
99
+ let fileData;
100
+ try {
101
+ fileData = readFileSync(resolvedPath);
102
+ }
103
+ catch (err) {
104
+ return { ok: false, error: `Failed to read file: ${err.message}` };
105
+ }
106
+ const result = await parseDocumentOnServer(authConfig, path.basename(resolvedPath), fileData, ext, args);
107
+ if (result.ok) {
108
+ result.filePath = path.relative(rootDir, resolvedPath) || resolvedPath;
109
+ }
110
+ return result;
111
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import { normalizeProjectRelativePath, shouldIgnoreArtifactPath, } from '../artifact-policy.js';
4
+ import { buildSecretFilePreview, isDotenvLikePath, looksLikeEditableDotenv, shouldUseSecretFilePreview, } from '../secret-preview.js';
5
+ import { readProjectFile } from '../patcher.js';
6
+ import { dotenvFitsRedactionBudget, getCurrentFileHash, recordReadCoverage, redactContentWithStableTokens, redactDotenvWithStableTokens, } from '../session-safety.js';
7
+ import { readFileRange, truncate } from '../utils.js';
8
+ const MAX_FILE_READ_CHARS = 12000;
9
+ export async function readFile(context, args) {
10
+ const rootDir = typeof context === 'string' ? context : context.rootDir;
11
+ const safety = typeof context === 'string' ? undefined : context.safety;
12
+ const filePath = String(args.filePath ?? '').trim();
13
+ if (!filePath) {
14
+ return { ok: false, error: 'filePath is required' };
15
+ }
16
+ const projectPath = normalizeProjectRelativePath(rootDir, filePath);
17
+ if (!projectPath && !path.isAbsolute(filePath)) {
18
+ return {
19
+ ok: false,
20
+ error: `Refusing to access path outside the project root: ${filePath}`,
21
+ };
22
+ }
23
+ if (projectPath && shouldIgnoreArtifactPath(projectPath)) {
24
+ return {
25
+ ok: false,
26
+ error: 'This path is not permitted.',
27
+ };
28
+ }
29
+ let content;
30
+ if (projectPath) {
31
+ try {
32
+ content = readProjectFile(rootDir, filePath);
33
+ }
34
+ catch (err) {
35
+ return { ok: false, error: err.message };
36
+ }
37
+ }
38
+ else {
39
+ const absPath = path.resolve(filePath);
40
+ if (!existsSync(absPath)) {
41
+ return { ok: false, error: `File does not exist: ${filePath}` };
42
+ }
43
+ try {
44
+ content = readFileSync(absPath, 'utf-8');
45
+ }
46
+ catch (err) {
47
+ return { ok: false, error: err.message };
48
+ }
49
+ }
50
+ const previewPath = projectPath ?? filePath;
51
+ // A clean dotenv file is shown with keys visible and values tokenized so the
52
+ // agent can still edit it (str_replace/write_file round-trip the tokens) and
53
+ // read coverage is recorded. Any other secret file — PEM, JSON credentials,
54
+ // or a dotenv with a stray non-assignment line — keeps the opaque blackout.
55
+ const editableDotenv = Boolean(projectPath) &&
56
+ Boolean(safety) &&
57
+ isDotenvLikePath(previewPath) &&
58
+ looksLikeEditableDotenv(content) &&
59
+ dotenvFitsRedactionBudget(content);
60
+ if (shouldUseSecretFilePreview(previewPath, content) && !editableDotenv) {
61
+ return {
62
+ ok: true,
63
+ ...buildSecretFilePreview(previewPath, content),
64
+ };
65
+ }
66
+ const range = readFileRange(content, args.startLine ? Number(args.startLine) : undefined, args.endLine ? Number(args.endLine) : undefined);
67
+ const hash = projectPath ? getCurrentFileHash(rootDir, filePath) : null;
68
+ const redacted = projectPath && safety
69
+ ? editableDotenv
70
+ ? redactDotenvWithStableTokens(safety, range.content, projectPath, hash)
71
+ : redactContentWithStableTokens(safety, range.content, projectPath, hash)
72
+ : { content: range.content, tokens: [] };
73
+ const contentTruncated = redacted.content.length > MAX_FILE_READ_CHARS;
74
+ const deliveredEndLine = contentTruncated
75
+ ? Math.min(range.endLine, range.startLine +
76
+ redacted.content.slice(0, MAX_FILE_READ_CHARS).split('\n').length -
77
+ 1)
78
+ : range.endLine;
79
+ const fullFile = range.startLine === 1 &&
80
+ deliveredEndLine === range.totalLines &&
81
+ !contentTruncated;
82
+ if (projectPath && safety) {
83
+ recordReadCoverage(safety, {
84
+ filePath: projectPath,
85
+ hash,
86
+ fullFile,
87
+ startLine: range.startLine,
88
+ endLine: deliveredEndLine,
89
+ totalLines: range.totalLines,
90
+ createdAt: new Date().toISOString(),
91
+ });
92
+ }
93
+ return {
94
+ ok: true,
95
+ filePath: projectPath ?? filePath,
96
+ totalLines: range.totalLines,
97
+ startLine: range.startLine,
98
+ endLine: deliveredEndLine,
99
+ content: truncate(redacted.content, MAX_FILE_READ_CHARS),
100
+ contentHash: hash,
101
+ readCoverage: {
102
+ fullFile,
103
+ hash,
104
+ cacheStatus: 'fresh',
105
+ contentTruncated,
106
+ },
107
+ redactionTokens: redacted.tokens,
108
+ };
109
+ }
@@ -0,0 +1,100 @@
1
+ import chalk from 'chalk';
2
+ import { isTuiMode } from '../runtime-mode.js';
3
+ import { createPromptCheckpoint, restoreCheckpointFiles, } from '../session-safety.js';
4
+ import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
5
+ function normalizeFilePaths(value) {
6
+ if (value == null)
7
+ return undefined;
8
+ if (!Array.isArray(value))
9
+ return [];
10
+ return value.map((item) => String(item ?? '').trim()).filter(Boolean);
11
+ }
12
+ export async function restoreToCheckpoint(context, args) {
13
+ if (!context.safety) {
14
+ return { ok: false, error: 'No session safety state is available.' };
15
+ }
16
+ const checkpointId = String(args.checkpointId ?? args.checkpoint_id ?? '').trim();
17
+ if (!checkpointId) {
18
+ return {
19
+ ok: false,
20
+ failureCategory: 'missing_required_argument',
21
+ error: 'checkpointId is required',
22
+ };
23
+ }
24
+ const filePaths = normalizeFilePaths(args.filePaths ?? args.file_paths);
25
+ const dryRun = args.dryRun === true || args.dry_run === true;
26
+ const checkpoint = context.safety.checkpoints.find((item) => item.id === checkpointId);
27
+ if (!checkpoint) {
28
+ return {
29
+ ok: false,
30
+ failureCategory: 'not_found',
31
+ checkpointId,
32
+ error: `Checkpoint not found: ${checkpointId}`,
33
+ };
34
+ }
35
+ if (dryRun) {
36
+ const targets = filePaths?.length
37
+ ? checkpoint.files.filter((file) => filePaths.includes(file.filePath))
38
+ : checkpoint.files;
39
+ return {
40
+ ok: true,
41
+ checkpointId,
42
+ dryRun: true,
43
+ changed: false,
44
+ restored: [],
45
+ preview: targets.map((file) => ({
46
+ filePath: file.filePath,
47
+ exists: file.exists,
48
+ hash: file.hash,
49
+ restorable: !file.skipped && (file.content !== null || !file.exists),
50
+ skipped: file.skipped,
51
+ })),
52
+ };
53
+ }
54
+ const result = await restoreCheckpointFiles({
55
+ state: context.safety,
56
+ rootDir: context.rootDir,
57
+ projectIndex: context.projectIndex,
58
+ checkpointId,
59
+ filePaths,
60
+ currentTurnId: context.currentTurnId ?? null,
61
+ currentToolCallId: context.currentToolCallId ?? null,
62
+ });
63
+ if (!result.ok) {
64
+ return {
65
+ ...result,
66
+ failureCategory: 'conflict',
67
+ failureDetails: {
68
+ category: 'conflict',
69
+ tool: 'restore_to_checkpoint',
70
+ action: 'Inspect the listed files before retrying restore. The tool refused to overwrite unknown current content.',
71
+ },
72
+ };
73
+ }
74
+ invalidateShellDiagnosticsCache(context.rootDir);
75
+ createPromptCheckpoint(context.safety, `after restore ${checkpointId}`, context.currentTurnId ?? null);
76
+ if (!isTuiMode()) {
77
+ console.log(chalk.green(` Restored checkpoint ${checkpointId}: ${result.restored
78
+ .map((item) => item.filePath)
79
+ .join(', ')}`));
80
+ }
81
+ return {
82
+ ...result,
83
+ operation: 'restore_to_checkpoint',
84
+ diagnostics: runShellDiagnostics(context.rootDir),
85
+ };
86
+ }
87
+ export async function restoreFilesToCheckpoint(context, args) {
88
+ const filePaths = normalizeFilePaths(args.filePaths ?? args.file_paths) ?? [];
89
+ if (!filePaths.length) {
90
+ return {
91
+ ok: false,
92
+ failureCategory: 'missing_required_argument',
93
+ error: 'filePaths is required for restore_files_to_checkpoint',
94
+ };
95
+ }
96
+ return restoreToCheckpoint(context, {
97
+ ...args,
98
+ filePaths,
99
+ });
100
+ }
@@ -0,0 +1,29 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ const require = createRequire(import.meta.url);
4
+ function readBundledRipgrepPath() {
5
+ try {
6
+ const ripgrep = require('@vscode/ripgrep');
7
+ const rgPath = ripgrep.rgPath;
8
+ return typeof rgPath === 'string' && existsSync(rgPath) ? rgPath : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function getRipgrepPath() {
15
+ return readBundledRipgrepPath() ?? 'rg';
16
+ }
17
+ export function getRipgrepPathCandidates() {
18
+ const bundled = readBundledRipgrepPath();
19
+ return bundled ? [bundled, 'rg'] : ['rg'];
20
+ }
21
+ export function isRipgrepExecutionUnavailable(error) {
22
+ return (error?.code === 'ENOENT' ||
23
+ error?.code === 'EACCES' ||
24
+ error?.code === 'ENOEXEC' ||
25
+ error?.errno === -2 ||
26
+ error?.errno === -13 ||
27
+ error?.errno === -8 ||
28
+ error?.errno === -26);
29
+ }
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk';
2
+ import { getBlockedCommandReason, runCommand, } from '../executor.js';
3
+ import { syncIndexFromDisk } from '../project-index.js';
4
+ import { isTuiMode } from '../runtime-mode.js';
5
+ import { redactConnectionStringCredentials } from '../secret-preview.js';
6
+ import { buildNestedGitHint } from '../session-safety.js';
7
+ import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
8
+ const MAX_OUTPUT_CHARS = 4000;
9
+ export async function runShellCommand(context, args) {
10
+ const { rootDir, projectIndex, autoYes, confirmCommand, requestSudoPassword, onStatus, } = context;
11
+ const command = String(args.command ?? '').trim();
12
+ if (!command) {
13
+ return { ok: false, error: 'command is required' };
14
+ }
15
+ const hasTimeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0;
16
+ const repoHint = buildNestedGitHint(rootDir, command);
17
+ const blockedReason = getBlockedCommandReason(command, hasTimeout, rootDir);
18
+ if (blockedReason) {
19
+ const error = blockedReason;
20
+ if (!isTuiMode())
21
+ console.log(chalk.red(`\n ✖ ${error}`));
22
+ return {
23
+ ok: false,
24
+ blocked: true,
25
+ command,
26
+ error,
27
+ repoHint,
28
+ };
29
+ }
30
+ if (!isTuiMode())
31
+ console.log(chalk.bold.yellow(`\n ⚡ Command: ${command}`));
32
+ if (!autoYes) {
33
+ if (confirmCommand == null) {
34
+ return {
35
+ ok: false,
36
+ command,
37
+ error: 'confirmCommand is required when autoYes is false',
38
+ };
39
+ }
40
+ const approved = await confirmCommand(command);
41
+ if (!approved) {
42
+ if (!isTuiMode())
43
+ console.log(chalk.dim(` ⏭ Skipped: ${command}`));
44
+ return {
45
+ ok: false,
46
+ skipped: true,
47
+ command,
48
+ error: 'User declined command execution',
49
+ };
50
+ }
51
+ }
52
+ const result = await runCommand(command, rootDir, {
53
+ requestSudoPassword,
54
+ timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
55
+ });
56
+ const repoSync = projectIndex.initialized
57
+ ? await syncIndexFromDisk(projectIndex)
58
+ : {
59
+ added: 0,
60
+ modified: 0,
61
+ removed: 0,
62
+ indexedChunks: 0,
63
+ retrievalTokensUsed: 0,
64
+ };
65
+ invalidateShellDiagnosticsCache(rootDir);
66
+ const diagnostics = buildDeferredShellDiagnostics('run_command');
67
+ if (repoSync.added || repoSync.modified || repoSync.removed) {
68
+ onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
69
+ }
70
+ // Redact connection-string passwords so shell output (e.g. `cat .env`,
71
+ // `printenv`) cannot leak them into history or telemetry.
72
+ let output = typeof result.output === 'string'
73
+ ? redactConnectionStringCredentials(result.output)
74
+ : result.output;
75
+ if (typeof output === 'string' && output.length > MAX_OUTPUT_CHARS) {
76
+ const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
77
+ const tailSize = MAX_OUTPUT_CHARS - headSize;
78
+ output =
79
+ output.slice(0, headSize) +
80
+ `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
81
+ output.slice(-tailSize);
82
+ }
83
+ return {
84
+ ok: result.exitCode === 0,
85
+ command,
86
+ exitCode: result.exitCode,
87
+ timedOut: result.timedOut,
88
+ output,
89
+ repoSync,
90
+ retrievalTokensUsed: repoSync.retrievalTokensUsed,
91
+ diagnostics,
92
+ repoHint,
93
+ };
94
+ }