metame-cli 1.3.2 → 1.3.3

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 CHANGED
@@ -571,11 +571,12 @@ if (isDaemon) {
571
571
  const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
572
572
 
573
573
  if (subCmd === 'init') {
574
- // Create config from template
575
- if (fs.existsSync(DAEMON_CONFIG)) {
576
- console.log("⚠️ daemon.yaml already exists at ~/.metame/daemon.yaml");
577
- console.log(" Delete it first if you want to re-initialize.");
578
- } else {
574
+ const readline = require('readline');
575
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
576
+ const ask = (q) => new Promise(r => rl.question(q, r));
577
+
578
+ // Create config from template if not exists
579
+ if (!fs.existsSync(DAEMON_CONFIG)) {
579
580
  const templateSrc = fs.existsSync(DAEMON_DEFAULT)
580
581
  ? DAEMON_DEFAULT
581
582
  : path.join(METAME_DIR, 'daemon-default.yaml');
@@ -585,45 +586,113 @@ if (isDaemon) {
585
586
  console.error("❌ Template not found. Reinstall MetaMe.");
586
587
  process.exit(1);
587
588
  }
588
- // Ensure directory permissions (700)
589
589
  try { fs.chmodSync(METAME_DIR, 0o700); } catch { /* ignore on Windows */ }
590
- console.log("✅ MetaMe daemon initialized.");
591
- console.log(` Config: ${DAEMON_CONFIG}`);
590
+ console.log("✅ Config created: ~/.metame/daemon.yaml\n");
591
+ } else {
592
+ console.log("✅ Config exists: ~/.metame/daemon.yaml\n");
593
+ }
594
+
595
+ const yaml = require(path.join(__dirname, 'node_modules', 'js-yaml'));
596
+ let cfg = yaml.load(fs.readFileSync(DAEMON_CONFIG, 'utf8')) || {};
597
+
598
+ // --- Telegram Setup ---
599
+ console.log("━━━ 📱 Telegram Setup ━━━");
600
+ console.log("Steps:");
601
+ console.log(" 1. Open Telegram, search @BotFather");
602
+ console.log(" 2. Send /newbot, follow prompts to create a bot");
603
+ console.log(" 3. Copy the bot token (looks like: 123456:ABC-DEF...)");
604
+ console.log("");
605
+
606
+ const tgToken = (await ask("Paste your Telegram bot token (Enter to skip): ")).trim();
607
+ if (tgToken) {
608
+ if (!cfg.telegram) cfg.telegram = {};
609
+ cfg.telegram.enabled = true;
610
+ cfg.telegram.bot_token = tgToken;
611
+
612
+ console.log("\nFinding your chat ID...");
613
+ console.log(" → Send any message to your bot in Telegram first, then press Enter.");
614
+ await ask("Press Enter after you've messaged your bot: ");
615
+
616
+ try {
617
+ const https = require('https');
618
+ const chatIds = await new Promise((resolve, reject) => {
619
+ https.get(`https://api.telegram.org/bot${tgToken}/getUpdates`, (res) => {
620
+ let body = '';
621
+ res.on('data', d => body += d);
622
+ res.on('end', () => {
623
+ try {
624
+ const data = JSON.parse(body);
625
+ const ids = new Set();
626
+ if (data.result) {
627
+ for (const u of data.result) {
628
+ if (u.message && u.message.chat) ids.add(u.message.chat.id);
629
+ }
630
+ }
631
+ resolve([...ids]);
632
+ } catch { resolve([]); }
633
+ });
634
+ }).on('error', () => resolve([]));
635
+ });
636
+
637
+ if (chatIds.length > 0) {
638
+ cfg.telegram.allowed_chat_ids = chatIds;
639
+ console.log(` ✅ Found chat ID(s): ${chatIds.join(', ')}`);
640
+ } else {
641
+ console.log(" ⚠️ No messages found. Make sure you messaged the bot.");
642
+ console.log(" You can set allowed_chat_ids manually in daemon.yaml later.");
643
+ }
644
+ } catch {
645
+ console.log(" ⚠️ Could not fetch chat ID. Set it manually in daemon.yaml.");
646
+ }
647
+ console.log(" ✅ Telegram configured!\n");
648
+ } else {
649
+ console.log(" Skipped.\n");
650
+ }
651
+
652
+ // --- Feishu Setup ---
653
+ console.log("━━━ 📘 Feishu (Lark) Setup ━━━");
654
+ console.log("Steps:");
655
+ console.log(" 1. Go to: https://open.feishu.cn/app");
656
+ console.log(" → Create App (企业自建应用)");
657
+ console.log(" 2. In 'Credentials' (凭证与基础信息), copy App ID & App Secret");
658
+ console.log(" 3. In 'Bot' (机器人), enable bot capability");
659
+ console.log(" 4. In 'Event Subscription' (事件订阅):");
660
+ console.log(" → Set mode to 'Long Connection' (使用长连接接收事件)");
661
+ console.log(" → Add event: im.message.receive_v1 (接收消息)");
662
+ console.log(" 5. In 'Permissions' (权限管理), add:");
663
+ console.log(" → im:message, im:message:send_as_bot, im:chat");
664
+ console.log(" 6. Publish the app version (创建版本 → 申请发布)");
665
+ console.log("");
666
+
667
+ const feishuAppId = (await ask("Paste your Feishu App ID (Enter to skip): ")).trim();
668
+ if (feishuAppId) {
669
+ const feishuSecret = (await ask("Paste your Feishu App Secret: ")).trim();
670
+ if (feishuSecret) {
671
+ if (!cfg.feishu) cfg.feishu = {};
672
+ cfg.feishu.enabled = true;
673
+ cfg.feishu.app_id = feishuAppId;
674
+ cfg.feishu.app_secret = feishuSecret;
675
+ if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
676
+ console.log(" ✅ Feishu configured!");
677
+ console.log(" Note: allowed_chat_ids is empty = allow all users.");
678
+ console.log(" To restrict, add chat IDs to daemon.yaml later.\n");
679
+ }
680
+ } else {
681
+ console.log(" Skipped.\n");
592
682
  }
593
683
 
594
- console.log("\n📱 Telegram Setup (optional):");
595
- console.log(" 1. Message @BotFather on Telegram → /newbot");
596
- console.log(" 2. Copy the bot token");
597
- console.log(" 3. Edit ~/.metame/daemon.yaml:");
598
- console.log(" telegram:");
599
- console.log(" enabled: true");
600
- console.log(" bot_token: \"YOUR_TOKEN\"");
601
- console.log(" allowed_chat_ids: [YOUR_CHAT_ID]");
602
- console.log(" 4. To find your chat_id: message your bot, then run:");
603
- console.log(" curl https://api.telegram.org/botYOUR_TOKEN/getUpdates");
604
- console.log("\n📘 Feishu Setup (optional):");
605
- console.log(" 1. Go to open.feishu.cn → Create App → get app_id & app_secret");
606
- console.log(" 2. Enable Bot capability + im:message events");
607
- console.log(" 3. Enable 'Long Connection' (长连接) mode in Event Subscription");
608
- console.log(" 4. Edit ~/.metame/daemon.yaml:");
609
- console.log(" feishu:");
610
- console.log(" enabled: true");
611
- console.log(" app_id: \"YOUR_APP_ID\"");
612
- console.log(" app_secret: \"YOUR_APP_SECRET\"");
613
- console.log(" allowed_chat_ids: [CHAT_ID]");
614
-
615
- console.log("\n Then: metame daemon start");
616
-
617
- // Optional launchd setup (macOS only)
684
+ // Write config
685
+ fs.writeFileSync(DAEMON_CONFIG, yaml.dump(cfg, { lineWidth: -1 }), 'utf8');
686
+ console.log("━━━ Setup Complete ━━━");
687
+ console.log(`Config saved: ${DAEMON_CONFIG}`);
688
+ console.log("\nNext steps:");
689
+ console.log(" metame daemon start — start the daemon");
690
+ console.log(" metame daemon status check status");
618
691
  if (process.platform === 'darwin') {
619
- const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
620
- const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
621
- console.log("\n🍎 Auto-start on macOS (optional):");
622
- console.log(" To start daemon automatically on login:");
623
- console.log(` metame daemon start (first time to verify it works)`);
624
- console.log(` Then create: ${plistPath}`);
625
- console.log(" Or run: metame daemon install-launchd");
692
+ console.log(" metame daemon install-launchd auto-start on login");
626
693
  }
694
+
695
+ rl.close();
627
696
  process.exit(0);
628
697
  }
629
698
 
@@ -676,18 +745,21 @@ if (isDaemon) {
676
745
  }
677
746
 
678
747
  if (subCmd === 'start') {
748
+ // Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
749
+ try {
750
+ const { execSync: es } = require('child_process');
751
+ const pids = es("pgrep -f 'node.*daemon\\.js' 2>/dev/null || true", { encoding: 'utf8' }).trim();
752
+ if (pids) {
753
+ for (const p of pids.split('\n').filter(Boolean)) {
754
+ const n = parseInt(p, 10);
755
+ if (n && n !== process.pid) try { process.kill(n, 'SIGKILL'); } catch { /* */ }
756
+ }
757
+ es('sleep 1');
758
+ }
759
+ } catch { /* ignore */ }
679
760
  // Check if already running
680
761
  if (fs.existsSync(DAEMON_PID)) {
681
- const existingPid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
682
- try {
683
- process.kill(existingPid, 0); // test if alive
684
- console.log(`⚠️ Daemon already running (PID: ${existingPid})`);
685
- console.log(" Use 'metame daemon stop' first.");
686
- process.exit(1);
687
- } catch {
688
- // Stale PID file — clean up
689
- fs.unlinkSync(DAEMON_PID);
690
- }
762
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
691
763
  }
692
764
  if (!fs.existsSync(DAEMON_CONFIG)) {
693
765
  console.error("❌ No config found. Run: metame daemon init");
@@ -717,11 +789,21 @@ if (isDaemon) {
717
789
  const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
718
790
  try {
719
791
  process.kill(pid, 'SIGTERM');
792
+ // Wait for process to die (up to 3s), then force kill
793
+ let dead = false;
794
+ for (let i = 0; i < 6; i++) {
795
+ const { execSync: es } = require('child_process');
796
+ es('sleep 0.5');
797
+ try { process.kill(pid, 0); } catch { dead = true; break; }
798
+ }
799
+ if (!dead) {
800
+ try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
801
+ }
720
802
  console.log(`✅ Daemon stopped (PID: ${pid})`);
721
803
  } catch (e) {
722
804
  console.log(`⚠️ Process ${pid} not found (may have already exited).`);
723
- fs.unlinkSync(DAEMON_PID);
724
805
  }
806
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
725
807
  process.exit(0);
726
808
  }
727
809
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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
@@ -615,12 +615,27 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
615
615
  await sendDirPicker(bot, chatId, 'new', 'Pick a workdir:');
616
616
  return;
617
617
  }
618
- if (!fs.existsSync(arg)) {
619
- await bot.sendMessage(chatId, `Path not found: ${arg}`);
620
- return;
618
+ // Parse: /new <path> [name] — if arg contains a space after a valid path, rest is name
619
+ let dirPath = arg;
620
+ let sessionName = '';
621
+ // Try full arg as path first; if not, split on spaces to find path + name
622
+ if (!fs.existsSync(dirPath)) {
623
+ const spaceIdx = arg.indexOf(' ');
624
+ if (spaceIdx > 0) {
625
+ const maybePath = arg.slice(0, spaceIdx);
626
+ if (fs.existsSync(maybePath)) {
627
+ dirPath = maybePath;
628
+ sessionName = arg.slice(spaceIdx + 1).trim();
629
+ }
630
+ }
631
+ if (!fs.existsSync(dirPath)) {
632
+ await bot.sendMessage(chatId, `Path not found: ${dirPath}`);
633
+ return;
634
+ }
621
635
  }
622
- const session = createSession(chatId, arg);
623
- await bot.sendMessage(chatId, `New session.\nWorkdir: ${session.cwd}`);
636
+ const session = createSession(chatId, dirPath, sessionName || '');
637
+ const label = sessionName ? `[${sessionName}]` : '';
638
+ await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
624
639
  return;
625
640
  }
626
641
 
@@ -669,11 +684,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
669
684
  return;
670
685
  }
671
686
 
672
- // Argument given → match by prefix or full ID
673
- const match = recentSessions.length > 0
674
- ? recentSessions.find(s => s.sessionId.startsWith(arg))
675
- : null;
676
- const fullMatch = match || listRecentSessions(50).find(s => s.sessionId.startsWith(arg));
687
+ // Argument given → match by name, then by session ID prefix
688
+ const allSessions = listRecentSessions(50);
689
+ const argLower = arg.toLowerCase();
690
+ // 1. Match by name (from session_names map)
691
+ let fullMatch = allSessions.find(s => {
692
+ const n = getSessionName(s.sessionId);
693
+ return n && n.toLowerCase() === argLower;
694
+ });
695
+ // 2. Partial name match
696
+ if (!fullMatch) {
697
+ fullMatch = allSessions.find(s => {
698
+ const n = getSessionName(s.sessionId);
699
+ return n && n.toLowerCase().includes(argLower);
700
+ });
701
+ }
702
+ // 3. Session ID prefix match
703
+ if (!fullMatch) {
704
+ fullMatch = recentSessions.find(s => s.sessionId.startsWith(arg))
705
+ || allSessions.find(s => s.sessionId.startsWith(arg));
706
+ }
677
707
  const sessionId = fullMatch ? fullMatch.sessionId : arg;
678
708
  const cwd = (fullMatch && fullMatch.projectPath) || (getSession(chatId) && getSession(chatId).cwd) || HOME;
679
709
 
@@ -684,8 +714,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
684
714
  created: new Date().toISOString(),
685
715
  started: true,
686
716
  };
717
+ if (fullMatch) {
718
+ const n = getSessionName(sessionId);
719
+ if (n) state2.sessions[chatId].name = n;
720
+ }
687
721
  saveState(state2);
688
- const label = fullMatch ? (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) : sessionId.slice(0, 8);
722
+ const name = getSessionName(sessionId);
723
+ const label = name || (fullMatch ? (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) : sessionId.slice(0, 8));
689
724
  await bot.sendMessage(chatId, `Resumed: ${label}\nWorkdir: ${cwd}`);
690
725
  return;
691
726
  }
