@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41

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 (68) hide show
  1. package/README.md +49 -4
  2. package/dist/bin/ai.js +119 -111
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +11 -6
  5. package/dist/src/api/auth.js +6 -4
  6. package/dist/src/api/browser-login.js +152 -37
  7. package/dist/src/api/chat.js +286 -44
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +127 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/background-jobs.js +4 -2
  13. package/dist/src/browser/bridge.js +232 -0
  14. package/dist/src/browser/framing.js +42 -0
  15. package/dist/src/browser/native-host.js +209 -0
  16. package/dist/src/browser/protocol.js +46 -0
  17. package/dist/src/browser/session-bridge.js +104 -0
  18. package/dist/src/cli-args.js +19 -5
  19. package/dist/src/client-environment.js +2 -0
  20. package/dist/src/core/clipboard.js +7 -13
  21. package/dist/src/core/image-limits.js +56 -0
  22. package/dist/src/core/image-path-extractor.js +70 -3
  23. package/dist/src/core/session-image-store.js +246 -0
  24. package/dist/src/edit-journal.js +33 -2
  25. package/dist/src/executor.js +25 -3
  26. package/dist/src/help-text.js +58 -17
  27. package/dist/src/patcher.js +43 -4
  28. package/dist/src/permissions.js +288 -0
  29. package/dist/src/session-safety.js +0 -12
  30. package/dist/src/session-store.js +121 -20
  31. package/dist/src/session.js +19 -4
  32. package/dist/src/signin.js +58 -0
  33. package/dist/src/tool-executor.js +12 -46
  34. package/dist/src/tools/browser.js +628 -0
  35. package/dist/src/tools/delete-file.js +20 -4
  36. package/dist/src/tools/index.js +32 -10
  37. package/dist/src/tools/patch-file.js +12 -26
  38. package/dist/src/tools/read-image-file.js +85 -0
  39. package/dist/src/tools/replace-document-text.js +28 -18
  40. package/dist/src/tools/restore-checkpoint.js +0 -1
  41. package/dist/src/tools/run-command.js +14 -71
  42. package/dist/src/tools/run-node-script.js +12 -81
  43. package/dist/src/tools/save-generated-image.js +120 -0
  44. package/dist/src/tools/str-replace.js +12 -26
  45. package/dist/src/tools/undo-edit.js +1 -6
  46. package/dist/src/tools/write-file.js +67 -11
  47. package/dist/src/ui/prompt-history-store.js +1 -1
  48. package/dist/src/ui/repl.js +802 -195
  49. package/dist/src/ui/tui/bridge.js +10 -0
  50. package/dist/src/ui/tui/build-frame.js +517 -165
  51. package/dist/src/ui/tui/composer-layout.js +76 -0
  52. package/dist/src/ui/tui/markdown-render.js +81 -73
  53. package/dist/src/ui/tui/shell-input.js +227 -67
  54. package/dist/src/ui/tui/terminal-theme.js +28 -0
  55. package/dist/src/ui/tui/terminal-title.js +3 -0
  56. package/dist/src/ui/tui/terminal-writes.js +48 -0
  57. package/dist/src/ui/tui/text.js +158 -4
  58. package/dist/src/ui/tui/user-input.js +568 -0
  59. package/dist/src/utils.js +9 -0
  60. package/package.json +20 -8
  61. package/dist/src/markdown-renderer.js +0 -112
  62. package/dist/src/project-index.js +0 -221
  63. package/dist/src/tools/code-intel.js +0 -472
  64. package/dist/src/tools/find-symbol.js +0 -70
  65. package/dist/src/tools/hover-symbol.js +0 -95
  66. package/dist/src/tools/list-symbols.js +0 -55
  67. package/dist/src/tools/search-code.js +0 -37
  68. package/dist/src/tools/signature-help.js +0 -118
@@ -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, projectIndex, autoYes, confirmPatch } = context;
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 (!autoYes && confirmPatch) {
78
- const confirmed = await confirmPatch(filePath, patch);
79
- if (!confirmed) {
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
- if (!context.autoYes && context.confirmPatch) {
182
- const confirmed = await context.confirmPatch(targetPath, preview);
183
- if (!confirmed) {
184
- if (!isTuiMode()) {
185
- console.log(chalk.dim(` replace_document_text skipped: ${targetPath}`));
186
- }
187
- return {
188
- ok: false,
189
- skipped: true,
190
- filePath: targetPath,
191
- failureCategory: 'user_declined',
192
- failureDetails: {
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 { syncIndexFromDisk } from '../project-index.js';
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, projectIndex, autoYes, confirmCommand, requestSudoPassword, onStatus, } = context;
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
- if (!autoYes) {
35
- if (confirmCommand == null) {
36
- return {
37
- ok: false,
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
- if (!approved) {
46
- if (!isTuiMode())
47
- console.log(chalk.dim(` ⏭ Skipped: ${command}`));
48
- return {
49
- ok: false,
50
- skipped: true,
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, projectIndex, onStatus } = context;
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 { execFileSync, spawn } from 'node:child_process';
3
- import { syncIndexFromDisk } from '../project-index.js';
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, autoYes, confirmCommand } = context;
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
- if (!autoYes) {
162
- if (confirmCommand == null) {
163
- return {
164
- ok: false,
165
- command: COMMAND_LABEL,
166
- error: 'confirmCommand is required when autoYes is false',
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
  }