metame-cli 1.3.7 โ†’ 1.3.9

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
@@ -6,20 +6,32 @@
6
6
 
7
7
  > **The Cognitive Profile Layer for Claude Code.**
8
8
  >
9
- > *Not a memory system โ€” a cognitive mirror. It knows how you think, decide, and communicate, and it protects your core values.*
9
+ > *Knows how you think. Works wherever you are.*
10
10
 
11
11
  ## ๐Ÿ“– Introduction
12
12
 
13
- **Claude Code** is a powerful tool, but it suffers from "Project Amnesia." Every time you switch folders, it forgets who you are, your communication style, and your specific constraints.
13
+ **Claude Code** is powerful, but it has two pain points:
14
14
 
15
- **MetaMe** solves this by wrapping Claude in a **Cognitive Profile Layer** . It creates a persistent "Global Brain" that travels with you across every project. Unlike ChatGPT/Claude/Gemini's built-in memory (which stores *facts* like "user lives in X"), MetaMe captures *how you think* โ€” your decision style, cognitive load preferences, motivation patterns, and communication traits.
15
+ 1. **Project Amnesia** โ€” Switch folders, and it forgets who you are. Your communication style, coding preferences, constraints โ€” gone. Every project, you start from scratch.
16
16
 
17
- It is not a memory system; it is a **Cognitive Mirror** .
17
+ 2. **Desktop-Bound** โ€” Leave your computer, work stops. You can't continue that debugging session on your phone. You can't vibe with Claude on the train, in bed, or waiting in line.
18
+
19
+ **MetaMe** solves both โ€” and more:
20
+
21
+ **๐Ÿง  Cognitive Profile** โ€” A persistent "Global Brain" (`~/.claude_profile.yaml`) that travels with you across every project. Unlike ChatGPT/Claude's built-in memory (which stores *facts* like "user lives in X"), MetaMe captures *how you think* โ€” your decision style, cognitive load preferences, and communication traits. It's not a memory system; it's a **Cognitive Mirror**.
22
+
23
+ **๐Ÿ“ฑ Mobile Bridge** โ€” Full Claude Code from your phone via Telegram or Feishu. Same tools, same files, same conversation history. Start on your computer, continue anywhere. `/cd last` syncs you to exactly where you left off.
24
+
25
+ **๐Ÿ”” Remote Wake** โ€” Daemon runs in the background on your computer. Send a message from your phone, and it wakes up Claude Code to do real work โ€” edit files, run commands, commit code โ€” even while you're away from your desk.
26
+
27
+ **๐Ÿ“‚ File Transfer** โ€” Send files from your computer to your phone (ask Claude to send any project file). Send files from your phone to your computer (just attach them in chat). Seamless both ways.
28
+
29
+ **โฐ Heartbeat Tasks** โ€” Schedule Claude to run automatically. Daily summaries, automated workflows, multi-step skill chains โ€” all running on your machine, pushing results to your phone.
18
30
 
19
31
  ## โœจ Key Features
20
32
 
21
33
  * **๐Ÿง  Global Brain (`~/.claude_profile.yaml`):** A single, portable source of truth โ€” your identity, cognitive traits, and preferences travel with you across every project.
22
- * **๐Ÿงฌ Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** โ€” silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** โ€” `!metame evolve` for explicit teaching; (3) **Confidence gates** โ€” strong directives ("always"/"ไปฅๅŽไธ€ๅพ‹") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
34
+ * **๐Ÿงฌ Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** โ€” silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** โ€” `!metame evolve` for explicit teaching; (3) **Confidence gates** โ€” strong directives ("always"/"from now on") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
23
35
  * **๐Ÿ›ก๏ธ Auto-Lock:** Mark any value with `# [LOCKED]` โ€” treated as a constitution, never auto-modified.
24
36
  * **๐Ÿชž Metacognition Layer (v1.3):** MetaMe now observes *how* you think, not just *what* you say. Behavioral pattern detection runs inside the existing Haiku distill call (zero extra cost). It tracks decision patterns, cognitive load, comfort zones, and avoidance topics across sessions. When persistent patterns emerge, MetaMe injects a one-line mirror observation โ€” e.g., *"You tend to avoid testing until forced"* โ€” with a 14-day cooldown per pattern. Conditional reflection prompts appear only when triggered (every 7th distill or 3x consecutive comfort zone). All injection logic runs in Node.js; Claude receives only pre-decided directives, never rules to self-evaluate.
