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.
@@ -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: ['read', 'grep', 'list', 'glob', 'tool_search', 'read_plan', 'update_plan']
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-project/project-map.json,.codemini-project/file-index.json)',
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
+ }
@@ -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 : []
@@ -18,7 +18,6 @@ export const INDEX_SKIP_DIRS = new Set([
18
18
  '.git',
19
19
  'node_modules',
20
20
  '.codemini',
21
- '.codemini-project',
22
21
  '.codemini-global',
23
22
  'dist',
24
23
  'coverage',