metame-cli 1.4.6 → 1.4.8

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 CHANGED
@@ -205,7 +205,7 @@ metame daemon install-launchd # Auto-start on boot + crash recovery
205
205
 
206
206
  Done. Open Telegram, message your bot.
207
207
 
208
- > **First message?** New chats aren't whitelisted yet. The bot will reply with a one-step setup command — just send `/bind personal ~/` and you're in.
208
+ > **First message?** New chats aren't whitelisted yet. The bot will reply with a one-step setup command — just send `/agent bind personal ~/` and you're in.
209
209
 
210
210
  ---
211
211
 
@@ -343,7 +343,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
343
343
  ## Security
344
344
 
345
345
  - All data stays on your machine. No cloud, no telemetry.
346
- - `allowed_chat_ids` whitelist — unauthorized users get a one-step `/bind` guide instead of silent rejection.
346
+ - `allowed_chat_ids` whitelist — unauthorized users get a one-step `/agent bind` guide instead of silent rejection.
347
347
  - `operator_ids` for shared groups — non-operators get read-only mode.
348
348
  - `~/.metame/` directory is mode 700.
349
349
  - Bot tokens stored locally, never transmitted.
package/index.js CHANGED
@@ -405,6 +405,7 @@ After writing the profile, ask: *"Want to set up mobile access so you can reach
405
405
 
406
406
  - If **Feishu:**
407
407
  1. Guide through: open.feishu.cn/app → create app → get App ID + Secret → enable bot → add event subscription (long connection mode) → add permissions (im:message, im:message.p2p_msg:readonly, im:message.group_at_msg:readonly, im:message:send_as_bot, im:resource) → publish.
408
+ **⚠️ 重要:** 在「事件订阅」页面,必须开启「接收消息 im.message.receive_v1」事件。然后在该事件的配置中,勾选「获取群组中所有消息」(否则 bot 在群聊中只能收到 @它 的消息,无法接收普通群消息)。
408
409
  2. Ask user to paste App ID and App Secret.
409
410
  3. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
410
411
  4. Tell user: "Now open Feishu and send any message to your new bot, then tell me you're done."
@@ -418,6 +419,23 @@ After writing the profile, ask: *"Want to set up mobile access so you can reach
418
419
 
419
420
  - If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
420
421
 