25
37
  * **๐Ÿ“ฑ Remote Claude Code (v1.3):** Full Claude Code from your phone via Telegram or Feishu (Lark). Stateful sessions with `--resume` โ€” same conversation history, tool use, and file editing as your terminal. Interactive buttons for project/session picking, directory browser, and macOS launchd auto-start.
@@ -34,15 +46,19 @@ MetaMe is a wrapper around **Claude Code**. You must have Node.js and the offici
34
46
 
35
47
  ## ๐Ÿ“ฆ Installation
36
48
 
37
- Install MetaMe globally via NPM:
38
-
39
- **Bash**
49
+ **Option A: NPM (recommended)** โ€” full CLI with daemon, mobile bridge, interview
40
50
 
41
- ```
51
+ ```bash
42
52
  npm install -g metame-cli
43
53
  ```
44
54
 
45
- *(Note: If you encounter permission errors on Mac/Linux, use `sudo npm install -g metame-cli`)*
55
+ **Option B: Claude Code Plugin** โ€” lightweight, profile injection + slash commands
56
+
57
+ ```bash
58
+ claude plugin install github:Yaron9/MetaMe/plugin
59
+ ```
60
+
61
+ *(NPM note: If you encounter permission errors on Mac/Linux, use `sudo npm install -g metame-cli`)*
46
62
 
47
63
  ## ๐Ÿš€ Usage
48
64
 
@@ -93,7 +109,7 @@ metame interview
93
109
 
94
110
  MetaMe learns who you are through two paths:
95
111
 
96
- **Automatic (zero effort):** A global hook captures your messages. On next launch, Haiku distills cognitive traits in the background. Strong directives ("always"/"ไปฅๅŽไธ€ๅพ‹") write immediately; normal observations need 3+ consistent sightings. All writes are schema-validated (41 fields, 800 token budget). You'll see:
112
+ **Automatic (zero effort):** A global hook captures your messages. On next launch, Haiku distills cognitive traits in the background. Strong directives ("always"/"from now on") write immediately; normal observations need 3+ consistent sightings. All writes are schema-validated (41 fields, 800 token budget). You'll see:
97
113
 
98
114
  ```
99
115
  ๐Ÿง  MetaMe: Distilling 7 moments in background...
@@ -161,16 +177,15 @@ metame daemon install-launchd # macOS auto-start (RunAtLoad + KeepAlive)
161
177
 
162
178
  | Command | Description |
163
179
  |---------|-------------|
164
- | `/last` | **Quick resume** โ€” ไผ˜ๅ…ˆๅฝ“ๅ‰็›ฎๅฝ•ๆœ€่ฟ‘ session๏ผŒๅฆๅˆ™ๅ…จๅฑ€ๆœ€่ฟ‘ |
180
+ | `/last` | **Quick resume** โ€” prefers current directory's recent session, falls back to global recent |
165
181
  | `/new` | Start new session โ€” pick project directory from button list |
166
- | `/new <name>` | Start new session with a name (e.g., `/new API้‡ๆž„`) |
182
+ | `/new <name>` | Start new session with a name (e.g., `/new API Refactor`) |
167
183
  | `/resume` | Resume a session โ€” clickable list, shows session names + real-time timestamps |
168
184
  | `/resume <name>` | Resume by name (supports partial match, cross-project) |
