codemini-cli 0.3.9 → 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 CHANGED
@@ -74,6 +74,11 @@ CodeMini CLI can optionally use `fff-mcp` as a faster backend for `grep`, `glob`
74
74
  | `codemini [prompt]` | Start an interactive coding session with an optional initial prompt |
75
75
  | `codemini chat [prompt]` | Chat mode — single-turn or multi-turn conversation |
76
76
  | `codemini run <task>` | Run a task non-interactively (e.g. `codemini run "fix the login bug"`) |
77
+ | `codemini run --harness <role> <task>` | Run a task with a specific sub-agent role (e.g. `coder`, `planner`, `reviewer`) |
78
+ | `codemini run --pipeline <task>` | Run a task through the full planning → coding → review pipeline |
79
+ | `codemini run <task> --max-steps N` | Limit the maximum number of agent steps for a run task |
80
+ | `codemini run <task> --model <name>` | Override the default model for a single run |
81
+ | `codemini [prompt] --plain` | Disable TUI and use plain terminal output |
77
82
  | `codemini config set\|get\|list <key> [value]` | Manage configuration (gateway, model, shell, UI, soul, etc.) |
78
83
  | `codemini doctor` | Run environment diagnostics and validate configuration |
79
84
  | `codemini skill list\|install\|enable\|disable\|inspect\|reindex` | Manage skills — list, install, toggle, or inspect bundled/third-party skills |
@@ -88,6 +93,23 @@ Built-in souls: `default`, `professional`, `ceo`, `playful`, `anime`, `caveman`,
88
93
  codemini config set soul.preset playful
89
94
  ```
90
95
 
96
+ ### Built-in Skills
97
+
98
+ Skills are reusable workflow patterns that guide how the agent approaches different types of tasks. They are loaded automatically when applicable.
99
+
100
+ | Skill | Trigger | Description |
101
+ |-------|---------|-------------|
102
+ | **superpowers-lite** | Default for all coding work | Lightweight operating style: prefer structured tools, keep context tight, use sub-agents, verify before claiming success |
103
+ | **brainstorm** | Multiple reasonable approaches exist | Explores options and tradeoffs before coding; asks one question at a time to resolve uncertainty |
104
+ | **writing-plans** | Non-trivial implementation task | Creates a step-by-step plan with exact file paths, code, and verification steps before touching code |
105
+
106
+ Skills are installed and managed via `codemini skill`:
107
+
108
+ ```bash
109
+ codemini skill list # List all available skills
110
+ codemini skill inspect <name> # Inspect a skill's details
111
+ ```
112
+
91
113
  ### How The Tool Model Works
92
114
 
93
115
  CodeMini CLI intentionally separates tools into two layers:
@@ -285,6 +307,11 @@ CodeMini CLI 可以可选地使用 `fff-mcp` 作为 `grep`、`glob` 和部分 `l
285
307
  | `codemini [prompt]` | 启动交互式编码会话,可附带初始提示 |
286
308
  | `codemini chat [prompt]` | 对话模式——单轮或多轮 |
287
309
  | `codemini run <task>` | 非交互式执行任务(如 `codemini run "修复登录 bug"`) |
310
+ | `codemini run --harness <role> <task>` | 以指定 sub-agent 角色执行任务(如 `coder`、`planner`、`reviewer`) |
311
+ | `codemini run --pipeline <task>` | 通过完整计划→编码→审查流水线执行任务 |
312
+ | `codemini run <task> --max-steps N` | 限制单次执行的最大 agent 步数 |
313
+ | `codemini run <task> --model <name>` | 单次执行时覆盖默认模型 |
314
+ | `codemini [prompt] --plain` | 禁用 TUI,使用纯文本终端输出 |
288
315
  | `codemini config set\|get\|list <key> [value]` | 管理配置(网关、模型、shell、UI、soul 等) |
289
316
  | `codemini doctor` | 运行环境诊断并验证配置 |
290
317
  | `codemini skill list\|install\|enable\|disable\|inspect\|reindex` | 管理 skill——列表、安装、启用/禁用、检查 |
@@ -299,6 +326,23 @@ CodeMini CLI 支持可切换的 "soul" 人格,仅改变语气和表达风格
299
326
  codemini config set soul.preset playful