422
+ **After setup, teach the user about Agent Dispatch:**
423
+
424
+ Tell the user: *"You now have multiple AI agents that can collaborate. Here's how to send messages between them:"*
425
+
426
+ - **From mobile (Telegram/Feishu):** Each chat group is bound to a specific agent (project). To send a message to another agent, use:
427
+ \`/dispatch <project_key> <message>\`
428
+ Example: \`/dispatch desktop 帮我检查一下桌面端的日志\`
429
+
430
+ - **From Claude Code terminal:** Use the dispatch command:
431
+ \`~/.metame/bin/dispatch_to <project_key> "message"\`
432
+
433
+ - **Natural language shortcut:** Just say "告诉小美…" or "让老马…" — the AI will automatically route the message to the right agent.
434
+
435
+ - **Available agents** are configured in \`~/.metame/daemon.yaml\` under \`projects:\`. Each project maps to an agent with its own working directory and Claude session.
436
+
437
+ - **To add a new agent:** Send \`/agent bind <name> <working_directory>\` from any chat. This creates a new project and binds it to that chat.
438
+
421
439
  **4. EVOLUTION MECHANISM (Manual Sync):**
422
440
  * **PHILOSOPHY:** You respect the User's flow. You do NOT interrupt.
423
441
  * **TOOLS:**
@@ -597,6 +615,71 @@ try {
597
615
  const newContent = finalProtocol + mirrorLine + reflectionLine + METAME_END + "\n" + fileContent;
598
616
  fs.writeFileSync(PROJECT_FILE, newContent, 'utf8');
599
617
 
618
+ // ---------------------------------------------------------
619
+ // 4.7 GLOBAL CLAUDE.MD INJECTION (Agent capabilities)
620
+ // ---------------------------------------------------------
621
+ // Inject MetaMe capabilities into ~/.claude/CLAUDE.md so ALL projects' agents
622
+ // automatically know about dispatch, memory, and skill systems.
623
+ const GLOBAL_CLAUDE_MD = path.join(os.homedir(), '.claude', 'CLAUDE.md');
624
+ const GLOBAL_MARKER_START = '<!-- METAME-GLOBAL:START -->';
625
+ const GLOBAL_MARKER_END = '<!-- METAME-GLOBAL:END -->';
626
+
627
+ // Sections to inject (each only injected if not already present in user's manual content)
628
+ const GLOBAL_SECTIONS = [
629
+ { detect: /dispatch_to|Agent Dispatch/i, text: [
630
+ '## Agent Dispatch',
631
+ '"告诉X/让X"→ `~/.metame/bin/dispatch_to <project_key> "内容"`,手机端 `/dispatch <key> <消息>`。',
632
+ '新增 Agent:`/agent bind <名称> <工作目录>`',
633
+ ]},
634
+ { detect: /memory-search\.js|跨会话记忆/i, text: [
635
+ '## 跨会话记忆',
636
+ '用户提"上次/之前"时搜索:`node ~/.metame/memory-search.js "关键词1" "keyword2"`',
637
+ '一次传 3-4 个关键词(中文+英文+函数名),`--facts` 只搜事实,`--sessions` 只搜会话。',
638
+ ]},
639
+ { detect: /skill-manager|Skills.*技能/i, text: [
640
+ '## Skills',
641
+ '能力不足/工具缺失/任务失败 → 先查 `cat ~/.claude/skills/skill-manager/SKILL.md`,不要自己猜。',
642
+ ]},
643
+ { detect: /\[\[FILE:|手机端文件/i, text: [
644
+ '## 手机端文件交互',
645
+ '**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
646
+ '**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
647
+ ]},
648
+ ];
649
+
650
+ try {
651
+ const globalDir = path.join(os.homedir(), '.claude');
652
+ if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true });
653
+
654
+ let globalContent = '';
655
+ if (fs.existsSync(GLOBAL_CLAUDE_MD)) {
656
+ globalContent = fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf8');
657
+ // Remove previous injection
658
+ globalContent = globalContent.replace(new RegExp(
659
+ GLOBAL_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
660
+ '[\\s\\S]*?' +
661
+ GLOBAL_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\n?'
662
+ ), '');
663
+ }
664
+
665
+ // Only inject sections not already present in user's manual content
666
+ const contentOutsideMarkers = globalContent;
667
+ const needed = GLOBAL_SECTIONS.filter(s => !s.detect.test(contentOutsideMarkers));
668
+
669
+ if (needed.length > 0) {
670
+ const injection = GLOBAL_MARKER_START + '\n\n# MetaMe 能力注入(自动生成,勿手动编辑)\n\n' +
671
+ needed.map(s => s.text.join('\n')).join('\n\n') + '\n\n' + GLOBAL_MARKER_END;
672
+ const finalGlobal = globalContent.trimEnd() + '\n\n' + injection + '\n';
673
+ fs.writeFileSync(GLOBAL_CLAUDE_MD, finalGlobal, 'utf8');
674
+ } else {
675
+ // All sections already present, just clean up stale marker block
676
+ fs.writeFileSync(GLOBAL_CLAUDE_MD, globalContent.trimEnd() + '\n', 'utf8');
677
+ }
678
+ } catch (e) {
679
+ // Non-fatal: global CLAUDE.md injection is best-effort
680
+ console.error(`⚠️ Failed to inject global CLAUDE.md: ${e.message}`);
681
+ }
682
+
600
683
  console.log("🔮 MetaMe: Link Established.");
601
684
  console.log("🧬 Protocol: Dynamic Handshake Active");
602
685
 
@@ -618,6 +701,38 @@ try {
618
701
  }
619
702
  } catch { /* non-fatal */ }
620
703
 
704
+ // ---------------------------------------------------------
705
+ // 4.9 AUTO-UPDATE CHECK (non-blocking)
706
+ // ---------------------------------------------------------
707
+ const CURRENT_VERSION = require('./package.json').version;
708
+
709
+ // Fire-and-forget: check npm for newer version and auto-update
710
+ (async () => {
711
+ try {
712
+ const https = require('https');
713
+ const latest = await new Promise((resolve, reject) => {
714
+ https.get('https://registry.npmjs.org/metame-cli/latest', { timeout: 5000 }, res => {
715
+ let data = '';
716
+ res.on('data', c => data += c);
717
+ res.on('end', () => {
718
+ try { resolve(JSON.parse(data).version); } catch { reject(); }
719
+ });
720
+ }).on('error', reject).on('timeout', function() { this.destroy(); reject(); });
721
+ });
722
+
723
+ if (latest && latest !== CURRENT_VERSION) {
724
+ console.log(`📦 MetaMe ${latest} 可用(当前 ${CURRENT_VERSION}),正在自动更新...`);
725
+ const { execSync } = require('child_process');
726
+ try {
727
+ execSync('npm update -g metame-cli', { stdio: 'pipe', timeout: 30000 });
728
+ console.log(`✅ 已更新到 ${latest},下次启动生效。`);
729
+ } catch (e) {
730
+ console.log(`⚠️ 自动更新失败,请手动执行: npm update -g metame-cli`);
731
+ }
732
+ }
733
+ } catch { /* network unavailable, skip silently */ }
734
+ })();
735
+
621
736
  // ---------------------------------------------------------
622
737
  // 5. LAUNCH CLAUDE (OR HOT RELOAD)
623
738
  // ---------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/scripts/daemon.js CHANGED
@@ -29,6 +29,19 @@ const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
29
29
  const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
30
30
  const SOCK_PATH = path.join(METAME_DIR, 'daemon.sock');
31
31
 
32
+ // Resolve claude binary path (daemon may not inherit user's full PATH)
33
+ const CLAUDE_BIN = (() => {
34
+ const candidates = [
35
+ path.join(HOME, '.local', 'bin', 'claude'), // npm global (Linux/Mac)
36
+ path.join(HOME, '.npm-global', 'bin', 'claude'), // custom npm prefix
37
+ '/usr/local/bin/claude',
38
+ '/opt/homebrew/bin/claude',
39
+ ];
40
+ try { return execSync('which claude 2>/dev/null', { encoding: 'utf8' }).trim(); } catch {}
41
+ for (const p of candidates) { if (fs.existsSync(p)) return p; }
42
+ return 'claude'; // fallback: hope it's in PATH
43
+ })();
44
+
32
45
  // Skill evolution module (hot path + cold path)
33
46
  let skillEvolution = null;
34
47
  try { skillEvolution = require('./skill-evolution'); } catch { /* graceful fallback */ }
@@ -395,7 +408,7 @@ function executeTask(task, config) {
395
408
  const asyncEnv = { ...process.env, ...getDaemonProviderEnv(), CLAUDECODE: undefined };
396
409
 
397
410
  return new Promise((resolve) => {
398
- const child = spawn('claude', asyncArgs, {
411
+ const child = spawn(CLAUDE_BIN, asyncArgs, {
399
412
  cwd: cwd || undefined,
400
413
  stdio: ['pipe', 'pipe', 'pipe'],
401
414
  detached: true, // own process group — kills sub-agents on timeout too
@@ -1129,7 +1142,7 @@ async function startTelegramBridge(config, executeTaskByName) {
1129
1142
  const isBindCmd = trimmedText && (trimmedText.startsWith('/bind') || trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
1130
1143
  if (!allowedIds.includes(chatId) && !isBindCmd) {
1131
1144
  log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
1132
- bot.sendMessage(chatId, `⚠️ This chat (ID: ${chatId}) is not authorized.\n\nTo get started, send:\n/bind personal ~/\n\nThis will register this chat and bind it to your home directory.`).catch(() => {});
1145
+ bot.sendMessage(chatId, `⚠️ This chat is not authorized.\n\nCopy and send this command to register:\n\n/agent bind personal`).catch(() => {});
1133
1146
  continue;
1134
1147
  }
1135
1148
 
@@ -3709,7 +3722,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
3709
3722
  */
3710
3723
  function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000) {
3711
3724
  return new Promise((resolve) => {
3712
- const child = spawn('claude', args, {
3725
+ const child = spawn(CLAUDE_BIN, args, {
3713
3726
  cwd,
3714
3727
  stdio: ['pipe', 'pipe', 'pipe'],
3715
3728
  env: { ...process.env, ...getActiveProviderEnv(), CLAUDECODE: undefined },
@@ -4005,7 +4018,7 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
4005
4018
  // Add stream-json output format (requires --verbose)
4006
4019
  const streamArgs = [...args, '--output-format', 'stream-json', '--verbose'];
4007
4020
 
4008
- const child = spawn('claude', streamArgs, {
4021
+ const child = spawn(CLAUDE_BIN, streamArgs, {
4009
4022
  cwd,
4010
4023
  stdio: ['pipe', 'pipe', 'pipe'],
4011
4024
  detached: true, // Create new process group so killing -pid kills all sub-agents too
@@ -4613,7 +4626,7 @@ async function startFeishuBridge(config, executeTaskByName) {
4613
4626
  const isBindCmd = trimmedText && (trimmedText.startsWith('/bind') || trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
4614
4627
  if (!allowedIds.includes(chatId) && !isBindCmd) {
4615
4628
  log('WARN', `Feishu: rejected message from ${chatId}`);
4616
- (bot.sendMarkdown ? bot.sendMarkdown(chatId, `⚠️ **此会话未授权**\n\n会话 ID: \`${chatId}\`\n\n发送以下命令注册:\n\`/bind personal ~/\`\n\n这会将此会话绑定到你的主目录。`) : bot.sendMessage(chatId, `⚠️ 此会话 (ID: ${chatId}) 未授权。\n\n发送以下命令注册:\n/bind personal ~/\n\n这会将此会话绑定到你的主目录。`)).catch(() => {});
4629
+ (bot.sendMarkdown ? bot.sendMarkdown(chatId, `⚠️ **此会话未授权**\n\n复制下方命令发送即可注册:\n\`\`\`\n/agent bind personal\n\`\`\``) : bot.sendMessage(chatId, `⚠️ 此会话未授权。\n\n复制下方命令发送即可注册:\n/agent bind personal`)).catch(() => {});
4617
4630
  return;
4618
4631
  }
4619
4632
 
@@ -4623,7 +4636,7 @@ async function startFeishuBridge(config, executeTaskByName) {
4623
4636
  log('INFO', `Feishu: read-only message from non-operator ${senderId} in ${chatId}: ${(text || '').slice(0, 50)}`);
4624
4637
  // Block slash commands for non-operators
4625
4638
  if (text && text.startsWith('/')) {
4626
- await bot.sendMessage(chatId, '⚠️ 该操作需要授权,请联系管理员。');
4639
+ await (bot.sendMarkdown ? bot.sendMarkdown(chatId, '⚠️ 该操作需要授权,请联系管理员。') : bot.sendMessage(chatId, '⚠️ 该操作需要授权,请联系管理员。'));
4627
4640
  return;
4628
4641
  }
4629
4642
  // Allow read-only chat (query/answer only, no write/edit/execute)