metame-cli 1.4.34 → 1.5.0

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.
Files changed (44) hide show
  1. package/README.md +146 -32
  2. package/index.js +148 -9
  3. package/package.json +6 -3
  4. package/scripts/daemon-admin-commands.js +254 -9
  5. package/scripts/daemon-agent-commands.js +64 -6
  6. package/scripts/daemon-agent-tools.js +26 -5
  7. package/scripts/daemon-bridges.js +110 -20
  8. package/scripts/daemon-claude-engine.js +698 -239
  9. package/scripts/daemon-command-router.js +24 -8
  10. package/scripts/daemon-default.yaml +28 -4
  11. package/scripts/daemon-engine-runtime.js +275 -0
  12. package/scripts/daemon-exec-commands.js +10 -4
  13. package/scripts/daemon-notify.js +37 -1
  14. package/scripts/daemon-runtime-lifecycle.js +2 -1
  15. package/scripts/daemon-session-commands.js +52 -4
  16. package/scripts/daemon-session-store.js +2 -1
  17. package/scripts/daemon-task-scheduler.js +68 -38
  18. package/scripts/daemon-user-acl.js +26 -9
  19. package/scripts/daemon.js +81 -17
  20. package/scripts/distill.js +323 -18
  21. package/scripts/docs/agent-guide.md +12 -0
  22. package/scripts/docs/maintenance-manual.md +119 -0
  23. package/scripts/docs/pointer-map.md +88 -0
  24. package/scripts/feishu-adapter.js +6 -1
  25. package/scripts/hooks/stop-session-capture.js +243 -0
  26. package/scripts/memory-extract.js +100 -5
  27. package/scripts/memory-nightly-reflect.js +196 -11
  28. package/scripts/memory.js +134 -3
  29. package/scripts/mentor-engine.js +405 -0
  30. package/scripts/platform.js +2 -0
  31. package/scripts/providers.js +169 -21
  32. package/scripts/schema.js +12 -0
  33. package/scripts/session-analytics.js +245 -12
  34. package/scripts/skill-changelog.js +245 -0
  35. package/scripts/skill-evolution.js +288 -5
  36. package/scripts/usage-classifier.js +1 -1
  37. package/scripts/daemon-admin-commands.test.js +0 -333
  38. package/scripts/daemon-task-envelope.test.js +0 -59
  39. package/scripts/daemon-task-scheduler.test.js +0 -106
  40. package/scripts/reliability-core.test.js +0 -280
  41. package/scripts/skill-evolution.test.js +0 -113
  42. package/scripts/task-board.test.js +0 -83
  43. package/scripts/test_daemon.js +0 -1407
  44. package/scripts/utils.test.js +0 -192
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ let userAcl = null;
4
+ try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
5
+
3
6
  function createBridgeStarter(deps) {
4
7
  const {
5
8
  fs,
@@ -15,6 +18,52 @@ function createBridgeStarter(deps) {
15
18
  pendingActivations, // optional — used to show smart activation hint
16
19
  } = deps;
17
20
 
21
+ async function sendAclReply(bot, chatId, text) {
22
+ if (!text) return;
23
+ try {
24
+ if (bot.sendMarkdown) await bot.sendMarkdown(chatId, text);
25
+ else await bot.sendMessage(chatId, text.replace(/[*_`]/g, ''));
26
+ } catch { /* non-fatal */ }
27
+ }
28
+
29
+ function normalizeSenderId(senderId) {
30
+ if (senderId === undefined || senderId === null) return null;
31
+ const text = String(senderId).trim();
32
+ return text || null;
33
+ }
34
+
35
+ async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
36
+ const trimmed = String(text || '').trim();
37
+ const normalizedSenderId = normalizeSenderId(senderId);
38
+ if (!trimmed || bypassAcl || !userAcl) {
39
+ return { blocked: false, readOnly: false, senderId: normalizedSenderId };
40
+ }
41
+
42
+ let userCtx;
43
+ try {
44
+ userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
45
+ } catch {
46
+ return { blocked: false, readOnly: false, senderId: normalizedSenderId };
47
+ }
48
+
49
+ const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
50
+ if (userCmd && userCmd.handled) {
51
+ await sendAclReply(bot, chatId, userCmd.reply);
52
+ return { blocked: true, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
53
+ }
54
+
55
+ const publicCmds = Array.isArray(userAcl.PUBLIC_COMMANDS) ? userAcl.PUBLIC_COMMANDS : [];
56
+ const isPublic = publicCmds.includes(trimmed.toLowerCase());
57
+ const action = userAcl.classifyCommandAction(trimmed);
58
+ const allowed = isPublic || (typeof userCtx.can === 'function' && userCtx.can(action));
59
+ if (!allowed) {
60
+ await sendAclReply(bot, chatId, `⚠️ 当前权限不足(角色: ${userCtx.role})\n命令类型: ${action}\n请联系管理员授权。`);
61
+ return { blocked: true, readOnly: true, senderId: normalizedSenderId };
62
+ }
63
+
64
+ return { blocked: false, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
65
+ }
66
+
18
67
  // Returns the best pending activation for a given chatId (excludes self-created)
19
68
  function getPendingActivationForChat(chatId) {
20
69
  if (!pendingActivations || pendingActivations.size === 0) return null;
@@ -67,12 +116,26 @@ function createBridgeStarter(deps) {
67
116
  if (update.callback_query) {
68
117
  const cb = update.callback_query;
69
118
  const chatId = cb.message && cb.message.chat.id;
119
+ const senderId = cb.from && cb.from.id ? String(cb.from.id) : null;
70
120
  bot.answerCallback(cb.id).catch(() => { });
71
121
  if (chatId && cb.data) {
72
122
  const liveCfg = loadConfig();
73
123
  const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
74
124
  if (!allowedIds.includes(chatId)) continue;
75
- handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName).catch(e => {
125
+ const isBindCmd = cb.data.startsWith('/agent bind')
126
+ || cb.data.startsWith('/agent-bind-dir')
127
+ || cb.data.startsWith('/browse bind')
128
+ || cb.data === '/activate';
129
+ const acl = await applyUserAcl({
130
+ bot,
131
+ chatId,
132
+ text: cb.data,
133
+ config: liveCfg,
134
+ senderId,
135
+ bypassAcl: !allowedIds.includes(chatId) && !!isBindCmd,
136
+ });
137
+ if (acl.blocked) continue;
138
+ handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
76
139
  log('ERROR', `Telegram callback handler error: ${e.message}`);
77
140
  });
78
141
  }
@@ -83,6 +146,7 @@ function createBridgeStarter(deps) {
83
146
 
84
147
  const msg = update.message;
85
148
  const chatId = msg.chat.id;
149
+ const senderId = msg.from && msg.from.id ? String(msg.from.id) : null;
86
150
 
87
151
  const liveCfg = loadConfig();
88
152
  const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
@@ -93,7 +157,8 @@ function createBridgeStarter(deps) {
93
157
  || trimmedText.startsWith('/browse bind')
94
158
  || trimmedText === '/activate'
95
159
  );
96
- if (!allowedIds.includes(chatId) && !isBindCmd) {
160
+ const isAllowedChat = allowedIds.includes(chatId);
161
+ if (!isAllowedChat && !isBindCmd) {
97
162
  log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
98
163
  bot.sendMessage(chatId, unauthorizedMsg(chatId)).catch(() => {});
99
164
  continue;
@@ -108,6 +173,15 @@ function createBridgeStarter(deps) {
108
173
  const fileId = msg.document ? msg.document.file_id : msg.photo[msg.photo.length - 1].file_id;
109
174
  const fileName = msg.document ? msg.document.file_name : `photo_${Date.now()}.jpg`;
110
175
  const caption = msg.caption || '';
176
+ const acl = await applyUserAcl({
177
+ bot,
178
+ chatId,
179
+ text: caption || '[file-upload]',
180
+ config: liveCfg,
181
+ senderId,
182
+ bypassAcl: !isAllowedChat && !!isBindCmd,
183
+ });
184
+ if (acl.blocked) continue;
111
185
 
112
186
  const session = getSession(chatId);
113
187
  const cwd = session?.cwd || HOME;
@@ -123,7 +197,7 @@ function createBridgeStarter(deps) {
123
197
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
124
198
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
125
199
 
126
- handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName).catch(e => {
200
+ handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
127
201
  log('ERROR', `Telegram file handler error: ${e.message}`);
128
202
  });
129
203
  } catch (err) {
@@ -134,7 +208,17 @@ function createBridgeStarter(deps) {
134
208
  }
135
209
 
136
210
  if (msg.text) {
137
- handleCommand(bot, chatId, msg.text.trim(), liveCfg, executeTaskByName).catch(e => {
211
+ const text = msg.text.trim();
212
+ const acl = await applyUserAcl({
213
+ bot,
214
+ chatId,
215
+ text,
216
+ config: liveCfg,
217
+ senderId,
218
+ bypassAcl: !isAllowedChat && !!isBindCmd,
219
+ });
220
+ if (acl.blocked) continue;
221
+ handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
138
222
  log('ERROR', `Telegram handler error: ${e.message}`);
139
223
  });
140
224
  }
@@ -182,27 +266,24 @@ function createBridgeStarter(deps) {
182
266
  || trimmedText.startsWith('/browse bind')
183
267
  || trimmedText === '/activate'
184
268
  );
185
- if (!allowedIds.includes(chatId) && !isBindCmd) {
269
+ const isAllowedChat = allowedIds.includes(chatId);
270
+ if (!isAllowedChat && !isBindCmd) {
186
271
  log('WARN', `Feishu: rejected message from ${chatId}`);
187
272
  const msg = unauthorizedMsg(chatId);
188
273
  (bot.sendMarkdown ? bot.sendMarkdown(chatId, msg) : bot.sendMessage(chatId, msg)).catch(() => {});
189
274
  return;
190
275
  }
191
276
 
192
- const operatorIds = (liveCfg.feishu && liveCfg.feishu.operator_ids) || [];
193
- if (operatorIds.length > 0 && senderId && !operatorIds.includes(senderId) && !isBindCmd) {
194
- log('INFO', `Feishu: read-only message from non-operator ${senderId} in ${chatId}: ${(text || '').slice(0, 50)}`);
195
- if (text && text.startsWith('/')) {
196
- await (bot.sendMarkdown ? bot.sendMarkdown(chatId, '⚠️ 该操作需要授权,请联系管理员。') : bot.sendMessage(chatId, '⚠️ 该操作需要授权,请联系管理员。'));
197
- return;
198
- }
199
- if (text) {
200
- await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId, true);
201
- }
202
- return;
203
- }
204
-
205
277
  if (fileInfo && fileInfo.fileKey) {
278
+ const acl = await applyUserAcl({
279
+ bot,
280
+ chatId,
281
+ text: text || '[file-upload]',
282
+ config: liveCfg,
283
+ senderId,
284
+ bypassAcl: !isAllowedChat && !!isBindCmd,
285
+ });
286
+ if (acl.blocked) return;
206
287
  log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
207
288
  const session = getSession(chatId);
208
289
  const cwd = session?.cwd || HOME;
@@ -218,7 +299,7 @@ function createBridgeStarter(deps) {
218
299
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
219
300
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
220
301
 
221
- await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName);
302
+ await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
222
303
  } catch (err) {
223
304
  log('ERROR', `Feishu file download failed: ${err.message}`);
224
305
  await bot.sendMessage(chatId, `❌ Download failed: ${err.message}`);
@@ -227,6 +308,15 @@ function createBridgeStarter(deps) {
227
308
  }
228
309
 
229
310
  if (text) {
311
+ const acl = await applyUserAcl({
312
+ bot,
313
+ chatId,
314
+ text,
315
+ config: liveCfg,
316
+ senderId,
317
+ bypassAcl: !isAllowedChat && !!isBindCmd,
318
+ });
319
+ if (acl.blocked) return;
230
320
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
231
321
  const parentId = event?.message?.parent_id;
232
322
  if (parentId) {
@@ -238,7 +328,7 @@ function createBridgeStarter(deps) {
238
328
  log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
239
329
  }
240
330
  }
241
- await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId);
331
+ await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
242
332
  }
243
333
  });
244
334