codemini-cli 0.6.4 → 0.6.6
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/codemini-web/dist/assets/{AboutDialog-MRopwNIL.js → AboutDialog-dFkTshay.js} +2 -2
- package/codemini-web/dist/assets/CodeWikiPanel-Dqup5Sen.js +1 -0
- package/codemini-web/dist/assets/ConfigDialog-DTehAh2r.js +1 -0
- package/codemini-web/dist/assets/{GitDiffDialog-gSysUg2J.js → GitDiffDialog-D4uMixee.js} +2 -2
- package/codemini-web/dist/assets/{MemoryDialog-DFUmo3Kl.js → MemoryDialog-DJqtWamU.js} +3 -3
- package/codemini-web/dist/assets/MessageBubble-BGnFIxcq.js +12 -0
- package/codemini-web/dist/assets/{PatchDiff-B8rwvEg5.js → PatchDiff-CpHAbmv3.js} +1 -1
- package/codemini-web/dist/assets/ProjectSelector-CgJDcTNL.js +1 -0
- package/codemini-web/dist/assets/SkillDialog-D1J46nMC.js +8 -0
- package/codemini-web/dist/assets/{SoulDialog-BLjUGqqB.js → SoulDialog-DIqK4utD.js} +1 -1
- package/codemini-web/dist/assets/chevron-right-BBG4s6Zh.js +1 -0
- package/codemini-web/dist/assets/{chunk-BO2N2NFS-6uELoidu.js → chunk-BO2N2NFS-fRXUeu1b.js} +4 -4
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-gb1UMBZ5.js → highlighted-body-OFNGDK62-CKKvx7S1.js} +1 -1
- package/codemini-web/dist/assets/index-DF9s7Tuc.css +2 -0
- package/codemini-web/dist/assets/{index-CDXQGwPs.js → index-DyIUhlc8.js} +6 -6
- package/codemini-web/dist/assets/input-BKNu4DOt.js +1 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-BM8yuNk5.js +1 -0
- package/codemini-web/dist/assets/{pencil-BhT11Ztp.js → pencil-C0uznNC_.js} +1 -1
- package/codemini-web/dist/assets/{refresh-cw-D7R5Lth6.js → refresh-cw-DUgU5bEu.js} +1 -1
- package/codemini-web/dist/assets/select-J6668k2a.js +1 -0
- package/codemini-web/dist/assets/{trash-2-BfNZcWfX.js → trash-2-H0DiWqiE.js} +1 -1
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +7 -0
- package/codemini-web/server.js +100 -74
- package/package.json +1 -1
- package/src/commands/skill.js +188 -48
- package/src/core/agent-loop.js +73 -34
- package/src/core/chat-runtime.js +22 -17
- package/src/core/config-store.js +2 -4
- package/src/core/git-oplog-change-tracker.js +96 -15
- package/src/core/shell-profile.js +3 -2
- package/src/core/tools.js +144 -51
- package/codemini-web/dist/assets/CodeWikiPanel-UpK5xGE3.js +0 -1
- package/codemini-web/dist/assets/ConfigDialog-CNl28wsj.js +0 -1
- package/codemini-web/dist/assets/MessageBubble-CGnnViv0.js +0 -12
- package/codemini-web/dist/assets/ProjectSelector-BF59M1zb.js +0 -1
- package/codemini-web/dist/assets/SkillDialog-CQTjbSiw.js +0 -8
- package/codemini-web/dist/assets/chevron-right--85xg7qk.js +0 -1
- package/codemini-web/dist/assets/index-1xqD0R5t.css +0 -2
- package/codemini-web/dist/assets/input-Ca8O_061.js +0 -1
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-ROliF8Yd.js +0 -1
- package/codemini-web/dist/assets/select-DBvcHBzs.js +0 -1
package/src/core/agent-loop.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { trimInline as _trimInline, normalizePath } from './string-utils.js';
|
|
3
3
|
import { captureToInbox, listInbox } from './memory-store.js';
|
|
4
|
-
import { requiresApprovalEvaluation } from './command-risk.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { requiresApprovalEvaluation } from './command-risk.js';
|
|
5
|
+
import { evaluateCommandPolicy } from './command-policy.js';
|
|
6
|
+
import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
|
|
7
|
+
import { normalizeToolArguments } from './tool-args.js';
|
|
8
|
+
import { storeResultIfNeeded, summarizeToolResult } from './tool-result-store.js';
|
|
9
|
+
import { markRunCommandSafeModeApproved } from './tools.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* 安全解析 JSON 字符串。
|
|
@@ -336,8 +338,12 @@ function extractToolResultMeta(toolName, result) {
|
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
export const trimInline = _trimInline;
|
|
339
|
-
|
|
340
|
-
function
|
|
341
|
+
|
|
342
|
+
export function shouldDenyHighRiskRunEvaluation(config = {}, evaluation = {}) {
|
|
343
|
+
return config?.policy?.allow_dangerous_commands !== true && String(evaluation?.risk || '').toLowerCase() === 'high';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeAssistantText(value) {
|
|
341
347
|
return String(value || '').trim();
|
|
342
348
|
}
|
|
343
349
|
|
|
@@ -613,10 +619,10 @@ export async function runAgentLoop({
|
|
|
613
619
|
let lastAutoDreamCheckStep = 0;
|
|
614
620
|
|
|
615
621
|
// Mutable tool list — grows as tool_search loads deferred tools
|
|
616
|
-
const activeTools = [...toolDefinitions];
|
|
617
|
-
|
|
618
|
-
async function maybeRunAutoDream(stepNumber = 0, { force = false } = {}) {
|
|
619
|
-
if (
|
|
622
|
+
const activeTools = [...toolDefinitions];
|
|
623
|
+
|
|
624
|
+
async function maybeRunAutoDream(stepNumber = 0, { force = false } = {}) {
|
|
625
|
+
if (executionMode === 'plan') return;
|
|
620
626
|
const interval = Math.max(1, Number(config?.memory?.auto_dream_check_interval_steps || 20));
|
|
621
627
|
const normalizedStep = Math.max(1, Number(stepNumber || 1));
|
|
622
628
|
if (!force && lastAutoDreamCheckStep > 0 && normalizedStep - lastAutoDreamCheckStep < interval) return;
|
|
@@ -724,14 +730,11 @@ export async function runAgentLoop({
|
|
|
724
730
|
|
|
725
731
|
pendingSummaryNudges = 0;
|
|
726
732
|
|
|
727
|
-
const workMode = executionMode === 'auto' ? 'normal' : executionMode;
|
|
728
733
|
const normalizedApprovalMode = ['review', 'auto', 'full_access'].includes(String(approvalMode || '').toLowerCase())
|
|
729
734
|
? String(approvalMode || '').toLowerCase()
|
|
730
|
-
:
|
|
731
|
-
? 'auto'
|
|
732
|
-
: 'review';
|
|
735
|
+
: 'review';
|
|
733
736
|
|
|
734
|
-
if (
|
|
737
|
+
if (executionMode === 'plan') {
|
|
735
738
|
const plannedLines = callsToPlanSummary(toolCalls);
|
|
736
739
|
finalText = [
|
|
737
740
|
assistantText || '',
|
|
@@ -762,9 +765,16 @@ export async function runAgentLoop({
|
|
|
762
765
|
let approved = true;
|
|
763
766
|
let approvalArgs = args;
|
|
764
767
|
let preflightErrorContent = '';
|
|
768
|
+
const runPolicyCheck = toolName === 'run'
|
|
769
|
+
? evaluateCommandPolicy(args?.command || '', config, config?.workspaceRoot || process.cwd())
|
|
770
|
+
: { allowed: true };
|
|
771
|
+
const isSafeModePolicyBlocked = toolName === 'run'
|
|
772
|
+
&& config?.policy?.safe_mode !== false
|
|
773
|
+
&& !runPolicyCheck.allowed
|
|
774
|
+
&& runPolicyCheck.reason !== 'blocked by dangerous command pattern';
|
|
765
775
|
const isSafeModeRun = toolName === 'run'
|
|
766
776
|
&& config?.policy?.safe_mode !== false
|
|
767
|
-
&& requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
|
|
777
|
+
&& (isSafeModePolicyBlocked || requiresApprovalEvaluation(args?.command || '', config?.shell?.default));
|
|
768
778
|
const isFileWriteTool = toolName === 'edit' || toolName === 'write' || toolName === 'delete';
|
|
769
779
|
const needsApproval = normalizedApprovalMode === 'full_access'
|
|
770
780
|
? false
|
|
@@ -790,20 +800,46 @@ export async function runAgentLoop({
|
|
|
790
800
|
if (toolName === 'run' && isSafeModeRun && !preflightErrorContent) {
|
|
791
801
|
try {
|
|
792
802
|
const { evaluateCommandWithLLM } = await import('./command-evaluator.js');
|
|
793
|
-
const evaluation = await evaluateCommandWithLLM({
|
|
794
|
-
command: args?.command || '',
|
|
795
|
-
config,
|
|
796
|
-
workspaceRoot: config?.workspaceRoot || process.cwd()
|
|
797
|
-
});
|
|
798
|
-
approvalArgs = {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
803
|
+
const evaluation = await evaluateCommandWithLLM({
|
|
804
|
+
command: args?.command || '',
|
|
805
|
+
config,
|
|
806
|
+
workspaceRoot: config?.workspaceRoot || process.cwd()
|
|
807
|
+
});
|
|
808
|
+
approvalArgs = {
|
|
809
|
+
...args,
|
|
810
|
+
_risk: isSafeModePolicyBlocked && evaluation.risk === 'low' ? 'medium' : evaluation.risk,
|
|
811
|
+
_evaluation: evaluation,
|
|
812
|
+
_policyBlock: isSafeModePolicyBlocked
|
|
813
|
+
? { reason: runPolicyCheck.reason, suggestion: runPolicyCheck.suggestion || '' }
|
|
814
|
+
: null
|
|
815
|
+
};
|
|
816
|
+
if (shouldDenyHighRiskRunEvaluation(config, evaluation)) {
|
|
817
|
+
preflightErrorContent = clipToolResult({
|
|
818
|
+
error: 'Command blocked by safe mode: high-risk command denied because dangerous commands are disabled',
|
|
819
|
+
evaluation
|
|
820
|
+
}, toolResultMaxChars);
|
|
821
|
+
approvalResults.set(call.id, {
|
|
822
|
+
approved: false,
|
|
823
|
+
args: approvalArgs,
|
|
824
|
+
errorContent: preflightErrorContent
|
|
825
|
+
});
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
/* LLM says low-risk + allow → auto-approve, skip confirmation panel */
|
|
829
|
+
if (!isSafeModePolicyBlocked && normalizedApprovalMode !== 'review' && evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
|
|
830
|
+
approvalResults.set(call.id, { approved: true, args: approvalArgs });
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
} catch (_) {
|
|
834
|
+
approvalArgs = {
|
|
835
|
+
...args,
|
|
836
|
+
_risk: isSafeModePolicyBlocked ? 'medium' : 'high',
|
|
837
|
+
_evaluation: null,
|
|
838
|
+
_policyBlock: isSafeModePolicyBlocked
|
|
839
|
+
? { reason: runPolicyCheck.reason, suggestion: runPolicyCheck.suggestion || '' }
|
|
840
|
+
: null
|
|
841
|
+
};
|
|
842
|
+
}
|
|
807
843
|
if (typeof handler?.prepareApproval === 'function') {
|
|
808
844
|
try {
|
|
809
845
|
const approval = await handler.prepareApproval(approvalArgs);
|
|
@@ -827,10 +863,13 @@ export async function runAgentLoop({
|
|
|
827
863
|
arguments: approvalArgs,
|
|
828
864
|
approvalDetails: toolName === 'delete' ? approvalArgs.approval
|
|
829
865
|
: (toolName === 'run' ? approvalArgs.approval : undefined)
|
|
830
|
-
});
|
|
831
|
-
approved = Boolean(decision?.approved);
|
|
832
|
-
|
|
833
|
-
|
|
866
|
+
});
|
|
867
|
+
approved = Boolean(decision?.approved);
|
|
868
|
+
if (approved && toolName === 'run' && isSafeModePolicyBlocked) {
|
|
869
|
+
approvalArgs = markRunCommandSafeModeApproved(approvalArgs);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
834
873
|
approvalResults.set(call.id, { approved, args: approvalArgs });
|
|
835
874
|
}
|
|
836
875
|
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -45,7 +45,8 @@ import {
|
|
|
45
45
|
createGitOplogChangeTracker,
|
|
46
46
|
listGitOplogChanges,
|
|
47
47
|
readGitOplogPatch,
|
|
48
|
-
undoGitOplogChange
|
|
48
|
+
undoGitOplogChange,
|
|
49
|
+
undoGitOplogChanges
|
|
49
50
|
} from './git-oplog-change-tracker.js';
|
|
50
51
|
import { createNonGitBackupManager } from './non-git-backup.js';
|
|
51
52
|
|
|
@@ -2606,8 +2607,8 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
|
|
|
2606
2607
|
sessionId: currentSession?.id || '',
|
|
2607
2608
|
sessionTitle: currentSession?.title || '',
|
|
2608
2609
|
messageCount: Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0,
|
|
2609
|
-
mode: executionMode
|
|
2610
|
-
approvalMode: config.execution?.approval_mode ||
|
|
2610
|
+
mode: executionMode || config.execution?.mode || 'normal',
|
|
2611
|
+
approvalMode: config.execution?.approval_mode || 'review',
|
|
2611
2612
|
sdkProvider: config.sdk?.provider || 'openai-compatible',
|
|
2612
2613
|
agentRole: 'general',
|
|
2613
2614
|
model: model || config.model?.name || '',
|
|
@@ -3103,18 +3104,21 @@ async function askModel({
|
|
|
3103
3104
|
projectContextGuidance
|
|
3104
3105
|
});
|
|
3105
3106
|
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
policy
|
|
3111
|
-
|
|
3112
|
-
allowed_paths: [
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3107
|
+
const toolConfig = {
|
|
3108
|
+
...config,
|
|
3109
|
+
workspaceRoot: process.cwd(),
|
|
3110
|
+
policy: {
|
|
3111
|
+
...(config.policy || {}),
|
|
3112
|
+
allowed_paths: [
|
|
3113
|
+
...(Array.isArray(config.policy?.allowed_paths) ? config.policy.allowed_paths : []),
|
|
3114
|
+
path.join(getSessionsDir(), String(session.id))
|
|
3115
|
+
]
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
|
|
3119
|
+
const { definitions, handlers, formatters, deferredDefinitions, dispose: disposeTools } = getBuiltinTools({
|
|
3120
|
+
workspaceRoot: process.cwd(),
|
|
3121
|
+
config: toolConfig,
|
|
3118
3122
|
sessionId: session.id,
|
|
3119
3123
|
onSystemEvent: onAgentEvent,
|
|
3120
3124
|
getTodos: () => normalizeTodos(session.todos),
|
|
@@ -3386,7 +3390,7 @@ async function askModel({
|
|
|
3386
3390
|
requestToolApproval,
|
|
3387
3391
|
signal,
|
|
3388
3392
|
skipAnalysisNudge,
|
|
3389
|
-
config,
|
|
3393
|
+
config: toolConfig,
|
|
3390
3394
|
changeTracker: changeTracker?.enabled
|
|
3391
3395
|
? {
|
|
3392
3396
|
begin: (meta) => beginGitOplogCapture(changeTracker, meta),
|
|
@@ -4697,7 +4701,7 @@ export async function createChatRuntime({
|
|
|
4697
4701
|
currentSession.model = model;
|
|
4698
4702
|
}
|
|
4699
4703
|
const baseSystemPrompt = systemPrompt;
|
|
4700
|
-
let executionMode = config.execution?.mode
|
|
4704
|
+
let executionMode = config.execution?.mode || 'normal';
|
|
4701
4705
|
if (hasPendingPlanApproval(currentSession)) {
|
|
4702
4706
|
executionMode = 'plan';
|
|
4703
4707
|
}
|
|
@@ -6701,6 +6705,7 @@ export async function createChatRuntime({
|
|
|
6701
6705
|
getChangeSets: () => listGitOplogChanges(changeTracker),
|
|
6702
6706
|
getChangeSetPatch: (id) => readGitOplogPatch(changeTracker, id),
|
|
6703
6707
|
undoChangeSet: (id) => undoGitOplogChange(changeTracker, id),
|
|
6708
|
+
undoChangeSets: (ids) => undoGitOplogChanges(changeTracker, ids),
|
|
6704
6709
|
reloadConfig: async (options = {}) => {
|
|
6705
6710
|
config = await loadConfig();
|
|
6706
6711
|
config.runtime = {
|
package/src/core/config-store.js
CHANGED
|
@@ -150,15 +150,13 @@ function normalizePolicyLists(config) {
|
|
|
150
150
|
next.model.name = String(next.model.name || DEFAULT_CONFIG.model.name).trim() || DEFAULT_CONFIG.model.name;
|
|
151
151
|
next.model.fast_name = String(next.model.fast_name || '').trim();
|
|
152
152
|
const rawExecutionMode = String(next.execution.mode || '').toLowerCase();
|
|
153
|
-
const rawApprovalMode = String(next.execution.approval_mode ||
|
|
153
|
+
const rawApprovalMode = String(next.execution.approval_mode || '').toLowerCase().replace(/-/g, '_');
|
|
154
154
|
next.execution.mode = ['normal', 'plan'].includes(rawExecutionMode)
|
|
155
155
|
? rawExecutionMode
|
|
156
156
|
: 'normal';
|
|
157
157
|
next.execution.approval_mode = ['review', 'auto', 'full_access'].includes(rawApprovalMode)
|
|
158
158
|
? rawApprovalMode
|
|
159
|
-
:
|
|
160
|
-
? 'auto'
|
|
161
|
-
: 'review';
|
|
159
|
+
: 'review';
|
|
162
160
|
const rawTools = Array.isArray(next.execution.always_allow_tools)
|
|
163
161
|
? next.execution.always_allow_tools
|
|
164
162
|
: [];
|
|
@@ -190,13 +190,26 @@ async function buildPatchForFile(root, relativePath, before, after) {
|
|
|
190
190
|
oldPath,
|
|
191
191
|
newPath
|
|
192
192
|
], { cwd: tmp, allowFailure: true, timeoutMs: 120_000 });
|
|
193
|
-
if (result.code !== 1) return '';
|
|
194
|
-
const escaped = relativePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
195
|
-
|
|
196
|
-
.replace(new RegExp(`diff --git a/old/${escaped} b/new/${escaped}`), `diff --git a/${relativePath} b/${relativePath}`)
|
|
197
|
-
.replace(new RegExp(`--- a/old/${escaped}`), before.exists ? `--- a/${relativePath}` : '--- /dev/null')
|
|
198
|
-
.replace(new RegExp(`\\+\\+\\+ b/new/${escaped}`), after.exists ? `+++ b/${relativePath}` : '+++ /dev/null');
|
|
199
|
-
|
|
193
|
+
if (result.code !== 1) return '';
|
|
194
|
+
const escaped = relativePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
195
|
+
let patch = result.stdout
|
|
196
|
+
.replace(new RegExp(`diff --git a/old/${escaped} b/new/${escaped}`), `diff --git a/${relativePath} b/${relativePath}`)
|
|
197
|
+
.replace(new RegExp(`--- a/old/${escaped}`), before.exists ? `--- a/${relativePath}` : '--- /dev/null')
|
|
198
|
+
.replace(new RegExp(`\\+\\+\\+ b/new/${escaped}`), after.exists ? `+++ b/${relativePath}` : '+++ /dev/null');
|
|
199
|
+
if (!before.exists && after.exists && !/^new file mode /m.test(patch)) {
|
|
200
|
+
patch = patch.replace(
|
|
201
|
+
new RegExp(`^(diff --git a/${escaped} b/${escaped})$`, 'm'),
|
|
202
|
+
`$1\nnew file mode 100644`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (before.exists && !after.exists && !/^deleted file mode /m.test(patch)) {
|
|
206
|
+
patch = patch.replace(
|
|
207
|
+
new RegExp(`^(diff --git a/${escaped} b/${escaped})$`, 'm'),
|
|
208
|
+
`$1\ndeleted file mode 100644`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return patch;
|
|
212
|
+
} finally {
|
|
200
213
|
await fs.rm(tmp, { recursive: true, force: true }).catch(() => {});
|
|
201
214
|
}
|
|
202
215
|
}
|
|
@@ -360,11 +373,11 @@ export async function readGitOplogPatch(tracker, opId) {
|
|
|
360
373
|
return fs.readFile(op.patchPath, 'utf8');
|
|
361
374
|
}
|
|
362
375
|
|
|
363
|
-
export async function undoGitOplogChange(tracker, opId) {
|
|
364
|
-
if (!isGitOplogChangeTrackerAvailable(tracker)) throw new Error('Git change oplog is not available for this session');
|
|
365
|
-
const op = await readGitOplogChange(tracker, opId);
|
|
366
|
-
if (op.revertedAt) {
|
|
367
|
-
return { ok: false, alreadyReverted: true, changeSetId: op.id, message: 'Change already reverted' };
|
|
376
|
+
export async function undoGitOplogChange(tracker, opId) {
|
|
377
|
+
if (!isGitOplogChangeTrackerAvailable(tracker)) throw new Error('Git change oplog is not available for this session');
|
|
378
|
+
const op = await readGitOplogChange(tracker, opId);
|
|
379
|
+
if (op.revertedAt) {
|
|
380
|
+
return { ok: false, alreadyReverted: true, changeSetId: op.id, message: 'Change already reverted' };
|
|
368
381
|
}
|
|
369
382
|
const patch = await fs.readFile(op.patchPath, 'utf8');
|
|
370
383
|
try {
|
|
@@ -382,6 +395,74 @@ export async function undoGitOplogChange(tracker, opId) {
|
|
|
382
395
|
timeoutMs: 120_000
|
|
383
396
|
});
|
|
384
397
|
op.revertedAt = new Date().toISOString();
|
|
385
|
-
await writeJson(path.join(tracker.opsDir, `${op.id}.json`), op);
|
|
386
|
-
return { ok: true, changeSetId: op.id };
|
|
387
|
-
}
|
|
398
|
+
await writeJson(path.join(tracker.opsDir, `${op.id}.json`), op);
|
|
399
|
+
return { ok: true, changeSetId: op.id };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function undoGitOplogChanges(tracker, opIds = []) {
|
|
403
|
+
if (!isGitOplogChangeTrackerAvailable(tracker)) throw new Error('Git change oplog is not available for this session');
|
|
404
|
+
const ids = [];
|
|
405
|
+
const seen = new Set();
|
|
406
|
+
for (const rawId of Array.isArray(opIds) ? opIds : [opIds]) {
|
|
407
|
+
const id = String(rawId || '').trim();
|
|
408
|
+
if (!id || seen.has(id)) continue;
|
|
409
|
+
seen.add(id);
|
|
410
|
+
ids.push(id);
|
|
411
|
+
}
|
|
412
|
+
if (!ids.length) throw new Error('Missing change ids');
|
|
413
|
+
if (ids.length === 1) return undoGitOplogChange(tracker, ids[0]);
|
|
414
|
+
|
|
415
|
+
const ops = await Promise.all(ids.map((id) => readGitOplogChange(tracker, id)));
|
|
416
|
+
const reverted = ops.find((op) => op.revertedAt);
|
|
417
|
+
if (reverted) {
|
|
418
|
+
return { ok: false, alreadyReverted: true, changeSetId: reverted.id, message: 'Change already reverted' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const order = new Map(ids.map((id, index) => [id, index]));
|
|
422
|
+
ops.sort((a, b) => {
|
|
423
|
+
const byTime = String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
|
|
424
|
+
if (byTime) return byTime;
|
|
425
|
+
return (order.get(b.id) ?? 0) - (order.get(a.id) ?? 0);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const patches = await Promise.all(ops.map(async (op) => ({
|
|
429
|
+
op,
|
|
430
|
+
patch: await fs.readFile(op.patchPath, 'utf8')
|
|
431
|
+
})));
|
|
432
|
+
if (!patches.some((item) => String(item.patch || '').trim())) throw new Error('Missing change patch');
|
|
433
|
+
|
|
434
|
+
const applied = [];
|
|
435
|
+
try {
|
|
436
|
+
for (const item of patches) {
|
|
437
|
+
if (!String(item.patch || '').trim()) continue;
|
|
438
|
+
await runGit(['apply', '-R', '--check', '--whitespace=nowarn'], {
|
|
439
|
+
cwd: tracker.workspaceRoot,
|
|
440
|
+
input: item.patch,
|
|
441
|
+
timeoutMs: 120_000
|
|
442
|
+
});
|
|
443
|
+
await runGit(['apply', '-R', '--whitespace=nowarn'], {
|
|
444
|
+
cwd: tracker.workspaceRoot,
|
|
445
|
+
input: item.patch,
|
|
446
|
+
timeoutMs: 120_000
|
|
447
|
+
});
|
|
448
|
+
applied.push(item);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
for (const item of applied.reverse()) {
|
|
452
|
+
await runGit(['apply', '--whitespace=nowarn'], {
|
|
453
|
+
cwd: tracker.workspaceRoot,
|
|
454
|
+
input: item.patch,
|
|
455
|
+
allowFailure: true,
|
|
456
|
+
timeoutMs: 120_000
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
throw new Error(`Cannot undo this change cleanly because newer edits conflict with it. Undo newer changes first, or revert it manually. ${error?.message || ''}`.trim());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const revertedAt = new Date().toISOString();
|
|
463
|
+
for (const op of ops) {
|
|
464
|
+
op.revertedAt = revertedAt;
|
|
465
|
+
await writeJson(path.join(tracker.opsDir, `${op.id}.json`), op);
|
|
466
|
+
}
|
|
467
|
+
return { ok: true, changeSetIds: ops.map((op) => op.id) };
|
|
468
|
+
}
|
package/src/core/tools.js
CHANGED
|
@@ -30,8 +30,8 @@ import {
|
|
|
30
30
|
sanitizeTextForModel,
|
|
31
31
|
summarizeRunOutput
|
|
32
32
|
} from './tool-output.js';
|
|
33
|
-
import {
|
|
34
|
-
normalizeFilePathValue,
|
|
33
|
+
import {
|
|
34
|
+
normalizeFilePathValue,
|
|
35
35
|
normalizePathArgs,
|
|
36
36
|
parseInlineRangePath,
|
|
37
37
|
normalizePatternArgs,
|
|
@@ -39,14 +39,28 @@ import {
|
|
|
39
39
|
normalizeWebFetchArgs,
|
|
40
40
|
normalizeWebSearchArgs,
|
|
41
41
|
normalizeWriteArgs
|
|
42
|
-
} from './tool-args.js';
|
|
43
|
-
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
44
|
-
const BACKGROUND_TASK_POLL_MS = 150;
|
|
45
|
-
const MAX_AST_ENCLOSING_BYTES = 300_000;
|
|
46
|
-
const MAX_AST_ENCLOSING_LINES = 5_000;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
let
|
|
42
|
+
} from './tool-args.js';
|
|
43
|
+
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
44
|
+
const BACKGROUND_TASK_POLL_MS = 150;
|
|
45
|
+
const MAX_AST_ENCLOSING_BYTES = 300_000;
|
|
46
|
+
const MAX_AST_ENCLOSING_LINES = 5_000;
|
|
47
|
+
const RUN_COMMAND_SAFE_MODE_APPROVED = Symbol('runCommandSafeModeApproved');
|
|
48
|
+
const backgroundTaskRegistry = new Map();
|
|
49
|
+
let backgroundTaskCounter = 0;
|
|
50
|
+
let backgroundTaskLogCursorCounter = 0;
|
|
51
|
+
|
|
52
|
+
export function markRunCommandSafeModeApproved(args = {}) {
|
|
53
|
+
const next = { ...(args && typeof args === 'object' ? args : {}) };
|
|
54
|
+
Object.defineProperty(next, RUN_COMMAND_SAFE_MODE_APPROVED, {
|
|
55
|
+
value: true,
|
|
56
|
+
enumerable: false
|
|
57
|
+
});
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasRunCommandSafeModeApproval(args = {}) {
|
|
62
|
+
return Boolean(args?.[RUN_COMMAND_SAFE_MODE_APPROVED]);
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
async function realpathIfExists(targetPath) {
|
|
52
66
|
try {
|
|
@@ -1125,11 +1139,11 @@ async function runCommand(root, config, args) {
|
|
|
1125
1139
|
throw new Error('Command blocked by policy');
|
|
1126
1140
|
}
|
|
1127
1141
|
|
|
1128
|
-
const check = evaluateCommandPolicy(command, config, root);
|
|
1129
|
-
if (!check.allowed) {
|
|
1130
|
-
throw new Error(
|
|
1131
|
-
`Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
|
|
1132
|
-
);
|
|
1142
|
+
const check = evaluateCommandPolicy(command, config, root);
|
|
1143
|
+
if (!check.allowed && !hasRunCommandSafeModeApproval(args)) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
|
|
1146
|
+
);
|
|
1133
1147
|
}
|
|
1134
1148
|
|
|
1135
1149
|
const shouldBackground =
|
|
@@ -1296,11 +1310,11 @@ async function startBackgroundTask(root, config, args) {
|
|
|
1296
1310
|
) {
|
|
1297
1311
|
throw new Error('Command blocked by policy');
|
|
1298
1312
|
}
|
|
1299
|
-
const check = evaluateCommandPolicy(command, config, root);
|
|
1300
|
-
if (!check.allowed) {
|
|
1301
|
-
throw new Error(
|
|
1302
|
-
`Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
|
|
1303
|
-
);
|
|
1313
|
+
const check = evaluateCommandPolicy(command, config, root);
|
|
1314
|
+
if (!check.allowed && !hasRunCommandSafeModeApproval(args)) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
|
|
1317
|
+
);
|
|
1304
1318
|
}
|
|
1305
1319
|
|
|
1306
1320
|
const shellSpec = resolveShell(config.shell.default);
|
|
@@ -1664,9 +1678,9 @@ function editResult(pathText, action, beforeContent, afterContent, changedLine =
|
|
|
1664
1678
|
};
|
|
1665
1679
|
}
|
|
1666
1680
|
|
|
1667
|
-
function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
1668
|
-
const lines = splitLines(content);
|
|
1669
|
-
const totalLines = lines.length;
|
|
1681
|
+
function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
1682
|
+
const lines = splitLines(content);
|
|
1683
|
+
const totalLines = lines.length;
|
|
1670
1684
|
const startLine = Math.max(1, Math.min(totalLines, Number(startLineRaw) || 1));
|
|
1671
1685
|
const endLine = Math.max(startLine, Math.min(totalLines, Number(endLineRaw) || startLine));
|
|
1672
1686
|
let startOffset = 0;
|
|
@@ -1678,12 +1692,64 @@ function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
|
1678
1692
|
endOffset += lines[i - 1].length;
|
|
1679
1693
|
if (i < endLine) endOffset += 1;
|
|
1680
1694
|
}
|
|
1681
|
-
return { startLine, endLine, startOffset, endOffset };
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const
|
|
1686
|
-
const
|
|
1695
|
+
return { startLine, endLine, startOffset, endOffset };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function normalizeNewlinesWithMap(text) {
|
|
1699
|
+
const source = String(text || '');
|
|
1700
|
+
const chars = [];
|
|
1701
|
+
const indexMap = [];
|
|
1702
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1703
|
+
const ch = source[i];
|
|
1704
|
+
if (ch === '\r') {
|
|
1705
|
+
chars.push('\n');
|
|
1706
|
+
indexMap.push(i);
|
|
1707
|
+
if (source[i + 1] === '\n') i += 1;
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
chars.push(ch);
|
|
1711
|
+
indexMap.push(i);
|
|
1712
|
+
}
|
|
1713
|
+
return { text: chars.join(''), indexMap };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function detectEol(text) {
|
|
1717
|
+
const sample = String(text || '');
|
|
1718
|
+
const crlf = (sample.match(/\r\n/g) || []).length;
|
|
1719
|
+
const loneLf = (sample.match(/(?<!\r)\n/g) || []).length;
|
|
1720
|
+
const loneCr = (sample.match(/\r(?!\n)/g) || []).length;
|
|
1721
|
+
if (crlf >= loneLf && crlf >= loneCr && crlf > 0) return '\r\n';
|
|
1722
|
+
if (loneCr > loneLf && loneCr > 0) return '\r';
|
|
1723
|
+
return '\n';
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function applyEol(text, eol) {
|
|
1727
|
+
return String(text || '').replace(/\r\n|\r|\n/g, eol || '\n');
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function findLineEndingEquivalentMatches(content, oldText) {
|
|
1731
|
+
const normalizedOld = normalizeNewlinesWithMap(oldText).text;
|
|
1732
|
+
if (!normalizedOld) return [];
|
|
1733
|
+
const normalizedContent = normalizeNewlinesWithMap(content);
|
|
1734
|
+
const matches = [];
|
|
1735
|
+
let pos = 0;
|
|
1736
|
+
while (true) {
|
|
1737
|
+
const found = normalizedContent.text.indexOf(normalizedOld, pos);
|
|
1738
|
+
if (found === -1) break;
|
|
1739
|
+
const start = normalizedContent.indexMap[found] ?? 0;
|
|
1740
|
+
const endNorm = found + normalizedOld.length;
|
|
1741
|
+
const end = endNorm >= normalizedContent.text.length
|
|
1742
|
+
? String(content || '').length
|
|
1743
|
+
: normalizedContent.indexMap[endNorm];
|
|
1744
|
+
matches.push({ start, end });
|
|
1745
|
+
pos = found + Math.max(1, normalizedOld.length);
|
|
1746
|
+
}
|
|
1747
|
+
return matches;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
async function replaceBlock(root, args, config = {}) {
|
|
1751
|
+
const relativePath = String(args?.path || '').trim();
|
|
1752
|
+
const newContent = String(args?.new_content || args?.content || '');
|
|
1687
1753
|
const target = args?.target || {};
|
|
1688
1754
|
const state = await getFileState(root, relativePath, config);
|
|
1689
1755
|
const resolved = resolveReplaceBlockTarget(state, target);
|
|
@@ -1712,14 +1778,39 @@ async function replaceText(root, args, config = {}) {
|
|
|
1712
1778
|
const rangeStart = Number(args?.start_line || args?.line);
|
|
1713
1779
|
const rangeEnd = Number(args?.end_line || args?.line);
|
|
1714
1780
|
const hasRange = Number.isFinite(rangeStart) && rangeStart > 0;
|
|
1715
|
-
const range = hasRange
|
|
1716
|
-
? lineRangeToOffsets(state.content, rangeStart, Number.isFinite(rangeEnd) && rangeEnd >= rangeStart ? rangeEnd : rangeStart)
|
|
1717
|
-
: null;
|
|
1718
|
-
const searchContent = range ? state.content.slice(range.startOffset, range.endOffset) : state.content;
|
|
1719
|
-
const occurrences = searchContent.split(oldText).length - 1;
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1781
|
+
const range = hasRange
|
|
1782
|
+
? lineRangeToOffsets(state.content, rangeStart, Number.isFinite(rangeEnd) && rangeEnd >= rangeStart ? rangeEnd : rangeStart)
|
|
1783
|
+
: null;
|
|
1784
|
+
const searchContent = range ? state.content.slice(range.startOffset, range.endOffset) : state.content;
|
|
1785
|
+
const occurrences = searchContent.split(oldText).length - 1;
|
|
1786
|
+
let newlineMatches = null;
|
|
1787
|
+
if (occurrences === 0 && /[\r\n]/.test(oldText)) {
|
|
1788
|
+
newlineMatches = findLineEndingEquivalentMatches(searchContent, oldText);
|
|
1789
|
+
if ((replaceAll && newlineMatches.length > 0) || newlineMatches.length === 1) {
|
|
1790
|
+
let cursor = 0;
|
|
1791
|
+
let replaced = '';
|
|
1792
|
+
for (const match of newlineMatches) {
|
|
1793
|
+
const originalMatch = searchContent.slice(match.start, match.end);
|
|
1794
|
+
replaced += searchContent.slice(cursor, match.start);
|
|
1795
|
+
replaced += applyEol(newText, detectEol(originalMatch));
|
|
1796
|
+
cursor = match.end;
|
|
1797
|
+
if (!replaceAll) break;
|
|
1798
|
+
}
|
|
1799
|
+
replaced += searchContent.slice(cursor);
|
|
1800
|
+
const afterContent = range
|
|
1801
|
+
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1802
|
+
: replaced;
|
|
1803
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1804
|
+
const first = newlineMatches[0];
|
|
1805
|
+
const changedLine = range
|
|
1806
|
+
? range.startLine + splitLines(searchContent.slice(0, first.start)).length - 1
|
|
1807
|
+
: splitLines(state.content.slice(0, first.start)).length;
|
|
1808
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (occurrences !== 1) {
|
|
1812
|
+
if (replaceAll && occurrences > 0) {
|
|
1813
|
+
const replaced = searchContent.replaceAll(oldText, newText);
|
|
1723
1814
|
const afterContent = range
|
|
1724
1815
|
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1725
1816
|
: state.content.replaceAll(oldText, newText);
|
|
@@ -1744,13 +1835,14 @@ async function replaceText(root, args, config = {}) {
|
|
|
1744
1835
|
lineDetails.push(` Line ${lineNum}: ${lineText}`);
|
|
1745
1836
|
searchPos = pos + oldText.length;
|
|
1746
1837
|
}
|
|
1747
|
-
const lineHint = lineDetails.length > 0 ? `\n${lineDetails.join('\n')}\n` : ' ';
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1838
|
+
const lineHint = lineDetails.length > 0 ? `\n${lineDetails.join('\n')}\n` : ' ';
|
|
1839
|
+
const effectiveOccurrences = newlineMatches?.length || occurrences;
|
|
1840
|
+
throw new Error(
|
|
1841
|
+
effectiveOccurrences === 0
|
|
1842
|
+
? 'replace_text old_text not found'
|
|
1843
|
+
: `replace_text old_text not unique; found ${effectiveOccurrences} occurrences:${lineHint}Use path:"${relativePath}:N-M" to narrow the range, set replace_all=true, or provide more unique old_text`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1754
1846
|
const replaced = searchContent.replace(oldText, newText);
|
|
1755
1847
|
const afterContent = range
|
|
1756
1848
|
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
@@ -2842,13 +2934,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2842
2934
|
run: Object.assign(
|
|
2843
2935
|
(args) => runCommand(workspaceRoot, config, args),
|
|
2844
2936
|
{
|
|
2845
|
-
prepareApproval: async (args) => ({
|
|
2846
|
-
command: args?.command || '',
|
|
2847
|
-
risk: args?._risk || 'high',
|
|
2848
|
-
evaluation: args?._evaluation || null
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2937
|
+
prepareApproval: async (args) => ({
|
|
2938
|
+
command: args?.command || '',
|
|
2939
|
+
risk: args?._risk || 'high',
|
|
2940
|
+
evaluation: args?._evaluation || null,
|
|
2941
|
+
policyBlock: args?._policyBlock || null
|
|
2942
|
+
})
|
|
2943
|
+
}
|
|
2944
|
+
),
|
|
2852
2945
|
save_memory: async (args = {}) => {
|
|
2853
2946
|
const rawScope = String(args.scope || 'global').toLowerCase();
|
|
2854
2947
|
const memoryScope = rawScope === 'repo' || rawScope === 'project' ? 'project'
|