@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,266 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getClientStateDir } from './client-state.js';
5
+ import { normalizeAssistantEditJournal } from './edit-journal.js';
6
+ import { cloneSessionSafetyState, createSessionSafetyState, mergeLocalSessionSafetyState, normalizeSessionSafetyState, } from './session-safety.js';
7
+ import { truncate } from './utils.js';
8
+ const SESSION_STORE_VERSION = 1;
9
+ const MAX_RECENT_SESSIONS = 5;
10
+ function cloneJson(value) {
11
+ return JSON.parse(JSON.stringify(value ?? null));
12
+ }
13
+ function normalizeIsoDate(value) {
14
+ const text = typeof value === 'string' ? value : '';
15
+ const time = Date.parse(text);
16
+ return Number.isFinite(time) ? new Date(time).toISOString() : new Date().toISOString();
17
+ }
18
+ function normalizeSessionName(name) {
19
+ const text = String(name ?? '').trim();
20
+ if (!text)
21
+ return null;
22
+ if (text.length > 120) {
23
+ throw new Error('Session name must be 120 characters or fewer.');
24
+ }
25
+ if (/[\r\n\t]/.test(text)) {
26
+ throw new Error('Session name cannot contain control whitespace.');
27
+ }
28
+ return text;
29
+ }
30
+ function sanitizeOpaqueState(value) {
31
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
32
+ return {};
33
+ }
34
+ return cloneJson(value);
35
+ }
36
+ function sanitizeClientState(value) {
37
+ const raw = value && typeof value === 'object' ? value : {};
38
+ return {
39
+ editCounter: Math.max(0, Number.parseInt(String(raw.editCounter ?? 0), 10) || 0),
40
+ editJournal: normalizeAssistantEditJournal(raw.editJournal),
41
+ stickyFilePaths: Array.isArray(raw.stickyFilePaths)
42
+ ? raw.stickyFilePaths.map((item) => String(item ?? '')).filter(Boolean)
43
+ : [],
44
+ safety: normalizeSessionSafetyState(raw.safety),
45
+ };
46
+ }
47
+ function safeSessionFileName(id) {
48
+ const normalized = String(id ?? '').trim();
49
+ if (!/^[a-zA-Z0-9_-]+$/.test(normalized)) {
50
+ throw new Error(`Invalid session id "${id}".`);
51
+ }
52
+ return `${normalized}.json`;
53
+ }
54
+ function getSessionBaseDir(env = process.env) {
55
+ return getClientStateDir(env);
56
+ }
57
+ export function getSessionProjectKey(rootDir) {
58
+ const resolvedRoot = path.resolve(rootDir);
59
+ const projectName = path.basename(resolvedRoot).replace(/[^a-zA-Z0-9._-]/g, '_') || 'project';
60
+ const hash = createHash('sha256')
61
+ .update(resolvedRoot)
62
+ .digest('hex')
63
+ .slice(0, 16);
64
+ return `${projectName}-${hash}`;
65
+ }
66
+ function getSessionProjectDir(rootDir, env = process.env) {
67
+ return path.join(getSessionBaseDir(env), 'sessions', 'projects', getSessionProjectKey(rootDir));
68
+ }
69
+ function getSessionPath(rootDir, sessionId, env = process.env) {
70
+ return path.join(getSessionProjectDir(rootDir, env), safeSessionFileName(sessionId));
71
+ }
72
+ function listSessionFiles(rootDir, env = process.env) {
73
+ const dir = getSessionProjectDir(rootDir, env);
74
+ if (!existsSync(dir))
75
+ return [];
76
+ return readdirSync(dir)
77
+ .filter((name) => name.endsWith('.json'))
78
+ .map((name) => path.join(dir, name));
79
+ }
80
+ function normalizeHistory(value) {
81
+ if (!Array.isArray(value))
82
+ return [];
83
+ return value.filter((entry) => entry &&
84
+ typeof entry === 'object' &&
85
+ typeof entry.role === 'string' &&
86
+ Array.isArray(entry.parts));
87
+ }
88
+ function normalizeSnapshot(raw, rootDir) {
89
+ if (!raw || typeof raw !== 'object') {
90
+ throw new Error('Session file is not valid JSON object data.');
91
+ }
92
+ if (raw.version !== SESSION_STORE_VERSION) {
93
+ throw new Error(`Unsupported session file version "${raw.version}".`);
94
+ }
95
+ const id = String(raw.id ?? '').trim();
96
+ safeSessionFileName(id);
97
+ const modelId = Number(raw.modelId);
98
+ if (!Number.isInteger(modelId) || modelId <= 0) {
99
+ throw new Error(`Session "${id}" is missing modelId.`);
100
+ }
101
+ return {
102
+ version: SESSION_STORE_VERSION,
103
+ id,
104
+ name: normalizeSessionName(raw.name ?? null),
105
+ rootDir: path.resolve(rootDir),
106
+ projectKey: getSessionProjectKey(rootDir),
107
+ createdAt: normalizeIsoDate(raw.createdAt),
108
+ updatedAt: normalizeIsoDate(raw.updatedAt),
109
+ modelId,
110
+ history: cloneJson(normalizeHistory(raw.history)),
111
+ clientState: sanitizeClientState(raw.clientState),
112
+ serverState: sanitizeOpaqueState(raw.serverState),
113
+ };
114
+ }
115
+ function loadSnapshotFile(filePath, rootDir) {
116
+ return normalizeSnapshot(JSON.parse(readFileSync(filePath, 'utf-8')), rootDir);
117
+ }
118
+ function loadAllSnapshots(rootDir, env = process.env) {
119
+ const snapshots = [];
120
+ for (const filePath of listSessionFiles(rootDir, env)) {
121
+ try {
122
+ snapshots.push(loadSnapshotFile(filePath, rootDir));
123
+ }
124
+ catch {
125
+ // Skip corrupted snapshots silently — customers have no actionable debug path here.
126
+ }
127
+ }
128
+ return snapshots.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
129
+ }
130
+ function writeSnapshot(snapshot, env = process.env) {
131
+ const dir = getSessionProjectDir(snapshot.rootDir, env);
132
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
133
+ const filePath = getSessionPath(snapshot.rootDir, snapshot.id, env);
134
+ const tempPath = `${filePath}.${process.pid}.tmp`;
135
+ writeFileSync(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, {
136
+ encoding: 'utf8',
137
+ mode: 0o600,
138
+ });
139
+ renameSync(tempPath, filePath);
140
+ }
141
+ function assertSessionNameAvailable(rootDir, name, sessionId, env = process.env) {
142
+ if (!name)
143
+ return;
144
+ const duplicate = loadAllSnapshots(rootDir, env).find((snapshot) => snapshot.name === name && snapshot.id !== sessionId);
145
+ if (duplicate) {
146
+ throw new Error(`Session name "${name}" is already used by ${duplicate.id}. Use --session "${name}" to resume it or choose a different name.`);
147
+ }
148
+ }
149
+ export function pruneSavedSessions(rootDir, env = process.env) {
150
+ const snapshots = loadAllSnapshots(rootDir, env);
151
+ const keep = new Set(snapshots.slice(0, MAX_RECENT_SESSIONS).map((snapshot) => snapshot.id));
152
+ for (const snapshot of snapshots.slice(MAX_RECENT_SESSIONS)) {
153
+ if (!keep.has(snapshot.id)) {
154
+ rmSync(getSessionPath(rootDir, snapshot.id, env), { force: true });
155
+ }
156
+ }
157
+ }
158
+ export function snapshotFromSession(session) {
159
+ const now = new Date().toISOString();
160
+ const createdAt = normalizeIsoDate(session.sessionCreatedAt ?? now);
161
+ session.sessionCreatedAt = createdAt;
162
+ session.sessionUpdatedAt = now;
163
+ return {
164
+ version: SESSION_STORE_VERSION,
165
+ id: session.sessionId,
166
+ name: normalizeSessionName(session.sessionName ?? null),
167
+ rootDir: path.resolve(session.rootDir),
168
+ projectKey: getSessionProjectKey(session.rootDir),
169
+ createdAt,
170
+ updatedAt: now,
171
+ modelId: session.modelId,
172
+ history: cloneJson(session.history),
173
+ clientState: {
174
+ editCounter: session.clientState.editCounter,
175
+ editJournal: normalizeAssistantEditJournal(session.clientState.editJournal),
176
+ stickyFilePaths: Array.from(session.clientState.stickyFilePaths),
177
+ safety: cloneSessionSafetyState(session.clientState.safety ?? createSessionSafetyState()),
178
+ },
179
+ serverState: sanitizeOpaqueState(session.serverState),
180
+ };
181
+ }
182
+ export function saveSessionState(session, env = process.env) {
183
+ const snapshot = snapshotFromSession(session);
184
+ assertSessionNameAvailable(snapshot.rootDir, snapshot.name, snapshot.id, env);
185
+ writeSnapshot(snapshot, env);
186
+ pruneSavedSessions(snapshot.rootDir, env);
187
+ return snapshot;
188
+ }
189
+ export function applySessionSnapshot(session, snapshot, options = {}) {
190
+ const currentAgentMode = session.agentMode;
191
+ session.sessionId = snapshot.id;
192
+ session.sessionName = snapshot.name;
193
+ session.sessionCreatedAt = snapshot.createdAt;
194
+ session.sessionUpdatedAt = snapshot.updatedAt;
195
+ session.modelId = Number(options.modelOverride ?? snapshot.modelId);
196
+ session.history = cloneJson(snapshot.history);
197
+ session.serverState = sanitizeOpaqueState(snapshot.serverState);
198
+ session.agentMode = options.preserveAgentMode ? currentAgentMode : 'default';
199
+ session.autoYes = session.agentMode === 'auto-accept';
200
+ session.turnState = {
201
+ id: null,
202
+ historyStartIndex: session.history.length,
203
+ retrievedFilePaths: [],
204
+ injectedContext: '',
205
+ userInput: '',
206
+ };
207
+ session.clientState = {
208
+ stickyFilePaths: new Set(snapshot.clientState.stickyFilePaths),
209
+ editJournal: normalizeAssistantEditJournal(snapshot.clientState.editJournal),
210
+ editCounter: Math.max(0, snapshot.clientState.editCounter ?? 0),
211
+ safety: mergeLocalSessionSafetyState(session.clientState.safety, snapshot.clientState.safety),
212
+ };
213
+ }
214
+ function extractMarkedSection(text, marker) {
215
+ const index = text.indexOf(marker);
216
+ if (index === -1)
217
+ return '';
218
+ const after = text.slice(index + marker.length).trimStart();
219
+ const end = after.indexOf('\n\n');
220
+ return (end === -1 ? after : after.slice(0, end)).trim();
221
+ }
222
+ function extractLastUserMessage(history) {
223
+ for (let i = history.length - 1; i >= 0; i--) {
224
+ const entry = history[i];
225
+ if (!entry || entry.role !== 'user')
226
+ continue;
227
+ const text = (entry.parts ?? [])
228
+ .map((part) => (typeof part?.text === 'string' ? part.text : ''))
229
+ .filter(Boolean)
230
+ .join('\n')
231
+ .trim();
232
+ if (!text)
233
+ continue;
234
+ const request = extractMarkedSection(text, 'Current user request:') ||
235
+ extractMarkedSection(text, 'User request:');
236
+ const message = extractMarkedSection(text, 'Current user message:') ||
237
+ extractMarkedSection(text, 'User message:');
238
+ return truncate(request || message || text, 120);
239
+ }
240
+ return '';
241
+ }
242
+ function metadataFromSnapshot(snapshot) {
243
+ return {
244
+ id: snapshot.id,
245
+ name: snapshot.name,
246
+ rootDir: snapshot.rootDir,
247
+ createdAt: snapshot.createdAt,
248
+ updatedAt: snapshot.updatedAt,
249
+ modelId: snapshot.modelId,
250
+ messageCount: snapshot.history.length,
251
+ lastUserMessage: extractLastUserMessage(snapshot.history),
252
+ summaryPreview: '',
253
+ };
254
+ }
255
+ export function listSessionMetadata(rootDir, env = process.env) {
256
+ return loadAllSnapshots(rootDir, env).map(metadataFromSnapshot);
257
+ }
258
+ export function loadSessionSnapshot(rootDir, identifier, env = process.env) {
259
+ const target = String(identifier ?? '').trim();
260
+ if (!target)
261
+ return null;
262
+ const snapshots = loadAllSnapshots(rootDir, env);
263
+ return (snapshots.find((snapshot) => snapshot.id === target) ??
264
+ snapshots.find((snapshot) => snapshot.name === target) ??
265
+ null);
266
+ }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import { normalizeAgentMode, } from './agent-mode.js';
3
+ import { createSessionSafetyState, } from './session-safety.js';
4
+ import { clampInteger } from './utils.js';
5
+ const DEFAULT_MAX_TOOL_STEPS = 32;
6
+ function defaultStatus(message) {
7
+ if (message.trim()) {
8
+ console.log(message);
9
+ }
10
+ }
11
+ function defaultContextLog(message) {
12
+ if (message.trim()) {
13
+ console.log(message);
14
+ }
15
+ }
16
+ function createSessionId() {
17
+ return `session_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
18
+ }
19
+ function cloneOpaqueState(value) {
20
+ if (!value || typeof value !== 'object') {
21
+ return {};
22
+ }
23
+ return JSON.parse(JSON.stringify(value));
24
+ }
25
+ function preserveProviderSelection(serverState) {
26
+ const clone = cloneOpaqueState(serverState);
27
+ const providerSelection = clone.providerSelection &&
28
+ typeof clone.providerSelection === 'object' &&
29
+ !Array.isArray(clone.providerSelection)
30
+ ? clone.providerSelection
31
+ : null;
32
+ return providerSelection ? { providerSelection } : {};
33
+ }
34
+ export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, confirmCommand = null, confirmPatch = null, requestSudoPassword = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
35
+ const createdAt = new Date().toISOString();
36
+ const initialAgentMode = normalizeAgentMode(agentMode ?? (autoYes ? 'auto-accept' : 'default'));
37
+ return {
38
+ rootDir: path.resolve(rootDir),
39
+ env,
40
+ autoYes: initialAgentMode === 'auto-accept',
41
+ agentMode: initialAgentMode,
42
+ maxToolSteps: clampInteger(maxToolSteps, DEFAULT_MAX_TOOL_STEPS, 128),
43
+ onStatus: onStatus ?? defaultStatus,
44
+ onContextLog: onContextLog ?? defaultContextLog,
45
+ onToolEvent,
46
+ confirmCommand,
47
+ confirmPatch,
48
+ requestSudoPassword,
49
+ history: JSON.parse(JSON.stringify(history)),
50
+ initialized: true,
51
+ sessionId,
52
+ sessionName,
53
+ sessionCreatedAt: createdAt,
54
+ sessionUpdatedAt: createdAt,
55
+ modelId: Number(modelId),
56
+ turnState: {
57
+ id: null,
58
+ historyStartIndex: history.length,
59
+ retrievedFilePaths: [],
60
+ injectedContext: '',
61
+ userInput: '',
62
+ },
63
+ clientState: {
64
+ stickyFilePaths: new Set(stickyFilePaths),
65
+ editJournal: [...editJournal],
66
+ editCounter: Math.max(0, editCounter),
67
+ safety,
68
+ },
69
+ serverState: cloneOpaqueState(serverState),
70
+ };
71
+ }
72
+ export function clearConversation(session) {
73
+ session.history = [];
74
+ session.serverState = preserveProviderSelection(session.serverState);
75
+ session.turnState = {
76
+ id: null,
77
+ historyStartIndex: 0,
78
+ retrievedFilePaths: [],
79
+ injectedContext: '',
80
+ userInput: '',
81
+ };
82
+ session.clientState = {
83
+ stickyFilePaths: new Set(),
84
+ editJournal: [],
85
+ editCounter: 0,
86
+ safety: createSessionSafetyState(),
87
+ };
88
+ }
89
+ export async function disposeSession(_session) { }
90
+ export function switchModel(session, modelId) {
91
+ session.modelId = Number(modelId);
92
+ return { id: session.modelId };
93
+ }
@@ -0,0 +1,188 @@
1
+ import { canStoreEditSnapshot, isEditToolName, isGitWorkTree, MAX_EDIT_JOURNAL_RECORDS, operationFromSnapshots, readFileEditSnapshot, } from './edit-journal.js';
2
+ import { clearEditFailure, collectCommandMutations, captureMutationBaseline, ensureActiveCheckpoint, recordEditFailure, recordSessionEdit, rememberCheckpointFiles, } from './session-safety.js';
3
+ import { buildAgentModeToolBlockedResult, } from './agent-mode.js';
4
+ import { dispatchTool } from './tools/index.js';
5
+ import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
6
+ const EDIT_FILE_PATH_ARG_ALIASES = [
7
+ 'filePath',
8
+ 'file_path',
9
+ 'filepath',
10
+ 'path',
11
+ 'file',
12
+ 'filename',
13
+ ];
14
+ function toolCallSummary(call) {
15
+ const args = call.args && typeof call.args === 'object' ? call.args : {};
16
+ if (call.name === 'run_command') {
17
+ return String(args.command ?? args.cmd ?? '').trim();
18
+ }
19
+ if (call.name === 'run_node_script') {
20
+ return String(args.script ?? '').trim().slice(0, 120);
21
+ }
22
+ const filePath = getEditToolFilePath(call);
23
+ if (filePath)
24
+ return filePath;
25
+ if (typeof args.query === 'string')
26
+ return args.query.slice(0, 120);
27
+ return '';
28
+ }
29
+ export function formatToolCallForStatus(call) {
30
+ const summary = toolCallSummary(call);
31
+ return summary ? `Tool: ${call.name} ${summary}` : `Tool: ${call.name}`;
32
+ }
33
+ function getEditToolFilePath(call) {
34
+ const args = call.args && typeof call.args === 'object' ? call.args : {};
35
+ for (const key of EDIT_FILE_PATH_ARG_ALIASES) {
36
+ const value = args[key];
37
+ if (typeof value === 'string' && value.trim())
38
+ return value.trim();
39
+ }
40
+ return '';
41
+ }
42
+ function recordAssistantEdit(session, call, result, before) {
43
+ if (!before || !isEditToolName(call.name))
44
+ return;
45
+ if (!result || typeof result !== 'object' || result.ok !== true) {
46
+ const filePath = getEditToolFilePath(call);
47
+ if (filePath && result?.error) {
48
+ const failure = recordEditFailure(session.clientState.safety, filePath, call.name, String(result.error));
49
+ if (failure && failure.count >= 2) {
50
+ result.staleViewEscalation = {
51
+ filePath,
52
+ failedEditAttempts: failure.count,
53
+ action: 'Re-read the full file and inspect diagnostics before retrying this edit.',
54
+ };
55
+ }
56
+ }
57
+ return;
58
+ }
59
+ const filePath = String(result.filePath ?? getEditToolFilePath(call)).trim();
60
+ if (!filePath)
61
+ return;
62
+ const after = readFileEditSnapshot(session.rootDir, filePath);
63
+ if (before.error || after.error)
64
+ return;
65
+ const operation = operationFromSnapshots(before, after);
66
+ if (!operation || !canStoreEditSnapshot(before))
67
+ return;
68
+ if ((operation === 'update' || operation === 'delete') && before.content === null) {
69
+ return;
70
+ }
71
+ const id = `edit_${++session.clientState.editCounter}`;
72
+ session.clientState.editJournal.push({
73
+ id,
74
+ turnId: session.turnState.id,
75
+ toolCallId: call.id,
76
+ toolName: call.name,
77
+ filePath,
78
+ operation,
79
+ beforeHash: before.hash,
80
+ afterHash: after.hash,
81
+ beforeContent: operation === 'create' ? null : before.content,
82
+ createdAt: new Date().toISOString(),
83
+ revertedAt: null,
84
+ revertedByToolCallId: null,
85
+ gitWorkTree: isGitWorkTree(session.rootDir),
86
+ });
87
+ if (session.clientState.editJournal.length > MAX_EDIT_JOURNAL_RECORDS) {
88
+ session.clientState.editJournal.splice(0, session.clientState.editJournal.length - MAX_EDIT_JOURNAL_RECORDS);
89
+ }
90
+ const checkpoint = ensureActiveCheckpoint(session.clientState.safety, session.turnState.id);
91
+ recordSessionEdit(session.clientState.safety, {
92
+ kind: 'tool_edit',
93
+ turnId: session.turnState.id,
94
+ toolCallId: call.id,
95
+ toolName: call.name,
96
+ filePath,
97
+ operation,
98
+ beforeHash: before.hash,
99
+ afterHash: after.hash,
100
+ beforeContent: operation === 'create' ? null : before.content,
101
+ checkpointId: checkpoint.id,
102
+ });
103
+ clearEditFailure(session.clientState.safety, filePath);
104
+ }
105
+ export async function executeLocalToolCall(toolContext, session, call) {
106
+ session.onStatus(formatToolCallForStatus(call));
107
+ try {
108
+ const agentModeBlocked = buildAgentModeToolBlockedResult(session.agentMode, call);
109
+ if (agentModeBlocked) {
110
+ const result = agentModeBlocked;
111
+ session.onToolEvent?.({ call, result });
112
+ return result;
113
+ }
114
+ const filePathBeforeEdit = isEditToolName(call.name) ? getEditToolFilePath(call) : '';
115
+ if (filePathBeforeEdit) {
116
+ rememberCheckpointFiles(session.clientState.safety, session.rootDir, [filePathBeforeEdit], session.turnState.id);
117
+ }
118
+ const beforeEditSnapshot = filePathBeforeEdit
119
+ ? readFileEditSnapshot(session.rootDir, filePathBeforeEdit)
120
+ : null;
121
+ const commandTracker = call.name === 'run_command' || call.name === 'run_node_script'
122
+ ? captureMutationBaseline(session.rootDir)
123
+ : null;
124
+ const context = {
125
+ rootDir: session.rootDir,
126
+ projectIndex: toolContext.projectIndex,
127
+ autoYes: session.autoYes,
128
+ confirmCommand: session.confirmCommand,
129
+ confirmPatch: session.confirmPatch,
130
+ requestSudoPassword: session.requestSudoPassword,
131
+ onStatus: session.onStatus,
132
+ editJournal: session.clientState.editJournal,
133
+ safety: session.clientState.safety,
134
+ currentTurnId: session.turnState.id,
135
+ currentToolCallId: call.id,
136
+ env: session.env,
137
+ markEditReverted: (editId, toolCallId) => {
138
+ const record = session.clientState.editJournal.find((entry) => entry.id === editId);
139
+ if (record) {
140
+ record.revertedAt = new Date().toISOString();
141
+ record.revertedByToolCallId = toolCallId;
142
+ }
143
+ },
144
+ };
145
+ const result = await dispatchTool(context, call);
146
+ recordAssistantEdit(session, call, result, beforeEditSnapshot);
147
+ if (result && commandTracker && (call.name === 'run_command' || call.name === 'run_node_script')) {
148
+ const checkpoint = ensureActiveCheckpoint(session.clientState.safety, session.turnState.id);
149
+ const records = collectCommandMutations({
150
+ state: session.clientState.safety,
151
+ rootDir: session.rootDir,
152
+ tracker: commandTracker,
153
+ toolName: call.name,
154
+ toolCallId: call.id,
155
+ turnId: session.turnState.id,
156
+ checkpointId: checkpoint.id,
157
+ });
158
+ if (records.length) {
159
+ invalidateShellDiagnosticsCache(session.rootDir);
160
+ rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), session.turnState.id);
161
+ result.sessionEdits = records.map((record) => ({
162
+ id: record.id,
163
+ filePath: record.filePath,
164
+ operation: record.operation,
165
+ beforeHash: record.beforeHash,
166
+ afterHash: record.afterHash,
167
+ }));
168
+ result.diagnostics = runShellDiagnostics(session.rootDir);
169
+ }
170
+ }
171
+ session.onToolEvent?.({ call, result });
172
+ return result;
173
+ }
174
+ catch (error) {
175
+ const result = {
176
+ ok: false,
177
+ failureCategory: 'tool_exception',
178
+ failureDetails: {
179
+ category: 'tool_exception',
180
+ tool: call.name,
181
+ action: 'Retry with corrected parameters or report the tool exception if it persists.',
182
+ },
183
+ error: error instanceof Error ? error.message : String(error),
184
+ };
185
+ session.onToolEvent?.({ call, result });
186
+ return result;
187
+ }
188
+ }