codemini-cli 0.3.8 → 0.4.0
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 +121 -1
- package/deployment.md +6 -6
- package/package.json +6 -1
- package/skills/brainstorm/SKILL.md +49 -29
- package/skills/superpowers-lite/SKILL.md +82 -90
- package/skills/writing-plans/SKILL.md +67 -0
- package/src/commands/chat.js +51 -47
- package/src/commands/doctor.js +27 -7
- package/src/commands/run.js +36 -28
- package/src/core/agent-loop.js +191 -10
- package/src/core/chat-runtime.js +170 -11
- 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 +7 -0
- package/src/core/constants.js +0 -1
- package/src/core/default-system-prompt.js +27 -0
- package/src/core/dream-audit.js +93 -0
- package/src/core/dream-consolidate.js +157 -0
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +386 -0
- package/src/core/memory-prompt.js +23 -0
- package/src/core/memory-store.js +228 -1
- package/src/core/paths.js +13 -1
- package/src/core/project-index.js +2 -2
- package/src/core/shell-profile.js +5 -1
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +425 -110
- package/src/tui/chat-app.js +376 -47
- 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,8 @@ 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 } from './memory-store.js';
|
|
28
|
+
import { forgetMemory, listMemories, searchMemories, captureToInbox, listInbox } from './memory-store.js';
|
|
29
|
+
import { runDreamConsolidation } from './dream-consolidate.js';
|
|
29
30
|
import { normalizePlanState } from './plan-state.js';
|
|
30
31
|
import { countActiveTodos, normalizeTodos } from './todo-state.js';
|
|
31
32
|
|
|
@@ -151,10 +152,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
151
152
|
agents: '列出/运行子代理角色',
|
|
152
153
|
config: '设置/读取/列出/重置配置',
|
|
153
154
|
memory: '查看/搜索/删除持久记忆',
|
|
155
|
+
dream: '整理记忆收件箱(dream consolidation)',
|
|
154
156
|
history: '查看/恢复会话',
|
|
155
157
|
debug: '运行时调试开关',
|
|
156
158
|
retry: '重试上一条用户请求',
|
|
157
159
|
stop: '中止当前回答',
|
|
160
|
+
new: '开始新会话',
|
|
158
161
|
yes: '确认当前待审批计划并开始执行',
|
|
159
162
|
edit: '修改当前待审批计划',
|
|
160
163
|
reject: '拒绝当前待审批计划'
|
|
@@ -168,6 +171,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
168
171
|
planCommand: '规划命令',
|
|
169
172
|
agentCommand: '子代理命令',
|
|
170
173
|
memoryCommand: '记忆命令',
|
|
174
|
+
dreamCommand: '记忆整理命令',
|
|
171
175
|
debugCommand: '调试命令',
|
|
172
176
|
keyboardDebugCommand: '键盘调试命令',
|
|
173
177
|
compactCommand: '上下文压缩命令',
|
|
@@ -245,10 +249,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
245
249
|
agents: 'run/list sub-agent roles',
|
|
246
250
|
config: 'set/get/list/reset config values',
|
|
247
251
|
memory: 'list/search/delete persistent memories',
|
|
252
|
+
dream: 'consolidate memory inbox (dream)',
|
|
248
253
|
history: 'list/resume sessions',
|
|
249
254
|
debug: 'runtime debug switches',
|
|
250
255
|
retry: 'retry the last user request',
|
|
251
256
|
stop: 'stop the current response',
|
|
257
|
+
new: 'start a new session',
|
|
252
258
|
yes: 'approve the pending plan and start execution',
|
|
253
259
|
edit: 'revise the pending plan',
|
|
254
260
|
reject: 'reject the pending plan'
|
|
@@ -262,6 +268,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
262
268
|
planCommand: 'planning command',
|
|
263
269
|
agentCommand: 'sub-agent command',
|
|
264
270
|
memoryCommand: 'memory command',
|
|
271
|
+
dreamCommand: 'dream consolidation command',
|
|
265
272
|
debugCommand: 'debug command',
|
|
266
273
|
keyboardDebugCommand: 'keyboard debug command',
|
|
267
274
|
compactCommand: 'context compaction command',
|
|
@@ -282,12 +289,12 @@ function describeConfigKey(key, mode = 'set', language = 'zh') {
|
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'];
|
|
285
|
-
const ROLE_TOOL_POLICY = {
|
|
286
|
-
planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node', 'read_plan', 'update_plan'],
|
|
287
|
-
coder: ['read', 'grep', 'list', 'edit', 'write', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'update_todos', 'read_plan', 'update_plan'],
|
|
292
|
+
export const ROLE_TOOL_POLICY = {
|
|
293
|
+
planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node', 'web_fetch', 'web_search', 'read_plan', 'update_plan'],
|
|
294
|
+
coder: ['read', 'grep', 'list', 'edit', 'write', 'delete', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'web_fetch', 'web_search', 'update_todos', 'read_plan', 'update_plan'],
|
|
288
295
|
reviewer: ['read', 'grep', 'list', 'glob', 'tool_search', 'ast_query', 'read_ast_node', 'read_plan'],
|
|
289
296
|
tester: ['read', 'grep', 'list', 'run', 'glob', 'tool_search', 'read_plan'],
|
|
290
|
-
summarizer: ['
|
|
297
|
+
summarizer: ['read_plan']
|
|
291
298
|
};
|
|
292
299
|
const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
|
|
293
300
|
const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
|
|
@@ -1635,6 +1642,18 @@ async function writeMarkdownInProjectDir(subDir, title, body, fallbackName, sess
|
|
|
1635
1642
|
return filePath;
|
|
1636
1643
|
}
|
|
1637
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
|
+
|
|
1638
1657
|
function buildSpecTemplate(topic) {
|
|
1639
1658
|
return `
|
|
1640
1659
|
# Spec: ${topic}
|
|
@@ -2152,7 +2171,7 @@ async function askModel({
|
|
|
2152
2171
|
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance and verify important details with fresh reads when needed.`
|
|
2153
2172
|
: systemPrompt;
|
|
2154
2173
|
|
|
2155
|
-
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
2174
|
+
const { definitions, handlers, formatters, deferredDefinitions, dispose: disposeTools } = getBuiltinTools({
|
|
2156
2175
|
workspaceRoot: process.cwd(),
|
|
2157
2176
|
config,
|
|
2158
2177
|
sessionId: session.id,
|
|
@@ -2241,6 +2260,7 @@ async function askModel({
|
|
|
2241
2260
|
requestToolApproval,
|
|
2242
2261
|
signal,
|
|
2243
2262
|
skipAnalysisNudge,
|
|
2263
|
+
config,
|
|
2244
2264
|
requestCompletion: async ({ messages, tools, model: selectedModel }) => {
|
|
2245
2265
|
let started = false;
|
|
2246
2266
|
const startAssistantStream = () => {
|
|
@@ -2775,7 +2795,7 @@ export async function createChatRuntime({
|
|
|
2775
2795
|
if (initialIndex?.summary) {
|
|
2776
2796
|
startupEvents.push({
|
|
2777
2797
|
type: 'system_tool',
|
|
2778
|
-
name: 'project_index(.codemini
|
|
2798
|
+
name: 'project_index(.codemini/project-map.json,.codemini/file-index.json)',
|
|
2779
2799
|
status: 'done',
|
|
2780
2800
|
summary: initialIndex.summary
|
|
2781
2801
|
});
|
|
@@ -2889,6 +2909,9 @@ export async function createChatRuntime({
|
|
|
2889
2909
|
'/status',
|
|
2890
2910
|
'/config',
|
|
2891
2911
|
'/memory',
|
|
2912
|
+
'/capture',
|
|
2913
|
+
'/inbox',
|
|
2914
|
+
'/dream',
|
|
2892
2915
|
'/mode',
|
|
2893
2916
|
'/plan',
|
|
2894
2917
|
'/history',
|
|
@@ -2896,7 +2919,8 @@ export async function createChatRuntime({
|
|
|
2896
2919
|
'/agents',
|
|
2897
2920
|
'/compact',
|
|
2898
2921
|
'/debug',
|
|
2899
|
-
'/retry'
|
|
2922
|
+
'/retry',
|
|
2923
|
+
'/new'
|
|
2900
2924
|
];
|
|
2901
2925
|
const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
|
|
2902
2926
|
|
|
@@ -2915,10 +2939,12 @@ export async function createChatRuntime({
|
|
|
2915
2939
|
{ name: 'agents', description: completionCopy.commands.agents },
|
|
2916
2940
|
{ name: 'config', description: completionCopy.commands.config },
|
|
2917
2941
|
{ name: 'memory', description: completionCopy.commands.memory },
|
|
2942
|
+
{ name: 'dream', description: completionCopy.commands.dream },
|
|
2918
2943
|
{ name: 'history', description: completionCopy.commands.history },
|
|
2919
2944
|
{ name: 'debug', description: completionCopy.commands.debug },
|
|
2920
2945
|
{ name: 'retry', description: completionCopy.commands.retry },
|
|
2921
|
-
{ name: 'stop', description: completionCopy.commands.stop }
|
|
2946
|
+
{ name: 'stop', description: completionCopy.commands.stop },
|
|
2947
|
+
{ name: 'new', description: completionCopy.commands.new }
|
|
2922
2948
|
];
|
|
2923
2949
|
const out = [];
|
|
2924
2950
|
for (const cmd of commands.values()) {
|
|
@@ -2964,6 +2990,7 @@ export async function createChatRuntime({
|
|
|
2964
2990
|
const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
|
|
2965
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>'];
|
|
2966
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'];
|
|
2967
2994
|
const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
|
|
2968
2995
|
const slashTemplates = [
|
|
2969
2996
|
...configTemplates,
|
|
@@ -2975,6 +3002,7 @@ export async function createChatRuntime({
|
|
|
2975
3002
|
...planTemplates,
|
|
2976
3003
|
...agentTemplates,
|
|
2977
3004
|
...debugTemplates,
|
|
3005
|
+
...dreamTemplates,
|
|
2978
3006
|
...compactTemplates,
|
|
2979
3007
|
'/retry',
|
|
2980
3008
|
'/status'
|
|
@@ -3041,6 +3069,7 @@ export async function createChatRuntime({
|
|
|
3041
3069
|
}
|
|
3042
3070
|
for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
|
|
3043
3071
|
for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
|
|
3072
|
+
for (const template of dreamTemplates) registerSuggestion(template, completionCopy.generic.dreamCommand);
|
|
3044
3073
|
for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
|
|
3045
3074
|
registerSuggestion('/retry', completionCopy.generic.retryCommand);
|
|
3046
3075
|
registerSuggestion('/status', completionCopy.generic.statusCommand);
|
|
@@ -3329,6 +3358,38 @@ export async function createChatRuntime({
|
|
|
3329
3358
|
const { signal } = activeAbortController;
|
|
3330
3359
|
const activeReplySystemPrompt = await buildActiveSystemPrompt();
|
|
3331
3360
|
const parsedInput = parseInput(line);
|
|
3361
|
+
const maybeAutoDreamFromRuntime = async () => {
|
|
3362
|
+
const threshold = Number(config?.memory?.auto_dream_threshold ?? 10);
|
|
3363
|
+
if (!(threshold > 0)) return null;
|
|
3364
|
+
let entries = [];
|
|
3365
|
+
try {
|
|
3366
|
+
entries = await listInbox();
|
|
3367
|
+
} catch {
|
|
3368
|
+
return null;
|
|
3369
|
+
}
|
|
3370
|
+
if (entries.length < threshold) return null;
|
|
3371
|
+
if (onAgentEvent) onAgentEvent({ type: 'dream:auto', message: 'inbox threshold reached' });
|
|
3372
|
+
try {
|
|
3373
|
+
const report = await runDreamConsolidation({
|
|
3374
|
+
dryRun: false,
|
|
3375
|
+
workspaceRoot: process.cwd(),
|
|
3376
|
+
config,
|
|
3377
|
+
writeAudit: true
|
|
3378
|
+
});
|
|
3379
|
+
if (onAgentEvent) {
|
|
3380
|
+
onAgentEvent({ type: 'dream:complete', report });
|
|
3381
|
+
}
|
|
3382
|
+
return report;
|
|
3383
|
+
} catch (error) {
|
|
3384
|
+
if (onAgentEvent) {
|
|
3385
|
+
onAgentEvent({
|
|
3386
|
+
type: 'dream:complete',
|
|
3387
|
+
report: { ok: false, error: String(error?.message || error || 'unknown dream error') }
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3332
3393
|
try {
|
|
3333
3394
|
if (shouldPersistInputHistory(parsedInput)) {
|
|
3334
3395
|
await appendInputHistory(line);
|
|
@@ -3345,10 +3406,27 @@ export async function createChatRuntime({
|
|
|
3345
3406
|
}
|
|
3346
3407
|
if (parsedInput.type === 'slash') {
|
|
3347
3408
|
if (parsedInput.command === 'exit') return { type: 'exit' };
|
|
3409
|
+
if (parsedInput.command === 'new') {
|
|
3410
|
+
const fresh = await createSession();
|
|
3411
|
+
currentSession = fresh;
|
|
3412
|
+
executionMode = config.execution?.mode || 'auto';
|
|
3413
|
+
compactState.backupMessages = null;
|
|
3414
|
+
setResultDir(path.join(getSessionsDir(), String(fresh.id)));
|
|
3415
|
+
historyIdCache = [fresh.id, ...historyIdCache.filter((id) => id !== fresh.id)];
|
|
3416
|
+
historySessionCache = [
|
|
3417
|
+
{ id: fresh.id, messageCount: 0 },
|
|
3418
|
+
...historySessionCache.filter((s) => s.id !== fresh.id)
|
|
3419
|
+
];
|
|
3420
|
+
return {
|
|
3421
|
+
type: 'system',
|
|
3422
|
+
text: `New session started: ${fresh.id}`,
|
|
3423
|
+
restoredMessages: []
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3348
3426
|
if (parsedInput.command === 'help') {
|
|
3349
3427
|
return {
|
|
3350
3428
|
type: 'system',
|
|
3351
|
-
text: 'Commands: /help /exit /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /edit /reject /agents /config /memory /history /debug /retry /<custom> !<shell>'
|
|
3429
|
+
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>'
|
|
3352
3430
|
};
|
|
3353
3431
|
}
|
|
3354
3432
|
if (parsedInput.command === 'status') {
|
|
@@ -3391,6 +3469,7 @@ export async function createChatRuntime({
|
|
|
3391
3469
|
});
|
|
3392
3470
|
activeSubSession = null;
|
|
3393
3471
|
currentSession.planState = null;
|
|
3472
|
+
await removePlanFileIfPresent(planState);
|
|
3394
3473
|
executionMode = 'auto';
|
|
3395
3474
|
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3396
3475
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
@@ -3420,7 +3499,9 @@ export async function createChatRuntime({
|
|
|
3420
3499
|
if (!hasPendingPlanApproval(currentSession)) {
|
|
3421
3500
|
return { type: 'system', text: 'No pending plan approval.' };
|
|
3422
3501
|
}
|
|
3502
|
+
const planState = { ...currentSession.planState };
|
|
3423
3503
|
currentSession.planState = null;
|
|
3504
|
+
await removePlanFileIfPresent(planState);
|
|
3424
3505
|
executionMode = 'auto';
|
|
3425
3506
|
const text = 'Pending plan rejected and cleared.';
|
|
3426
3507
|
await persistLocalExchange(line, text);
|
|
@@ -3514,6 +3595,7 @@ export async function createChatRuntime({
|
|
|
3514
3595
|
}
|
|
3515
3596
|
const goal = parsedInput.args.slice(1).join(' ').trim();
|
|
3516
3597
|
if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal>' };
|
|
3598
|
+
await maybeAutoDreamFromRuntime();
|
|
3517
3599
|
const auto = await buildAutoPlanAndRun({
|
|
3518
3600
|
goal,
|
|
3519
3601
|
session: currentSession,
|
|
@@ -3559,6 +3641,7 @@ export async function createChatRuntime({
|
|
|
3559
3641
|
});
|
|
3560
3642
|
activeSubSession = null;
|
|
3561
3643
|
currentSession.planState = null;
|
|
3644
|
+
await removePlanFileIfPresent(planState);
|
|
3562
3645
|
executionMode = 'auto';
|
|
3563
3646
|
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3564
3647
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
@@ -3750,6 +3833,75 @@ export async function createChatRuntime({
|
|
|
3750
3833
|
}
|
|
3751
3834
|
return { type: 'system', text: `Unknown /memory subcommand: ${sub}` };
|
|
3752
3835
|
}
|
|
3836
|
+
if (parsedInput.command === 'capture') {
|
|
3837
|
+
const summary = parsedInput.args.join(' ').trim();
|
|
3838
|
+
if (!summary) return { type: 'system', text: 'Usage: /capture <summary> [--scope global|repo|thread] [--type observation|correction|failure|preference|pattern|win|gap|decision]' };
|
|
3839
|
+
let scope = 'global';
|
|
3840
|
+
let capType = 'observation';
|
|
3841
|
+
const filtered = [];
|
|
3842
|
+
for (const arg of parsedInput.args) {
|
|
3843
|
+
if (arg.startsWith('--scope=')) { scope = arg.slice(7); continue; }
|
|
3844
|
+
if (arg.startsWith('--type=')) { capType = arg.slice(7); continue; }
|
|
3845
|
+
if (arg === '--scope') { scope = ''; continue; }
|
|
3846
|
+
if (arg === '--type') { capType = ''; continue; }
|
|
3847
|
+
filtered.push(arg);
|
|
3848
|
+
}
|
|
3849
|
+
const capSummary = filtered.join(' ').trim();
|
|
3850
|
+
if (!capSummary) return { type: 'system', text: 'Usage: /capture <summary>' };
|
|
3851
|
+
try {
|
|
3852
|
+
const entry = await captureToInbox({ summary: capSummary, scope, type: capType, source: 'slash' });
|
|
3853
|
+
const text = `Captured to inbox: ${entry.id} [${entry.lifecycle}] ${entry.summary}`;
|
|
3854
|
+
return { type: 'system', text };
|
|
3855
|
+
} catch (err) {
|
|
3856
|
+
return { type: 'system', text: `Capture failed: ${err.message}` };
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
if (parsedInput.command === 'inbox') {
|
|
3860
|
+
const since = parsedInput.args[0] || '';
|
|
3861
|
+
try {
|
|
3862
|
+
const entries = await listInbox({ since: since || undefined });
|
|
3863
|
+
if (entries.length === 0) return { type: 'system', text: 'Inbox is empty.' };
|
|
3864
|
+
const rows = entries.map((e) => `[${e.lifecycle}] ${e.scope}/${e.type}: ${e.summary} (${e.id})`);
|
|
3865
|
+
return { type: 'system', text: `Inbox (${entries.length}):\n${rows.join('\n')}` };
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
return { type: 'system', text: `Failed to list inbox: ${err.message}` };
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
if (parsedInput.command === 'dream') {
|
|
3871
|
+
let dryRun = false;
|
|
3872
|
+
let scope = null;
|
|
3873
|
+
for (const arg of parsedInput.args) {
|
|
3874
|
+
if (arg === '--dry-run') {
|
|
3875
|
+
dryRun = true;
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
if (arg.startsWith('--scope=')) {
|
|
3879
|
+
scope = arg.slice(8) || null;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
try {
|
|
3883
|
+
const report = await runDreamConsolidation({
|
|
3884
|
+
dryRun,
|
|
3885
|
+
scope,
|
|
3886
|
+
workspaceRoot: process.cwd(),
|
|
3887
|
+
config,
|
|
3888
|
+
writeAudit: true
|
|
3889
|
+
});
|
|
3890
|
+
const summary = [
|
|
3891
|
+
`Dream done${dryRun ? ' (dry-run)' : ''}.`,
|
|
3892
|
+
`Candidates: ${Number(report.candidatesGenerated || 0)}`,
|
|
3893
|
+
`Promotions: ${Array.isArray(report.promotions) ? report.promotions.length : 0}`,
|
|
3894
|
+
`Rejections: ${Array.isArray(report.rejections) ? report.rejections.length : 0}`,
|
|
3895
|
+
`Archives: ${Array.isArray(report.archives) ? report.archives.length : 0}`,
|
|
3896
|
+
report.auditReport ? `Audit: ${report.auditReport}` : ''
|
|
3897
|
+
]
|
|
3898
|
+
.filter(Boolean)
|
|
3899
|
+
.join('\n');
|
|
3900
|
+
return { type: 'system', text: summary };
|
|
3901
|
+
} catch (err) {
|
|
3902
|
+
return { type: 'system', text: `Dream failed: ${err.message}` };
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3753
3905
|
if (parsedInput.command === 'retry') {
|
|
3754
3906
|
const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
|
|
3755
3907
|
if (!lastUser?.content) {
|
|
@@ -3987,6 +4139,7 @@ export async function createChatRuntime({
|
|
|
3987
4139
|
const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
|
|
3988
4140
|
const autoRoute = classifyAutoRoute(expandedText);
|
|
3989
4141
|
if (autoRoute.autoPlan) {
|
|
4142
|
+
await maybeAutoDreamFromRuntime();
|
|
3990
4143
|
const auto = await buildAutoPlanAndRun({
|
|
3991
4144
|
goal: expandedText,
|
|
3992
4145
|
session: currentSession,
|
|
@@ -4057,6 +4210,12 @@ export async function createChatRuntime({
|
|
|
4057
4210
|
activeRequestToolApproval = typeof handler === 'function' ? handler : null;
|
|
4058
4211
|
return true;
|
|
4059
4212
|
},
|
|
4213
|
+
dispose: async () => {
|
|
4214
|
+
if (typeof disposeTools === 'function') {
|
|
4215
|
+
await disposeTools();
|
|
4216
|
+
}
|
|
4217
|
+
return true;
|
|
4218
|
+
},
|
|
4060
4219
|
getRuntimeState: () =>
|
|
4061
4220
|
buildRuntimeStateSnapshot({
|
|
4062
4221
|
currentSession,
|
|
@@ -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
|
@@ -66,6 +66,7 @@ const DEFAULT_CONFIG = {
|
|
|
66
66
|
enabled: true,
|
|
67
67
|
auto_write: true,
|
|
68
68
|
inject_on_session_start: true,
|
|
69
|
+
auto_dream_threshold: 10,
|
|
69
70
|
max_items_per_scope: 12,
|
|
70
71
|
max_prompt_chars: 4000,
|
|
71
72
|
max_user_chars: 1375,
|
|
@@ -77,6 +78,9 @@ const DEFAULT_CONFIG = {
|
|
|
77
78
|
preset: 'default',
|
|
78
79
|
custom_path: ''
|
|
79
80
|
},
|
|
81
|
+
web: {
|
|
82
|
+
search_enabled: true
|
|
83
|
+
},
|
|
80
84
|
policy: {
|
|
81
85
|
safe_mode: true,
|
|
82
86
|
allow_dangerous_commands: false,
|
|
@@ -163,6 +167,7 @@ function normalizePolicyLists(config) {
|
|
|
163
167
|
next.memory.auto_write = next.memory.auto_write !== false;
|
|
164
168
|
next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
|
|
165
169
|
next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
|
|
170
|
+
next.memory.auto_dream_threshold = Number(next.memory.auto_dream_threshold ?? 10);
|
|
166
171
|
next.memory.max_prompt_chars = Math.max(200, Number(next.memory.max_prompt_chars || 4000));
|
|
167
172
|
next.memory.max_user_chars = Math.max(80, Number(next.memory.max_user_chars || 1375));
|
|
168
173
|
next.memory.max_global_chars = Math.max(80, Number(next.memory.max_global_chars || 2200));
|
|
@@ -170,6 +175,8 @@ function normalizePolicyLists(config) {
|
|
|
170
175
|
next.memory.project_binding = ['path', 'alias', 'path-or-alias'].includes(String(next.memory.project_binding || ''))
|
|
171
176
|
? String(next.memory.project_binding)
|
|
172
177
|
: 'path-or-alias';
|
|
178
|
+
next.web = next.web || {};
|
|
179
|
+
next.web.search_enabled = next.web.search_enabled !== false;
|
|
173
180
|
next.policy = next.policy || {};
|
|
174
181
|
next.policy.command_allowlist = uniqueStrings(
|
|
175
182
|
Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
|