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.
@@ -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-project/project-map.json,.codemini-project/file-index.json)',
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
+ }
@@ -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);
@@ -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',
@@ -37,20 +37,30 @@ function modeToKeepRecent(mode) {
37
37
  }
38
38
 
39
39
  function buildLocalSummary(messages) {
40
- const lines = [];
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
- lines.push(`- tool_result: ${summary}`);
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
- lines.push(`- tool_result: ${clipped}`);
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
- lines.push(`- assistant: ${clipped}${toolInfo}`);
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
- lines.push(`- user: ${clipped}`);
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
- lines.push(`- ${msg.role}: ${clipped}`);
85
+ openThreads.push(`${msg.role}: ${clipped}`);
75
86
  }
76
- return `Context Summary\n${lines.join('\n')}`.trim();
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' } = {}) {