@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.31
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 +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- 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 +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +67 -18
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -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/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +649 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +453 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- 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/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { normalizeAgentMode, } from './agent-mode.js';
|
|
3
|
+
import { createSessionGrants, } from './permissions.js';
|
|
3
4
|
import { createSessionSafetyState, } from './session-safety.js';
|
|
4
5
|
import { clampInteger } from './utils.js';
|
|
5
6
|
const DEFAULT_MAX_TOOL_STEPS = 32;
|
|
@@ -31,7 +32,7 @@ function preserveProviderSelection(serverState) {
|
|
|
31
32
|
: null;
|
|
32
33
|
return providerSelection ? { providerSelection } : {};
|
|
33
34
|
}
|
|
34
|
-
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS,
|
|
35
|
+
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, requestPermission = null, requestSudoPassword = null, requestUserInput = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
35
36
|
const createdAt = new Date().toISOString();
|
|
36
37
|
const initialAgentMode = normalizeAgentMode(agentMode ?? (autoYes ? 'auto-accept' : 'default'));
|
|
37
38
|
return {
|
|
@@ -43,9 +44,10 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
43
44
|
onStatus: onStatus ?? defaultStatus,
|
|
44
45
|
onContextLog: onContextLog ?? defaultContextLog,
|
|
45
46
|
onToolEvent,
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
grants: createSessionGrants(),
|
|
48
|
+
requestPermission,
|
|
48
49
|
requestSudoPassword,
|
|
50
|
+
requestUserInput,
|
|
49
51
|
history: JSON.parse(JSON.stringify(history)),
|
|
50
52
|
initialized: true,
|
|
51
53
|
sessionId,
|
|
@@ -69,8 +71,17 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
69
71
|
serverState: cloneOpaqueState(serverState),
|
|
70
72
|
};
|
|
71
73
|
}
|
|
74
|
+
export function startNewConversation(session) {
|
|
75
|
+
clearConversation(session);
|
|
76
|
+
const createdAt = new Date().toISOString();
|
|
77
|
+
session.sessionId = createSessionId();
|
|
78
|
+
session.sessionName = null;
|
|
79
|
+
session.sessionCreatedAt = createdAt;
|
|
80
|
+
session.sessionUpdatedAt = createdAt;
|
|
81
|
+
}
|
|
72
82
|
export function clearConversation(session) {
|
|
73
83
|
session.history = [];
|
|
84
|
+
session.grants = createSessionGrants();
|
|
74
85
|
session.serverState = preserveProviderSelection(session.serverState);
|
|
75
86
|
session.turnState = {
|
|
76
87
|
id: null,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import chalk from './colors.js';
|
|
4
|
+
import { auth } from './api/index.js';
|
|
5
|
+
import { DEFAULT_THEGITAI_HOST } from './api/default-host.js';
|
|
6
|
+
import { loginViaBrowser } from './api/browser-login.js';
|
|
7
|
+
function hyperlink(url, label) {
|
|
8
|
+
if (process.stdout.isTTY !== true)
|
|
9
|
+
return label;
|
|
10
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
11
|
+
}
|
|
12
|
+
export function formatSignInScreen({ url }) {
|
|
13
|
+
return [
|
|
14
|
+
'',
|
|
15
|
+
` ${chalk.bold.cyan('TheGitAI')}`,
|
|
16
|
+
'',
|
|
17
|
+
` ${chalk.bold('Sign in to continue.')}`,
|
|
18
|
+
' Your browser should open automatically. If not, copy this URL:',
|
|
19
|
+
'',
|
|
20
|
+
` ${chalk.cyan(url)}`,
|
|
21
|
+
'',
|
|
22
|
+
` ${hyperlink(url, chalk.cyan('→ Click here to authenticate'))}`,
|
|
23
|
+
'',
|
|
24
|
+
' If this terminal is not on the computer where that page opened, choose',
|
|
25
|
+
` ${chalk.bold('"On another computer"')} there and paste the code it gives you below.`,
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
async function promptForCode(signal) {
|
|
30
|
+
const rl = readline.createInterface({ input, output });
|
|
31
|
+
try {
|
|
32
|
+
return await rl.question(` ${chalk.bold('Authorization code:')} `, { signal });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function runSignIn({ env = process.env, login = loginViaBrowser, write = auth.writeCliAuthConfig, log = (line) => console.log(line), } = {}) {
|
|
39
|
+
const result = await login({
|
|
40
|
+
serverUrl: DEFAULT_THEGITAI_HOST,
|
|
41
|
+
deviceName: env.THEGITAI_DEVICE_NAME?.trim() || undefined,
|
|
42
|
+
onUrl: (url) => log(formatSignInScreen({ url })),
|
|
43
|
+
onBrowserOpen: (opened) => {
|
|
44
|
+
if (!opened) {
|
|
45
|
+
log(chalk.dim(' (no browser opened — use the URL or the code box above)'));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
promptCode: promptForCode,
|
|
49
|
+
onPasteRejected: (message) => log(` ${chalk.red('✖')} ${message}\n` +
|
|
50
|
+
' Codes last 10 minutes. If that page has been open a while, run `ai`\n' +
|
|
51
|
+
' again for a fresh one.'),
|
|
52
|
+
});
|
|
53
|
+
write(result, env);
|
|
54
|
+
log('');
|
|
55
|
+
log(chalk.green(` ✓ Signed in as ${result.customer.email}`));
|
|
56
|
+
log('');
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
@@ -4,7 +4,6 @@ import { clearEditFailure, collectCommandMutations, captureMutationBaseline, ens
|
|
|
4
4
|
import { buildAgentModeToolBlockedResult, } from './agent-mode.js';
|
|
5
5
|
import { classifyProjectPath } from './patcher.js';
|
|
6
6
|
import { dispatchTool } from './tools/index.js';
|
|
7
|
-
import { syncIndexFromDisk } from './project-index.js';
|
|
8
7
|
import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
|
|
9
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
|
|
10
9
|
import { extractTodosArg } from './todo-list.js';
|
|
@@ -69,7 +68,7 @@ function editToolWritesSeparateOutput(call) {
|
|
|
69
68
|
const output = args.outputPath ?? args.output_path;
|
|
70
69
|
return typeof output === 'string' && output.trim().length > 0;
|
|
71
70
|
}
|
|
72
|
-
async function collectTrackedCommandMutations({ session,
|
|
71
|
+
async function collectTrackedCommandMutations({ session, result, tracker, toolName, toolCallId, turnId, }) {
|
|
73
72
|
const checkpoint = ensureActiveCheckpoint(session.clientState.safety, turnId);
|
|
74
73
|
const records = collectCommandMutations({
|
|
75
74
|
state: session.clientState.safety,
|
|
@@ -84,40 +83,11 @@ async function collectTrackedCommandMutations({ session, projectIndex, result, t
|
|
|
84
83
|
return;
|
|
85
84
|
invalidateShellDiagnosticsCache(session.rootDir);
|
|
86
85
|
rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), turnId);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
removed: records.filter((record) => record.operation === 'delete').length,
|
|
93
|
-
indexedChunks: 0,
|
|
94
|
-
retrievalTokensUsed: 0,
|
|
95
|
-
};
|
|
96
|
-
if (priorSync?.error) {
|
|
97
|
-
result.repoSync = {
|
|
98
|
-
...mutationCounts,
|
|
99
|
-
indexSyncError: priorSync.error,
|
|
100
|
-
skipped: true,
|
|
101
|
-
reason: 'local index sync failed after command execution',
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
try {
|
|
106
|
-
const repoSync = await syncIndexFromDisk(projectIndex);
|
|
107
|
-
result.repoSync = {
|
|
108
|
-
...repoSync,
|
|
109
|
-
added: Math.max(repoSync.added, mutationCounts.added),
|
|
110
|
-
modified: Math.max(repoSync.modified, mutationCounts.modified),
|
|
111
|
-
removed: Math.max(repoSync.removed, mutationCounts.removed),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
const indexSyncError = error instanceof Error ? error.message : String(error);
|
|
116
|
-
result.repoSync = { ...mutationCounts, indexSyncError };
|
|
117
|
-
session.onStatus(`Repository changes were recorded, but local index sync failed: ${indexSyncError}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
86
|
+
result.repoSync = {
|
|
87
|
+
added: records.filter((record) => record.operation === 'create').length,
|
|
88
|
+
modified: records.filter((record) => record.operation === 'update').length,
|
|
89
|
+
removed: records.filter((record) => record.operation === 'delete').length,
|
|
90
|
+
};
|
|
121
91
|
result.sessionEdits = records.map((record) => ({
|
|
122
92
|
id: record.id,
|
|
123
93
|
filePath: record.filePath,
|
|
@@ -127,7 +97,7 @@ async function collectTrackedCommandMutations({ session, projectIndex, result, t
|
|
|
127
97
|
}));
|
|
128
98
|
result.diagnostics = runShellDiagnostics(session.rootDir);
|
|
129
99
|
}
|
|
130
|
-
export async function collectBackgroundJobUiKillMutations({ session,
|
|
100
|
+
export async function collectBackgroundJobUiKillMutations({ session, jobId, result, }) {
|
|
131
101
|
const normalizedJobId = String(jobId ?? result.snapshot?.id ?? '').trim();
|
|
132
102
|
if (!normalizedJobId || !result.ok)
|
|
133
103
|
return;
|
|
@@ -137,7 +107,6 @@ export async function collectBackgroundJobUiKillMutations({ session, projectInde
|
|
|
137
107
|
const mutationResult = result;
|
|
138
108
|
await collectTrackedCommandMutations({
|
|
139
109
|
session,
|
|
140
|
-
projectIndex,
|
|
141
110
|
result: mutationResult,
|
|
142
111
|
tracker: tracked.tracker,
|
|
143
112
|
toolName: tracked.toolName,
|
|
@@ -151,7 +120,7 @@ export async function collectBackgroundJobUiKillMutations({ session, projectInde
|
|
|
151
120
|
backgroundCommandTrackers.delete(normalizedJobId);
|
|
152
121
|
}
|
|
153
122
|
}
|
|
154
|
-
export async function collectBackgroundJobUiOutputMutations({ session,
|
|
123
|
+
export async function collectBackgroundJobUiOutputMutations({ session, jobId, }) {
|
|
155
124
|
const normalizedJobId = String(jobId ?? '').trim();
|
|
156
125
|
if (!normalizedJobId)
|
|
157
126
|
return;
|
|
@@ -165,7 +134,6 @@ export async function collectBackgroundJobUiOutputMutations({ session, projectIn
|
|
|
165
134
|
return;
|
|
166
135
|
await collectTrackedCommandMutations({
|
|
167
136
|
session,
|
|
168
|
-
projectIndex,
|
|
169
137
|
result: {},
|
|
170
138
|
tracker: tracked.tracker,
|
|
171
139
|
toolName: tracked.toolName,
|
|
@@ -244,7 +212,7 @@ function recordAssistantEdit(session, call, result, before) {
|
|
|
244
212
|
});
|
|
245
213
|
clearEditFailure(session.clientState.safety, filePath);
|
|
246
214
|
}
|
|
247
|
-
export async function executeLocalToolCall(
|
|
215
|
+
export async function executeLocalToolCall(session, call) {
|
|
248
216
|
session.onStatus(formatToolCallForStatus(call));
|
|
249
217
|
try {
|
|
250
218
|
const agentModeBlocked = buildAgentModeToolBlockedResult(session.agentMode, call);
|
|
@@ -275,10 +243,9 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
275
243
|
const context = {
|
|
276
244
|
rootDir: session.rootDir,
|
|
277
245
|
sessionId: session.sessionId,
|
|
278
|
-
projectIndex: toolContext.projectIndex,
|
|
279
246
|
autoYes: session.autoYes,
|
|
280
|
-
|
|
281
|
-
|
|
247
|
+
grants: session.grants,
|
|
248
|
+
requestPermission: session.requestPermission,
|
|
282
249
|
requestSudoPassword: session.requestSudoPassword,
|
|
283
250
|
onStatus: session.onStatus,
|
|
284
251
|
editJournal: session.clientState.editJournal,
|
|
@@ -299,7 +266,6 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
299
266
|
if (result && typeof result === 'object' && commandTracker) {
|
|
300
267
|
await collectTrackedCommandMutations({
|
|
301
268
|
session,
|
|
302
|
-
projectIndex: toolContext.projectIndex,
|
|
303
269
|
result,
|
|
304
270
|
tracker: commandTracker,
|
|
305
271
|
toolName: call.name,
|
|
@@ -326,7 +292,6 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
326
292
|
if (tracked) {
|
|
327
293
|
await collectTrackedCommandMutations({
|
|
328
294
|
session,
|
|
329
|
-
projectIndex: toolContext.projectIndex,
|
|
330
295
|
result,
|
|
331
296
|
tracker: tracked.tracker,
|
|
332
297
|
toolName: tracked.toolName,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
2
|
import { classifyProjectPath, deleteProjectFile } from '../patcher.js';
|
|
3
3
|
import { isTuiMode } from '../runtime-mode.js';
|
|
4
|
-
import { removeIndexFile } from '../project-index.js';
|
|
5
4
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
|
|
5
|
+
import { ensurePermission } from '../permissions.js';
|
|
6
6
|
export async function deleteFile(context, args) {
|
|
7
|
-
const { rootDir
|
|
7
|
+
const { rootDir } = context;
|
|
8
8
|
const filePath = String(args.filePath ?? '').trim();
|
|
9
9
|
if (!filePath) {
|
|
10
10
|
return { ok: false, error: 'filePath is required' };
|
|
@@ -24,10 +24,22 @@ export async function deleteFile(context, args) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
const scratchPath = pathKind === 'scratch';
|
|
27
|
+
if (!scratchPath) {
|
|
28
|
+
const denied = await ensurePermission(context, {
|
|
29
|
+
bucket: 'delete',
|
|
30
|
+
title: 'Approve file deletion?',
|
|
31
|
+
body: `Delete ${filePath}`,
|
|
32
|
+
filePath,
|
|
33
|
+
}, 'delete_file', { filePath });
|
|
34
|
+
if (denied) {
|
|
35
|
+
if (!isTuiMode())
|
|
36
|
+
console.log(chalk.dim(` ⏭ Delete skipped: ${filePath}`));
|
|
37
|
+
return denied;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
const result = deleteProjectFile(rootDir, filePath);
|
|
28
41
|
if (result.deleted) {
|
|
29
42
|
if (!scratchPath) {
|
|
30
|
-
await removeIndexFile(projectIndex, filePath);
|
|
31
43
|
invalidateShellDiagnosticsCache(rootDir, filePath);
|
|
32
44
|
}
|
|
33
45
|
if (!isTuiMode())
|
package/dist/src/tools/index.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { shellJobOutput } from './shell-job-output.js';
|
|
2
2
|
import { deleteFile } from './delete-file.js';
|
|
3
|
-
import { findSymbol } from './find-symbol.js';
|
|
4
3
|
import { getDiagnostics } from './get-diagnostics.js';
|
|
5
4
|
import { grepCode } from './grep-code.js';
|
|
6
|
-
import { hoverSymbol } from './hover-symbol.js';
|
|
7
5
|
import { listDirectories } from './list-directories.js';
|
|
8
6
|
import { listCheckpoints } from './list-checkpoints.js';
|
|
9
7
|
import { listFiles } from './list-files.js';
|
|
10
8
|
import { listSessionEdits } from './list-session-edits.js';
|
|
11
|
-
import { listSymbols } from './list-symbols.js';
|
|
12
9
|
import { patchFile } from './patch-file.js';
|
|
13
10
|
import { readDocument } from './read-document.js';
|
|
14
11
|
import { readFile } from './read-file.js';
|
|
@@ -16,25 +13,20 @@ import { replaceDocumentText } from './replace-document-text.js';
|
|
|
16
13
|
import { runShellCommand } from './run-command.js';
|
|
17
14
|
import { runNodeScript } from './run-node-script.js';
|
|
18
15
|
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
19
|
-
import { searchCode } from './search-code.js';
|
|
20
|
-
import { getSignatureHelp } from './signature-help.js';
|
|
21
16
|
import { shellJobKill } from './shell-job-kill.js';
|
|
22
17
|
import { strReplace } from './str-replace.js';
|
|
23
18
|
import { undoEdit } from './undo-edit.js';
|
|
24
19
|
import { updateTodos } from './update-todos.js';
|
|
20
|
+
import { readImageFile } from './read-image-file.js';
|
|
21
|
+
import { saveGeneratedImage } from './save-generated-image.js';
|
|
25
22
|
import { writeFile } from './write-file.js';
|
|
26
23
|
export const TOOL_MAP = {
|
|
27
|
-
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
28
24
|
list_files: (context, args) => listFiles(context, args),
|
|
29
25
|
list_directories: (context, args) => listDirectories(context, args),
|
|
30
26
|
read_file: (context, args) => readFile(context, args),
|
|
31
27
|
read_document: (context, args) => readDocument(context.rootDir, args, context.env),
|
|
32
28
|
replace_document_text: replaceDocumentText,
|
|
33
29
|
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
30
|
get_diagnostics: (context, args) => getDiagnostics(context, args),
|
|
39
31
|
list_checkpoints: (context) => listCheckpoints(context),
|
|
40
32
|
list_session_edits: (context) => listSessionEdits(context),
|
|
@@ -50,6 +42,17 @@ export const TOOL_MAP = {
|
|
|
50
42
|
shell_job_output: shellJobOutput,
|
|
51
43
|
shell_job_kill: shellJobKill,
|
|
52
44
|
update_todos: updateTodos,
|
|
45
|
+
analyze_image: (context, args) => readImageFile(context, args),
|
|
46
|
+
generate_image: (_context, args) => {
|
|
47
|
+
if (!String(args.base64Data ?? '').trim()) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: 'This CLI build cannot generate images without server-supplied image bytes. Update TheGitAI CLI.',
|
|
51
|
+
failureCategory: 'tool_exception',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return saveGeneratedImage(args);
|
|
55
|
+
},
|
|
53
56
|
};
|
|
54
57
|
function invalidToolCall(error) {
|
|
55
58
|
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,
|