169
185
  | `/name <name>` | Name the current session (syncs with computer's `/rename`) |
170
186
  | `/cd` | Change working directory โ€” with directory browser |
171
187
  | `/cd last` | **Sync to computer** โ€” jump to the most recent session's directory |
172
188
  | `/session` | Current session info |
173
- | `/continue` | Continue the most recent terminal session |
174
189
 
175
190
  Just type naturally for conversation โ€” every message stays in the same Claude Code session with full context.
176
191
 
@@ -190,18 +205,30 @@ Each chat gets a persistent session via `claude -p --resume <session-id>`. This
190
205
  ๐Ÿ’ป Bash: ใ€Œgit statusใ€
191
206
  ```
192
207
 
193
- **File sending (v1.3.7):** Ask Claude to send any file to your phone:
208
+ **File transfer (v1.3.8):** Seamlessly move files between your phone and computer.
209
+
210
+ *Computer โ†’ Phone (download):* Ask Claude to send any project file:
194
211
 
195
212
  ```
196
- You: ๆŠŠ report.md ๅ‘่ฟ‡ๆฅ
197
- Claude: ่ฏทๆŸฅๆ”ถ~!
213
+ You: Send me report.md
214
+ Claude: Here you go!
198
215
  [๐Ÿ“Ž report.md] โ† tap to download
199
216
  ```
200
217
 
201
218
  Works for documents, audio, images, etc. Click button to download. Links valid for 30 minutes.
202
219
 
220
+ *Phone โ†’ Computer (upload):* Send files directly to your project:
221
+
222
+ ```
223
+ [๐Ÿ“Ž You send a PDF, image, or any file]
224
+ Claude: ๐Ÿ“ฅ Saved: document.pdf
225
+ File is in your project's upload/ folder.
226
+ ```
227
+
228
+ Uploaded files are saved to `<project>/upload/`. Claude won't read large files automatically โ€” just tell it when you want it to process them.
229
+
203
230
  - **Telegram:** Works out of the box
204
- - **Feishu:** Requires `im:resource` permission in app settings
231
+ - **Feishu:** Requires `im:resource` + `im:message` permissions in app settings
205
232
 
206
233
  **Other commands:**
207
234
 
package/index.js CHANGED
@@ -598,10 +598,15 @@ if (isDaemon) {
598
598
 
599
599
  // --- Telegram Setup ---
600
600
  console.log("โ”โ”โ” ๐Ÿ“ฑ Telegram Setup โ”โ”โ”");
601
- console.log("Steps:");
602
- console.log(" 1. Open Telegram, search @BotFather");
603
- console.log(" 2. Send /newbot, follow prompts to create a bot");
604
- console.log(" 3. Copy the bot token (looks like: 123456:ABC-DEF...)");
601
+ console.log("");
602
+ console.log("Step 1: Create a Bot");
603
+ console.log(" โ€ข Open Telegram app on your phone or desktop");
604
+ console.log(" โ€ข Search for @BotFather (official Telegram bot)");
605
+ console.log(" โ€ข Send /newbot command");
606
+ console.log(" โ€ข Enter a display name (e.g., 'My MetaMe Bot')");
607
+ console.log(" โ€ข Enter a username (must end in 'bot', e.g., 'my_metame_bot')");
608
+ console.log(" โ€ข BotFather will reply with your bot token");
609
+ console.log(" (looks like: 123456789:ABCdefGHI-jklMNOpqrSTUvwxYZ)");
605
610
  console.log("");
606
611
 
607
612
  const tgToken = (await ask("Paste your Telegram bot token (Enter to skip): ")).trim();
@@ -652,17 +657,38 @@ if (isDaemon) {
652
657
 
653
658
  // --- Feishu Setup ---
654
659
  console.log("โ”โ”โ” ๐Ÿ“˜ Feishu (Lark) Setup โ”โ”โ”");
655
- console.log("Steps:");
656
- console.log(" 1. Go to: https://open.feishu.cn/app");
657
- console.log(" โ†’ Create App (ไผไธš่‡ชๅปบๅบ”็”จ)");
658
- console.log(" 2. In 'Credentials' (ๅ‡ญ่ฏไธŽๅŸบ็ก€ไฟกๆฏ), copy App ID & App Secret");
659
- console.log(" 3. In 'Bot' (ๆœบๅ™จไบบ), enable bot capability");
660
- console.log(" 4. In 'Event Subscription' (ไบ‹ไปถ่ฎข้˜…):");
661
- console.log(" โ†’ Set mode to 'Long Connection' (ไฝฟ็”จ้•ฟ่ฟžๆŽฅๆŽฅๆ”ถไบ‹ไปถ)");
662
- console.log(" โ†’ Add event: im.message.receive_v1 (ๆŽฅๆ”ถๆถˆๆฏ)");
663
- console.log(" 5. In 'Permissions' (ๆƒ้™็ฎก็†), add:");
664
- console.log(" โ†’ im:message, im:message:send_as_bot, im:chat");
665
- console.log(" 6. Publish the app version (ๅˆ›ๅปบ็‰ˆๆœฌ โ†’ ็”ณ่ฏทๅ‘ๅธƒ)");
660
+ console.log("");
661
+ console.log("Step 1: Create an App");
662
+ console.log(" โ€ข Go to: https://open.feishu.cn/app");
663
+ console.log(" โ€ข Click 'ๅˆ›ๅปบไผไธš่‡ชๅปบๅบ”็”จ' (Create Enterprise App)");
664
+ console.log(" โ€ข Fill in app name and description");
665
+ console.log("");
666
+ console.log("Step 2: Get Credentials");
667
+ console.log(" โ€ข In left sidebar โ†’ 'ๅ‡ญ่ฏไธŽๅŸบ็ก€ไฟกๆฏ' (Credentials)");
668
+ console.log(" โ€ข Copy App ID and App Secret");
669
+ console.log("");
670
+ console.log("Step 3: Enable Bot");
671
+ console.log(" โ€ข In left sidebar โ†’ 'ๅบ”็”จ่ƒฝๅŠ›' โ†’ 'ๆœบๅ™จไบบ' (Bot)");
672
+ console.log(" โ€ข Enable the bot capability");
673
+ console.log("");
674
+ console.log("Step 4: Configure Events");
675
+ console.log(" โ€ข In left sidebar โ†’ 'ไบ‹ไปถ่ฎข้˜…' (Event Subscription)");
676
+ console.log(" โ€ข Choose 'ไฝฟ็”จ้•ฟ่ฟžๆŽฅๆŽฅๆ”ถไบ‹ไปถ' (Long Connection mode) โ€” important!");
677
+ console.log(" โ€ข Add event: im.message.receive_v1 (ๆŽฅๆ”ถๆถˆๆฏ)");
678
+ console.log("");
679
+ console.log("Step 5: Add Permissions");
680
+ console.log(" โ€ข In left sidebar โ†’ 'ๆƒ้™็ฎก็†' (Permissions)");
681
+ console.log(" โ€ข Search and enable these 5 permissions:");
682
+ console.log(" โ†’ im:message (่Žทๅ–ไธŽๅ‘้€ๅ•่Šใ€็พค็ป„ๆถˆๆฏ)");
683
+ console.log(" โ†’ im:message.p2p_msg:readonly (่ฏปๅ–็”จๆˆทๅ‘็ป™ๆœบๅ™จไบบ็š„ๅ•่Šๆถˆๆฏ)");
684
+ console.log(" โ†’ im:message.group_at_msg:readonly (ๆŽฅๆ”ถ็พค่Šไธญ@ๆœบๅ™จไบบๆถˆๆฏไบ‹ไปถ)");
685
+ console.log(" โ†’ im:message:send_as_bot (ไปฅๅบ”็”จ็š„่บซไปฝๅ‘ๆถˆๆฏ)");
686
+ console.log(" โ†’ im:resource (ๆ–‡ไปถไธŠไผ ไธ‹่ฝฝ - for file transfer)");
687
+ console.log("");
688
+ console.log("Step 6: Publish");
689
+ console.log(" โ€ข In left sidebar โ†’ '็‰ˆๆœฌ็ฎก็†ไธŽๅ‘ๅธƒ' (Version Management)");
690
+ console.log(" โ€ข Click 'ๅˆ›ๅปบ็‰ˆๆœฌ' โ†’ fill version (e.g., 1.0.0)");
691
+ console.log(" โ€ข Click '็”ณ่ฏทๅ‘ๅธƒ' (Apply for Release)");
666
692
  console.log("");
667
693
 
668
694
  const feishuAppId = (await ask("Paste your Feishu App ID (Enter to skip): ")).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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": {
@@ -12,8 +12,9 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "start": "node index.js",
15
- "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/daemon.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml plugin/scripts/ && echo 'โœ… Plugin scripts synced'",
16
- "precommit": "npm run sync:plugin"
15
+ "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml plugin/scripts/ && echo 'โœ… Plugin scripts synced'",
16
+ "restart:daemon": "node index.js daemon stop 2>/dev/null; sleep 1; node index.js daemon start 2>/dev/null || echo 'โš ๏ธ Daemon not running or restart failed'",
17
+ "precommit": "npm run sync:plugin && npm run restart:daemon"
17
18
  },
18
19
  "keywords": [
19
20
  "claude",
package/scripts/daemon.js CHANGED
@@ -492,6 +492,36 @@ async function startTelegramBridge(config, executeTaskByName) {
492
492
  continue;
493
493
  }
494
494
 
495
+ // File/document message โ†’ download and pass to Claude
496
+ if (msg.document || msg.photo) {
497
+ const fileId = msg.document ? msg.document.file_id : msg.photo[msg.photo.length - 1].file_id;
498
+ const fileName = msg.document ? msg.document.file_name : `photo_${Date.now()}.jpg`;
499
+ const caption = msg.caption || '';
500
+
501
+ // Save to project's upload/ folder
502
+ const session = getSession(chatId);
503
+ const cwd = session?.cwd || HOME;
504
+ const uploadDir = path.join(cwd, 'upload');
505
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
506
+ const destPath = path.join(uploadDir, fileName);
507
+
508
+ try {
509
+ await bot.downloadFile(fileId, destPath);
510
+ await bot.sendMessage(chatId, `๐Ÿ“ฅ Saved: ${fileName}`);
511
+
512
+ // Build prompt - don't ask Claude to read large files automatically
513
+ const prompt = caption
514
+ ? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
515
+ : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
516
+
517
+ await handleCommand(bot, chatId, prompt, config, executeTaskByName);
518
+ } catch (err) {
519
+ log('ERROR', `File download failed: ${err.message}`);
520
+ await bot.sendMessage(chatId, `โŒ Download failed: ${err.message}`);
521
+ }
522
+ continue;
523
+ }
524
+
495
525
  // Text message (commands or natural language)
496
526
  if (msg.text) {
497
527
  await handleCommand(bot, chatId, msg.text.trim(), config, executeTaskByName);
@@ -639,22 +669,6 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
639
669
  return;
640
670
  }
641
671
 
642
- if (text === '/continue') {
643
- // Continue the most recent conversation in current workdir
644
- const session = getSession(chatId);
645
- const cwd = session ? session.cwd : HOME;
646
- const state2 = loadState();
647
- state2.sessions[chatId] = {
648
- id: '__continue__',
649
- cwd,
650
- created: new Date().toISOString(),
651
- started: true,
652
- };
653
- saveState(state2);
654
- await bot.sendMessage(chatId, `Resuming last conversation in ${cwd}`);
655
- return;
656
- }
657
-
658
672
  // /file <shortId> โ€” send cached file (from button callback)
659
673
  if (text.startsWith('/file ')) {
660
674
  const shortId = text.slice(6).trim();
@@ -802,11 +816,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
802
816
  await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
803
817
  return;
804
818
  }
805
- // /cd last โ€” jump to the most recent session's directory globally
819
+ // /cd last โ€” sync to computer: switch to most recent session AND its directory
806
820
  if (newCwd === 'last') {
807
- const recent = listRecentSessions(1);
808
- if (recent.length > 0 && recent[0].projectPath) {
809
- newCwd = recent[0].projectPath;
821
+ const currentSession = getSession(chatId);
822
+ const excludeId = currentSession?.id;
823
+ const recent = listRecentSessions(10);
824
+ const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
825
+ if (filtered.length > 0 && filtered[0].projectPath) {
826
+ const target = filtered[0];
827
+ // Switch to that session (like /resume) AND its directory
828
+ const state2 = loadState();
829
+ state2.sessions[chatId] = {
830
+ id: target.sessionId,
831
+ cwd: target.projectPath,
832
+ started: true,
833
+ };
834
+ saveState(state2);
835
+ const name = target.customTitle || target.summary || '';
836
+ const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
837
+ await bot.sendMessage(chatId, `๐Ÿ”„ Synced to: ${label}\n๐Ÿ“ ${path.basename(target.projectPath)}`);
838
+ return;
810
839
  } else {
811
840
  await bot.sendMessage(chatId, 'No recent session found.');
812
841
  return;
@@ -817,13 +846,27 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
817
846
  return;
818
847
  }
819
848
  const state2 = loadState();
820
- if (!state2.sessions[chatId]) {
849
+ // Try to find existing session in this directory
850
+ const recentInDir = listRecentSessions(1, newCwd);
851
+ if (recentInDir.length > 0 && recentInDir[0].sessionId) {
852
+ // Attach to existing session in this directory
853
+ const target = recentInDir[0];
854
+ state2.sessions[chatId] = {
855
+ id: target.sessionId,
856
+ cwd: newCwd,
857
+ started: true,
858
+ };
859
+ saveState(state2);
860
+ const label = target.customTitle || target.summary?.slice(0, 30) || target.sessionId.slice(0, 8);
861
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)}\n๐Ÿ”„ Attached: ${label}`);
862
+ } else if (!state2.sessions[chatId]) {
821
863
  createSession(chatId, newCwd);
864
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)} (new session)`);
822
865
  } else {
823
866
  state2.sessions[chatId].cwd = newCwd;
824
867
  saveState(state2);
868
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)}`);
825
869
  }