300
327
  ```
301
328
 
329
+ ### 内置 Skills
330
+
331
+ Skill 是可复用的工作流模式,指导 agent 如何处理不同类型的任务。适用时会自动加载。
332
+
333
+ | Skill | 触发条件 | 说明 |
334
+ |-------|----------|------|
335
+ | **superpowers-lite** | 所有编码工作的默认 skill | 轻量操作风格:优先结构化工具、保持上下文精简、使用 sub-agent、验证后再报告完成 |
336
+ | **brainstorm** | 存在多种合理方案时 | 在编码前探索选项和权衡;每次只问一个问题来消除不确定性 |
337
+ | **writing-plans** | 非平凡的实现任务 | 在动手之前创建包含精确文件路径、代码和验证步骤的分步计划 |
338
+
339
+ 通过 `codemini skill` 管理技能:
340
+
341
+ ```bash
342
+ codemini skill list # 列出所有可用 skill
343
+ codemini skill inspect <name> # 查看某个 skill 的详细信息
344
+ ```
345
+
302
346
  ### 工具模型怎么设计
303
347
 
304
348
  CodeMini CLI 把工具分成两层:
package/deployment.md CHANGED
@@ -13,13 +13,13 @@ npm pack
13
13
  Expected output:
14
14
 
15
15
  ```text
16
- codemini-cli-0.1.0.tgz
16
+ codemini-cli-0.4.0.tgz
17
17
  ```
18
18
 
19
19
  If you want to verify the package contents:
20
20
 
21
21
  ```bash
22
- tar -tf codemini-cli-0.1.0.tgz
22
+ tar -tf codemini-cli-0.4.0.tgz
23
23
  ```
24
24
 
25
25
  ## 2. Copy To The Target Machine
@@ -34,7 +34,7 @@ Copy the generated `.tgz` file to the Win10 machine by one of these methods:
34
34
  Recommended target path:
35
35
 
36
36
  ```powershell
37
- C:\temp\codemini-cli-0.1.0.tgz
37
+ C:\temp\codemini-cli-0.4.0.tgz
38
38
  ```
39
39
 
40
40
  ## 3. Environment Requirements
@@ -42,7 +42,7 @@ C:\temp\codemini-cli-0.1.0.tgz
42
42
  Target machine requirements:
43
43
 
44
44
  - Windows 10
45
- - Node.js 20 or newer
45
+ - Node.js 22 or newer
46
46
  - npm available
47
47
  - PowerShell available
48
48
 
@@ -58,7 +58,7 @@ npm -v
58
58
  Global install:
59
59
 
60
60
  ```powershell
61
- npm install -g C:\temp\codemini-cli-0.1.0.tgz
61
+ npm install -g C:\temp\codemini-cli-0.4.0.tgz
62
62
  ```
63
63
 
64
64
  If global install is blocked by company policy, install in a working directory instead:
@@ -66,7 +66,7 @@ If global install is blocked by company policy, install in a working directory i
66
66
  ```powershell
67
67
  mkdir C:\temp\coder-test
68
68
  cd C:\temp\coder-test
