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 +44 -0
- package/deployment.md +6 -6
- package/package.json +3 -1
- package/src/core/agent-loop.js +87 -11
- package/src/core/chat-runtime.js +50 -5
- 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/constants.js +0 -1
- package/src/core/default-system-prompt.js +10 -3
- package/src/core/dream-consolidate.js +54 -14
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +3 -2
- package/src/core/paths.js +1 -1
- package/src/core/project-index.js +2 -2
- package/src/core/shell-profile.js +5 -1
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +100 -155
- package/src/tui/chat-app.js +339 -44
- package/src/tui/tool-activity/presenters/system.js +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
"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"
|
package/src/core/agent-loop.js
CHANGED
|
@@ -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
|
-
|
|
174
|
-
|
|
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: '
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
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';
|
|
@@ -152,10 +152,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
152
152
|
agents: '列出/运行子代理角色',
|
|
153
153
|
config: '设置/读取/列出/重置配置',
|
|
154
154
|
memory: '查看/搜索/删除持久记忆',
|
|
155
|
+
dream: '整理记忆收件箱(dream consolidation)',
|
|
155
156
|
history: '查看/恢复会话',
|
|
156
157
|
debug: '运行时调试开关',
|
|
157
158
|
retry: '重试上一条用户请求',
|
|
158
159
|
stop: '中止当前回答',
|
|
160
|
+
new: '开始新会话',
|
|
159
161
|
yes: '确认当前待审批计划并开始执行',
|
|
160
162
|
edit: '修改当前待审批计划',
|
|
161
163
|
reject: '拒绝当前待审批计划'
|
|
@@ -169,6 +171,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
169
171
|
planCommand: '规划命令',
|
|
170
172
|
agentCommand: '子代理命令',
|
|
171
173
|
memoryCommand: '记忆命令',
|
|
174
|
+
dreamCommand: '记忆整理命令',
|
|
172
175
|
debugCommand: '调试命令',
|
|
173
176
|
keyboardDebugCommand: '键盘调试命令',
|
|
174
177
|
compactCommand: '上下文压缩命令',
|
|
@@ -246,10 +249,12 @@ function getCompletionCopy(language = 'zh') {
|
|
|
246
249
|
agents: 'run/list sub-agent roles',
|
|
247
250
|
config: 'set/get/list/reset config values',
|
|
248
251
|
memory: 'list/search/delete persistent memories',
|
|
252
|
+
dream: 'consolidate memory inbox (dream)',
|
|
249
253
|
history: 'list/resume sessions',
|
|
250
254
|
debug: 'runtime debug switches',
|
|
251
255
|
retry: 'retry the last user request',
|
|
252
256
|
stop: 'stop the current response',
|
|
257
|
+
new: 'start a new session',
|
|
253
258
|
yes: 'approve the pending plan and start execution',
|
|
254
259
|
edit: 'revise the pending plan',
|
|
255
260
|
reject: 'reject the pending plan'
|
|
@@ -263,6 +268,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
263
268
|
planCommand: 'planning command',
|
|
264
269
|
agentCommand: 'sub-agent command',
|
|
265
270
|
memoryCommand: 'memory command',
|
|
271
|
+
dreamCommand: 'dream consolidation command',
|
|
266
272
|
debugCommand: 'debug command',
|
|
267
273
|
keyboardDebugCommand: 'keyboard debug command',
|
|
268
274
|
compactCommand: 'context compaction command',
|
|
@@ -1636,6 +1642,18 @@ async function writeMarkdownInProjectDir(subDir, title, body, fallbackName, sess
|
|
|
1636
1642
|
return filePath;
|
|
1637
1643
|
}
|
|
1638
1644
|
|
|
1645
|
+
async function removePlanFileIfPresent(planState) {
|
|
1646
|
+
const filePath = String(planState?.filePath || '').trim();
|
|
1647
|
+
if (!filePath) return;
|
|
1648
|
+
try {
|
|
1649
|
+
await fs.unlink(filePath);
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (error?.code !== 'ENOENT') {
|
|
1652
|
+
// Best-effort cleanup: keep the main approval flow moving.
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1639
1657
|
function buildSpecTemplate(topic) {
|
|
1640
1658
|
return `
|
|
1641
1659
|
# Spec: ${topic}
|
|
@@ -2777,7 +2795,7 @@ export async function createChatRuntime({
|
|
|
2777
2795
|
if (initialIndex?.summary) {
|
|
2778
2796
|
startupEvents.push({
|
|
2779
2797
|
type: 'system_tool',
|
|
2780
|
-
name: 'project_index(.codemini
|
|
2798
|
+
name: 'project_index(.codemini/project-map.json,.codemini/file-index.json)',
|
|
2781
2799
|
status: 'done',
|
|
2782
2800
|
summary: initialIndex.summary
|
|
2783
2801
|
});
|
|
@@ -2901,7 +2919,8 @@ export async function createChatRuntime({
|
|
|
2901
2919
|
'/agents',
|
|
2902
2920
|
'/compact',
|
|
2903
2921
|
'/debug',
|
|
2904
|
-
'/retry'
|
|
2922
|
+
'/retry',
|
|
2923
|
+
'/new'
|
|
2905
2924
|
];
|
|
2906
2925
|
const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
|
|
2907
2926
|
|
|
@@ -2920,10 +2939,12 @@ export async function createChatRuntime({
|
|
|
2920
2939
|
{ name: 'agents', description: completionCopy.commands.agents },
|
|
2921
2940
|
{ name: 'config', description: completionCopy.commands.config },
|
|
2922
2941
|
{ name: 'memory', description: completionCopy.commands.memory },
|
|
2942
|
+
{ name: 'dream', description: completionCopy.commands.dream },
|
|
2923
2943
|
{ name: 'history', description: completionCopy.commands.history },
|
|
2924
2944
|
{ name: 'debug', description: completionCopy.commands.debug },
|
|
2925
2945
|
{ name: 'retry', description: completionCopy.commands.retry },
|
|
2926
|
-
{ name: 'stop', description: completionCopy.commands.stop }
|
|
2946
|
+
{ name: 'stop', description: completionCopy.commands.stop },
|
|
2947
|
+
{ name: 'new', description: completionCopy.commands.new }
|
|
2927
2948
|
];
|
|
2928
2949
|
const out = [];
|
|
2929
2950
|
for (const cmd of commands.values()) {
|
|
@@ -2969,6 +2990,7 @@ export async function createChatRuntime({
|
|
|
2969
2990
|
const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
|
|
2970
2991
|
const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
|
|
2971
2992
|
const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
|
|
2993
|
+
const dreamTemplates = ['/dream', '/dream --dry-run', '/dream --scope=project', '/dream --scope=global'];
|
|
2972
2994
|
const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
|
|
2973
2995
|
const slashTemplates = [
|
|
2974
2996
|
...configTemplates,
|
|
@@ -2980,6 +3002,7 @@ export async function createChatRuntime({
|
|
|
2980
3002
|
...planTemplates,
|
|
2981
3003
|
...agentTemplates,
|
|
2982
3004
|
...debugTemplates,
|
|
3005
|
+
...dreamTemplates,
|
|
2983
3006
|
...compactTemplates,
|
|
2984
3007
|
'/retry',
|
|
2985
3008
|
'/status'
|
|
@@ -3046,6 +3069,7 @@ export async function createChatRuntime({
|
|
|
3046
3069
|
}
|
|
3047
3070
|
for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
|
|
3048
3071
|
for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
|
|
3072
|
+
for (const template of dreamTemplates) registerSuggestion(template, completionCopy.generic.dreamCommand);
|
|
3049
3073
|
for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
|
|
3050
3074
|
registerSuggestion('/retry', completionCopy.generic.retryCommand);
|
|
3051
3075
|
registerSuggestion('/status', completionCopy.generic.statusCommand);
|
|
@@ -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 };
|