826
- await bot.sendMessage(chatId, `Workdir: ${newCwd}`);
827
870
  return;
828
871
  }
829
872
 
@@ -947,7 +990,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
947
990
  '/cd <path> โ€” ๅˆ‡ๆขๅทฅไฝœ็›ฎๅฝ•',
948
991
  '/session โ€” ๆŸฅ็œ‹ๅฝ“ๅ‰ไผš่ฏ',
949
992
  '',
950
- 'โš™๏ธ /status /tasks /budget /reload',
993
+ 'โš™๏ธ /status /tasks /run <name> /budget /reload',
951
994
  '',
952
995
  '็›ดๆŽฅๆ‰“ๅญ—ๅณๅฏๅฏน่ฏ ๐Ÿ’ฌ',
953
996
  ].join('\n'));
@@ -1518,7 +1561,22 @@ async function askClaude(bot, chatId, prompt) {
1518
1561
 
1519
1562
  let session = getSession(chatId);
1520
1563
  if (!session) {
1521
- session = createSession(chatId);
1564
+ // Auto-attach to most recent Claude session (unified session management)
1565
+ const recent = listRecentSessions(1);
1566
+ if (recent.length > 0 && recent[0].sessionId && recent[0].projectPath) {
1567
+ const target = recent[0];
1568
+ const state = loadState();
1569
+ state.sessions[chatId] = {
1570
+ id: target.sessionId,
1571
+ cwd: target.projectPath,
1572
+ started: true, // Already has history
1573
+ };
1574
+ saveState(state);
1575
+ session = state.sessions[chatId];
1576
+ log('INFO', `Auto-attached ${chatId} to recent session: ${target.sessionId.slice(0, 8)} (${path.basename(target.projectPath)})`);
1577
+ } else {
1578
+ session = createSession(chatId);
1579
+ }
1522
1580
  }
1523
1581
 
1524
1582
  // Build claude command
@@ -1657,15 +1715,45 @@ async function startFeishuBridge(config, executeTaskByName) {
1657
1715
  const allowedIds = config.feishu.allowed_chat_ids || [];
1658
1716
 
1659
1717
  try {
1660
- const receiver = await bot.startReceiving((chatId, text, event) => {
1718
+ const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo) => {
1661
1719
  // Security: check whitelist (empty = allow all)
1662
1720
  if (allowedIds.length > 0 && !allowedIds.includes(chatId)) {
1663
1721
  log('WARN', `Feishu: rejected message from ${chatId}`);
1664
1722
  return;
1665
1723
  }
1666
1724
 
1667
- log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
1668
- handleCommand(bot, chatId, text, config, executeTaskByName);
1725
+ // Handle file message
1726
+ if (fileInfo && fileInfo.fileKey) {
1727
+ log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
1728
+ // Save to project's upload/ folder
1729
+ const session = getSession(chatId);
1730
+ const cwd = session?.cwd || HOME;
1731
+ const uploadDir = path.join(cwd, 'upload');
1732
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
1733
+ const destPath = path.join(uploadDir, fileInfo.fileName);
1734
+
1735
+ try {
1736
+ await bot.downloadFile(fileInfo.messageId, fileInfo.fileKey, destPath, fileInfo.msgType);
1737
+ await bot.sendMessage(chatId, `๐Ÿ“ฅ Saved: ${fileInfo.fileName}`);
1738
+
1739
+ // Build prompt - don't ask Claude to read large files automatically
1740
+ const prompt = text
1741
+ ? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
1742
+ : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
1743
+
1744
+ handleCommand(bot, chatId, prompt, config, executeTaskByName);
1745
+ } catch (err) {
1746
+ log('ERROR', `Feishu file download failed: ${err.message}`);
1747
+ await bot.sendMessage(chatId, `โŒ Download failed: ${err.message}`);
1748
+ }
1749
+ return;
1750
+ }
1751
+
1752
+ // Handle text message
1753
+ if (text) {
1754
+ log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
1755
+ handleCommand(bot, chatId, text, config, executeTaskByName);
1756
+ }
1669
1757
  });
1670
1758
 
1671
1759
  log('INFO', 'Feishu bot connected (WebSocket long connection)');
@@ -1800,6 +1888,27 @@ async function main() {
1800
1888
  }, 1000);
1801
1889
  });
