metame-cli 1.4.19 → 1.4.21
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 +30 -24
- package/index.js +39 -1
- package/package.json +1 -1
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +73 -63
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -39
- package/scripts/daemon-command-router.js +38 -35
- package/scripts/daemon-default.yaml +18 -0
- package/scripts/daemon-exec-commands.js +6 -12
- package/scripts/daemon-file-browser.js +6 -5
- package/scripts/daemon-runtime-lifecycle.js +19 -5
- package/scripts/daemon-session-commands.js +8 -3
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon.js +38 -6
- package/scripts/distill.js +11 -12
- package/scripts/memory-gc.js +239 -0
- package/scripts/memory-index.js +103 -0
- package/scripts/memory-nightly-reflect.js +299 -0
- package/scripts/memory-write.js +192 -0
- package/scripts/memory.js +144 -6
- package/scripts/schema.js +30 -9
- package/scripts/self-reflect.js +121 -5
- package/scripts/session-analytics.js +9 -10
- package/scripts/task-board.js +9 -3
- package/scripts/telegram-adapter.js +77 -9
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* memory-nightly-reflect.js — Nightly Hot-Fact Distillation
|
|
5
|
+
*
|
|
6
|
+
* Reads "hot zone" facts from memory.db (search_count >= 3, last 7 days),
|
|
7
|
+
* calls Haiku to distill high-level patterns, and writes results to:
|
|
8
|
+
* - ~/.metame/memory/decisions/YYYY-MM-DD-nightly-reflect.md (strategic/architectural)
|
|
9
|
+
* - ~/.metame/memory/lessons/YYYY-MM-DD-nightly-reflect.md (operational SOPs)
|
|
10
|
+
*
|
|
11
|
+
* Designed to run nightly at 01:00 via daemon.yaml scheduler (require_idle).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
const HOME = os.homedir();
|
|
21
|
+
const METAME_DIR = path.join(HOME, '.metame');
|
|
22
|
+
const DB_PATH = path.join(METAME_DIR, 'memory.db');
|
|
23
|
+
const LOCK_FILE = path.join(METAME_DIR, 'memory-nightly-reflect.lock');
|
|
24
|
+
const REFLECT_LOG_FILE = path.join(METAME_DIR, 'memory_reflect_log.jsonl');
|
|
25
|
+
|
|
26
|
+
const MEMORY_DIR = path.join(HOME, '.metame', 'memory');
|
|
27
|
+
const DECISIONS_DIR = path.join(MEMORY_DIR, 'decisions');
|
|
28
|
+
const LESSONS_DIR = path.join(MEMORY_DIR, 'lessons');
|
|
29
|
+
|
|
30
|
+
// Hot zone thresholds
|
|
31
|
+
const MIN_SEARCH_COUNT = 3;
|
|
32
|
+
const WINDOW_DAYS = 7;
|
|
33
|
+
const MAX_FACTS = 20;
|
|
34
|
+
const LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
35
|
+
|
|
36
|
+
// Ensure output directories exist at startup
|
|
37
|
+
[MEMORY_DIR, DECISIONS_DIR, LESSONS_DIR].forEach(d => fs.mkdirSync(d, { recursive: true }));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load callHaiku + buildDistillEnv from deployed path, fallback to scripts dir.
|
|
41
|
+
*/
|
|
42
|
+
function loadHelper(name) {
|
|
43
|
+
const candidates = [
|
|
44
|
+
path.join(HOME, '.metame', name),
|
|
45
|
+
path.join(__dirname, name),
|
|
46
|
+
];
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
try { return require(p); } catch {}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Cannot load ${name}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Acquire atomic lock using O_EXCL — prevents concurrent runs.
|
|
55
|
+
*/
|
|
56
|
+
function acquireLock() {
|
|
57
|
+
try {
|
|
58
|
+
const fd = fs.openSync(LOCK_FILE, 'wx');
|
|
59
|
+
fs.writeSync(fd, process.pid.toString());
|
|
60
|
+
fs.closeSync(fd);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e.code === 'EEXIST') {
|
|
64
|
+
try {
|
|
65
|
+
const lockAge = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
66
|
+
if (lockAge < LOCK_TIMEOUT_MS) {
|
|
67
|
+
console.log('[NIGHTLY-REFLECT] Already running (lock held), skipping.');
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Stale lock — remove and re-acquire
|
|
71
|
+
fs.unlinkSync(LOCK_FILE);
|
|
72
|
+
const fd = fs.openSync(LOCK_FILE, 'wx');
|
|
73
|
+
fs.writeSync(fd, process.pid.toString());
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
console.log('[NIGHTLY-REFLECT] Could not acquire lock, skipping.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Release the atomic lock.
|
|
87
|
+
*/
|
|
88
|
+
function releaseLock() {
|
|
89
|
+
try { fs.unlinkSync(LOCK_FILE); } catch { /* non-fatal */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Append a run record to the audit log.
|
|
94
|
+
*/
|
|
95
|
+
function writeReflectLog(record) {
|
|
96
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n';
|
|
97
|
+
try {
|
|
98
|
+
fs.mkdirSync(path.dirname(REFLECT_LOG_FILE), { recursive: true });
|
|
99
|
+
fs.appendFileSync(REFLECT_LOG_FILE, line, 'utf8');
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.log(`[NIGHTLY-REFLECT] Warning: could not write reflect log: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Query hot zone facts from memory.db.
|
|
107
|
+
* Returns array of plain objects.
|
|
108
|
+
*/
|
|
109
|
+
function queryHotFacts(db) {
|
|
110
|
+
const stmt = db.prepare(`
|
|
111
|
+
SELECT entity, relation, value, confidence, search_count, created_at
|
|
112
|
+
FROM facts
|
|
113
|
+
WHERE search_count >= ${MIN_SEARCH_COUNT}
|
|
114
|
+
AND created_at >= datetime('now', '-${WINDOW_DAYS} days')
|
|
115
|
+
AND superseded_by IS NULL
|
|
116
|
+
AND (conflict_status IS NULL OR conflict_status = 'OK')
|
|
117
|
+
AND relation != 'project_milestone'
|
|
118
|
+
ORDER BY search_count DESC, created_at DESC
|
|
119
|
+
LIMIT ${MAX_FACTS}
|
|
120
|
+
`);
|
|
121
|
+
return stmt.all();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Write a reflect Markdown file with frontmatter.
|
|
126
|
+
*/
|
|
127
|
+
function writeReflectFile(filePath, entries, factsCount, sourceType) {
|
|
128
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
129
|
+
const sections = entries
|
|
130
|
+
.map(e => `## ${e.title}\n\n${e.content}`)
|
|
131
|
+
.join('\n\n---\n\n');
|
|
132
|
+
|
|
133
|
+
const content = `---
|
|
134
|
+
date: ${today}
|
|
135
|
+
source: nightly-reflect
|
|
136
|
+
type: ${sourceType}
|
|
137
|
+
facts_analyzed: ${factsCount}
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
${sections}
|
|
141
|
+
`;
|
|
142
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Main nightly reflect run.
|
|
147
|
+
*/
|
|
148
|
+
async function run() {
|
|
149
|
+
console.log('[NIGHTLY-REFLECT] Starting nightly reflect run...');
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(DB_PATH)) {
|
|
152
|
+
console.log('[NIGHTLY-REFLECT] memory.db not found, skipping.');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!acquireLock()) return;
|
|
157
|
+
|
|
158
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
159
|
+
const decisionFile = path.join(DECISIONS_DIR, `${today}-nightly-reflect.md`);
|
|
160
|
+
const lessonFile = path.join(LESSONS_DIR, `${today}-nightly-reflect.md`);
|
|
161
|
+
|
|
162
|
+
// Prevent duplicate runs for the same day
|
|
163
|
+
if (fs.existsSync(decisionFile) || fs.existsSync(lessonFile)) {
|
|
164
|
+
console.log('[NIGHTLY-REFLECT] Already ran today, skipping.');
|
|
165
|
+
releaseLock();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let db;
|
|
170
|
+
try {
|
|
171
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
172
|
+
db = new DatabaseSync(DB_PATH);
|
|
173
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
174
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
175
|
+
|
|
176
|
+
const hotFacts = queryHotFacts(db);
|
|
177
|
+
console.log(`[NIGHTLY-REFLECT] Found ${hotFacts.length} hot-zone facts.`);
|
|
178
|
+
|
|
179
|
+
if (hotFacts.length < 3) {
|
|
180
|
+
console.log('[NIGHTLY-REFLECT] Insufficient hot facts (< 3), skipping distillation.');
|
|
181
|
+
writeReflectLog({ status: 'skipped', reason: 'insufficient_facts', facts_found: hotFacts.length });
|
|
182
|
+
releaseLock();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Load Haiku helper from providers.js (callHaiku lives there)
|
|
187
|
+
let callHaiku, buildDistillEnv;
|
|
188
|
+
try {
|
|
189
|
+
({ callHaiku, buildDistillEnv } = loadHelper('providers.js'));
|
|
190
|
+
} catch (e) {
|
|
191
|
+
throw new Error(`Cannot load Haiku helper from providers.js: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let distillEnv = {};
|
|
195
|
+
try { distillEnv = buildDistillEnv(); } catch {}
|
|
196
|
+
|
|
197
|
+
const factsJson = JSON.stringify(
|
|
198
|
+
hotFacts.map(f => ({
|
|
199
|
+
entity: f.entity,
|
|
200
|
+
relation: f.relation,
|
|
201
|
+
value: f.value,
|
|
202
|
+
confidence: f.confidence,
|
|
203
|
+
search_count: f.search_count,
|
|
204
|
+
})),
|
|
205
|
+
null,
|
|
206
|
+
2
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const prompt = `You are extracting high-level patterns from an AI assistant's recent memory facts.
|
|
210
|
+
|
|
211
|
+
Recent high-frequency facts (JSON):
|
|
212
|
+
${factsJson}
|
|
213
|
+
|
|
214
|
+
Analyze and output a JSON object:
|
|
215
|
+
{
|
|
216
|
+
"decisions": [{"title": "中文标题", "content": "## 背景\\n...\\n## 结论\\n..."}],
|
|
217
|
+
"lessons": [{"title": "中文标题", "content": "## 问题\\n...\\n## 操作手册\\n1. ..."}]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Rules:
|
|
221
|
+
- decisions: strategic/architectural insights (why we chose X over Y)
|
|
222
|
+
- lessons: operational SOPs (how to do X correctly)
|
|
223
|
+
- Each array can be empty if no pattern found
|
|
224
|
+
- content in 中文, 100-250 chars each
|
|
225
|
+
- Output ONLY the JSON object`;
|
|
226
|
+
|
|
227
|
+
console.log('[NIGHTLY-REFLECT] Calling Haiku for distillation...');
|
|
228
|
+
let raw;
|
|
229
|
+
try {
|
|
230
|
+
raw = await Promise.race([
|
|
231
|
+
callHaiku(prompt, distillEnv, 90000),
|
|
232
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 95000)),
|
|
233
|
+
]);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.log(`[NIGHTLY-REFLECT] Haiku call failed: ${e.message}`);
|
|
236
|
+
writeReflectLog({ status: 'error', reason: 'haiku_failed', error: e.message, facts_found: hotFacts.length });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Parse Haiku response
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
244
|
+
parsed = JSON.parse(cleaned);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.log(`[NIGHTLY-REFLECT] Failed to parse Haiku output: ${e.message}`);
|
|
247
|
+
writeReflectLog({ status: 'error', reason: 'parse_failed', facts_found: hotFacts.length });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const decisions = Array.isArray(parsed.decisions) ? parsed.decisions.filter(d => d.title && d.content) : [];
|
|
252
|
+
const lessons = Array.isArray(parsed.lessons) ? parsed.lessons.filter(l => l.title && l.content) : [];
|
|
253
|
+
|
|
254
|
+
console.log(`[NIGHTLY-REFLECT] Distilled: ${decisions.length} decision(s), ${lessons.length} lesson(s).`);
|
|
255
|
+
|
|
256
|
+
// Write decisions file (even if empty array — record the run)
|
|
257
|
+
if (decisions.length > 0) {
|
|
258
|
+
writeReflectFile(decisionFile, decisions, hotFacts.length, 'decisions');
|
|
259
|
+
console.log(`[NIGHTLY-REFLECT] Decisions written: ${decisionFile}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Write lessons file
|
|
263
|
+
if (lessons.length > 0) {
|
|
264
|
+
writeReflectFile(lessonFile, lessons, hotFacts.length, 'lessons');
|
|
265
|
+
console.log(`[NIGHTLY-REFLECT] Lessons written: ${lessonFile}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Write audit log
|
|
269
|
+
writeReflectLog({
|
|
270
|
+
status: 'success',
|
|
271
|
+
facts_analyzed: hotFacts.length,
|
|
272
|
+
decisions_written: decisions.length,
|
|
273
|
+
lessons_written: lessons.length,
|
|
274
|
+
decision_file: decisions.length > 0 ? decisionFile : null,
|
|
275
|
+
lesson_file: lessons.length > 0 ? lessonFile : null,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.log('[NIGHTLY-REFLECT] Run complete.');
|
|
279
|
+
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.error(`[NIGHTLY-REFLECT] Fatal error: ${e.message}`);
|
|
282
|
+
writeReflectLog({ status: 'error', reason: 'fatal', error: e.message });
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
} finally {
|
|
285
|
+
try { if (db) db.close(); } catch { /* non-fatal */ }
|
|
286
|
+
releaseLock();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (require.main === module) {
|
|
291
|
+
run().then(() => {
|
|
292
|
+
console.log('✅ nightly-reflect complete');
|
|
293
|
+
}).catch(e => {
|
|
294
|
+
console.error(`[NIGHTLY-REFLECT] Fatal: ${e.message}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = { run };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* memory-write.js — Active memory injection CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node memory-write.js "entity" "relation" "value"
|
|
7
|
+
* node memory-write.js "entity" "relation" "value" --confidence high --project metame --tags "tag1,tag2"
|
|
8
|
+
* node memory-write.js --help
|
|
9
|
+
*
|
|
10
|
+
* Reuses memory.js saveFacts() and acquire/release pattern. Zero new dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// ── Legal relation types ──────────────────────────────────────────────────────
|
|
19
|
+
const VALID_RELATIONS = new Set([
|
|
20
|
+
'tech_decision',
|
|
21
|
+
'bug_lesson',
|
|
22
|
+
'arch_convention',
|
|
23
|
+
'config_fact',
|
|
24
|
+
'config_change',
|
|
25
|
+
'user_pref',
|
|
26
|
+
'workflow_rule',
|
|
27
|
+
'project_milestone',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// ── Entity whitelist (no dot required for these) ──────────────────────────────
|
|
31
|
+
const ENTITY_WHITELIST = new Set(['王总', 'system', 'user', 'MetaMe']);
|
|
32
|
+
|
|
33
|
+
// ── Help text ─────────────────────────────────────────────────────────────────
|
|
34
|
+
const HELP = `
|
|
35
|
+
memory-write.js — 主动写入事实到 memory.db
|
|
36
|
+
|
|
37
|
+
用法:
|
|
38
|
+
node memory-write.js <entity> <relation> <value> [options]
|
|
39
|
+
|
|
40
|
+
参数:
|
|
41
|
+
entity 知识主体,须含点号(如 MetaMe.daemon)或在白名单(王总/system/user/MetaMe)
|
|
42
|
+
relation 关系类型,合法值:
|
|
43
|
+
${[...VALID_RELATIONS].join(', ')}
|
|
44
|
+
value 事实内容,20-300 字符
|
|
45
|
+
|
|
46
|
+
选项:
|
|
47
|
+
--confidence <level> high | medium | low (默认 medium)
|
|
48
|
+
--project <key> 项目标识(默认从 cwd 推断,推断不到则 '*')
|
|
49
|
+
--tags <tag1,tag2> 最多 3 个标签,逗号分隔
|
|
50
|
+
--help 显示此帮助
|
|
51
|
+
|
|
52
|
+
示例:
|
|
53
|
+
node memory-write.js "MetaMe.daemon" "bug_lesson" "修复X前必须先Y,否则Z会挂"
|
|
54
|
+
node memory-write.js "MetaMe.bridge" "tech_decision" "飞书回调走 3000 端口,nginx 转发" --confidence high --project metame
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
57
|
+
// ── Project inference ─────────────────────────────────────────────────────────
|
|
58
|
+
function inferProject(cwd) {
|
|
59
|
+
if (!cwd) return '*';
|
|
60
|
+
const known = { 'MetaMe': 'metame', 'metame-desktop': 'desktop' };
|
|
61
|
+
for (const [dir, key] of Object.entries(known)) {
|
|
62
|
+
if (cwd.includes(dir)) return key;
|
|
63
|
+
}
|
|
64
|
+
return '*';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Argv parser (zero deps) ───────────────────────────────────────────────────
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const positional = [];
|
|
70
|
+
const opts = {};
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < argv.length) {
|
|
73
|
+
const a = argv[i];
|
|
74
|
+
if (a === '--help' || a === '-h') { opts.help = true; i++; continue; }
|
|
75
|
+
if (a.startsWith('--')) {
|
|
76
|
+
const key = a.slice(2);
|
|
77
|
+
const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
|
|
78
|
+
if (val !== null) { opts[key] = val; i += 2; } else { opts[key] = true; i++; }
|
|
79
|
+
} else {
|
|
80
|
+
positional.push(a);
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { positional, opts };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Validation ────────────────────────────────────────────────────────────────
|
|
88
|
+
function validateEntity(entity) {
|
|
89
|
+
if (!entity) return 'entity 不能为空';
|
|
90
|
+
if (ENTITY_WHITELIST.has(entity)) return null;
|
|
91
|
+
if (!entity.includes('.')) return `entity 格式不合法:须含点号(如 MetaMe.daemon)或为白名单值(${[...ENTITY_WHITELIST].join('/')})`;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateRelation(relation) {
|
|
96
|
+
if (!relation) return 'relation 不能为空';
|
|
97
|
+
if (!VALID_RELATIONS.has(relation)) {
|
|
98
|
+
return `relation 不合法:"${relation}"\n合法值: ${[...VALID_RELATIONS].join(', ')}`;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateValue(value) {
|
|
104
|
+
if (!value) return 'value 不能为空';
|
|
105
|
+
if (value.length < 20) return `value 太短(${value.length} 字符),最少 20 字符`;
|
|
106
|
+
if (value.length > 300) return `value 太长(${value.length} 字符),最多 300 字符`;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateConfidence(conf) {
|
|
111
|
+
const valid = ['high', 'medium', 'low'];
|
|
112
|
+
if (!valid.includes(conf)) return `confidence 不合法:"${conf}",合法值: high | medium | low`;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Resolve memory.js (supports both dev and deployed paths) ──────────────────
|
|
117
|
+
function resolveMemory() {
|
|
118
|
+
const candidates = [
|
|
119
|
+
path.join(os.homedir(), '.metame', 'memory.js'),
|
|
120
|
+
path.join(__dirname, 'memory.js'),
|
|
121
|
+
];
|
|
122
|
+
for (const p of candidates) {
|
|
123
|
+
try { require.resolve(p); return p; } catch { }
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
129
|
+
function main() {
|
|
130
|
+
const { positional, opts } = parseArgs(process.argv.slice(2));
|
|
131
|
+
|
|
132
|
+
if (opts.help) {
|
|
133
|
+
console.log(HELP);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [entity, relation, value] = positional;
|
|
138
|
+
const confidence = opts.confidence || 'medium';
|
|
139
|
+
const project = opts.project || process.env.METAME_PROJECT || inferProject(process.cwd());
|
|
140
|
+
const tagsRaw = opts.tags || '';
|
|
141
|
+
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean).slice(0, 3) : [];
|
|
142
|
+
|
|
143
|
+
// Validate
|
|
144
|
+
const errors = [
|
|
145
|
+
validateEntity(entity),
|
|
146
|
+
validateRelation(relation),
|
|
147
|
+
validateValue(value),
|
|
148
|
+
opts.confidence ? validateConfidence(confidence) : null,
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
|
|
151
|
+
if (errors.length) {
|
|
152
|
+
for (const e of errors) console.error('错误: ' + e);
|
|
153
|
+
console.error('\n运行 node memory-write.js --help 查看用法');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Resolve memory module
|
|
158
|
+
const memoryPath = resolveMemory();
|
|
159
|
+
if (!memoryPath) {
|
|
160
|
+
console.error('错误: 找不到 memory.js,请确认 MetaMe 已部署');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const memory = require(memoryPath);
|
|
165
|
+
const sessionId = 'manual-' + Date.now();
|
|
166
|
+
|
|
167
|
+
if (typeof memory.acquire === 'function') memory.acquire();
|
|
168
|
+
try {
|
|
169
|
+
const result = memory.saveFacts(sessionId, project, [
|
|
170
|
+
{ entity, relation, value, confidence, tags, source_type: 'manual' },
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (result.saved > 0) {
|
|
174
|
+
const preview = value.slice(0, 30) + (value.length > 30 ? '...' : '');
|
|
175
|
+
console.log(`✓ 已保存 [${relation}] ${entity}: "${preview}"`);
|
|
176
|
+
if (result.superseded > 0) {
|
|
177
|
+
console.log(` (已将 ${result.superseded} 条旧记录标记为 superseded)`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// skipped: duplicate or validation filtered by saveFacts
|
|
181
|
+
console.log(`⚠ 未保存(可能是重复内容)。已跳过: ${result.skipped}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('错误: 写入失败 —', err.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
} finally {
|
|
188
|
+
try { if (typeof memory.release === 'function') memory.release(); } catch { }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main();
|