metame-cli 1.4.12 → 1.4.15
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 +9 -9
- package/index.js +205 -57
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +365 -0
- package/scripts/daemon-agent-commands.js +491 -0
- package/scripts/daemon-agent-tools.js +256 -0
- package/scripts/daemon-bridges.js +236 -0
- package/scripts/daemon-checkpoints.js +89 -0
- package/scripts/daemon-claude-engine.js +909 -0
- package/scripts/daemon-command-router.js +416 -0
- package/scripts/daemon-default.yaml +2 -2
- package/scripts/daemon-exec-commands.js +290 -0
- package/scripts/daemon-file-browser.js +219 -0
- package/scripts/daemon-notify.js +64 -0
- package/scripts/daemon-ops-commands.js +275 -0
- package/scripts/daemon-runtime-lifecycle.js +133 -0
- package/scripts/daemon-session-commands.js +436 -0
- package/scripts/daemon-session-store.js +423 -0
- package/scripts/daemon-task-scheduler.js +539 -0
- package/scripts/daemon.js +555 -4316
- package/scripts/memory-extract.js +15 -9
- package/scripts/session-analytics.js +116 -0
- package/scripts/test_daemon.js +1407 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createAdminCommandHandler(deps) {
|
|
4
|
+
const {
|
|
5
|
+
fs,
|
|
6
|
+
yaml,
|
|
7
|
+
execSync,
|
|
8
|
+
BRAIN_FILE,
|
|
9
|
+
CONFIG_FILE,
|
|
10
|
+
DISPATCH_LOG,
|
|
11
|
+
providerMod,
|
|
12
|
+
loadConfig,
|
|
13
|
+
backupConfig,
|
|
14
|
+
writeConfigSafe,
|
|
15
|
+
restoreConfig,
|
|
16
|
+
getSession,
|
|
17
|
+
getAllTasks,
|
|
18
|
+
dispatchTask,
|
|
19
|
+
log,
|
|
20
|
+
} = deps;
|
|
21
|
+
|
|
22
|
+
async function handleAdminCommand(ctx) {
|
|
23
|
+
const { bot, chatId, text } = ctx;
|
|
24
|
+
const state = ctx.state || {};
|
|
25
|
+
let config = ctx.config || {};
|
|
26
|
+
|
|
27
|
+
if (text === '/status') {
|
|
28
|
+
const session = getSession(chatId);
|
|
29
|
+
let msg = `MetaMe Daemon\nStatus: Running\nStarted: ${state.started_at || 'unknown'}\n`;
|
|
30
|
+
msg += `Budget: ${state.budget.tokens_used}/${(config.budget && config.budget.daily_limit) || 50000} tokens`;
|
|
31
|
+
if (session) msg += `\nSession: ${session.id.slice(0, 8)}... (${session.cwd})`;
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(BRAIN_FILE)) {
|
|
34
|
+
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
35
|
+
if (doc.identity) msg += `\nProfile: ${doc.identity.nickname || 'unknown'}`;
|
|
36
|
+
if (doc.context && doc.context.focus) msg += `\nFocus: ${doc.context.focus}`;
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
await bot.sendMessage(chatId, msg);
|
|
40
|
+
return { handled: true, config };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (text === '/tasks') {
|
|
44
|
+
const { general, project } = getAllTasks(config);
|
|
45
|
+
let msg = '';
|
|
46
|
+
if (general.length > 0) {
|
|
47
|
+
msg += '📋 General:\n';
|
|
48
|
+
for (const t of general) {
|
|
49
|
+
const ts = state.tasks[t.name] || {};
|
|
50
|
+
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Project tasks grouped by _project
|
|
54
|
+
const byProject = new Map();
|
|
55
|
+
for (const t of project) {
|
|
56
|
+
const pk = t._project.key;
|
|
57
|
+
if (!byProject.has(pk)) byProject.set(pk, { proj: t._project, tasks: [] });
|
|
58
|
+
byProject.get(pk).tasks.push(t);
|
|
59
|
+
}
|
|
60
|
+
for (const [, { proj, tasks }] of byProject) {
|
|
61
|
+
msg += `\n${proj.icon} ${proj.name}:\n`;
|
|
62
|
+
for (const t of tasks) {
|
|
63
|
+
const ts = state.tasks[t.name] || {};
|
|
64
|
+
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!msg) {
|
|
68
|
+
await bot.sendMessage(chatId, 'No heartbeat tasks configured.');
|
|
69
|
+
return { handled: true, config };
|
|
70
|
+
}
|
|
71
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
72
|
+
return { handled: true, config };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// /dispatch — inter-agent task dispatch
|
|
76
|
+
if (text.startsWith('/dispatch')) {
|
|
77
|
+
const args = text.slice('/dispatch'.length).trim();
|
|
78
|
+
|
|
79
|
+
if (!args || args === 'status') {
|
|
80
|
+
// Show dispatch status from log
|
|
81
|
+
let msg = '📬 Agent Dispatch 状态\n─────────────\n';
|
|
82
|
+
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
83
|
+
msg += `${proj.icon || '🤖'} ${proj.name || key} — 就绪\n`;
|
|
84
|
+
}
|
|
85
|
+
if (fs.existsSync(DISPATCH_LOG)) {
|
|
86
|
+
const lines = fs.readFileSync(DISPATCH_LOG, 'utf8').trim().split('\n').filter(Boolean);
|
|
87
|
+
const recent = lines.slice(-5).reverse();
|
|
88
|
+
if (recent.length > 0) {
|
|
89
|
+
msg += '\n📤 最近派发:\n';
|
|
90
|
+
for (const l of recent) {
|
|
91
|
+
try {
|
|
92
|
+
const e = JSON.parse(l);
|
|
93
|
+
msg += `${e.from}→${e.to}: ${(e.payload.title || e.payload.prompt || '').slice(0, 40)} (${e.type})\n`;
|
|
94
|
+
} catch { /* skip */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
99
|
+
return { handled: true, config };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (args === 'log') {
|
|
103
|
+
if (!fs.existsSync(DISPATCH_LOG)) {
|
|
104
|
+
await bot.sendMessage(chatId, '无派发记录。');
|
|
105
|
+
return { handled: true, config };
|
|
106
|
+
}
|
|
107
|
+
const lines = fs.readFileSync(DISPATCH_LOG, 'utf8').trim().split('\n').filter(Boolean);
|
|
108
|
+
const recent = lines.slice(-10).reverse();
|
|
109
|
+
let msg = '📤 最近 10 条派发记录:\n';
|
|
110
|
+
for (const l of recent) {
|
|
111
|
+
try {
|
|
112
|
+
const e = JSON.parse(l);
|
|
113
|
+
const time = new Date(e.dispatched_at).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
|
114
|
+
msg += `[${time}] ${e.from}→${e.to} ${e.type}: ${(e.payload.title || e.payload.prompt || '').slice(0, 40)}\n`;
|
|
115
|
+
} catch { /* skip */ }
|
|
116
|
+
}
|
|
117
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
118
|
+
return { handled: true, config };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// /dispatch to <agent> <prompt>
|
|
122
|
+
const toMatch = args.match(/^to\s+(\S+)\s+(.+)$/s);
|
|
123
|
+
if (toMatch) {
|
|
124
|
+
const targetName = toMatch[1];
|
|
125
|
+
const prompt = toMatch[2].trim();
|
|
126
|
+
|
|
127
|
+
// Resolve target by project key or nickname
|
|
128
|
+
let targetKey = null;
|
|
129
|
+
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
130
|
+
if (key === targetName || (proj.nicknames || []).some(n => n === targetName)) {
|
|
131
|
+
targetKey = key;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!targetKey) {
|
|
136
|
+
await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
|
|
137
|
+
return { handled: true, config };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Determine sender from current chat's project mapping
|
|
141
|
+
const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
142
|
+
const senderKey = chatAgentMap[chatId] || 'user';
|
|
143
|
+
|
|
144
|
+
const projInfo = config.projects[targetKey] || {};
|
|
145
|
+
// Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
|
|
146
|
+
const feishuChatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
147
|
+
const targetChatId = Object.entries(feishuChatAgentMap).find(([, v]) => v === targetKey)?.[0] || null;
|
|
148
|
+
// Stream work directly to target's channel if available; otherwise fallback replyFn
|
|
149
|
+
const dispatchStreamOptions = targetChatId ? { bot, chatId: targetChatId } : null;
|
|
150
|
+
const replyFn = targetChatId ? null : (output) => {
|
|
151
|
+
const text2 = `${projInfo.icon || '📬'} **${projInfo.name || targetKey}**\n\n${output.slice(0, 2000)}`;
|
|
152
|
+
bot.sendMarkdown(chatId, text2)
|
|
153
|
+
.catch(e => {
|
|
154
|
+
log('WARN', `Dispatch sendMarkdown failed: ${e.message}, trying sendMessage`);
|
|
155
|
+
bot.sendMessage(chatId, text2).catch(e2 => log('ERROR', `Dispatch reply failed: ${e2.message}`));
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const result = dispatchTask(targetKey, {
|
|
160
|
+
from: senderKey,
|
|
161
|
+
type: 'task',
|
|
162
|
+
priority: 'normal',
|
|
163
|
+
payload: { title: prompt.slice(0, 60), prompt },
|
|
164
|
+
callback: false,
|
|
165
|
+
}, config, replyFn, dispatchStreamOptions);
|
|
166
|
+
|
|
167
|
+
if (result.success) {
|
|
168
|
+
await bot.sendMessage(chatId, `✅ 已派发给 ${projInfo.name || targetName},执行中…`);
|
|
169
|
+
} else {
|
|
170
|
+
await bot.sendMessage(chatId, `❌ 派发失败: ${result.error}`);
|
|
171
|
+
}
|
|
172
|
+
return { handled: true, config };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await bot.sendMessage(chatId, '用法:\n/dispatch status — 查看状态\n/dispatch log — 查看记录\n/dispatch to <agent> <任务内容>');
|
|
176
|
+
return { handled: true, config };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (text === '/budget') {
|
|
180
|
+
const limit = (config.budget && config.budget.daily_limit) || 50000;
|
|
181
|
+
const used = state.budget.tokens_used;
|
|
182
|
+
await bot.sendMessage(chatId, `Budget: ${used}/${limit} tokens (${((used / limit) * 100).toFixed(1)}%)`);
|
|
183
|
+
return { handled: true, config };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (text === '/quiet') {
|
|
187
|
+
try {
|
|
188
|
+
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
189
|
+
if (!doc.growth) doc.growth = {};
|
|
190
|
+
doc.growth.quiet_until = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString();
|
|
191
|
+
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
192
|
+
await bot.sendMessage(chatId, 'Mirror & reflections silenced for 48h.');
|
|
193
|
+
} catch (e) {
|
|
194
|
+
await bot.sendMessage(chatId, `Error: ${e.message}`);
|
|
195
|
+
}
|
|
196
|
+
return { handled: true, config };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (text === '/reload') {
|
|
200
|
+
if (global._metameReload) {
|
|
201
|
+
const r = global._metameReload();
|
|
202
|
+
if (r.success) {
|
|
203
|
+
await bot.sendMessage(chatId, `✅ Config reloaded. ${r.tasks} heartbeat tasks active.`);
|
|
204
|
+
} else {
|
|
205
|
+
await bot.sendMessage(chatId, `❌ Reload failed: ${r.error}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
await bot.sendMessage(chatId, '❌ Reload not available (daemon not fully started).');
|
|
209
|
+
}
|
|
210
|
+
return { handled: true, config };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// /doctor — diagnostics; /fix — restore backup; /reset — reset model to sonnet
|
|
214
|
+
if (text === '/fix') {
|
|
215
|
+
if (restoreConfig()) {
|
|
216
|
+
await bot.sendMessage(chatId, '✅ 已从备份恢复配置');
|
|
217
|
+
} else {
|
|
218
|
+
await bot.sendMessage(chatId, '❌ 无备份文件');
|
|
219
|
+
}
|
|
220
|
+
return { handled: true, config };
|
|
221
|
+
}
|
|
222
|
+
if (text === '/reset') {
|
|
223
|
+
try {
|
|
224
|
+
backupConfig();
|
|
225
|
+
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
226
|
+
if (!cfg.daemon) cfg.daemon = {};
|
|
227
|
+
cfg.daemon.model = 'opus';
|
|
228
|
+
writeConfigSafe(cfg);
|
|
229
|
+
config = loadConfig();
|
|
230
|
+
await bot.sendMessage(chatId, '✅ 模型已重置为 opus');
|
|
231
|
+
} catch (e) {
|
|
232
|
+
await bot.sendMessage(chatId, `❌ ${e.message}`);
|
|
233
|
+
}
|
|
234
|
+
return { handled: true, config };
|
|
235
|
+
}
|
|
236
|
+
if (text === '/doctor') {
|
|
237
|
+
const validModels = ['sonnet', 'opus', 'haiku'];
|
|
238
|
+
const checks = [];
|
|
239
|
+
let issues = 0;
|
|
240
|
+
|
|
241
|
+
let cfg = null;
|
|
242
|
+
try {
|
|
243
|
+
cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
244
|
+
checks.push('✅ 配置可解析');
|
|
245
|
+
} catch {
|
|
246
|
+
checks.push('❌ 配置解析失败');
|
|
247
|
+
issues++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
|
|
251
|
+
if (validModels.includes(m)) {
|
|
252
|
+
checks.push(`✅ 模型: ${m}`);
|
|
253
|
+
} else {
|
|
254
|
+
checks.push(`❌ 模型: ${m} (无效)`);
|
|
255
|
+
issues++;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
execSync('which claude', { encoding: 'utf8' });
|
|
260
|
+
checks.push('✅ Claude CLI');
|
|
261
|
+
} catch {
|
|
262
|
+
checks.push('❌ Claude CLI 未找到');
|
|
263
|
+
issues++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const bakFile = CONFIG_FILE + '.bak';
|
|
267
|
+
const hasBak = fs.existsSync(bakFile);
|
|
268
|
+
checks.push(hasBak ? '✅ 有备份' : '⚠️ 无备份');
|
|
269
|
+
|
|
270
|
+
let msg = `🏥 诊断\n${checks.join('\n')}`;
|
|
271
|
+
if (issues > 0) {
|
|
272
|
+
if (bot.sendButtons) {
|
|
273
|
+
const buttons = [];
|
|
274
|
+
if (hasBak) buttons.push([{ text: '🔧 恢复备份', callback_data: '/fix' }]);
|
|
275
|
+
buttons.push([{ text: '🔄 重置opus', callback_data: '/reset' }]);
|
|
276
|
+
await bot.sendButtons(chatId, msg, buttons);
|
|
277
|
+
} else {
|
|
278
|
+
msg += '\n/fix 恢复备份 /reset 重置opus';
|
|
279
|
+
await bot.sendMessage(chatId, msg);
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
await bot.sendMessage(chatId, msg + '\n\n全部正常 ✅');
|
|
283
|
+
}
|
|
284
|
+
return { handled: true, config };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// /model [name] — switch model (interactive, accepts any name for custom providers)
|
|
288
|
+
if (text === '/model' || text.startsWith('/model ')) {
|
|
289
|
+
const arg = text.slice(6).trim();
|
|
290
|
+
const builtinModels = ['sonnet', 'opus', 'haiku'];
|
|
291
|
+
const currentModel = (config.daemon && config.daemon.model) || 'opus';
|
|
292
|
+
const activeProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
293
|
+
const isCustomProvider = activeProvider !== 'anthropic';
|
|
294
|
+
|
|
295
|
+
if (!arg) {
|
|
296
|
+
const hint = isCustomProvider ? `\n💡 ${activeProvider} 可输入任意模型名` : '';
|
|
297
|
+
if (bot.sendButtons) {
|
|
298
|
+
const buttons = builtinModels.map(m => [{
|
|
299
|
+
text: m === currentModel ? `${m} ✓` : m,
|
|
300
|
+
callback_data: `/model ${m}`,
|
|
301
|
+
}]);
|
|
302
|
+
await bot.sendButtons(chatId, `🤖 当前模型: ${currentModel}${hint}`, buttons);
|
|
303
|
+
} else {
|
|
304
|
+
await bot.sendMessage(chatId, `🤖 当前模型: ${currentModel}\n可选: ${builtinModels.join(', ')}${hint}`);
|
|
305
|
+
}
|
|
306
|
+
return { handled: true, config };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const normalizedArg = arg.toLowerCase();
|
|
310
|
+
// Builtin providers only accept builtin model names
|
|
311
|
+
if (!isCustomProvider && !builtinModels.includes(normalizedArg)) {
|
|
312
|
+
await bot.sendMessage(chatId, `❌ 无效模型: ${arg}\n可选: ${builtinModels.join(', ')}\n💡 切换到自定义 provider 后可用任意模型名`);
|
|
313
|
+
return { handled: true, config };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const modelName = builtinModels.includes(normalizedArg) ? normalizedArg : arg;
|
|
317
|
+
if (modelName === currentModel) {
|
|
318
|
+
await bot.sendMessage(chatId, `🤖 已经是 ${modelName}`);
|
|
319
|
+
return { handled: true, config };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
backupConfig();
|
|
324
|
+
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
325
|
+
if (!cfg.daemon) cfg.daemon = {};
|
|
326
|
+
cfg.daemon.model = modelName;
|
|
327
|
+
writeConfigSafe(cfg);
|
|
328
|
+
config = loadConfig();
|
|
329
|
+
await bot.sendMessage(chatId, `✅ 模型已切换: ${currentModel} → ${modelName}`);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
await bot.sendMessage(chatId, `❌ 切换失败: ${e.message}`);
|
|
332
|
+
}
|
|
333
|
+
return { handled: true, config };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// /provider [name] — list or switch provider
|
|
337
|
+
if (text === '/provider' || text.startsWith('/provider ')) {
|
|
338
|
+
if (!providerMod) {
|
|
339
|
+
await bot.sendMessage(chatId, '❌ Provider module not available.');
|
|
340
|
+
return { handled: true, config };
|
|
341
|
+
}
|
|
342
|
+
const arg = text.slice(9).trim();
|
|
343
|
+
if (!arg) {
|
|
344
|
+
const list = providerMod.listFormatted();
|
|
345
|
+
await bot.sendMessage(chatId, `🔌 Providers:\n${list}\n\n用法: /provider <name>`);
|
|
346
|
+
return { handled: true, config };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
backupConfig();
|
|
350
|
+
providerMod.setActive(arg);
|
|
351
|
+
const p = providerMod.getActiveProvider();
|
|
352
|
+
await bot.sendMessage(chatId, `✅ Provider: ${arg} (${p.label || arg})`);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
await bot.sendMessage(chatId, `❌ ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
return { handled: true, config };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return { handled: false, config };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { handleAdminCommand };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = { createAdminCommandHandler };
|