metame-cli 1.5.26 → 1.6.1
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/index.js +4 -1
- package/package.json +1 -1
- package/scripts/agent-layer.js +36 -0
- package/scripts/core/chunker.js +100 -0
- package/scripts/core/embedding.js +225 -0
- package/scripts/core/hybrid-search.js +296 -0
- package/scripts/core/wiki-db.js +545 -0
- package/scripts/core/wiki-prompt.js +88 -0
- package/scripts/core/wiki-slug.js +66 -0
- package/scripts/core/wiki-staleness.js +18 -0
- package/scripts/daemon-agent-commands.js +10 -4
- package/scripts/daemon-bridges.js +16 -0
- package/scripts/daemon-claude-engine.js +62 -8
- package/scripts/daemon-command-router.js +40 -1
- package/scripts/daemon-default.yaml +33 -3
- package/scripts/daemon-embedding.js +162 -0
- package/scripts/daemon-engine-runtime.js +1 -1
- package/scripts/daemon-health-scan.js +185 -0
- package/scripts/daemon-ops-commands.js +9 -18
- package/scripts/daemon-runtime-lifecycle.js +1 -1
- package/scripts/daemon-session-commands.js +4 -0
- package/scripts/daemon-task-scheduler.js +5 -3
- package/scripts/daemon-warm-pool.js +15 -0
- package/scripts/daemon-wiki.js +420 -0
- package/scripts/daemon.js +10 -5
- package/scripts/distill.js +1 -1
- package/scripts/docs/file-transfer.md +0 -1
- package/scripts/docs/maintenance-manual.md +2 -55
- package/scripts/docs/pointer-map.md +0 -34
- package/scripts/feishu-adapter.js +25 -0
- package/scripts/hooks/intent-file-transfer.js +1 -2
- package/scripts/memory-backfill-chunks.js +92 -0
- package/scripts/memory-search.js +49 -6
- package/scripts/memory-wiki-schema.js +255 -0
- package/scripts/memory.js +103 -3
- package/scripts/signal-capture.js +1 -1
- package/scripts/skill-evolution.js +2 -11
- package/scripts/wiki-cluster.js +121 -0
- package/scripts/wiki-extract.js +171 -0
- package/scripts/wiki-facts.js +351 -0
- package/scripts/wiki-import.js +256 -0
- package/scripts/wiki-reflect-build.js +441 -0
- package/scripts/wiki-reflect-export.js +448 -0
- package/scripts/wiki-reflect-query.js +109 -0
- package/scripts/wiki-reflect.js +338 -0
- package/scripts/wiki-synthesis.js +224 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* daemon-health-scan.js — Daily Daemon Health Report
|
|
5
|
+
*
|
|
6
|
+
* Reads ~/.metame/daemon.log for last 24h ERROR/WARN entries,
|
|
7
|
+
* calls LLM (Haiku) to analyze root causes and propose fixes,
|
|
8
|
+
* saves report to ~/.metame/health-report-latest.json,
|
|
9
|
+
* then prints a formatted summary to stdout.
|
|
10
|
+
*
|
|
11
|
+
* Heartbeat: daily at 08:30 via daemon.yaml
|
|
12
|
+
* notify: true → daemon sends stdout to Feishu automatically.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const { callHaiku, buildDistillEnv } = require('./providers');
|
|
19
|
+
|
|
20
|
+
const HOME = os.homedir();
|
|
21
|
+
const LOG_FILE = path.join(HOME, '.metame', 'daemon.log');
|
|
22
|
+
const REPORT_FILE = path.join(HOME, '.metame', 'health-report-latest.json');
|
|
23
|
+
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
const MAX_UNIQUE_ERRORS = 8;
|
|
25
|
+
const MAX_LINE_LEN = 280;
|
|
26
|
+
|
|
27
|
+
// Match log lines that contain an ERROR or WARN level tag
|
|
28
|
+
const LEVEL_PATTERN = /\[(ERROR|WARN)\]/;
|
|
29
|
+
// Extract ISO timestamp from log line prefix like [2026-04-10T08:00:00
|
|
30
|
+
const TS_PATTERN = /^\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/;
|
|
31
|
+
|
|
32
|
+
function readRecentErrors(logFile, windowMs) {
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = fs.readFileSync(logFile, 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cutoff = Date.now() - windowMs;
|
|
41
|
+
const lines = content.split('\n').filter(Boolean);
|
|
42
|
+
const result = [];
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
if (!LEVEL_PATTERN.test(line)) continue;
|
|
46
|
+
const tsMatch = line.match(TS_PATTERN);
|
|
47
|
+
if (tsMatch) {
|
|
48
|
+
const ts = new Date(tsMatch[1]).getTime();
|
|
49
|
+
if (ts < cutoff) continue;
|
|
50
|
+
}
|
|
51
|
+
result.push(line.slice(0, MAX_LINE_LEN));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function groupErrors(lines) {
|
|
58
|
+
const counts = new Map();
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
// Normalize numbers to reduce noise, use first 100 chars as bucket key
|
|
61
|
+
const key = line.slice(0, 100).replace(/\d+/g, 'N').replace(/[a-f0-9]{8,}/gi, 'HASH');
|
|
62
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
// Sort by frequency descending
|
|
65
|
+
return Array.from(counts.entries())
|
|
66
|
+
.sort((a, b) => b[1] - a[1])
|
|
67
|
+
.slice(0, MAX_UNIQUE_ERRORS)
|
|
68
|
+
.map(([key, count]) => ({ key: key.trim(), count }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function analyzeWithLLM(grouped, totalCount) {
|
|
72
|
+
const errorList = grouped
|
|
73
|
+
.map(({ key, count }) => `[×${count}] ${key}`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
|
|
76
|
+
const prompt = `你是 MetaMe daemon 的健康分析师。以下是过去24小时的错误/警告日志(已去重,按频次排序):
|
|
77
|
+
|
|
78
|
+
${errorList}
|
|
79
|
+
|
|
80
|
+
请分析并以 JSON 格式回复:
|
|
81
|
+
{
|
|
82
|
+
"summary": "一句话总结(20字以内)",
|
|
83
|
+
"severity": "low|medium|high",
|
|
84
|
+
"issues": [
|
|
85
|
+
{
|
|
86
|
+
"name": "问题名称(10字以内)",
|
|
87
|
+
"count": 频次,
|
|
88
|
+
"cause": "根因(30字以内)",
|
|
89
|
+
"fix": "修复建议(50字以内)"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"action": "最紧迫的下一步行动(30字以内)"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
severity 判断:high=影响功能/数据/重复崩溃,medium=有异常但仍可运行,low=轻微警告。
|
|
96
|
+
只输出 JSON,不要解释。`;
|
|
97
|
+
|
|
98
|
+
let distillEnv = {};
|
|
99
|
+
try { distillEnv = buildDistillEnv(); } catch { /* ignore */ }
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const raw = await Promise.race([
|
|
103
|
+
callHaiku(prompt, distillEnv, 60000),
|
|
104
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('llm_timeout')), 90000)),
|
|
105
|
+
]);
|
|
106
|
+
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
107
|
+
const parsed = JSON.parse(cleaned);
|
|
108
|
+
// Basic validation
|
|
109
|
+
if (!parsed.summary || !parsed.severity || !Array.isArray(parsed.issues)) {
|
|
110
|
+
throw new Error('invalid structure');
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
} catch {
|
|
114
|
+
// Fallback: no LLM
|
|
115
|
+
return {
|
|
116
|
+
summary: `发现 ${totalCount} 条错误/警告`,
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
issues: grouped.slice(0, 5).map(({ key, count }) => ({
|
|
119
|
+
name: key.slice(0, 30),
|
|
120
|
+
count,
|
|
121
|
+
cause: '待分析',
|
|
122
|
+
fix: '手动检查 daemon.log',
|
|
123
|
+
})),
|
|
124
|
+
action: '手动检查 ~/.metame/daemon.log',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatReport(analysis, totalCount, uniqueTypes) {
|
|
130
|
+
const emoji = { low: '🟡', medium: '🟠', high: '🔴' }[analysis.severity] || '🟠';
|
|
131
|
+
const date = new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
|
|
132
|
+
|
|
133
|
+
const issueLines = (analysis.issues || []).slice(0, 5).map(issue =>
|
|
134
|
+
`• ${issue.name}(×${issue.count})\n 根因:${issue.cause}\n 建议:${issue.fix}`
|
|
135
|
+
).join('\n\n');
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
`${emoji} Daemon 健康报告 · ${date}`,
|
|
139
|
+
``,
|
|
140
|
+
`📊 过去24h:${totalCount} 条错误/警告,${uniqueTypes} 种类型`,
|
|
141
|
+
`📝 摘要:${analysis.summary}`,
|
|
142
|
+
``,
|
|
143
|
+
`🔍 问题详情:`,
|
|
144
|
+
issueLines,
|
|
145
|
+
``,
|
|
146
|
+
`⚡ 建议:${analysis.action}`,
|
|
147
|
+
``,
|
|
148
|
+
`---`,
|
|
149
|
+
`需要修复?回复「修」,我来处理。`,
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function run() {
|
|
154
|
+
const errorLines = readRecentErrors(LOG_FILE, WINDOW_MS);
|
|
155
|
+
|
|
156
|
+
if (errorLines.length === 0) {
|
|
157
|
+
console.log('✅ Daemon 健康正常 · 过去24小时无错误/警告');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const grouped = groupErrors(errorLines);
|
|
162
|
+
const analysis = await analyzeWithLLM(grouped, errorLines.length);
|
|
163
|
+
|
|
164
|
+
// Save full report for "修" handler to load
|
|
165
|
+
const report = {
|
|
166
|
+
generated_at: new Date().toISOString(),
|
|
167
|
+
total_errors: errorLines.length,
|
|
168
|
+
unique_types: grouped.length,
|
|
169
|
+
analysis,
|
|
170
|
+
raw_grouped: grouped,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2), 'utf8');
|
|
175
|
+
} catch (e) {
|
|
176
|
+
process.stderr.write(`[health-scan] failed to write report: ${e.message}\n`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(formatReport(analysis, errorLines.length, grouped.length));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
run().catch(e => {
|
|
183
|
+
process.stderr.write(`[daemon-health-scan] fatal: ${e.message}\n`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
@@ -87,30 +87,22 @@ function createOpsCommandHandler(deps) {
|
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
89
|
let diffFiles = '';
|
|
90
|
-
let diffFailed = false;
|
|
91
90
|
const _wh = process.platform === 'win32' ? { windowsHide: true } : {};
|
|
92
|
-
try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000, ..._wh }).trim(); } catch {
|
|
91
|
+
try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000, ..._wh }).trim(); } catch { }
|
|
93
92
|
const changedFiles = diffFiles ? diffFiles.split('\n').filter(Boolean) : [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
// Restore only changed files (not entire worktree) to preserve user's manual edits
|
|
102
|
-
if (changedFiles.length > 0) {
|
|
103
|
-
execFileSync('git', ['checkout', match.hash, '--', ...changedFiles], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
104
|
-
} else {
|
|
105
|
-
// diff failed but we still reset — fallback to full restore
|
|
106
|
-
execFileSync('git', ['checkout', match.hash, '--', '.'], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
107
|
-
}
|
|
93
|
+
// Reset HEAD to checkpoint's parent (removes any commits Claude made)
|
|
94
|
+
if (match.parentHash) {
|
|
95
|
+
execSync(`git reset --hard ${match.parentHash}`, { cwd, stdio: 'ignore', timeout: 10000, ..._wh });
|
|
96
|
+
}
|
|
97
|
+
// Restore only files touched between the checkpoint and HEAD; unrelated user edits must survive.
|
|
98
|
+
if (changedFiles.length > 0) {
|
|
99
|
+
execFileSync('git', ['checkout', match.hash, '--', ...changedFiles], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
108
100
|
}
|
|
109
101
|
// Truncate context to checkpoint time (covers multi-turn rollback)
|
|
110
102
|
truncateSessionToCheckpoint(session.id, match.message);
|
|
103
|
+
const fileList = changedFiles.map(f => path.basename(f)).join(', ');
|
|
111
104
|
const fileCount = changedFiles.length;
|
|
112
105
|
let msg = `⏪ 已回退到 ${cpDisplayLabel(match.message)}`;
|
|
113
|
-
const fileList = changedFiles.map(f => path.basename(f)).join(', ');
|
|
114
106
|
if (fileCount > 0) msg += `\n📁 ${fileCount} 个文件恢复: ${fileList}`;
|
|
115
107
|
log('INFO', `/undo <hash> executed for ${chatId}: reset to ${match.hash.slice(0, 8)}, files=${fileCount}`);
|
|
116
108
|
await bot.sendMessage(chatId, msg);
|
|
@@ -251,7 +243,6 @@ function createOpsCommandHandler(deps) {
|
|
|
251
243
|
if (cpMatch.parentHash) {
|
|
252
244
|
execSync(`git reset --hard ${cpMatch.parentHash}`, { cwd: cwd2, stdio: 'ignore', timeout: 10000, ..._wh2 });
|
|
253
245
|
}
|
|
254
|
-
// Restore only changed files (not entire worktree) to preserve user's manual edits
|
|
255
246
|
execFileSync('git', ['checkout', cpMatch.hash, '--', ...changedFiles2], { cwd: cwd2, stdio: 'ignore', timeout: 10000 });
|
|
256
247
|
gitMsg2 = `\n📁 ${changedFiles2.length} 个文件已恢复`;
|
|
257
248
|
cleanupCheckpoints(cwd2);
|
|
@@ -153,7 +153,7 @@ function setupRuntimeWatchers(deps) {
|
|
|
153
153
|
refreshLogMaxSize(newConfig);
|
|
154
154
|
const timer = getHeartbeatTimer();
|
|
155
155
|
if (timer) clearInterval(timer);
|
|
156
|
-
setHeartbeatTimer(startHeartbeat(newConfig, notifyFn, notifyPersonalFn));
|
|
156
|
+
setHeartbeatTimer(startHeartbeat(newConfig, notifyFn, notifyPersonalFn, adminNotifyFn));
|
|
157
157
|
const { general, project } = getAllTasks(newConfig);
|
|
158
158
|
const totalCount = general.length + project.length;
|
|
159
159
|
log('INFO', `Config reloaded: ${totalCount} tasks (${project.length} in projects)`);
|
|
@@ -36,6 +36,7 @@ function createSessionCommandHandler(deps) {
|
|
|
36
36
|
getSessionRecentContext,
|
|
37
37
|
getSessionRecentDialogue,
|
|
38
38
|
getDefaultEngine = () => 'claude',
|
|
39
|
+
releaseWarmPool,
|
|
39
40
|
} = deps;
|
|
40
41
|
|
|
41
42
|
function normalizeEngineName(name) {
|
|
@@ -412,10 +413,13 @@ function createSessionCommandHandler(deps) {
|
|
|
412
413
|
return true;
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
const sessionKeyForWarm = getSessionRoute(chatId).sessionChatId;
|
|
415
417
|
const state2 = loadState();
|
|
416
418
|
const targetEngine = normalizeEngineName(target.engine) || getCurrentEngine(chatId);
|
|
417
419
|
const attached = attachResolvedTarget(state2, chatId, targetEngine, target, target.projectPath || HOME);
|
|
418
420
|
saveState(state2);
|
|
421
|
+
// Evict warm process so next spawn uses --resume <new-session-id>
|
|
422
|
+
if (typeof releaseWarmPool === 'function') releaseWarmPool(sessionKeyForWarm);
|
|
419
423
|
|
|
420
424
|
const recentCtx = typeof getSessionRecentContext === 'function'
|
|
421
425
|
? getSessionRecentContext(target.sessionId)
|
|
@@ -660,7 +660,7 @@ function createTaskScheduler(deps) {
|
|
|
660
660
|
return found || null;
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
-
function startHeartbeat(config, notifyFn, notifyPersonalFn) {
|
|
663
|
+
function startHeartbeat(config, notifyFn, notifyPersonalFn, adminNotifyFn) {
|
|
664
664
|
const { all: tasks } = getAllTasks(config);
|
|
665
665
|
|
|
666
666
|
const enabledTasks = tasks.filter(t => t.enabled !== false);
|
|
@@ -787,10 +787,12 @@ function createTaskScheduler(deps) {
|
|
|
787
787
|
}
|
|
788
788
|
if (task.notify && notifyFn && !result.skipped) {
|
|
789
789
|
const proj = task._project || null;
|
|
790
|
+
// Tasks without a project are system-level — send to admin chat only
|
|
791
|
+
const sendFn = proj ? notifyFn : (adminNotifyFn || notifyFn);
|
|
790
792
|
if (result.success) {
|
|
791
|
-
|
|
793
|
+
sendFn(`✅ *${task.name}* completed\n\n${result.output}`, proj);
|
|
792
794
|
} else {
|
|
793
|
-
|
|
795
|
+
sendFn(`❌ *${task.name}* failed: ${result.error}`, proj);
|
|
794
796
|
}
|
|
795
797
|
}
|
|
796
798
|
})
|
|
@@ -161,6 +161,20 @@ function createWarmPool(deps) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Non-destructive validity check. Returns true if a live warm process exists for the key.
|
|
166
|
+
* Mirrors acquireWarm's dead-process checks without consuming the entry.
|
|
167
|
+
*/
|
|
168
|
+
function hasWarm(sessionKey) {
|
|
169
|
+
const entry = pool.get(sessionKey);
|
|
170
|
+
if (!entry) return false;
|
|
171
|
+
if (entry.child.killed || entry.child.exitCode !== null) {
|
|
172
|
+
_cleanup(sessionKey);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
/**
|
|
165
179
|
* Build the stream-json user message for stdin.
|
|
166
180
|
*/
|
|
@@ -179,6 +193,7 @@ function createWarmPool(deps) {
|
|
|
179
193
|
releaseWarm,
|
|
180
194
|
releaseAll,
|
|
181
195
|
buildStreamMessage,
|
|
196
|
+
hasWarm,
|
|
182
197
|
_pool: pool,
|
|
183
198
|
};
|
|
184
199
|
}
|