codemini-cli 0.4.1 → 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.
- package/OPERATIONS.md +4 -2
- package/README.md +83 -5
- package/deployment.md +14 -7
- package/package.json +1 -2
- package/src/cli.js +1 -1
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +1 -206
- package/src/core/chat-runtime.js +306 -53
- package/src/core/command-loader.js +12 -5
- package/src/core/context-compact.js +2 -1
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/reflect-skill.js +178 -0
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +126 -30
- package/src/tui/chat-app.js +247 -27
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
166
|
+
setCommand(out, name, {
|
|
160
167
|
name,
|
|
161
168
|
source: 'registry-skill',
|
|
162
169
|
path: full,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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;
|
package/src/core/dream-audit.js
CHANGED
|
@@ -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 {
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
160
|
+
const resultMap = new Map(llmResults.map((r) => [r.id, r]));
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
162
|
+
/* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
|
|
163
|
+
for (const entry of candidates) {
|
|
164
|
+
const evaluation = resultMap.get(entry.id);
|
|
99
165
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/fff-adapter.js
CHANGED
package/src/core/memory-store.js
CHANGED
|
@@ -41,22 +41,92 @@ async function ensureParent(filePath) {
|
|
|
41
41
|
function buildFilePath(scope, workspaceRoot = process.cwd(), projectAlias = '') {
|
|
42
42
|
if (scope === 'user') return path.join(getMemoryDir(), 'user.json');
|
|
43
43
|
if (scope === 'global') return path.join(getMemoryDir(), 'global.json');
|
|
44
|
-
return path.join(getProjectMemoryDir(workspaceRoot),
|
|
44
|
+
return path.join(getProjectMemoryDir(workspaceRoot), 'project.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listProjectMemoryFiles(workspaceRoot = process.cwd()) {
|
|
48
|
+
const dir = getProjectMemoryDir(workspaceRoot);
|
|
49
|
+
try {
|
|
50
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
51
|
+
return entries
|
|
52
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
53
|
+
.map((entry) => path.join(dir, entry.name))
|
|
54
|
+
.sort();
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
async function readMemoryBucket(filePath) {
|
|
61
|
+
const doc = await readMemoryBucketDocument(filePath);
|
|
62
|
+
return doc.items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readMemoryBucketDocument(filePath) {
|
|
48
66
|
try {
|
|
49
67
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
50
68
|
const parsed = JSON.parse(raw);
|
|
51
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
items: Array.isArray(parsed?.items) ? parsed.items : [],
|
|
71
|
+
maintenance: parsed?.maintenance && typeof parsed.maintenance === 'object' ? parsed.maintenance : null
|
|
72
|
+
};
|
|
52
73
|
} catch {
|
|
53
|
-
return [];
|
|
74
|
+
return { items: [], maintenance: null };
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
|
|
78
|
+
function memoryBucketHash(items = []) {
|
|
79
|
+
const stable = (Array.isArray(items) ? items : [])
|
|
80
|
+
.map((item) => ({
|
|
81
|
+
id: String(item?.id || ''),
|
|
82
|
+
kind: String(item?.kind || ''),
|
|
83
|
+
content: normalizeMemoryText(item?.content || ''),
|
|
84
|
+
summary: normalizeMemoryText(item?.summary || ''),
|
|
85
|
+
lifecycle: String(item?.lifecycle || ''),
|
|
86
|
+
pinned: item?.pinned === true
|
|
87
|
+
}))
|
|
88
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
89
|
+
return sha256(JSON.stringify(stable));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function writeMemoryBucket(filePath, items, { maintenance = null } = {}) {
|
|
58
93
|
await ensureParent(filePath);
|
|
59
|
-
|
|
94
|
+
const doc = { items };
|
|
95
|
+
if (maintenance) doc.maintenance = maintenance;
|
|
96
|
+
await fs.writeFile(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dedupeMemoryItems(items = []) {
|
|
100
|
+
const deduped = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const key = item.id ? `id:${item.id}` : `${item.kind}:${normalizeMemoryText(item.content)}`;
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
deduped.push(item);
|
|
107
|
+
}
|
|
108
|
+
return deduped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readProjectMemoryItems(workspaceRoot = process.cwd(), projectAlias = '') {
|
|
112
|
+
const projectKey = getProjectMemoryKey(workspaceRoot, projectAlias);
|
|
113
|
+
const files = await listProjectMemoryFiles(workspaceRoot);
|
|
114
|
+
const items = [];
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const bucket = await readMemoryBucket(file);
|
|
117
|
+
items.push(...bucket.map((item) => normalizeMemoryItem(item, 'project', projectKey)));
|
|
118
|
+
}
|
|
119
|
+
return dedupeMemoryItems(items)
|
|
120
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readScopeMemoryItems(scope, workspaceRoot = process.cwd(), projectAlias = '') {
|
|
124
|
+
const normalizedScope = ensureScope(scope);
|
|
125
|
+
if (normalizedScope === 'project') return readProjectMemoryItems(workspaceRoot, projectAlias);
|
|
126
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
127
|
+
return (await readMemoryBucket(filePath))
|
|
128
|
+
.map((item) => normalizeMemoryItem(item, normalizedScope, ''))
|
|
129
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
60
130
|
}
|
|
61
131
|
|
|
62
132
|
function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
@@ -74,7 +144,8 @@ function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
|
74
144
|
createdAt: String(item?.createdAt || now),
|
|
75
145
|
updatedAt: String(item?.updatedAt || now),
|
|
76
146
|
hits: Number.isFinite(Number(item?.hits)) ? Number(item.hits) : 0,
|
|
77
|
-
pinned: item?.pinned === true
|
|
147
|
+
pinned: item?.pinned === true,
|
|
148
|
+
...(item?.lifecycle ? { lifecycle: String(item.lifecycle) } : {})
|
|
78
149
|
};
|
|
79
150
|
}
|
|
80
151
|
|
|
@@ -96,13 +167,69 @@ function budgetForScope(scope, config = {}) {
|
|
|
96
167
|
}
|
|
97
168
|
|
|
98
169
|
export async function listMemories({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
170
|
+
const normalizedScope = ensureScope(scope);
|
|
171
|
+
return readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function getMemoryBucketMaintenance({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
175
|
+
const normalizedScope = ensureScope(scope);
|
|
176
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
177
|
+
const doc = await readMemoryBucketDocument(filePath);
|
|
178
|
+
const items = normalizedScope === 'project'
|
|
179
|
+
? await readProjectMemoryItems(workspaceRoot, projectAlias)
|
|
180
|
+
: doc.items.map((item) => normalizeMemoryItem(item, normalizedScope, ''));
|
|
181
|
+
const currentHash = memoryBucketHash(items);
|
|
182
|
+
const storedHash = String(doc.maintenance?.contentHash || '');
|
|
183
|
+
const maintainedAt = String(doc.maintenance?.maintainedAt || '');
|
|
184
|
+
return {
|
|
185
|
+
scope: normalizedScope,
|
|
186
|
+
itemCount: items.length,
|
|
187
|
+
contentHash: currentHash,
|
|
188
|
+
storedHash,
|
|
189
|
+
maintainedAt,
|
|
190
|
+
fresh: Boolean(maintainedAt && storedHash && storedHash === currentHash)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function markMemoryBucketMaintained({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
195
|
+
const normalizedScope = ensureScope(scope);
|
|
196
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
197
|
+
const items = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
198
|
+
const maintenance = {
|
|
199
|
+
maintainedAt: nowIso(),
|
|
200
|
+
contentHash: memoryBucketHash(items),
|
|
201
|
+
itemCount: items.length
|
|
202
|
+
};
|
|
203
|
+
await writeMemoryBucket(filePath, items, { maintenance });
|
|
204
|
+
return { scope: normalizedScope, ...maintenance };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function replaceMemoryBucket({
|
|
208
|
+
scope,
|
|
209
|
+
items = [],
|
|
210
|
+
workspaceRoot = process.cwd(),
|
|
211
|
+
projectAlias = '',
|
|
212
|
+
markMaintained = false
|
|
213
|
+
} = {}) {
|
|
99
214
|
const normalizedScope = ensureScope(scope);
|
|
100
215
|
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
101
216
|
const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
|
|
102
|
-
const
|
|
103
|
-
return items
|
|
217
|
+
const normalizedItems = (Array.isArray(items) ? items : [])
|
|
104
218
|
.map((item) => normalizeMemoryItem(item, normalizedScope, projectKey))
|
|
105
|
-
.
|
|
219
|
+
.filter((item) => item.content);
|
|
220
|
+
const maintenance = markMaintained
|
|
221
|
+
? {
|
|
222
|
+
maintainedAt: nowIso(),
|
|
223
|
+
contentHash: memoryBucketHash(normalizedItems),
|
|
224
|
+
itemCount: normalizedItems.length
|
|
225
|
+
}
|
|
226
|
+
: null;
|
|
227
|
+
await writeMemoryBucket(filePath, normalizedItems, { maintenance });
|
|
228
|
+
return {
|
|
229
|
+
scope: normalizedScope,
|
|
230
|
+
items: normalizedItems,
|
|
231
|
+
maintenance
|
|
232
|
+
};
|
|
106
233
|
}
|
|
107
234
|
|
|
108
235
|
export async function rememberMemory({
|
|
@@ -125,7 +252,7 @@ export async function rememberMemory({
|
|
|
125
252
|
|
|
126
253
|
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
127
254
|
const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
|
|
128
|
-
const existing =
|
|
255
|
+
const existing = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
129
256
|
const probe = normalizeMemoryItem({ content: normalizedContent, kind, summary, source, confidence, pinned }, normalizedScope, projectKey);
|
|
130
257
|
|
|
131
258
|
const replaceIndex = replaceSimilar ? existing.findIndex((item) => sameMemory(item, probe)) : -1;
|
|
@@ -170,6 +297,14 @@ export async function forgetMemory({ scope, id, workspaceRoot = process.cwd(), p
|
|
|
170
297
|
const existing = await listMemories({ scope: normalizedScope, workspaceRoot, projectAlias });
|
|
171
298
|
const kept = existing.filter((item) => item.id !== id);
|
|
172
299
|
await writeMemoryBucket(filePath, kept);
|
|
300
|
+
if (normalizedScope === 'project') {
|
|
301
|
+
const files = (await listProjectMemoryFiles(workspaceRoot)).filter((file) => file !== filePath);
|
|
302
|
+
await Promise.all(files.map(async (file) => {
|
|
303
|
+
const bucket = await readMemoryBucket(file);
|
|
304
|
+
const next = bucket.filter((item) => String(item?.id || '') !== id);
|
|
305
|
+
if (next.length !== bucket.length) await writeMemoryBucket(file, next);
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
173
308
|
return { removed: existing.length - kept.length };
|
|
174
309
|
}
|
|
175
310
|
|