1802
1890
 
1891
+ // Auto-restart: watch daemon.js for code changes (hot restart)
1892
+ const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
1893
+ let _restartDebounce = null;
1894
+ fs.watchFile(DAEMON_SCRIPT, { interval: 3000 }, (curr, prev) => {
1895
+ if (curr.mtimeMs === prev.mtimeMs) return;
1896
+ if (_restartDebounce) clearTimeout(_restartDebounce);
1897
+ _restartDebounce = setTimeout(async () => {
1898
+ log('INFO', 'daemon.js changed on disk โ€” auto-restarting...');
1899
+ await notifyFn('๐Ÿ”„ Code updated, daemon restarting...').catch(() => {});
1900
+ // Spawn new daemon process, then exit
1901
+ const { spawn } = require('child_process');
1902
+ const newDaemon = spawn(process.execPath, [DAEMON_SCRIPT], {
1903
+ detached: true,
1904
+ stdio: 'ignore',
1905
+ env: { ...process.env, METAME_ROOT: process.env.METAME_ROOT || path.dirname(__dirname) },
1906
+ });
1907
+ newDaemon.unref();
1908
+ setTimeout(() => process.exit(0), 500);
1909
+ }, 2000);
1910
+ });
1911
+
1803
1912
  // Start bridges (both can run simultaneously)