@@ -711,12 +746,32 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
711
746
  return;
712
747
  }
713
748
 
749
+ if (text.startsWith('/name ')) {
750
+ const name = text.slice(6).trim();
751
+ if (!name) {
752
+ await bot.sendMessage(chatId, 'Usage: /name <session name>');
753
+ return;
754
+ }
755
+ const state2 = loadState();
756
+ if (!state2.sessions[chatId]) {
757
+ await bot.sendMessage(chatId, 'No active session. Start one first.');
758
+ return;
759
+ }
760
+ state2.sessions[chatId].name = name;
761
+ if (!state2.session_names) state2.session_names = {};
762
+ state2.session_names[state2.sessions[chatId].id] = name;
763
+ saveState(state2);
764
+ await bot.sendMessage(chatId, `Session named: ${name}`);
765
+ return;
766
+ }
767
+
714
768
  if (text === '/session') {
715
769
  const session = getSession(chatId);
716
770
  if (!session) {
717
771
  await bot.sendMessage(chatId, 'No active session. Send any message to start one.');
718
772
  } else {
719
- await bot.sendMessage(chatId, `Session: ${session.id.slice(0, 8)}...\nWorkdir: ${session.cwd}\nStarted: ${session.created}`);
773
+ const nameTag = session.name ? ` [${session.name}]` : '';
774
+ await bot.sendMessage(chatId, `Session: ${session.id.slice(0, 8)}...${nameTag}\nWorkdir: ${session.cwd}\nStarted: ${session.created}`);
720
775
  }
721
776
  return;
722
777
  }
