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 +45 -18
- package/index.js +41 -15
- package/package.json +4 -3
- package/scripts/daemon.js +137 -27
- package/scripts/feishu-adapter.js +63 -2
- package/scripts/telegram-adapter.js +40 -0
package/README.md
CHANGED
|
@@ -6,20 +6,32 @@
|
|
|
6
6
|
|
|
7
7
|
> **The Cognitive Profile Layer for Claude Code.**
|
|
8
8
|
>
|
|
9
|
-
> *
|
|
9
|
+
> *Knows how you think. Works wherever you are.*
|
|
10
10
|
|
|
11
11
|
## ๐ Introduction
|
|
12
12
|
|
|
13
|
-
**Claude Code** is
|
|
13
|
+
**Claude Code** is powerful, but it has two pain points:
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
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"/"
|
|
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
|
-
|
|
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
|
-
|
|
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"/"
|
|
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** โ
|
|
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
|
|
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:
|
|
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`
|
|
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("
|
|
602
|
-
console.log("
|
|
603
|
-
console.log("
|
|
604
|
-
console.log("
|
|
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("
|
|
656
|
-
console.log("
|
|
657
|
-
console.log("
|
|
658
|
-
console.log("
|
|
659
|
-
console.log("
|
|
660
|
-
console.log("
|
|
661
|
-
console.log("
|
|
662
|
-
console.log("
|
|
663
|
-
console.log("
|
|
664
|
-
console.log("
|
|
665
|
-
console.log("
|
|
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.
|
|
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
|
-
"
|
|
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 โ
|
|
819
|
+
// /cd last โ sync to computer: switch to most recent session AND its directory
|
|
806
820
|
if (newCwd === 'last') {
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1668
|
-
|
|
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
|