1804
1913
  telegramBridge = await startTelegramBridge(config, executeTaskByName);
1805
1914
  feishuBridge = await startFeishuBridge(config, executeTaskByName);
@@ -1808,6 +1917,7 @@ async function main() {
1808
1917
  const shutdown = () => {
1809
1918
  log('INFO', 'Daemon shutting down...');
1810
1919
  fs.unwatchFile(CONFIG_FILE);
1920
+ fs.unwatchFile(DAEMON_SCRIPT);
1811
1921
  if (heartbeatTimer) clearInterval(heartbeatTimer);
1812
1922
  if (telegramBridge) telegramBridge.stop();
1813
1923
  if (feishuBridge) feishuBridge.stop();
@@ -111,6 +111,55 @@ function createBot(config) {
111
111
  return { app_id, app_name: 'MetaMe' };
112
112
  },
113
113
 
114
+ /**
115
+ * Download a file from Feishu to local disk
116
+ * @param {string} messageId - Message ID containing the file
117
+ * @param {string} fileKey - File key from message content
118
+ * @param {string} destPath - Local destination path
119
+ * @returns {Promise<string>} The destination path
120
+ */
121
+ async downloadFile(messageId, fileKey, destPath, msgType = 'file') {
122
+ try {
123
+ let res;
124
+ if (msgType === 'image') {
125
+ // Images use im.image.get API
126
+ res = await client.im.image.get({
127
+ path: { image_key: fileKey },
128
+ });
129
+ } else {
130
+ // Files and media use im.messageResource.get API
131
+ res = await client.im.messageResource.get({
132
+ path: { message_id: messageId, file_key: fileKey },
133
+ params: { type: 'file' },
134
+ });
135
+ }
136
+
137
+ // SDK returns writeFile method or getReadableStream
138
+ if (res && res.writeFile) {
139
+ await res.writeFile(destPath);
140
+ return destPath;
141
+ } else if (res && res.getReadableStream) {
142
+ const stream = res.getReadableStream();
143
+ const fileStream = fs.createWriteStream(destPath);
144
+ return new Promise((resolve, reject) => {
145
+ stream.pipe(fileStream);
146
+ fileStream.on('finish', () => {
147
+ fileStream.close();
148
+ resolve(destPath);
149
+ });
150
+ fileStream.on('error', (err) => {
151
+ fs.unlink(destPath, () => {});
152
+ reject(err);
153
+ });
154
+ });
155
+ }
156
+ throw new Error('No writeFile or stream in response');
157
+ } catch (err) {
158
+ const detail = err.message || String(err);
159
+ throw new Error(detail);
160
+ }
161
+ },
162
+
114
163
  /**
115
164
  * Send a file/document
116
165
  * @param {string} chatId
@@ -228,6 +277,7 @@ function createBot(config) {
228
277
 
229
278
  const chatId = msg.chat_id;
230
279
  let text = '';
280
+ let fileInfo = null;
231
281
 
232
282
  if (msg.message_type === 'text') {
233
283
  try {
@@ -236,14 +286,25 @@ function createBot(config) {
236
286
  } catch {
237
287
  text = msg.content || '';
238
288
  }
289
+ } else if (msg.message_type === 'file' || msg.message_type === 'image' || msg.message_type === 'media') {
290
+ // File, image or media (video) message
291
+ try {
292
+ const content = JSON.parse(msg.content);
293
+ fileInfo = {
294
+ messageId: msg.message_id,
295
+ fileKey: content.file_key || content.image_key,
296
+ fileName: content.file_name || content.image_key || `file_${Date.now()}`,
297
+ msgType: msg.message_type, // 'file', 'image', or 'media'
298
+ };
299
+ } catch {}
239
300
  }
240
301
 
241
302
  // Strip @mention prefix if present
242
303
  text = text.replace(/@_user_\d+\s*/g, '').trim();
243
304
 
244
- if (text) {
305
+ if (text || fileInfo) {
245
306
  // Fire-and-forget: don't block the event loop (SDK needs fast ack)
246
- Promise.resolve().then(() => onMessage(chatId, text, data)).catch(() => {});
307
+ Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo)).catch(() => {});
247
308
  }