69
- npm install C:\temp\codemini-cli-0.1.0.tgz
69
+ npm install C:\temp\codemini-cli-0.4.0.tgz
70
70
  ```
71
71
 
72
72
  ## 5. Confirm Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -48,10 +48,12 @@
48
48
  "dependencies": {
49
49
  "@cursorless/tree-sitter-wasms": "^0.8.1",
50
50
  "cheerio": "^1.1.2",
51
+ "cli-truncate": "^6.0.0",
51
52
  "duck-duck-scrape": "^2.2.7",
52
53
  "ink": "^7.0.0",
53
54
  "playwright": "^1.54.2",
54
55
  "react": "^19.2.5",
56
+ "strip-ansi": "^7.2.0",
55
57
  "web-tree-sitter": "^0.26.8"
56
58
  },
57
59
  "license": "MIT"
@@ -4,6 +4,8 @@ import fs from 'node:fs/promises';
4
4
  import { BoundedCache } from './bounded-cache.js';
5
5
  import { trimInline as _trimInline, normalizePath } from './string-utils.js';
6
6
  import { captureToInbox, listInbox } from './memory-store.js';
7
+ import { requiresApprovalEvaluation } from './command-risk.js';
8
+ import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
7
9
 
8
10
  /**
9
11
  * 安全解析 JSON 字符串。
@@ -162,7 +164,7 @@ function emptyToolResultMarker(toolName) {
162
164
  }
163
165
 
164
166
  function clipToolResult(result, maxChars = 12000) {
165
- const raw = typeof result === 'string' ? result : JSON.stringify(result);
167
+ const raw = sanitizeTextForModel(typeof result === 'string' ? result : JSON.stringify(result));
166
168
  if (!maxChars || raw.length <= maxChars) return raw;
167
169
  return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
168
170
  }
@@ -170,8 +172,9 @@ function clipToolResult(result, maxChars = 12000) {
170
172
  function compactToolResult(result, toolName, args, maxChars = 12000) {
171
173
  if (result === null || result === undefined) return 'no output';
172
174
  if (typeof result === 'string') {
173
- if (result.length <= maxChars) return result;
174
- return `${result.slice(0, maxChars)}\n... [tool result truncated ${result.length - maxChars} chars, original: ${result.length}]`;
175
+ const sanitized = sanitizeTextForModel(result);
176
+ if (sanitized.length <= maxChars) return sanitized;
177
+ return `${sanitized.slice(0, maxChars)}\n... [tool result truncated ${sanitized.length - maxChars} chars, original: ${sanitized.length}]`;
175
178
  }
176
179
  if (typeof result !== 'object') return String(result);
177
180
 
@@ -387,7 +390,18 @@ function shouldAutoCaptureError(toolName, message) {
387
390
  /not found$/i,
388
391
  /already exists$/i,
389
392
  /cancelled/i,
390
- /aborted/i
393
+ /aborted/i,
394
+ /blocked by (?:safe mode|policy|dangerous command)/i,
395
+ /exit 127/i,
396
+ /command not found/i,
397
+ /permission denied/i,
398
+ /args\?\s/i,
399
+ /Raw tool arguments/i,
400
+ /edit requires/i,
401
+ /write requires/i,
402
+ /requires file/i,
403
+ /path.*outside workspace/i,
404
+ /escapes workspace/i
391
405
  ];
392
406
  if (noisePatterns.some((p) => p.test(message))) return false;
393
407
  lastAutoCaptureByTool.set(toolName, now);
@@ -400,7 +414,7 @@ function fireAndForgetCapture(toolName, message, args) {
400
414
  ? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
401
415
  : `Tool: ${toolName}\nError: ${message}`;
402
416
  captureToInbox({
403
- scope: 'global',
417
+ scope: 'auto',
404
418
  type: 'failure',
405
419
  summary,
406
420
  details,
@@ -421,6 +435,33 @@ async function checkAutoDreamThreshold(config) {
421
435
 
422
436
  // ─── Exported helpers ────────────────────────────────────────────────
423
437
 
438
+ function extractFileChange(toolName, result) {
439
+ if (!result || typeof result !== 'object') return null;
440
+ const FILE_TOOLS = new Set(['edit', 'write', 'delete']);
441
+ if (!FILE_TOOLS.has(toolName)) return null;
442
+
443
+ /* delete */
444
+ if ('deleted' in result && result.deleted) {
445
+ return { path: String(result.path || ''), action: 'delete', linesAdded: 0, linesRemoved: 0 };
446
+ }
447
+
448
+ /* edit / write */
449
+ if ('path' in result && 'action' in result) {
450
+ const action = String(result.action || '');
451
+ const isCreate = action === 'create';
452
+ const added = Number(result.lines_added || 0);
453
+ const removed = Number(result.lines_removed || 0);
454
+ return {
455
+ path: String(result.path || ''),
456
+ action: isCreate ? 'create' : 'edit',
457
+ linesAdded: added,
458
+ linesRemoved: removed
459
+ };
460
+ }
461
+
462
+ return null;
463
+ }
464
+
424
465
  export function summarizeToolResult(result) {
425
466
  if (result === null || result === undefined) return 'no output';
426
467
  if (typeof result === 'string') {
@@ -640,7 +681,7 @@ function blockedExplorationReason(toolName, args, state) {
640
681
  const top = topLevelPath(target);
641
682
  if (!top) return '';
642
683
 
643
- if (['skills', 'souls', 'templates', '.codemini', '.codemini-project'].includes(top)) {
684
+ if (['skills', 'souls', 'templates', '.codemini', '.codemini-global'].includes(top)) {
644
685
  return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
645
686
  }
646
687
  return '';
@@ -736,14 +777,17 @@ function formatToolDisplayName(name, args) {
736
777
  // ─── Format a single tool result using per-tool formatter or fallback ──
737
778
 
738
779
  function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
780
+ const sanitizeOptions = getToolOutputSanitizeOptions(toolName);
739
781
  if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
740
782
  const formatted = toolFormatters[toolName](toolResult, args);
741
783
  if (typeof formatted === 'string') {
742
- return formatted.trim() ? formatted : emptyToolResultMarker(toolName);
784
+ const sanitized = sanitizeTextForModel(formatted, sanitizeOptions);
785
+ return sanitized.trim() ? sanitized : emptyToolResultMarker(toolName);
743
786
  }
744
787
  }
745
788
  const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
746
- return String(fallback || '').trim() ? fallback : emptyToolResultMarker(toolName);
789
+ const sanitizedFallback = sanitizeTextForModel(fallback, sanitizeOptions);
790
+ return String(sanitizedFallback || '').trim() ? sanitizedFallback : emptyToolResultMarker(toolName);
747
791
  }
748
792
 
749
793
  // ─── Main agent loop ────────────────────────────────────────────────
@@ -924,7 +968,11 @@ export async function runAgentLoop({
924
968
  let approved = true;
925
969
  let approvalArgs = args;
926
970
  let preflightErrorContent = '';
927
- const needsApproval = toolName === 'delete' || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
971
+ const isSafeModeRun = toolName === 'run'
972
+ && config?.policy?.safe_mode !== false
973
+ && requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
974
+ const needsApproval = toolName === 'delete' || isSafeModeRun
975
+ || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
928
976
  if (needsApproval) {
929
977
  approved = false;
930
978
  const handler = toolHandlers[toolName];
@@ -940,6 +988,31 @@ export async function runAgentLoop({
940
988
  preflightErrorContent = clipToolResult({ error: message }, toolResultMaxChars);
941
989
  }
942
990
  }
991
+ /* Run tool: safe mode LLM-based command evaluation */
992
+ if (toolName === 'run' && isSafeModeRun && !preflightErrorContent) {
993
+ try {
994
+ const { evaluateCommandWithLLM } = await import('./command-evaluator.js');
995
+ const evaluation = await evaluateCommandWithLLM({
996
+ command: args?.command || '',
997
+ config,
998
+ workspaceRoot: config?.workspaceRoot || process.cwd()
999
+ });
1000
+ approvalArgs = { ...args, _risk: evaluation.risk, _evaluation: evaluation };
1001
+ /* LLM says low-risk + allow → auto-approve, skip confirmation panel */
1002
+ if (evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
1003
+ approvalResults.set(call.id, { approved: true, args: approvalArgs });
1004
+ continue;
1005
+ }
1006
+ } catch (_) {
1007
+ approvalArgs = { ...args, _risk: 'high', _evaluation: null };
1008
+ }
1009
+ if (typeof handler?.prepareApproval === 'function') {
1010
+ try {
1011
+ const approval = await handler.prepareApproval(approvalArgs);
1012
+ approvalArgs = { ...approvalArgs, approval };
1013
+ } catch (_) { /* skip */ }
1014
+ }
1015
+ }
943
1016
  if (preflightErrorContent) {
944
1017
  approvalResults.set(call.id, {
945
1018
  approved: false,
@@ -954,7 +1027,8 @@ export async function runAgentLoop({
954
1027
  name: toolName,
955
1028
  displayName,
956
1029
  arguments: approvalArgs,
957
- approvalDetails: toolName === 'delete' ? approvalArgs.approval : undefined
1030
+ approvalDetails: toolName === 'delete' ? approvalArgs.approval
1031
+ : (toolName === 'run' ? approvalArgs.approval : undefined)
958
1032
  });
959
1033
  approved = Boolean(decision?.approved);
960
1034
  }
@@ -1035,8 +1109,10 @@ export async function runAgentLoop({
1035
1109
  }
1036
1110
 
1037
1111
  const durationMs = Date.now() - startedAt;
1112
+ /* 提取文件改动统计 */
1113
+ const fileChange = extractFileChange(toolName, toolResult);
1038
1114
  if (onEvent) {
1039
- onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult) });
1115
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult), fileChange });
1040
1116
  }
1041
1117
 
1042
1118
  // Auto-capture non-throwing tool failures (e.g. shell non-zero exit)
@@ -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';
@@ -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);
@@ -3382,10 +3406,27 @@ export async function createChatRuntime({
3382
3406
  }
3383
3407
  if (parsedInput.type === 'slash') {
3384
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
+ }
3385
3426
  if (parsedInput.command === 'help') {
3386
3427
  return {
3387
3428
  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>'
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>'
3389
3430
  };
3390
3431
  }
3391
3432
  if (parsedInput.command === 'status') {
@@ -3428,6 +3469,7 @@ export async function createChatRuntime({
3428
3469
  });
3429
3470
  activeSubSession = null;
3430
3471
  currentSession.planState = null;
3472
+ await removePlanFileIfPresent(planState);
3431
3473
  executionMode = 'auto';
3432
3474
  await persistAssistantExchange(line, result.text || '', { includeUser: false });
3433
3475
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
@@ -3457,7 +3499,9 @@ export async function createChatRuntime({
3457
3499
  if (!hasPendingPlanApproval(currentSession)) {
3458
3500
  return { type: 'system', text: 'No pending plan approval.' };
3459
3501
  }
3502
+ const planState = { ...currentSession.planState };
3460
3503
  currentSession.planState = null;
3504
+ await removePlanFileIfPresent(planState);
3461
3505
  executionMode = 'auto';
3462
3506
  const text = 'Pending plan rejected and cleared.';
3463
3507
  await persistLocalExchange(line, text);
@@ -3597,6 +3641,7 @@ export async function createChatRuntime({
3597
3641
  });
3598
3642
  activeSubSession = null;
3599
3643
  currentSession.planState = null;
3644
+ await removePlanFileIfPresent(planState);
3600
3645
  executionMode = 'auto';
3601
3646
  await persistAssistantExchange(line, result.text || '', { includeUser: false });
3602
3647
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
@@ -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 };