@@ -796,9 +851,10 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
796
851
  if (text.startsWith('/')) {
797
852
  await bot.sendMessage(chatId, [
798
853
  'Commands:',
799
- '/new [path] — new session',
854
+ '/new [path] [name] — new session (optional name)',
800
855
  '/continue — resume last computer session',
801
- '/resume <id> — resume specific session',
856
+ '/resume <name|id> — resume by name or session ID',
857
+ '/name <name> — name current session',
802
858
  '/cd <path> — change workdir',
803
859
  '/session — current session info',
804
860
  '/status /tasks /run /budget /quiet /reload',
@@ -865,8 +921,10 @@ function listRecentSessions(limit, cwd) {
865
921
  * Format a session entry into a short, readable label for buttons
866
922
  */
867
923
  function sessionLabel(s) {
924
+ const name = getSessionName(s.sessionId);
868
925
  const proj = s.projectPath ? path.basename(s.projectPath) : '';
869
926
  const date = new Date(s.modified).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' });
927
+ if (name) return `${date} [${name}] ${proj}`;
870
928
  const title = (s.summary || '').slice(0, 28);
871
929
  return `${date} ${proj ? proj + ': ' : ''}${title}`;
872
930
  }
@@ -901,7 +959,7 @@ function getSession(chatId) {
901
959
  return state.sessions[chatId] || null;
902
960
  }
903
961
 
904
- function createSession(chatId, cwd) {
962
+ function createSession(chatId, cwd, name) {
905
963
  const state = loadState();
906
964
  const sessionId = crypto.randomUUID();
907
965
  state.sessions[chatId] = {
@@ -910,11 +968,21 @@ function createSession(chatId, cwd) {
910
968
  created: new Date().toISOString(),
911
969
  started: false, // true after first message sent
912
970
  };
971
+ if (name) {
972
+ state.sessions[chatId].name = name;
973
+ if (!state.session_names) state.session_names = {};
974
+ state.session_names[sessionId] = name;
975
+ }
913
976
  saveState(state);
914
- log('INFO', `New session for ${chatId}: ${sessionId} (cwd: ${state.sessions[chatId].cwd})`);
977
+ log('INFO', `New session for ${chatId}: ${sessionId}${name ? ' [' + name + ']' : ''} (cwd: ${state.sessions[chatId].cwd})`);
915
978
  return state.sessions[chatId];
916
979
  }
917
980
 
981
+ function getSessionName(sessionId) {
982
+ const state = loadState();
983
+ return (state.session_names && state.session_names[sessionId]) || '';
984
+ }
985
+
918
986
  function markSessionStarted(chatId) {
919
987
  const state = loadState();
920
988
  if (state.sessions[chatId]) {
@@ -121,12 +121,32 @@ function createBot(config) {
121
121
  loggerLevel: Lark.LoggerLevel.info,
122
122
  });
123
123
 
124
+ // Dedup: track recent message_ids (Feishu may redeliver on slow ack)
125
+ const _seenMsgIds = new Map(); // message_id → timestamp
126
+ const DEDUP_TTL = 60000; // 60s window
127
+ function isDuplicate(msgId) {
128
+ if (!msgId) return false;
129
+ const now = Date.now();
130
+ // Cleanup old entries
131
+ if (_seenMsgIds.size > 200) {
132
+ for (const [k, t] of _seenMsgIds) {
133
+ if (now - t > DEDUP_TTL) _seenMsgIds.delete(k);
134
+ }
135
+ }
136
+ if (_seenMsgIds.has(msgId)) return true;
137
+ _seenMsgIds.set(msgId, now);
138
+ return false;
139
+ }
140
+
124
141
  const eventDispatcher = new Lark.EventDispatcher({}).register({
125
142
  'im.message.receive_v1': async (data) => {
126
143
  try {
127
144
  const msg = data.message;
128
145
  if (!msg) return;
129
146
 
147
+ // Dedup by message_id
148
+ if (isDuplicate(msg.message_id)) return;
149
+
130
150
  const chatId = msg.chat_id;
131
151
  let text = '';
132
152
 
@@ -143,7 +163,8 @@ function createBot(config) {
143
163
  text = text.replace(/@_user_\d+\s*/g, '').trim();
144
164
 
145
165
  if (text) {
146
- onMessage(chatId, text, data);
166
+ // Fire-and-forget: don't block the event loop (SDK needs fast ack)
167
+ Promise.resolve().then(() => onMessage(chatId, text, data)).catch(() => {});
147
168
  }
148
169
  } catch (e) {
149
170
  // Non-fatal
@@ -159,7 +180,7 @@ function createBot(config) {
159
180
  if (action && chatId) {
160
181
  const cmd = action.value && action.value.cmd;
161
182
  if (cmd) {
162
- onMessage(chatId, cmd, data);
183
+ Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch(() => {});
163
184
  }
164
185
  }
165
186
  } catch (e) {