codemini-cli 0.3.9 → 0.4.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.
- package/README.md +50 -6
- package/deployment.md +6 -6
- package/package.json +3 -1
- package/src/core/agent-loop.js +103 -115
- package/src/core/chat-runtime.js +134 -6
- package/src/core/command-evaluator.js +66 -0
- package/src/core/command-policy.js +16 -0
- package/src/core/command-risk.js +148 -0
- package/src/core/config-store.js +2 -0
- package/src/core/constants.js +0 -1
- package/src/core/context-compact.js +32 -8
- package/src/core/default-system-prompt.js +15 -8
- package/src/core/dream-consolidate.js +54 -14
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +3 -2
- package/src/core/paths.js +1 -1
- package/src/core/project-index.js +2 -2
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/shell-profile.js +13 -9
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +118 -315
- package/src/tui/chat-app.js +362 -45
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/tui/tool-activity/presenters/system.js +1 -1
package/src/core/chat-runtime.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from './provider/index.js';
|
|
11
11
|
import { isDangerousCommand, runShellCommand } from './shell.js';
|
|
12
12
|
import { getBuiltinTools } from './tools.js';
|
|
13
|
-
import { listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
|
|
13
|
+
import { createSession, listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
|
|
14
14
|
import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
|
|
15
15
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
16
16
|
import { appendInputHistory, loadInputHistory } from './input-history-store.js';
|
|
@@ -25,7 +25,7 @@ import { buildSystemPromptWithSoul } from './soul.js';
|
|
|
25
25
|
import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
|
|
26
26
|
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
27
27
|
import { buildMemorySnapshot } from './memory-prompt.js';
|
|
28
|
-
import { forgetMemory, listMemories, searchMemories, captureToInbox, listInbox } from './memory-store.js';
|
|
28
|
+
import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToInbox, listInbox } from './memory-store.js';
|
|
29
29
|
import { runDreamConsolidation } from './dream-consolidate.js';
|
|
30
30
|
import { normalizePlanState } from './plan-state.js';
|
|
31
31
|
import { countActiveTodos, normalizeTodos } from './todo-state.js';
|
|
@@ -152,10 +152,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
152
152
|
agents: '列出/运行子代理角色',
|
|
153
153
|
config: '设置/读取/列出/重置配置',
|
|
154
154
|
memory: '查看/搜索/删除持久记忆',
|
|
155
|
+
dream: '整理记忆收件箱(dream consolidation)',
|
|
155
156
|
history: '查看/恢复会话',
|
|
156
157
|
debug: '运行时调试开关',
|
|
157
158
|
retry: '重试上一条用户请求',
|
|
158
159
|
stop: '中止当前回答',
|
|
160
|
+
new: '开始新会话',
|
|
159
161
|
yes: '确认当前待审批计划并开始执行',
|
|
160
162
|
edit: '修改当前待审批计划',
|
|
161
163
|
reject: '拒绝当前待审批计划'
|
|
@@ -169,6 +171,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
169
171
|
planCommand: '规划命令',
|
|
170
172
|
agentCommand: '子代理命令',
|
|
171
173
|
memoryCommand: '记忆命令',
|
|
174
|
+
dreamCommand: '记忆整理命令',
|
|
172
175
|
debugCommand: '调试命令',
|
|
173
176
|
keyboardDebugCommand: '键盘调试命令',
|
|
174
177
|
compactCommand: '上下文压缩命令',
|
|
@@ -246,10 +249,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
246
249
|
agents: 'run/list sub-agent roles',
|
|
247
250
|
config: 'set/get/list/reset config values',
|
|
248
251
|
memory: 'list/search/delete persistent memories',
|
|
252
|
+
dream: 'consolidate memory inbox (dream)',
|
|
249
253
|
history: 'list/resume sessions',
|
|
250
254
|
debug: 'runtime debug switches',
|
|
251
255
|
retry: 'retry the last user request',
|
|
252
256
|
stop: 'stop the current response',
|
|
257
|
+
new: 'start a new session',
|
|
253
258
|
yes: 'approve the pending plan and start execution',
|
|
254
259
|
edit: 'revise the pending plan',
|
|
255
260
|
reject: 'reject the pending plan'
|
|
@@ -263,6 +268,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
263
268
|
planCommand: 'planning command',
|
|
264
269
|
agentCommand: 'sub-agent command',
|
|
265
270
|
memoryCommand: 'memory command',
|
|
271
|
+
dreamCommand: 'dream consolidation command',
|
|
266
272
|
debugCommand: 'debug command',
|
|
267
273
|
keyboardDebugCommand: 'keyboard debug command',
|
|
268
274
|
compactCommand: 'context compaction command',
|
|
@@ -1636,6 +1642,18 @@ async function writeMarkdownInProjectDir(subDir, title, body, fallbackName, sess
|
|
|
1636
1642
|
return filePath;
|
|
1637
1643
|
}
|
|
1638
1644
|
|
|
1645
|
+
async function removePlanFileIfPresent(planState) {
|
|
1646
|
+
const filePath = String(planState?.filePath || '').trim();
|
|
1647
|
+
if (!filePath) return;
|
|
1648
|
+
try {
|
|
1649
|
+
await fs.unlink(filePath);
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (error?.code !== 'ENOENT') {
|
|
1652
|
+
// Best-effort cleanup: keep the main approval flow moving.
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1639
1657
|
function buildSpecTemplate(topic) {
|
|
1640
1658
|
return `
|
|
1641
1659
|
# Spec: ${topic}
|
|
@@ -2777,7 +2795,7 @@ export async function createChatRuntime({
|
|
|
2777
2795
|
if (initialIndex?.summary) {
|
|
2778
2796
|
startupEvents.push({
|
|
2779
2797
|
type: 'system_tool',
|
|
2780
|
-
name: 'project_index(.codemini
|
|
2798
|
+
name: 'project_index(.codemini/project-map.json,.codemini/file-index.json)',
|
|
2781
2799
|
status: 'done',
|
|
2782
2800
|
summary: initialIndex.summary
|
|
2783
2801
|
});
|
|
@@ -2901,7 +2919,8 @@ export async function createChatRuntime({
|
|
|
2901
2919
|
'/agents',
|
|
2902
2920
|
'/compact',
|
|
2903
2921
|
'/debug',
|
|
2904
|
-
'/retry'
|
|
2922
|
+
'/retry',
|
|
2923
|
+
'/new'
|
|
2905
2924
|
];
|
|
2906
2925
|
const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
|
|
2907
2926
|
|
|
@@ -2920,10 +2939,12 @@ export async function createChatRuntime({
|
|
|
2920
2939
|
{ name: 'agents', description: completionCopy.commands.agents },
|
|
2921
2940
|
{ name: 'config', description: completionCopy.commands.config },
|
|
2922
2941
|
{ name: 'memory', description: completionCopy.commands.memory },
|
|
2942
|
+
{ name: 'dream', description: completionCopy.commands.dream },
|
|
2923
2943
|
{ name: 'history', description: completionCopy.commands.history },
|
|
2924
2944
|
{ name: 'debug', description: completionCopy.commands.debug },
|
|
2925
2945
|
{ name: 'retry', description: completionCopy.commands.retry },
|
|
2926
|
-
{ name: 'stop', description: completionCopy.commands.stop }
|
|
2946
|
+
{ name: 'stop', description: completionCopy.commands.stop },
|
|
2947
|
+
{ name: 'new', description: completionCopy.commands.new }
|
|
2927
2948
|
];
|
|
2928
2949
|
const out = [];
|
|
2929
2950
|
for (const cmd of commands.values()) {
|
|
@@ -2969,6 +2990,7 @@ export async function createChatRuntime({
|
|
|
2969
2990
|
const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
|
|
2970
2991
|
const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
|
|
2971
2992
|
const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
|
|
2993
|
+
const dreamTemplates = ['/dream', '/dream --dry-run', '/dream --scope=project', '/dream --scope=global'];
|
|
2972
2994
|
const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
|
|
2973
2995
|
const slashTemplates = [
|
|
2974
2996
|
...configTemplates,
|
|
@@ -2980,6 +3002,7 @@ export async function createChatRuntime({
|
|
|
2980
3002
|
...planTemplates,
|
|
2981
3003
|
...agentTemplates,
|
|
2982
3004
|
...debugTemplates,
|
|
3005
|
+
...dreamTemplates,
|
|
2983
3006
|
...compactTemplates,
|
|
2984
3007
|
'/retry',
|
|
2985
3008
|
'/status'
|
|
@@ -3046,6 +3069,7 @@ export async function createChatRuntime({
|
|
|
3046
3069
|
}
|
|
3047
3070
|
for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
|
|
3048
3071
|
for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
|
|
3072
|
+
for (const template of dreamTemplates) registerSuggestion(template, completionCopy.generic.dreamCommand);
|
|
3049
3073
|
for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
|
|
3050
3074
|
registerSuggestion('/retry', completionCopy.generic.retryCommand);
|
|
3051
3075
|
registerSuggestion('/status', completionCopy.generic.statusCommand);
|
|
@@ -3288,6 +3312,75 @@ export async function createChatRuntime({
|
|
|
3288
3312
|
await saveSession(currentSession);
|
|
3289
3313
|
};
|
|
3290
3314
|
|
|
3315
|
+
const captureCompactSummary = async ({ summary, mode, beforeTokens, afterTokens }) => {
|
|
3316
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
|
|
3317
|
+
const normalizedSummary = String(summary || '').trim();
|
|
3318
|
+
if (!normalizedSummary) return null;
|
|
3319
|
+
const entrySummary = `Context compacted (${mode}): ${beforeTokens} -> ${afterTokens} tokens`;
|
|
3320
|
+
return captureToInbox({
|
|
3321
|
+
scope: 'repo',
|
|
3322
|
+
type: 'observation',
|
|
3323
|
+
summary: entrySummary,
|
|
3324
|
+
details: normalizedSummary,
|
|
3325
|
+
tags: ['compact', 'context-summary'],
|
|
3326
|
+
source: 'auto-compact'
|
|
3327
|
+
}).catch(() => null);
|
|
3328
|
+
};
|
|
3329
|
+
|
|
3330
|
+
const shouldAutoCaptureUserPrompt = (text) => {
|
|
3331
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return false;
|
|
3332
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3333
|
+
if (value.length < 12) return false;
|
|
3334
|
+
const actionPattern =
|
|
3335
|
+
/\b(add|build|fix|implement|change|update|refactor|test|debug|remember|capture|continue|review)\b|实现|增加|添加|修复|修改|更新|重构|测试|调试|记住|继续|检查|沉淀|捕获/i;
|
|
3336
|
+
return actionPattern.test(value);
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const classifyDirectMemoryPrompt = (text) => {
|
|
3340
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
|
|
3341
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3342
|
+
if (value.length < 6) return null;
|
|
3343
|
+
const userPreferencePattern =
|
|
3344
|
+
/(?:记住|请记住|以后|后续|我偏好|我的偏好|我喜欢|我习惯|不要再|别再|always remember|remember that|i prefer|my preference|don't|do not)/i;
|
|
3345
|
+
if (!userPreferencePattern.test(value)) return null;
|
|
3346
|
+
const projectPattern = /(?:本项目|这个项目|当前项目|这个仓库|当前仓库|repo|repository|project)/i;
|
|
3347
|
+
const isProject = projectPattern.test(value);
|
|
3348
|
+
return {
|
|
3349
|
+
scope: isProject ? 'project' : 'user',
|
|
3350
|
+
kind: isProject ? 'workflow' : 'preference',
|
|
3351
|
+
content: value
|
|
3352
|
+
};
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
const saveDirectMemoryPrompt = async (text) => {
|
|
3356
|
+
const direct = classifyDirectMemoryPrompt(text);
|
|
3357
|
+
if (!direct) return null;
|
|
3358
|
+
return rememberMemory({
|
|
3359
|
+
scope: direct.scope,
|
|
3360
|
+
content: direct.content,
|
|
3361
|
+
kind: direct.kind,
|
|
3362
|
+
summary: direct.content.slice(0, 80),
|
|
3363
|
+
source: 'auto-user-directive',
|
|
3364
|
+
replaceSimilar: true,
|
|
3365
|
+
workspaceRoot: process.cwd(),
|
|
3366
|
+
config
|
|
3367
|
+
}).catch(() => null);
|
|
3368
|
+
};
|
|
3369
|
+
|
|
3370
|
+
const captureUserPromptForDream = async (text) => {
|
|
3371
|
+
if (classifyDirectMemoryPrompt(text)) return null;
|
|
3372
|
+
if (!shouldAutoCaptureUserPrompt(text)) return null;
|
|
3373
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3374
|
+
return captureToInbox({
|
|
3375
|
+
scope: 'repo',
|
|
3376
|
+
type: 'observation',
|
|
3377
|
+
summary: `User task: ${value.slice(0, 120)}`,
|
|
3378
|
+
details: value,
|
|
3379
|
+
tags: ['user-prompt'],
|
|
3380
|
+
source: 'auto-user-prompt'
|
|
3381
|
+
}).catch(() => null);
|
|
3382
|
+
};
|
|
3383
|
+
|
|
3291
3384
|
const buildActiveSystemPrompt = async () => {
|
|
3292
3385
|
const soulPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
|
|
3293
3386
|
const memorySnapshot = await buildMemorySnapshot({
|
|
@@ -3382,10 +3475,27 @@ export async function createChatRuntime({
|
|
|
3382
3475
|
}
|
|
3383
3476
|
if (parsedInput.type === 'slash') {
|
|
3384
3477
|
if (parsedInput.command === 'exit') return { type: 'exit' };
|
|
3478
|
+
if (parsedInput.command === 'new') {
|
|
3479
|
+
const fresh = await createSession();
|
|
3480
|
+
currentSession = fresh;
|
|
3481
|
+
executionMode = config.execution?.mode || 'auto';
|
|
3482
|
+
compactState.backupMessages = null;
|
|
3483
|
+
setResultDir(path.join(getSessionsDir(), String(fresh.id)));
|
|
3484
|
+
historyIdCache = [fresh.id, ...historyIdCache.filter((id) => id !== fresh.id)];
|
|
3485
|
+
historySessionCache = [
|
|
3486
|
+
{ id: fresh.id, messageCount: 0 },
|
|
3487
|
+
...historySessionCache.filter((s) => s.id !== fresh.id)
|
|
3488
|
+
];
|
|
3489
|
+
return {
|
|
3490
|
+
type: 'system',
|
|
3491
|
+
text: `New session started: ${fresh.id}`,
|
|
3492
|
+
restoredMessages: []
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3385
3495
|
if (parsedInput.command === 'help') {
|
|
3386
3496
|
return {
|
|
3387
3497
|
type: 'system',
|
|
3388
|
-
text: 'Commands: /help /exit /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /edit /reject /agents /config /memory /capture /inbox /dream /history /debug /retry /<custom> !<shell>'
|
|
3498
|
+
text: 'Commands: /help /exit /new /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /edit /reject /agents /config /memory /capture /inbox /dream /history /debug /retry /<custom> !<shell>'
|
|
3389
3499
|
};
|
|
3390
3500
|
}
|
|
3391
3501
|
if (parsedInput.command === 'status') {
|
|
@@ -3428,6 +3538,7 @@ export async function createChatRuntime({
|
|
|
3428
3538
|
});
|
|
3429
3539
|
activeSubSession = null;
|
|
3430
3540
|
currentSession.planState = null;
|
|
3541
|
+
await removePlanFileIfPresent(planState);
|
|
3431
3542
|
executionMode = 'auto';
|
|
3432
3543
|
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3433
3544
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
@@ -3457,7 +3568,9 @@ export async function createChatRuntime({
|
|
|
3457
3568
|
if (!hasPendingPlanApproval(currentSession)) {
|
|
3458
3569
|
return { type: 'system', text: 'No pending plan approval.' };
|
|
3459
3570
|
}
|
|
3571
|
+
const planState = { ...currentSession.planState };
|
|
3460
3572
|
currentSession.planState = null;
|
|
3573
|
+
await removePlanFileIfPresent(planState);
|
|
3461
3574
|
executionMode = 'auto';
|
|
3462
3575
|
const text = 'Pending plan rejected and cleared.';
|
|
3463
3576
|
await persistLocalExchange(line, text);
|
|
@@ -3597,6 +3710,7 @@ export async function createChatRuntime({
|
|
|
3597
3710
|
});
|
|
3598
3711
|
activeSubSession = null;
|
|
3599
3712
|
currentSession.planState = null;
|
|
3713
|
+
await removePlanFileIfPresent(planState);
|
|
3600
3714
|
executionMode = 'auto';
|
|
3601
3715
|
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3602
3716
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
@@ -3960,6 +4074,12 @@ export async function createChatRuntime({
|
|
|
3960
4074
|
compactState.backupMessages = structuredClone(currentSession.messages);
|
|
3961
4075
|
currentSession.messages = result.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
|
|
3962
4076
|
await saveSession(currentSession);
|
|
4077
|
+
await captureCompactSummary({
|
|
4078
|
+
summary: result.summary,
|
|
4079
|
+
mode: compactState.mode,
|
|
4080
|
+
beforeTokens,
|
|
4081
|
+
afterTokens
|
|
4082
|
+
});
|
|
3963
4083
|
await persistLocalExchange(line, report, { includeUser: false });
|
|
3964
4084
|
return { type: 'system', text: report };
|
|
3965
4085
|
}
|
|
@@ -4080,6 +4200,12 @@ export async function createChatRuntime({
|
|
|
4080
4200
|
at: new Date().toISOString()
|
|
4081
4201
|
}));
|
|
4082
4202
|
await saveSession(currentSession);
|
|
4203
|
+
await captureCompactSummary({
|
|
4204
|
+
summary: autoResult.summary,
|
|
4205
|
+
mode: compactState.mode,
|
|
4206
|
+
beforeTokens: currentTokens,
|
|
4207
|
+
afterTokens: estimateMessagesTokens(currentSession.messages)
|
|
4208
|
+
});
|
|
4083
4209
|
if (onAgentEvent) {
|
|
4084
4210
|
onAgentEvent({
|
|
4085
4211
|
type: 'compact:auto',
|
|
@@ -4092,6 +4218,7 @@ export async function createChatRuntime({
|
|
|
4092
4218
|
}
|
|
4093
4219
|
|
|
4094
4220
|
const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
|
|
4221
|
+
await saveDirectMemoryPrompt(expandedText);
|
|
4095
4222
|
const autoRoute = classifyAutoRoute(expandedText);
|
|
4096
4223
|
if (autoRoute.autoPlan) {
|
|
4097
4224
|
await maybeAutoDreamFromRuntime();
|
|
@@ -4143,6 +4270,7 @@ export async function createChatRuntime({
|
|
|
4143
4270
|
executionMode,
|
|
4144
4271
|
signal
|
|
4145
4272
|
});
|
|
4273
|
+
await captureUserPromptForDream(expandedText);
|
|
4146
4274
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
4147
4275
|
};
|
|
4148
4276
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createChatCompletion } from './provider/index.js';
|
|
2
|
+
|
|
3
|
+
const EVAL_TIMEOUT_MS = 15000;
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT = `You are a command safety evaluator for a coding assistant. Analyze the shell command and respond with valid JSON only, no markdown fences:
|
|
6
|
+
{"risk":"low|medium|high","description":"what this command does in one sentence","sideEffects":"potential side effects in one sentence, or none","recommendation":"allow|deny"}
|
|
7
|
+
|
|
8
|
+
Rules:
|
|
9
|
+
- Read-only commands (ls, cat, git status, git diff, grep, find, etc.) are low risk and allow.
|
|
10
|
+
- Commands that install/uninstall packages, modify files, push code, start servers, or have network side effects are medium or high.
|
|
11
|
+
- Destructive commands (rm -rf, format, sudo, dd) are high risk and deny.
|
|
12
|
+
- Consider the workspace context: the command runs in the project directory.
|
|
13
|
+
- Be concise. Maximum 1 sentence per field.`;
|
|
14
|
+
|
|
15
|
+
const FAIL_CLOSED_RESULT = Object.freeze({
|
|
16
|
+
risk: 'high',
|
|
17
|
+
description: '',
|
|
18
|
+
sideEffects: '',
|
|
19
|
+
recommendation: 'deny'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function parseEvaluation(text) {
|
|
23
|
+
try {
|
|
24
|
+
const json = JSON.parse(text);
|
|
25
|
+
const risk = String(json?.risk || '').toLowerCase();
|
|
26
|
+
const recommendation = String(json?.recommendation || '').toLowerCase();
|
|
27
|
+
return {
|
|
28
|
+
risk: ['low', 'medium', 'high'].includes(risk) ? risk : 'high',
|
|
29
|
+
description: String(json?.description || '').slice(0, 200),
|
|
30
|
+
sideEffects: String(json?.sideEffects || '').slice(0, 200),
|
|
31
|
+
recommendation: recommendation === 'allow' ? 'allow' : 'deny'
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return { ...FAIL_CLOSED_RESULT };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 用轻量 LLM 调用评估命令风险。
|
|
40
|
+
* @param {{ command: string, config: object, workspaceRoot?: string }} params
|
|
41
|
+
* @returns {Promise<{ risk: 'low'|'medium'|'high', description: string, sideEffects: string, recommendation: 'allow'|'deny' }>}
|
|
42
|
+
*/
|
|
43
|
+
export async function evaluateCommandWithLLM({ command, config, workspaceRoot }) {
|
|
44
|
+
const cmd = String(command || '').trim();
|
|
45
|
+
if (!cmd) return { ...FAIL_CLOSED_RESULT };
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await createChatCompletion({
|
|
49
|
+
sdkProvider: config?.sdk?.provider,
|
|
50
|
+
baseUrl: config?.gateway?.base_url,
|
|
51
|
+
apiKey: config?.gateway?.api_key,
|
|
52
|
+
model: config?.model?.name,
|
|
53
|
+
messages: [
|
|
54
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
55
|
+
{ role: 'user', content: `Command: ${cmd}\nWorkspace: ${workspaceRoot || process.cwd()}` }
|
|
56
|
+
],
|
|
57
|
+
temperature: 0,
|
|
58
|
+
timeoutMs: EVAL_TIMEOUT_MS
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const text = result?.text || '';
|
|
62
|
+
return parseEvaluation(text);
|
|
63
|
+
} catch {
|
|
64
|
+
return { ...FAIL_CLOSED_RESULT };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -169,8 +169,22 @@ function includesAny(haystackLower, patterns = []) {
|
|
|
169
169
|
return patterns.some((p) => haystackLower.includes(String(p).toLowerCase()));
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** bash 下会被阻止的删除类命令 token */
|
|
173
|
+
const BASH_DELETE_TOKENS = new Set(['rm', 'rmdir']);
|
|
174
|
+
/** PowerShell 下会被阻止的删除类命令 token */
|
|
175
|
+
const POWERSHELL_DELETE_TOKENS = new Set(['del', 'erase', 'rmdir', 'rd', 'remove-item', 'ri']);
|
|
176
|
+
|
|
172
177
|
function suggestionForToken(token, config) {
|
|
173
178
|
const shell = String(config?.shell?.default || '').toLowerCase();
|
|
179
|
+
|
|
180
|
+
/* 删除类命令:优先引导 LLM 使用 delete 工具 */
|
|
181
|
+
if (
|
|
182
|
+
(shell !== 'powershell' && BASH_DELETE_TOKENS.has(token)) ||
|
|
183
|
+
(shell === 'powershell' && POWERSHELL_DELETE_TOKENS.has(token))
|
|
184
|
+
) {
|
|
185
|
+
return 'Use the delete tool to remove files or directories inside the workspace. Do not use shell commands for deletion.';
|
|
186
|
+
}
|
|
187
|
+
|
|
174
188
|
if (token === 'find' || token === 'grep') {
|
|
175
189
|
return shell === 'powershell'
|
|
176
190
|
? 'Prefer structured tools like grep, list, read, and edit first. If you need shell fallback, use allowed search and context commands such as Get-ChildItem, Select-String, Get-Content, or rg when available.'
|
|
@@ -259,3 +273,5 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
259
273
|
|
|
260
274
|
return { allowed: true };
|
|
261
275
|
}
|
|
276
|
+
|
|
277
|
+
export { collectCommandTokens, firstToken };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { collectCommandTokens, firstToken } from './command-policy.js';
|
|
2
|
+
|
|
3
|
+
/* ── 只读命令 token ───────────────────────────────────────────── */
|
|
4
|
+
const READ_ONLY_TOKENS = new Set([
|
|
5
|
+
'ls', 'cat', 'head', 'tail', 'pwd', 'wc', 'sort', 'uniq',
|
|
6
|
+
'cut', 'tr', 'basename', 'dirname', 'test', 'true', 'false',
|
|
7
|
+
'whoami', 'uname', 'date', 'env', 'printenv', 'hostname',
|
|
8
|
+
'rg', 'find', 'grep', 'ag', 'ack', 'fd', 'bat',
|
|
9
|
+
'git', 'node', 'npm', 'npx', 'python', 'python3', 'py', 'pip', 'pip3',
|
|
10
|
+
'echo', 'printf', 'seq', 'yes'
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/* 只读时需要检查子命令的 token */
|
|
14
|
+
const READ_ONLY_SUBCOMMANDS = {
|
|
15
|
+
git: new Set([
|
|
16
|
+
'status', 'log', 'diff', 'branch', 'show', 'tag', 'stash',
|
|
17
|
+
'list', 'remote', 'rev-parse', 'describe', 'blame',
|
|
18
|
+
'shortlog', 'count', 'ls-files', 'ls-remote', 'ls-tree',
|
|
19
|
+
'config', '--version', 'var', 'for-each-ref', 'name-rev',
|
|
20
|
+
'merge-base', 'cherry'
|
|
21
|
+
]),
|
|
22
|
+
node: new Set(['--version', '-v', '-e', '--eval', '--print', '-p', '--help']),
|
|
23
|
+
npm: new Set([
|
|
24
|
+
'--version', '-v', 'view', 'info', 'list', 'ls', 'll', 'la',
|
|
25
|
+
'outdated', 'audit', 'pack', 'cache', 'config', 'doctor',
|
|
26
|
+
'help', 'explore', 'run', 'run-script', 'start', 'test',
|
|
27
|
+
'restart', 'stop', 'version', 'whoami'
|
|
28
|
+
]),
|
|
29
|
+
npx: new Set(['--version', '-v', '--help']),
|
|
30
|
+
python: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
31
|
+
python3: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
32
|
+
py: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
33
|
+
pip: new Set(['--version', '-V', 'list', 'show', 'search', 'check', 'debug', 'help']),
|
|
34
|
+
pip3: new Set(['--version', '-V', 'list', 'show', 'search', 'check', 'debug', 'help'])
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/* ── 高风险 pattern ────────────────────────────────────────────── */
|
|
38
|
+
const HIGH_RISK_PATTERNS = [
|
|
39
|
+
/\binstall\b/i,
|
|
40
|
+
/\bpublish\b/i,
|
|
41
|
+
/\bpush\b/i,
|
|
42
|
+
/\bcommit\b/i,
|
|
43
|
+
/\brebase\b/i,
|
|
44
|
+
/\breset\s/i,
|
|
45
|
+
/\bcheckout\s+--/i,
|
|
46
|
+
/\brm\b/i,
|
|
47
|
+
/\bdel\b/i,
|
|
48
|
+
/\bmkdi[ri]\b/i,
|
|
49
|
+
/\btouch\b/i,
|
|
50
|
+
/\bcp\b/i,
|
|
51
|
+
/\bmv\b/i,
|
|
52
|
+
/\bchmod\b/i,
|
|
53
|
+
/\bchown\b/i,
|
|
54
|
+
/\bmktemp\b/i,
|
|
55
|
+
/\btee\b/i,
|
|
56
|
+
/\bsudo\b/i,
|
|
57
|
+
/\bsu\b/,
|
|
58
|
+
/\bkill\b/i,
|
|
59
|
+
/\bpkill\b/i,
|
|
60
|
+
/\bcurl\s+.*-[A-Z]\s*(POST|PUT|DELETE|PATCH)/i,
|
|
61
|
+
/\bwget\b/i,
|
|
62
|
+
/\bdocker\s+(rm|stop|kill|rmi)\b/i,
|
|
63
|
+
/\bsystemctl\b/i,
|
|
64
|
+
/\bservice\b/i,
|
|
65
|
+
/\blaunchctl\b/i,
|
|
66
|
+
/>\s*\S/,
|
|
67
|
+
/>>\s*\S/,
|
|
68
|
+
/\|&\s*\S/
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/* ── 核心分类逻辑 ──────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 判断单个 token 是否为只读命令(含子命令检查)。
|
|
75
|
+
*/
|
|
76
|
+
function isReadOnlyToken(token, rawSegment) {
|
|
77
|
+
if (!READ_ONLY_TOKENS.has(token)) return false;
|
|
78
|
+
|
|
79
|
+
/* 需要 子命令 校验的 token */
|
|
80
|
+
const allowedSubs = READ_ONLY_SUBCOMMANDS[token];
|
|
81
|
+
if (!allowedSubs) return true; // 如 ls, pwd 等本身只读
|
|
82
|
+
|
|
83
|
+
/* 提取子命令:去掉 token 后第一个非 flag 参数 */
|
|
84
|
+
const rest = String(rawSegment || '').trim().slice(token.length).trim();
|
|
85
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
86
|
+
/* 以 - 开头的 flag 视为安全,取第一个非 flag 参数 */
|
|
87
|
+
let subcmd = '';
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (part.startsWith('-')) continue;
|
|
90
|
+
subcmd = part;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
/* 只有 token 本身或全部是 flags → 视为安全 */
|
|
94
|
+
if (!subcmd) return true;
|
|
95
|
+
if (allowedSubs.has(subcmd)) return true;
|
|
96
|
+
/* 子命令 不在白名单 → 不确定 */
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 对命令文本做快速 高风险 pattern 扫描。
|
|
102
|
+
*/
|
|
103
|
+
function matchesHighRiskPattern(text) {
|
|
104
|
+
return HIGH_RISK_PATTERNS.some((p) => p.test(text));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 分类命令风险等级。
|
|
109
|
+
* @param {string} command
|
|
110
|
+
* @param {string} [shellName='bash']
|
|
111
|
+
* @returns {'read-only'|'write-high-risk'|'ambiguous'}
|
|
112
|
+
*/
|
|
113
|
+
export function classifyCommandRisk(command, shellName = 'bash') {
|
|
114
|
+
const cmd = String(command || '').trim();
|
|
115
|
+
if (!cmd) return 'read-only';
|
|
116
|
+
|
|
117
|
+
/* 高风险 pattern 优先判断 */
|
|
118
|
+
if (matchesHighRiskPattern(cmd)) return 'write-high-risk';
|
|
119
|
+
|
|
120
|
+
/* 解析链式命令的每个 segment */
|
|
121
|
+
const tokens = collectCommandTokens(cmd);
|
|
122
|
+
if (tokens.length === 0) return 'ambiguous';
|
|
123
|
+
|
|
124
|
+
let highestRisk = 'read-only';
|
|
125
|
+
const RISK_ORDER = { 'read-only': 0, ambiguous: 1, 'write-high-risk': 2 };
|
|
126
|
+
|
|
127
|
+
for (const { token, raw } of tokens) {
|
|
128
|
+
if (isReadOnlyToken(token, raw)) {
|
|
129
|
+
/* 保持当前级别 */
|
|
130
|
+
} else {
|
|
131
|
+
/* 不在只读集合 → 至少 ambiguous */
|
|
132
|
+
const segRisk = matchesHighRiskPattern(raw) ? 'write-high-risk' : 'ambiguous';
|
|
133
|
+
if (RISK_ORDER[segRisk] > RISK_ORDER[highestRisk]) {
|
|
134
|
+
highestRisk = segRisk;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return highestRisk;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 是否需要进入审批评估流程。
|
|
144
|
+
* 只读命令跳过,其余都需要。
|
|
145
|
+
*/
|
|
146
|
+
export function requiresApprovalEvaluation(command, shellName = 'bash') {
|
|
147
|
+
return classifyCommandRisk(command, shellName) !== 'read-only';
|
|
148
|
+
}
|
package/src/core/config-store.js
CHANGED
|
@@ -65,6 +65,7 @@ const DEFAULT_CONFIG = {
|
|
|
65
65
|
memory: {
|
|
66
66
|
enabled: true,
|
|
67
67
|
auto_write: true,
|
|
68
|
+
auto_capture: true,
|
|
68
69
|
inject_on_session_start: true,
|
|
69
70
|
auto_dream_threshold: 10,
|
|
70
71
|
max_items_per_scope: 12,
|
|
@@ -165,6 +166,7 @@ function normalizePolicyLists(config) {
|
|
|
165
166
|
next.memory = next.memory || {};
|
|
166
167
|
next.memory.enabled = next.memory.enabled !== false;
|
|
167
168
|
next.memory.auto_write = next.memory.auto_write !== false;
|
|
169
|
+
next.memory.auto_capture = next.memory.auto_capture !== false;
|
|
168
170
|
next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
|
|
169
171
|
next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
|
|
170
172
|
next.memory.auto_dream_threshold = Number(next.memory.auto_dream_threshold ?? 10);
|
package/src/core/constants.js
CHANGED
|
@@ -37,20 +37,30 @@ function modeToKeepRecent(mode) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function buildLocalSummary(messages) {
|
|
40
|
-
const
|
|
40
|
+
const goal = [];
|
|
41
|
+
const constraints = [];
|
|
42
|
+
const changedFiles = new Set();
|
|
43
|
+
const verification = [];
|
|
44
|
+
const openThreads = [];
|
|
41
45
|
const limit = 16;
|
|
42
46
|
for (const msg of messages.slice(-limit)) {
|
|
43
47
|
if (msg.role === 'tool') {
|
|
44
|
-
// Try to parse tool result as JSON for semantic summary
|
|
45
48
|
const text = textFromContent(msg.content);
|
|
46
49
|
let parsed;
|
|
47
50
|
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
48
51
|
if (parsed && typeof parsed === 'object') {
|
|
49
52
|
const summary = summarizeToolResult(parsed);
|
|
50
|
-
|
|
53
|
+
if (parsed.path) changedFiles.add(String(parsed.path));
|
|
54
|
+
if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
|
|
55
|
+
verification.push(summary);
|
|
56
|
+
} else {
|
|
57
|
+
openThreads.push(`tool_result: ${summary}`);
|
|
58
|
+
}
|
|
51
59
|
} else {
|
|
52
60
|
const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
53
|
-
|
|
61
|
+
const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
|
|
62
|
+
if (match) changedFiles.add(match[1]);
|
|
63
|
+
openThreads.push(`tool_result: ${clipped}`);
|
|
54
64
|
}
|
|
55
65
|
continue;
|
|
56
66
|
}
|
|
@@ -59,21 +69,35 @@ function buildLocalSummary(messages) {
|
|
|
59
69
|
const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
|
|
60
70
|
const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
|
|
61
71
|
const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
62
|
-
|
|
72
|
+
if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
|
|
63
73
|
continue;
|
|
64
74
|
}
|
|
65
75
|
if (msg.role === 'user') {
|
|
66
76
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
67
77
|
const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
68
|
-
|
|
78
|
+
if (goal.length === 0) goal.push(clipped);
|
|
79
|
+
else constraints.push(clipped);
|
|
69
80
|
continue;
|
|
70
81
|
}
|
|
71
82
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
72
83
|
if (!text) continue;
|
|
73
84
|
const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
74
|
-
|
|
85
|
+
openThreads.push(`${msg.role}: ${clipped}`);
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
const lines = [
|
|
88
|
+
'Context Summary',
|
|
89
|
+
'Goal:',
|
|
90
|
+
goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
|
|
91
|
+
'Key Constraints:',
|
|
92
|
+
...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
93
|
+
'Changed Files:',
|
|
94
|
+
...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
|
|
95
|
+
'Verification:',
|
|
96
|
+
...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
97
|
+
'Open Threads:',
|
|
98
|
+
...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
|
|
99
|
+
];
|
|
100
|
+
return lines.join('\n').trim();
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
|