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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { collectCommandTokens, firstToken } from './command-policy.js';
|
|
2
|
+
|
|
3
|
+
/* ── 只读命令 token ───────────────────────────────────────────── */
|
|
4
|
+
const READ_ONLY_TOKENS = new Set([
|
|
5
|
+
'ls', 'cat', 'head', 'tail', 'pwd', 'wc', 'sort', 'uniq',
|
|
6
|
+
'cut', 'tr', 'basename', 'dirname', 'test', 'true', 'false',
|
|
7
|
+
'whoami', 'uname', 'date', 'env', 'printenv', 'hostname',
|
|
8
|
+
'rg', 'find', 'grep', 'ag', 'ack', 'fd', 'bat',
|
|
9
|
+
'git', 'node', 'npm', 'npx', 'python', 'python3', 'py', 'pip', 'pip3',
|
|
10
|
+
'echo', 'printf', 'seq', 'yes'
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/* 只读时需要检查子命令的 token */
|
|
14
|
+
const READ_ONLY_SUBCOMMANDS = {
|
|
15
|
+
git: new Set([
|
|
16
|
+
'status', 'log', 'diff', 'branch', 'show', 'tag', 'stash',
|
|
17
|
+
'list', 'remote', 'rev-parse', 'describe', 'blame',
|
|
18
|
+
'shortlog', 'count', 'ls-files', 'ls-remote', 'ls-tree',
|
|
19
|
+
'config', '--version', 'var', 'for-each-ref', 'name-rev',
|
|
20
|
+
'merge-base', 'cherry'
|
|
21
|
+
]),
|
|
22
|
+
node: new Set(['--version', '-v', '-e', '--eval', '--print', '-p', '--help']),
|
|
23
|
+
npm: new Set([
|
|
24
|
+
'--version', '-v', 'view', 'info', 'list', 'ls', 'll', 'la',
|
|
25
|
+
'outdated', 'audit', 'pack', 'cache', 'config', 'doctor',
|
|
26
|
+
'help', 'explore', 'run', 'run-script', 'start', 'test',
|
|
27
|
+
'restart', 'stop', 'version', 'whoami'
|
|
28
|
+
]),
|
|
29
|
+
npx: new Set(['--version', '-v', '--help']),
|
|
30
|
+
python: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
31
|
+
python3: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
32
|
+
py: new Set(['--version', '-V', '--help', '-c', '-m']),
|
|
33
|
+
pip: new Set(['--version', '-V', 'list', 'show', 'search', 'check', 'debug', 'help']),
|
|
34
|
+
pip3: new Set(['--version', '-V', 'list', 'show', 'search', 'check', 'debug', 'help'])
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/* ── 高风险 pattern ────────────────────────────────────────────── */
|
|
38
|
+
const HIGH_RISK_PATTERNS = [
|
|
39
|
+
/\binstall\b/i,
|
|
40
|
+
/\bpublish\b/i,
|
|
41
|
+
/\bpush\b/i,
|
|
42
|
+
/\bcommit\b/i,
|
|
43
|
+
/\brebase\b/i,
|
|
44
|
+
/\breset\s/i,
|
|
45
|
+
/\bcheckout\s+--/i,
|
|
46
|
+
/\brm\b/i,
|
|
47
|
+
/\bdel\b/i,
|
|
48
|
+
/\bmkdi[ri]\b/i,
|
|
49
|
+
/\btouch\b/i,
|
|
50
|
+
/\bcp\b/i,
|
|
51
|
+
/\bmv\b/i,
|
|
52
|
+
/\bchmod\b/i,
|
|
53
|
+
/\bchown\b/i,
|
|
54
|
+
/\bmktemp\b/i,
|
|
55
|
+
/\btee\b/i,
|
|
56
|
+
/\bsudo\b/i,
|
|
57
|
+
/\bsu\b/,
|
|
58
|
+
/\bkill\b/i,
|
|
59
|
+
/\bpkill\b/i,
|
|
60
|
+
/\bcurl\s+.*-[A-Z]\s*(POST|PUT|DELETE|PATCH)/i,
|
|
61
|
+
/\bwget\b/i,
|
|
62
|
+
/\bdocker\s+(rm|stop|kill|rmi)\b/i,
|
|
63
|
+
/\bsystemctl\b/i,
|
|
64
|
+
/\bservice\b/i,
|
|
65
|
+
/\blaunchctl\b/i,
|
|
66
|
+
/>\s*\S/,
|
|
67
|
+
/>>\s*\S/,
|
|
68
|
+
/\|&\s*\S/
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/* ── 核心分类逻辑 ──────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 判断单个 token 是否为只读命令(含子命令检查)。
|
|
75
|
+
*/
|
|
76
|
+
function isReadOnlyToken(token, rawSegment) {
|
|
77
|
+
if (!READ_ONLY_TOKENS.has(token)) return false;
|
|
78
|
+
|
|
79
|
+
/* 需要 子命令 校验的 token */
|
|
80
|
+
const allowedSubs = READ_ONLY_SUBCOMMANDS[token];
|
|
81
|
+
if (!allowedSubs) return true; // 如 ls, pwd 等本身只读
|
|
82
|
+
|
|
83
|
+
/* 提取子命令:去掉 token 后第一个非 flag 参数 */
|
|
84
|
+
const rest = String(rawSegment || '').trim().slice(token.length).trim();
|
|
85
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
86
|
+
/* 以 - 开头的 flag 视为安全,取第一个非 flag 参数 */
|
|
87
|
+
let subcmd = '';
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (part.startsWith('-')) continue;
|
|
90
|
+
subcmd = part;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
/* 只有 token 本身或全部是 flags → 视为安全 */
|
|
94
|
+
if (!subcmd) return true;
|
|
95
|
+
if (allowedSubs.has(subcmd)) return true;
|
|
96
|
+
/* 子命令 不在白名单 → 不确定 */
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 对命令文本做快速 高风险 pattern 扫描。
|
|
102
|
+
*/
|
|
103
|
+
function matchesHighRiskPattern(text) {
|
|
104
|
+
return HIGH_RISK_PATTERNS.some((p) => p.test(text));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 分类命令风险等级。
|
|
109
|
+
* @param {string} command
|
|
110
|
+
* @param {string} [shellName='bash']
|
|
111
|
+
* @returns {'read-only'|'write-high-risk'|'ambiguous'}
|
|
112
|
+
*/
|
|
113
|
+
export function classifyCommandRisk(command, shellName = 'bash') {
|
|
114
|
+
const cmd = String(command || '').trim();
|
|
115
|
+
if (!cmd) return 'read-only';
|
|
116
|
+
|
|
117
|
+
/* 高风险 pattern 优先判断 */
|
|
118
|
+
if (matchesHighRiskPattern(cmd)) return 'write-high-risk';
|
|
119
|
+
|
|
120
|
+
/* 解析链式命令的每个 segment */
|
|
121
|
+
const tokens = collectCommandTokens(cmd);
|
|
122
|
+
if (tokens.length === 0) return 'ambiguous';
|
|
123
|
+
|
|
124
|
+
let highestRisk = 'read-only';
|
|
125
|
+
const RISK_ORDER = { 'read-only': 0, ambiguous: 1, 'write-high-risk': 2 };
|
|
126
|
+
|
|
127
|
+
for (const { token, raw } of tokens) {
|
|
128
|
+
if (isReadOnlyToken(token, raw)) {
|
|
129
|
+
/* 保持当前级别 */
|
|
130
|
+
} else {
|
|
131
|
+
/* 不在只读集合 → 至少 ambiguous */
|
|
132
|
+
const segRisk = matchesHighRiskPattern(raw) ? 'write-high-risk' : 'ambiguous';
|
|
133
|
+
if (RISK_ORDER[segRisk] > RISK_ORDER[highestRisk]) {
|
|
134
|
+
highestRisk = segRisk;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return highestRisk;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 是否需要进入审批评估流程。
|
|
144
|
+
* 只读命令跳过,其余都需要。
|
|
145
|
+
*/
|
|
146
|
+
export function requiresApprovalEvaluation(command, shellName = 'bash') {
|
|
147
|
+
return classifyCommandRisk(command, shellName) !== 'read-only';
|
|
148
|
+
}
|
package/src/core/constants.js
CHANGED
|
@@ -45,9 +45,16 @@ User: add a notes file
|
|
|
45
45
|
Assistant: create the file directly
|
|
46
46
|
Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
|
|
47
47
|
|
|
48
|
-
6.
|
|
49
|
-
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference —
|
|
50
|
-
|
|
48
|
+
6. Save a high-signal observation to memory
|
|
49
|
+
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — save it to persistent memory. Choose scope carefully:
|
|
50
|
+
- scope "user" for personal preferences (language, reply style, interaction habits)
|
|
51
|
+
- scope "global" for cross-project lessons (environment quirks, general tool workflows)
|
|
52
|
+
- scope "project" for project-specific knowledge (architecture conventions, local config, test commands, file locations)
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
Tool: save_memory({"content":"User prefers tab size 2 for all JSON files","scope":"user","kind":"preference"})
|
|
56
|
+
Tool: save_memory({"content":"This project uses vitest, not jest — run tests with npx vitest run","scope":"project","kind":"pattern"})
|
|
57
|
+
Tool: save_memory({"content":"WSL2 bash exec prefix does not support cd as a command","scope":"global","kind":"correction"})
|
|
51
58
|
|
|
52
59
|
7. Run a dream loop consolidation pass
|
|
53
60
|
When you want to review and consolidate inbox entries into long-term memory.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listMemories, listInbox, archiveEntry, promoteMemory } from './memory-store.js';
|
|
2
2
|
import { writeDreamAuditReport } from './dream-audit.js';
|
|
3
|
+
import { evaluateInboxBatch } from './dream-evaluator.js';
|
|
3
4
|
|
|
4
5
|
const LONGTERM_TYPES = new Set(['preference', 'pattern', 'win', 'decision']);
|
|
5
6
|
const OPERATIONAL_TYPES = new Set(['correction', 'failure', 'gap', 'observation']);
|
|
@@ -8,14 +9,6 @@ function normalizeText(value) {
|
|
|
8
9
|
return String(value || '').trim().toLowerCase();
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
function mapInboxScopeToMemoryScope(scope) {
|
|
12
|
-
const value = normalizeText(scope);
|
|
13
|
-
if (value === 'repo' || value === 'project') return 'project';
|
|
14
|
-
if (value === 'thread') return 'global';
|
|
15
|
-
if (value === 'user') return 'user';
|
|
16
|
-
return 'global';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
function chooseLifecycle(type) {
|
|
20
13
|
const value = normalizeText(type);
|
|
21
14
|
if (LONGTERM_TYPES.has(value)) return 'longterm';
|
|
@@ -55,7 +48,10 @@ export async function runDreamConsolidation({
|
|
|
55
48
|
const filesRead = ['memory/inbox/*', 'memory/global.json', 'memory/user.json', 'memory/project/*.json'];
|
|
56
49
|
const filesChanged = [];
|
|
57
50
|
|
|
51
|
+
/* ── Phase 1: 规则预过滤(快速剔除明显垃圾) ─────────────────── */
|
|
52
|
+
const candidates = [];
|
|
58
53
|
const seen = new Map();
|
|
54
|
+
|
|
59
55
|
for (const entry of inbox) {
|
|
60
56
|
const summaryKey = normalizeText(entry.summary);
|
|
61
57
|
if (!summaryKey) {
|
|
@@ -78,14 +74,58 @@ export async function runDreamConsolidation({
|
|
|
78
74
|
continue;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
candidates.push(entry);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (candidates.length === 0) {
|
|
81
|
+
const report = { timestamp: new Date().toISOString(), filesRead, filesChanged: [], candidatesGenerated: inbox.length, promotions, rejections, archives };
|
|
82
|
+
if (!dryRun && writeAudit) {
|
|
83
|
+
const reportPath = await writeDreamAuditReport(report);
|
|
84
|
+
report.auditReport = reportPath;
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, dryRun, ...report };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
|
|
90
|
+
const llmResults = dryRun
|
|
91
|
+
? candidates.map((e) => ({ id: e.id, action: 'keep', scope: 'global', kind: e.type || 'observation', content: e.details || e.summary, summary: e.summary, confidence: 0.9 }))
|
|
92
|
+
: await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
|
|
93
|
+
|
|
94
|
+
const resultMap = new Map(llmResults.map((r) => [r.id, r]));
|
|
95
|
+
|
|
96
|
+
/* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
|
|
97
|
+
for (const entry of candidates) {
|
|
98
|
+
const evaluation = resultMap.get(entry.id);
|
|
99
|
+
|
|
100
|
+
if (!evaluation || evaluation.action === 'discard') {
|
|
101
|
+
const reason = evaluation?.reason || 'LLM discarded';
|
|
102
|
+
if (!dryRun) await archiveEntry(entry, 'discarded-by-evaluator', reason);
|
|
103
|
+
rejections.push({ summary: entry.summary, reason: `evaluator-discard: ${reason}` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const promoteScope = evaluation.scope || 'global';
|
|
108
|
+
const lifecycle = chooseLifecycle(evaluation.kind);
|
|
109
|
+
const enrichedEntry = {
|
|
110
|
+
...entry,
|
|
111
|
+
/* 用 LLM 提炼后的内容覆盖原始报错 */
|
|
112
|
+
summary: evaluation.summary || entry.summary,
|
|
113
|
+
details: evaluation.content || entry.details || entry.summary,
|
|
114
|
+
type: evaluation.kind || entry.type || 'observation'
|
|
115
|
+
};
|
|
83
116
|
|
|
84
117
|
if (!dryRun) {
|
|
85
118
|
try {
|
|
86
|
-
await promoteMemory({
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
await promoteMemory({
|
|
120
|
+
entry: enrichedEntry,
|
|
121
|
+
scope: promoteScope,
|
|
122
|
+
lifecycle,
|
|
123
|
+
workspaceRoot,
|
|
124
|
+
config,
|
|
125
|
+
confidence: evaluation.confidence || 0.8
|
|
126
|
+
});
|
|
127
|
+
filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${enrichedEntry.summary}" as ${lifecycle} (${promoteScope})` });
|
|
128
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence });
|
|
89
129
|
} catch (error) {
|
|
90
130
|
const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
|
|
91
131
|
await archiveEntry(entry, 'promotion-failed', reason);
|
|
@@ -95,7 +135,7 @@ export async function runDreamConsolidation({
|
|
|
95
135
|
continue;
|
|
96
136
|
}
|
|
97
137
|
|
|
98
|
-
promotions.push({ summary:
|
|
138
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
|
|
99
139
|
}
|
|
100
140
|
|
|
101
141
|
const report = {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createChatCompletion } from './provider/index.js';
|
|
2
|
+
|
|
3
|
+
const EVAL_TIMEOUT_MS = 30000;
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT = `You are a memory consolidation evaluator for a coding assistant. You receive a batch of inbox items (tool errors, observations, etc.) and decide for each one:
|
|
6
|
+
|
|
7
|
+
1. **keep or discard** — Does this contain a reusable, durable insight? Discard transient errors, one-off issues, and noise.
|
|
8
|
+
2. **scope** — "global" for cross-project knowledge (e.g., "WSL bash exec does not support cd"), "project" for project-specific context (e.g., "this project uses vitest for testing").
|
|
9
|
+
3. **kind** — One of: pattern, observation, correction, decision, failure
|
|
10
|
+
4. **content** — A refined, actionable sentence describing the insight. NOT the raw error text.
|
|
11
|
+
5. **summary** — A short label (under 80 chars) for quick scanning.
|
|
12
|
+
6. **confidence** — 0.5–1.0 based on how certain and durable the insight is.
|
|
13
|
+
|
|
14
|
+
Respond with valid JSON only, no markdown fences:
|
|
15
|
+
{"results":[{"id":"<inbox-id>","action":"keep","scope":"global|project","kind":"pattern|observation|correction|decision|failure","content":"...","summary":"...","confidence":0.8},{"id":"<inbox-id>","action":"discard","reason":"..."}]}
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- Raw tool error messages are NOT insights by themselves. Only keep if they reveal a reusable lesson.
|
|
19
|
+
- "exit 127", "command not found", "permission denied", "blocked by policy" → always discard (transient/config issues)
|
|
20
|
+
- A repeated pattern across multiple errors → keep as a "pattern" or "correction"
|
|
21
|
+
- Project-specific paths, file names, or commands → scope "project"
|
|
22
|
+
- General coding/environment knowledge → scope "global"
|
|
23
|
+
- If in doubt, discard. Memory is expensive; only promote what future sessions will genuinely benefit from.`;
|
|
24
|
+
|
|
25
|
+
function parseResults(text) {
|
|
26
|
+
try {
|
|
27
|
+
const json = JSON.parse(text);
|
|
28
|
+
if (!json?.results || !Array.isArray(json.results)) return [];
|
|
29
|
+
return json.results.map((r) => ({
|
|
30
|
+
id: String(r.id || ''),
|
|
31
|
+
action: r.action === 'keep' ? 'keep' : 'discard',
|
|
32
|
+
scope: r.scope === 'project' ? 'project' : 'global',
|
|
33
|
+
kind: ['pattern', 'observation', 'correction', 'decision', 'failure'].includes(r.kind) ? r.kind : 'observation',
|
|
34
|
+
content: String(r.content || '').slice(0, 300),
|
|
35
|
+
summary: String(r.summary || '').slice(0, 120),
|
|
36
|
+
confidence: Math.min(1, Math.max(0.5, Number(r.confidence) || 0.7)),
|
|
37
|
+
reason: String(r.reason || '')
|
|
38
|
+
}));
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 用 LLM 批量评估 inbox 条目,决定保留/丢弃、scope、内容提炼。
|
|
46
|
+
* @param {{ entries: Array, config: object, workspaceRoot?: string }} params
|
|
47
|
+
* @returns {Promise<Array<{ id, action, scope?, kind?, content?, summary?, confidence?, reason? }>>}
|
|
48
|
+
*/
|
|
49
|
+
export async function evaluateInboxBatch({ entries, config, workspaceRoot }) {
|
|
50
|
+
if (!entries || entries.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
const batch = entries.map((e) => ({
|
|
53
|
+
id: e.id,
|
|
54
|
+
type: e.type || '',
|
|
55
|
+
source: e.source || '',
|
|
56
|
+
summary: (e.summary || '').slice(0, 150),
|
|
57
|
+
details: (e.details || '').slice(0, 400)
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await createChatCompletion({
|
|
62
|
+
sdkProvider: config?.sdk?.provider,
|
|
63
|
+
baseUrl: config?.gateway?.base_url,
|
|
64
|
+
apiKey: config?.gateway?.api_key,
|
|
65
|
+
model: config?.model?.name,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
68
|
+
{
|
|
69
|
+
role: 'user',
|
|
70
|
+
content: `Evaluate these ${batch.length} inbox items. Workspace: ${workspaceRoot || process.cwd()}\n\n${JSON.stringify(batch, null, 2)}`
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
temperature: 0,
|
|
74
|
+
timeoutMs: EVAL_TIMEOUT_MS
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const text = result?.text || '';
|
|
78
|
+
const parsed = parseResults(text);
|
|
79
|
+
/* 确保每个 entry 都有结果,LLM 没返回的一律 discard */
|
|
80
|
+
const covered = new Set(parsed.map((r) => r.id));
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!covered.has(entry.id)) {
|
|
83
|
+
parsed.push({
|
|
84
|
+
id: entry.id,
|
|
85
|
+
action: 'discard',
|
|
86
|
+
reason: 'LLM did not return a result for this entry'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
} catch {
|
|
92
|
+
/* LLM 调用失败 → 全部 discard(fail-safe) */
|
|
93
|
+
return entries.map((e) => ({
|
|
94
|
+
id: e.id,
|
|
95
|
+
action: 'discard',
|
|
96
|
+
reason: 'LLM evaluation failed'
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/core/fff-adapter.js
CHANGED
package/src/core/memory-store.js
CHANGED
|
@@ -375,7 +375,8 @@ export async function promoteMemory({
|
|
|
375
375
|
lifecycle = 'operational',
|
|
376
376
|
workspaceRoot = process.cwd(),
|
|
377
377
|
projectAlias = '',
|
|
378
|
-
config = {}
|
|
378
|
+
config = {},
|
|
379
|
+
confidence = 0.9
|
|
379
380
|
} = {}) {
|
|
380
381
|
if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
|
|
381
382
|
const lc = validateLifecycle(lifecycle);
|
|
@@ -386,7 +387,7 @@ export async function promoteMemory({
|
|
|
386
387
|
kind: entry.type || 'note',
|
|
387
388
|
summary: normalizeMemoryText(entry.summary),
|
|
388
389
|
source: `dream-promote:${entry.id}`,
|
|
389
|
-
confidence: 0.
|
|
390
|
+
confidence: Math.min(1, Math.max(0.5, confidence)),
|
|
390
391
|
replaceSimilar: true,
|
|
391
392
|
workspaceRoot,
|
|
392
393
|
projectAlias,
|
package/src/core/paths.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
const GLOBAL_APP_DIR = 'codemini-global';
|
|
5
5
|
const PROJECT_APP_DIR = '.codemini';
|
|
6
|
-
const PROJECT_INDEX_DIR = '.codemini
|
|
6
|
+
const PROJECT_INDEX_DIR = '.codemini';
|
|
7
7
|
|
|
8
8
|
export function getBaseConfigDir() {
|
|
9
9
|
if (process.env.CODEMINI_GLOBAL_DIR) {
|
|
@@ -387,7 +387,7 @@ export async function initializeProjectIndex(cwd = process.cwd()) {
|
|
|
387
387
|
projectRoot: targetRoot,
|
|
388
388
|
projectMap,
|
|
389
389
|
fileIndex,
|
|
390
|
-
summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini
|
|
390
|
+
summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
|
|
391
391
|
};
|
|
392
392
|
})();
|
|
393
393
|
initCache.set(cacheKey, promise);
|
|
@@ -447,7 +447,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
447
447
|
path: projectRelativePath,
|
|
448
448
|
projectRoot,
|
|
449
449
|
action,
|
|
450
|
-
summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini
|
|
450
|
+
summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
|
|
451
451
|
};
|
|
452
452
|
}
|
|
453
453
|
|
|
@@ -23,8 +23,10 @@ const SHELL_PROFILES = {
|
|
|
23
23
|
'npm',
|
|
24
24
|
'npx',
|
|
25
25
|
'python',
|
|
26
|
+
'python3',
|
|
26
27
|
'py',
|
|
27
28
|
'pip',
|
|
29
|
+
'pip3',
|
|
28
30
|
'get-childitem',
|
|
29
31
|
'get-content',
|
|
30
32
|
'select-string',
|
|
@@ -70,7 +72,9 @@ const SHELL_PROFILES = {
|
|
|
70
72
|
'npm',
|
|
71
73
|
'npx',
|
|
72
74
|
'python',
|
|
75
|
+
'python3',
|
|
73
76
|
'pip',
|
|
77
|
+
'pip3',
|
|
74
78
|
'ls',
|
|
75
79
|
'cat',
|
|
76
80
|
'sed',
|
|
@@ -164,7 +168,7 @@ Some tools are loaded on demand through tool_search. Common examples:
|
|
|
164
168
|
- glob for pattern-based file lookup
|
|
165
169
|
- ast_query and read_ast_node for advanced AST-scoped reads and edits
|
|
166
170
|
- list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
|
|
167
|
-
-
|
|
171
|
+
- save_memory, list_memory, search_memory, and forget_memory for persistent memory operations
|
|
168
172
|
|
|
169
173
|
For structural code edits (functions, classes, methods), prefer AST-scoped reads before editing:
|
|
170
174
|
- Common one-shot workflow: read(path, query=..., capture_name=...) → edit with symbol or ast_target
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import cliTruncate from 'cli-truncate';
|
|
2
|
+
import stripAnsi from 'strip-ansi';
|
|
3
|
+
import { classifyCommandIntent } from './shell.js';
|
|
4
|
+
|
|
5
|
+
const CONTROL_CHARS_RE = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
6
|
+
|
|
7
|
+
export function sanitizeTextForModel(
|
|
8
|
+
value,
|
|
9
|
+
{
|
|
10
|
+
maxChars = 0,
|
|
11
|
+
maxLineLength = 220,
|
|
12
|
+
maxConsecutiveBlankLines = 1
|
|
13
|
+
} = {}
|
|
14
|
+
) {
|
|
15
|
+
if (value == null) return '';
|
|
16
|
+
|
|
17
|
+
const lines = String(value)
|
|
18
|
+
.replace(/\r\n?/g, '\n')
|
|
19
|
+
.split('\n');
|
|
20
|
+
const output = [];
|
|
21
|
+
let blankRun = 0;
|
|
22
|
+
|
|
23
|
+
for (const rawLine of lines) {
|
|
24
|
+
const line = stripAnsi(rawLine).replace(CONTROL_CHARS_RE, '').replace(/[ \t]+$/g, '');
|
|
25
|
+
if (!line.trim()) {
|
|
26
|
+
blankRun += 1;
|
|
27
|
+
if (blankRun > maxConsecutiveBlankLines) continue;
|
|
28
|
+
output.push('');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
blankRun = 0;
|
|
33
|
+
output.push(
|
|
34
|
+
maxLineLength > 0
|
|
35
|
+
? cliTruncate(line, maxLineLength, { position: 'end' })
|
|
36
|
+
: line
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let sanitized = output.join('\n').trimEnd();
|
|
41
|
+
if (maxChars > 0 && sanitized.length > maxChars) {
|
|
42
|
+
sanitized = `${sanitized.slice(0, maxChars)}\n... [sanitized output truncated ${sanitized.length - maxChars} chars]`;
|
|
43
|
+
}
|
|
44
|
+
return sanitized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getToolOutputSanitizeOptions(toolName) {
|
|
48
|
+
const name = String(toolName || '').trim();
|
|
49
|
+
if (name === 'read' || name === 'read_ast_node' || name === 'run' || name === 'web_fetch') {
|
|
50
|
+
return {
|
|
51
|
+
maxLineLength: 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sanitizePreviewLines(value, { maxLineLength = 220 } = {}) {
|
|
58
|
+
const sanitized = sanitizeTextForModel(value, {
|
|
59
|
+
maxLineLength,
|
|
60
|
+
maxConsecutiveBlankLines: 0
|
|
61
|
+
});
|
|
62
|
+
if (!sanitized) return [];
|
|
63
|
+
return sanitized
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map((line) => line.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function summarizeGitStatusPorcelain(stdout) {
|
|
70
|
+
const modified = [];
|
|
71
|
+
const added = [];
|
|
72
|
+
const deleted = [];
|
|
73
|
+
const untracked = [];
|
|
74
|
+
|
|
75
|
+
for (const line of String(stdout || '').split('\n')) {
|
|
76
|
+
const trimmed = line.trimEnd();
|
|
77
|
+
if (!trimmed) continue;
|
|
78
|
+
const status = trimmed.slice(0, 2);
|
|
79
|
+
const file = trimmed.slice(3).trim();
|
|
80
|
+
if (!file) continue;
|
|
81
|
+
if (status === '??') {
|
|
82
|
+
untracked.push(file);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (status.includes('A')) added.push(file);
|
|
86
|
+
else if (status.includes('D')) deleted.push(file);
|
|
87
|
+
else modified.push(file);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const total = modified.length + added.length + deleted.length + untracked.length;
|
|
91
|
+
if (total === 0) return '';
|
|
92
|
+
const lines = [`[git status: ${total} file(s)]`];
|
|
93
|
+
if (modified.length) lines.push(`modified: ${modified.join(', ')}`);
|
|
94
|
+
if (added.length) lines.push(`added: ${added.join(', ')}`);
|
|
95
|
+
if (deleted.length) lines.push(`deleted: ${deleted.join(', ')}`);
|
|
96
|
+
if (untracked.length) lines.push(`untracked: ${untracked.join(', ')}`);
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function summarizeTestFailure(command, code, stdout, stderr) {
|
|
101
|
+
if (classifyCommandIntent(command).kind !== 'test') {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
if (Number(code ?? 0) === 0) return '';
|
|
105
|
+
|
|
106
|
+
const lines = sanitizePreviewLines([stdout, stderr].filter(Boolean).join('\n'), { maxLineLength: 220 });
|
|
107
|
+
const kept = [];
|
|
108
|
+
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (
|
|
111
|
+
/^FAIL\b/.test(line) ||
|
|
112
|
+
/^Test Suites:/.test(line) ||
|
|
113
|
+
/^Tests:/.test(line) ||
|
|
114
|
+
/AssertionError|Error:|Expected|expected .* to /i.test(line) ||
|
|
115
|
+
/^\s*at\b/.test(line) ||
|
|
116
|
+
/:\d+:\d+\)?$/.test(line)
|
|
117
|
+
) {
|
|
118
|
+
kept.push(line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (kept.length === 0) return '';
|
|
123
|
+
return [`[test failure: exit ${code ?? 1}]`, ...kept.slice(0, 8)].join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function summarizeInstallOutput(command, code, stdout) {
|
|
127
|
+
if (classifyCommandIntent(command).kind !== 'install') return '';
|
|
128
|
+
|
|
129
|
+
const lines = sanitizePreviewLines(stdout, { maxLineLength: 220 });
|
|
130
|
+
const kept = [];
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (
|
|
133
|
+
/\b(?:added|removed|changed|audited) \d+ package/i.test(line) ||
|
|
134
|
+
/\bvulnerabilit(?:y|ies)\b/i.test(line) ||
|
|
135
|
+
/looking for funding/i.test(line)
|
|
136
|
+
) {
|
|
137
|
+
kept.push(line);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (kept.length === 0) return '';
|
|
141
|
+
return [`[install summary: exit ${code ?? 0}]`, ...kept.slice(0, 6)].join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function summarizeBuildOutput(command, code, stdout, stderr) {
|
|
145
|
+
if (classifyCommandIntent(command).kind !== 'build') return '';
|
|
146
|
+
if (Number(code ?? 0) === 0) return '';
|
|
147
|
+
|
|
148
|
+
const lines = sanitizePreviewLines([stdout, stderr].filter(Boolean).join('\n'), { maxLineLength: 220 });
|
|
149
|
+
const kept = [];
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (
|
|
152
|
+
/\berror\b/i.test(line) ||
|
|
153
|
+
/Build failed/i.test(line) ||
|
|
154
|
+
/failed with/i.test(line)
|
|
155
|
+
) {
|
|
156
|
+
kept.push(line);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (kept.length === 0) return '';
|
|
160
|
+
return [`[build failure: exit ${code ?? 1}]`, ...kept.slice(0, 8)].join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function summarizeRunOutput(result) {
|
|
164
|
+
const command = String(result?.command || '').trim();
|
|
165
|
+
const stdout = String(result?.stdout || '');
|
|
166
|
+
const stderr = String(result?.stderr || '');
|
|
167
|
+
const code = result?.code ?? 0;
|
|
168
|
+
|
|
169
|
+
if (/^git\s+status\b.*(?:--short|-s)\b/i.test(command)) {
|
|
170
|
+
const gitSummary = summarizeGitStatusPorcelain(stdout);
|
|
171
|
+
if (gitSummary) return gitSummary;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const installSummary = summarizeInstallOutput(command, code, stdout);
|
|
175
|
+
if (installSummary) return installSummary;
|
|
176
|
+
|
|
177
|
+
const buildSummary = summarizeBuildOutput(command, code, stdout, stderr);
|
|
178
|
+
if (buildSummary) return buildSummary;
|
|
179
|
+
|
|
180
|
+
const testSummary = summarizeTestFailure(command, code, stdout, stderr);
|
|
181
|
+
if (testSummary) return testSummary;
|
|
182
|
+
|
|
183
|
+
return '';
|
|
184
|
+
}
|