248
309
  } catch (e) {
249
310
  // Non-fatal
@@ -171,6 +171,46 @@ function createBot(token) {
171
171
  });
172
172
  },
173
173
 
174
+ /**
175
+ * Download a file from Telegram to local disk
176
+ * @param {string} fileId - Telegram file_id
177
+ * @param {string} destPath - Local destination path
178
+ * @returns {Promise<string>} The destination path
179
+ */
180
+ async downloadFile(fileId, destPath) {
181
+ // 1. Get file path from Telegram
182
+ const fileInfo = await apiRequest(token, 'getFile', { file_id: fileId });
183
+ if (!fileInfo.file_path) {
184
+ throw new Error('Failed to get file path from Telegram');
185
+ }
186
+
187
+ // 2. Download file using stream (zero memory overhead)
188
+ const fileUrl = `${API_BASE}/file/bot${token}/${fileInfo.file_path}`;
189
+ return new Promise((resolve, reject) => {
190
+ const urlObj = new URL(fileUrl);
191
+ https.get({
192
+ hostname: urlObj.hostname,
193
+ path: urlObj.pathname,
194
+ timeout: 60000,
195
+ }, (res) => {
196
+ if (res.statusCode !== 200) {
197
+ reject(new Error(`Download failed: ${res.statusCode}`));
198
+ return;
199
+ }
200
+ const fileStream = fs.createWriteStream(destPath);
201
+ res.pipe(fileStream);
202
+ fileStream.on('finish', () => {
203
+ fileStream.close();
204
+ resolve(destPath);
205
+ });
206
+ fileStream.on('error', (err) => {
207
+ fs.unlink(destPath, () => {});
208
+ reject(err);
209
+ });
210
+ }).on('error', reject);
211
+ });
212
+ },
213
+
174
214
  /**
175
215
  * Send a file/document
176
216
  * @param {number|string} chatId - Target chat ID