codemini-cli 0.4.0 → 0.4.2

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.
@@ -58,6 +58,12 @@ function isSafeEntry(entry) {
58
58
  return entry !== '.' && entry !== '..' && !entry.includes('/') && !entry.includes('\\');
59
59
  }
60
60
 
61
+ function setCommand(out, name, command) {
62
+ const existing = out.get(name);
63
+ if (existing?.source === 'bundled-skill') return;
64
+ out.set(name, command);
65
+ }
66
+
61
67
  function loadMarkdownCommandsFromDir(baseDir, source, out) {
62
68
  if (!fs.existsSync(baseDir)) return;
63
69
  for (const entry of safeEntries(baseDir)) {
@@ -70,7 +76,7 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
70
76
  if (fs.existsSync(commandFile)) {
71
77
  const raw = fs.readFileSync(commandFile, 'utf8');
72
78
  const parsed = parseFrontmatter(raw);
73
- out.set(entry, {
79
+ setCommand(out, entry, {
74
80
  name: entry,
75
81
  source,
76
82
  path: commandFile,
@@ -85,7 +91,7 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
85
91
  const name = entry.replace(/\.md$/, '');
86
92
  const raw = fs.readFileSync(full, 'utf8');
87
93
  const parsed = parseFrontmatter(raw);
88
- out.set(name, {
94
+ setCommand(out, name, {
89
95
  name,
90
96
  source,
91
97
  path: full,
@@ -107,7 +113,7 @@ function loadLegacySkillsFromDir(baseDir, source, out) {
107
113
  if (!fs.existsSync(skillFile)) continue;
108
114
  const raw = fs.readFileSync(skillFile, 'utf8');
109
115
  const parsed = parseFrontmatter(raw);
110
- out.set(entry, {
116
+ setCommand(out, entry, {
111
117
  name: entry,
112
118
  source: `${source}-skill`,
113
119
  path: skillFile,
@@ -131,7 +137,7 @@ function loadBundledSkillsFromDir(baseDir, out) {
131
137
  if (!fs.existsSync(skillFile)) continue;
132
138
  const raw = fs.readFileSync(skillFile, 'utf8');
133
139
  const parsed = parseFrontmatter(raw);
134
- out.set(entry, {
140
+ setCommand(out, entry, {
135
141
  name: entry,
136
142
  source: 'bundled-skill',
137
143
  path: skillFile,
@@ -151,12 +157,13 @@ function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
151
157
  for (const skill of registry.skills) {
152
158
  if (skill.enabled === false) continue;
153
159
  const name = skill.name;
160
+ if (out.has(name)) continue;
154
161
  const entry = skill.entryFile || 'SKILL.md';
155
162
  const full = path.join(baseDir, name, entry);
156
163
  if (!fs.existsSync(full)) continue;
157
164
  const raw = fs.readFileSync(full, 'utf8');
158
165
  const parsed = parseFrontmatter(raw);
159
- out.set(name, {
166
+ setCommand(out, name, {
160
167
  name,
161
168
  source: 'registry-skill',
162
169
  path: full,
@@ -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);
@@ -1,4 +1,5 @@
1
- import { summarizeToolResult, trimInline } from './agent-loop.js';
1
+ import { trimInline } from './string-utils.js';
2
+ import { summarizeToolResult } from './tool-result-store.js';
2
3
 
3
4
  function textFromContent(content) {
4
5
  if (typeof content === 'string') return content;
@@ -37,20 +38,30 @@ function modeToKeepRecent(mode) {
37
38
  }
38
39
 
39
40
  function buildLocalSummary(messages) {
40
- const lines = [];
41
+ const goal = [];
42
+ const constraints = [];
43
+ const changedFiles = new Set();
44
+ const verification = [];
45
+ const openThreads = [];
41
46
  const limit = 16;
42
47
  for (const msg of messages.slice(-limit)) {
43
48
  if (msg.role === 'tool') {
44
- // Try to parse tool result as JSON for semantic summary
45
49
  const text = textFromContent(msg.content);
46
50
  let parsed;
47
51
  try { parsed = JSON.parse(text); } catch { parsed = null; }
48
52
  if (parsed && typeof parsed === 'object') {
49
53
  const summary = summarizeToolResult(parsed);
50
- lines.push(`- tool_result: ${summary}`);
54
+ if (parsed.path) changedFiles.add(String(parsed.path));
55
+ if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
56
+ verification.push(summary);
57
+ } else {
58
+ openThreads.push(`tool_result: ${summary}`);
59
+ }
51
60
  } else {
52
61
  const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
53
- lines.push(`- tool_result: ${clipped}`);
62
+ const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
63
+ if (match) changedFiles.add(match[1]);
64
+ openThreads.push(`tool_result: ${clipped}`);
54
65
  }
55
66
  continue;
56
67
  }
@@ -59,21 +70,35 @@ function buildLocalSummary(messages) {
59
70
  const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
60
71
  const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
61
72
  const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
62
- lines.push(`- assistant: ${clipped}${toolInfo}`);
73
+ if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
63
74
  continue;
64
75
  }
65
76
  if (msg.role === 'user') {
66
77
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
67
78
  const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
68
- lines.push(`- user: ${clipped}`);
79
+ if (goal.length === 0) goal.push(clipped);
80
+ else constraints.push(clipped);
69
81
  continue;
70
82
  }
71
83
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
72
84
  if (!text) continue;
73
85
  const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
74
- lines.push(`- ${msg.role}: ${clipped}`);
86
+ openThreads.push(`${msg.role}: ${clipped}`);
75
87
  }
76
- return `Context Summary\n${lines.join('\n')}`.trim();
88
+ const lines = [
89
+ 'Context Summary',
90
+ 'Goal:',
91
+ goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
92
+ 'Key Constraints:',
93
+ ...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
94
+ 'Changed Files:',
95
+ ...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
96
+ 'Verification:',
97
+ ...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
98
+ 'Open Threads:',
99
+ ...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
100
+ ];
101
+ return lines.join('\n').trim();
77
102
  }
78
103
 
79
104
  export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
@@ -9,14 +9,14 @@ function getToolFewShotBlock() {
9
9
  Use these as style examples for tool calls:
10
10
 
11
11
  Current working directory: ${cwd}
12
- When a tool takes file_path, build it from the current working directory and prefer absolute paths.
12
+ When a tool takes path, build it from the current working directory and prefer absolute paths.
13
13
  If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
14
14
 
15
15
  1. File discovery then read
16
16
  User: compare the auth flow
17
17
  Assistant: first narrow the search with the project index
18
18
  Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
19
- Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
19
+ Tool: read({"path":"${cwd}/src/auth/service.ts"})
20
20
 
21
21
  If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
22
22
  Example:
@@ -27,7 +27,7 @@ Tool: glob({"pattern":"src/**/*.ts"})
27
27
  User: rename loginUser to signInUser
28
28
  Assistant: first find the exact occurrences
29
29
  Tool: grep({"pattern":"loginUser","path":"src"})
30
- Tool: edit({"file_path":"${cwd}/src/auth/service.ts","old_string":"loginUser","new_string":"signInUser"})
30
+ Tool: edit({"path":"${cwd}/src/auth/service.ts","old_text":"loginUser","new_text":"signInUser"})
31
31
 
32
32
  3. Read a specific range
33
33
  User: inspect the reducer around line 120
@@ -43,7 +43,7 @@ Assistant: keep the checklist updated as each phase finishes, and do not give a
43
43
  5. Create a new file
44
44
  User: add a notes file
45
45
  Assistant: create the file directly
46
- Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
46
+ Tool: write({"path":"${cwd}/notes.txt","content":"todo\\n"})
47
47
 
48
48
  6. Save a high-signal observation to memory
49
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:
@@ -73,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
73
73
  Tool: web_search({"query":"latest pnpm release","max_results":5})
74
74
 
75
75
  Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
76
- Prefer explicit absolute file_path values when the current working directory is known.`;
76
+ Prefer explicit absolute path values when the current working directory is known.`;
77
77
  }
78
78
 
79
79
  function getEnvBlock() {
@@ -56,6 +56,18 @@ function renderReport(report) {
56
56
  lines.push('');
57
57
  }
58
58
 
59
+ if (report.maintenance?.length) {
60
+ lines.push('## Memory Maintenance');
61
+ for (const item of report.maintenance) {
62
+ if (item.skipped) {
63
+ lines.push(`- [${item.scope}] skipped: ${item.reason}`);
64
+ } else {
65
+ lines.push(`- [${item.scope}] ${item.before} -> ${item.after} item(s)${item.changed ? ' changed' : ' marked clean'}`);
66
+ }
67
+ }
68
+ lines.push('');
69
+ }
70
+
59
71
  if (report.disagreements?.length) {
60
72
  lines.push('## Reviewer Disagreements');
61
73
  for (const d of report.disagreements) {
@@ -1,6 +1,13 @@
1
- import { listMemories, listInbox, archiveEntry, promoteMemory } from './memory-store.js';
1
+ import {
2
+ getMemoryBucketMaintenance,
3
+ listMemories,
4
+ listInbox,
5
+ archiveEntry,
6
+ promoteMemory,
7
+ replaceMemoryBucket
8
+ } from './memory-store.js';
2
9
  import { writeDreamAuditReport } from './dream-audit.js';
3
- import { evaluateInboxBatch } from './dream-evaluator.js';
10
+ import { evaluateInboxBatch, evaluateMemoryMaintenance } from './dream-evaluator.js';
4
11
 
5
12
  const LONGTERM_TYPES = new Set(['preference', 'pattern', 'win', 'decision']);
6
13
  const OPERATIONAL_TYPES = new Set(['correction', 'failure', 'gap', 'observation']);
@@ -22,6 +29,76 @@ function memoryContainsSummary(memory, summaryKey) {
22
29
  return content.includes(summaryKey) || summary.includes(summaryKey);
23
30
  }
24
31
 
32
+ function maintenanceScopes(scopeFilter) {
33
+ const scope = normalizeText(scopeFilter);
34
+ if (!scope) return ['user', 'global', 'project'];
35
+ if (scope === 'repo') return ['project'];
36
+ if (['user', 'global', 'project'].includes(scope)) return [scope];
37
+ return ['user', 'global', 'project'];
38
+ }
39
+
40
+ async function runMemoryMaintenance({
41
+ dryRun = false,
42
+ scope = null,
43
+ workspaceRoot = process.cwd(),
44
+ config = {}
45
+ } = {}) {
46
+ const reports = [];
47
+ const filesChanged = [];
48
+
49
+ for (const memoryScope of maintenanceScopes(scope)) {
50
+ const maintenance = await getMemoryBucketMaintenance({ scope: memoryScope, workspaceRoot });
51
+ const items = await listMemories({ scope: memoryScope, workspaceRoot });
52
+ if (items.length === 0) {
53
+ reports.push({ scope: memoryScope, skipped: true, reason: 'empty' });
54
+ continue;
55
+ }
56
+ if (maintenance.fresh) {
57
+ reports.push({ scope: memoryScope, skipped: true, reason: 'already-maintained', itemCount: items.length });
58
+ continue;
59
+ }
60
+
61
+ const evaluated = dryRun
62
+ ? { items, archives: [] }
63
+ : await evaluateMemoryMaintenance({ scope: memoryScope, items, config, workspaceRoot });
64
+ if (evaluated.error) {
65
+ reports.push({ scope: memoryScope, skipped: true, reason: `maintenance-error: ${evaluated.error}`, itemCount: items.length });
66
+ continue;
67
+ }
68
+ const nextItems = Array.isArray(evaluated.items) && evaluated.items.length > 0 ? evaluated.items : items;
69
+ const changed =
70
+ JSON.stringify(nextItems.map((item) => [item.kind, item.content, item.summary, item.lifecycle || ''])) !==
71
+ JSON.stringify(items.map((item) => [item.kind, item.content, item.summary, item.lifecycle || '']));
72
+
73
+ if (!dryRun) {
74
+ await replaceMemoryBucket({
75
+ scope: memoryScope,
76
+ items: nextItems,
77
+ workspaceRoot,
78
+ markMaintained: true
79
+ });
80
+ filesChanged.push({
81
+ file: memoryScope === 'project' ? 'memory/project/*.json' : `memory/${memoryScope}.json`,
82
+ why: changed
83
+ ? `LLM-maintained ${items.length} item(s) into ${nextItems.length} item(s)`
84
+ : `Marked ${items.length} item(s) as maintained`
85
+ });
86
+ }
87
+
88
+ reports.push({
89
+ scope: memoryScope,
90
+ skipped: false,
91
+ before: items.length,
92
+ after: nextItems.length,
93
+ changed,
94
+ archives: evaluated.archives || [],
95
+ dryRun
96
+ });
97
+ }
98
+
99
+ return { reports, filesChanged };
100
+ }
101
+
25
102
  export async function runDreamConsolidation({
26
103
  dryRun = false,
27
104
  scope = null,
@@ -31,9 +108,6 @@ export async function runDreamConsolidation({
31
108
  } = {}) {
32
109
  const scopeFilter = scope || null;
33
110
  const inbox = await listInbox({ scope: scopeFilter });
34
- if (inbox.length === 0) {
35
- return { ok: true, dryRun, message: 'No inbox entries to consolidate.', promotions: [], rejections: [], archives: [] };
36
- }
37
111
 
38
112
  const [globalMemories, userMemories, projectMemories] = await Promise.all([
39
113
  listMemories({ scope: 'global', workspaceRoot }),
@@ -77,67 +151,63 @@ export async function runDreamConsolidation({
77
151
  candidates.push(entry);
78
152
  }
79
153
 
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 });
154
+ if (candidates.length > 0) {
155
+ /* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
156
+ const llmResults = dryRun
157
+ ? 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 }))
158
+ : await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
93
159
 
94
- const resultMap = new Map(llmResults.map((r) => [r.id, r]));
160
+ const resultMap = new Map(llmResults.map((r) => [r.id, r]));
95
161
 
96
- /* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
97
- for (const entry of candidates) {
98
- const evaluation = resultMap.get(entry.id);
162
+ /* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
163
+ for (const entry of candidates) {
164
+ const evaluation = resultMap.get(entry.id);
99
165
 
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
- };
166
+ if (!evaluation || evaluation.action === 'discard') {
167
+ const reason = evaluation?.reason || 'LLM discarded';
168
+ if (!dryRun) await archiveEntry(entry, 'discarded-by-evaluator', reason);
169
+ rejections.push({ summary: entry.summary, reason: `evaluator-discard: ${reason}` });
170
+ continue;
171
+ }
116
172
 
117
- if (!dryRun) {
118
- try {
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 });
129
- } catch (error) {
130
- const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
131
- await archiveEntry(entry, 'promotion-failed', reason);
132
- rejections.push({ summary: entry.summary, reason: `promotion-failed: ${reason}` });
133
- archives.push({ summary: entry.summary, reason: 'promotion-failed' });
173
+ const promoteScope = evaluation.scope || 'global';
174
+ const lifecycle = chooseLifecycle(evaluation.kind);
175
+ const enrichedEntry = {
176
+ ...entry,
177
+ /* 用 LLM 提炼后的内容覆盖原始报错 */
178
+ summary: evaluation.summary || entry.summary,
179
+ details: evaluation.content || entry.details || entry.summary,
180
+ type: evaluation.kind || entry.type || 'observation'
181
+ };
182
+
183
+ if (!dryRun) {
184
+ try {
185
+ await promoteMemory({
186
+ entry: enrichedEntry,
187
+ scope: promoteScope,
188
+ lifecycle,
189
+ workspaceRoot,
190
+ config,
191
+ confidence: evaluation.confidence || 0.8
192
+ });
193
+ filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${enrichedEntry.summary}" as ${lifecycle} (${promoteScope})` });
194
+ promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence });
195
+ } catch (error) {
196
+ const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
197
+ await archiveEntry(entry, 'promotion-failed', reason);
198
+ rejections.push({ summary: entry.summary, reason: `promotion-failed: ${reason}` });
199
+ archives.push({ summary: entry.summary, reason: 'promotion-failed' });
200
+ }
201
+ continue;
134
202
  }
135
- continue;
136
- }
137
203
 
138
- promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
204
+ promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
205
+ }
139
206
  }
140
207
 
208
+ const maintenance = await runMemoryMaintenance({ dryRun, scope: scopeFilter, workspaceRoot, config });
209
+ filesChanged.push(...maintenance.filesChanged);
210
+
141
211
  const report = {
142
212
  timestamp: new Date().toISOString(),
143
213
  filesRead,
@@ -145,7 +215,9 @@ export async function runDreamConsolidation({
145
215
  candidatesGenerated: inbox.length,
146
216
  promotions,
147
217
  rejections,
148
- archives
218
+ archives,
219
+ maintenance: maintenance.reports,
220
+ ...(inbox.length === 0 ? { message: 'No inbox entries to consolidate; maintained existing memory buckets.' } : {})
149
221
  };
150
222
 
151
223
  if (!dryRun && writeAudit) {
@@ -22,6 +22,26 @@ Rules:
22
22
  - General coding/environment knowledge → scope "global"
23
23
  - If in doubt, discard. Memory is expensive; only promote what future sessions will genuinely benefit from.`;
24
24
 
25
+ const MAINTENANCE_SYSTEM_PROMPT = `You are maintaining an existing persistent memory bucket for a coding assistant.
26
+
27
+ Your job:
28
+ 1. Merge duplicates and near-duplicates.
29
+ 2. Summarize clusters into fewer, higher-signal memories.
30
+ 3. Remove stale, contradictory, trivial, or overly specific noise.
31
+ 4. Preserve important exact commands, file paths, preferences, and constraints.
32
+ 5. Keep memories scoped exactly to the bucket you receive.
33
+
34
+ Respond with valid JSON only, no markdown fences:
35
+ {"items":[{"kind":"preference|workflow|pattern|observation|correction|decision|failure|architecture|module|note","content":"durable memory text","summary":"under 80 chars","confidence":0.5,"pinned":false,"lifecycle":"longterm|operational"}],"archives":[{"source_ids":["mem_..."],"reason":"merged|stale|duplicate|noise|contradiction"}]}
36
+
37
+ Rules:
38
+ - Prefer fewer, clearer items, but do not collapse unrelated facts.
39
+ - User preferences belong in user memory and should not become project rules.
40
+ - Project conventions belong in project memory and should not become user preferences.
41
+ - Global memory is only for reusable cross-project/tool/environment knowledge.
42
+ - If a pinned item is still valid, keep it.
43
+ - Return at least one item if the input has useful durable content.`;
44
+
25
45
  function parseResults(text) {
26
46
  try {
27
47
  const json = JSON.parse(text);
@@ -97,3 +117,69 @@ export async function evaluateInboxBatch({ entries, config, workspaceRoot }) {
97
117
  }));
98
118
  }
99
119
  }
120
+
121
+ function parseMaintenanceResult(text) {
122
+ try {
123
+ const json = JSON.parse(text);
124
+ const items = Array.isArray(json?.items) ? json.items : [];
125
+ const archives = Array.isArray(json?.archives) ? json.archives : [];
126
+ return {
127
+ items: items
128
+ .map((item) => ({
129
+ kind: String(item.kind || 'note').slice(0, 40),
130
+ content: String(item.content || '').slice(0, 600),
131
+ summary: String(item.summary || item.content || '').slice(0, 120),
132
+ confidence: Math.min(1, Math.max(0.5, Number(item.confidence) || 0.8)),
133
+ pinned: item.pinned === true,
134
+ lifecycle: ['longterm', 'operational'].includes(String(item.lifecycle || '')) ? String(item.lifecycle) : undefined
135
+ }))
136
+ .filter((item) => item.content.trim()),
137
+ archives: archives.map((archive) => ({
138
+ source_ids: Array.isArray(archive.source_ids) ? archive.source_ids.map((id) => String(id)).filter(Boolean) : [],
139
+ reason: String(archive.reason || '').slice(0, 160)
140
+ }))
141
+ };
142
+ } catch {
143
+ return { items: [], archives: [] };
144
+ }
145
+ }
146
+
147
+ export async function evaluateMemoryMaintenance({ scope, items, config, workspaceRoot }) {
148
+ const sourceItems = Array.isArray(items) ? items : [];
149
+ if (sourceItems.length === 0) return { items: [], archives: [] };
150
+
151
+ const compactItems = sourceItems.map((item) => ({
152
+ id: item.id,
153
+ kind: item.kind,
154
+ content: String(item.content || '').slice(0, 600),
155
+ summary: String(item.summary || '').slice(0, 160),
156
+ confidence: item.confidence,
157
+ pinned: item.pinned === true,
158
+ lifecycle: item.lifecycle || ''
159
+ }));
160
+
161
+ try {
162
+ const result = await createChatCompletion({
163
+ sdkProvider: config?.sdk?.provider,
164
+ baseUrl: config?.gateway?.base_url,
165
+ apiKey: config?.gateway?.api_key,
166
+ model: config?.model?.name,
167
+ messages: [
168
+ { role: 'system', content: MAINTENANCE_SYSTEM_PROMPT },
169
+ {
170
+ role: 'user',
171
+ content: `Maintain this ${scope} memory bucket. Workspace: ${workspaceRoot || process.cwd()}\n\n${JSON.stringify(compactItems, null, 2)}`
172
+ }
173
+ ],
174
+ temperature: 0,
175
+ timeoutMs: EVAL_TIMEOUT_MS
176
+ });
177
+ return parseMaintenanceResult(result?.text || '');
178
+ } catch (error) {
179
+ return {
180
+ items: sourceItems,
181
+ archives: [],
182
+ error: String(error?.message || error || 'memory maintenance failed')
183
+ };
184
+ }
185
+ }
@@ -111,7 +111,7 @@ class FffMcpClient {
111
111
  capabilities: {},
112
112
  clientInfo: {
113
113
  name: 'codemini-cli',
114
- version: '0.4.0'
114
+ version: '0.4.2'
115
115
  }
116
116
  });
117
117
  this.sendNotification('notifications/initialized', {});