metame-cli 1.3.13 โ 1.3.16
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 +58 -2
- package/index.js +9 -1
- package/package.json +2 -2
- package/scripts/daemon.js +330 -40
- package/scripts/feishu-adapter.js +8 -13
package/README.md
CHANGED
|
@@ -47,10 +47,13 @@
|
|
|
47
47
|
* **๐ช 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.
|
|
48
48
|
* **๐ฑ 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.
|
|
49
49
|
* **๐ Workflow Engine (v1.3):** Define multi-step skill chains as heartbeat tasks. Each workflow runs in a single Claude Code session via `--resume`, so step outputs flow as context to the next step. Example: `deep-research` โ `tech-writing` โ `wechat-publisher` โ fully automated content pipeline.
|
|
50
|
-
* **โน Full Terminal Control from Mobile (v1.3.
|
|
51
|
-
* **๐ฅ Emergency Recovery (v1.3.13):** `/doctor` interactive diagnostics with one-tap fix buttons, `/sh` direct shell access from your phone (bypasses Claude entirely โ the lifeline when everything else is broken), automatic config backup before any setting change, `/fix` to restore last known good config.
|
|
50
|
+
* **โน Full Terminal Control from Mobile (v1.3.10):** `/stop` (ESC), `/undo` (ESCร2) with native file-history restoration, concurrent task protection, daemon auto-restart, and `metame continue` for seamless mobile-to-desktop sync.
|
|
52
51
|
* **๐ฏ Goal Alignment & Drift Detection (v1.3.11):** MetaMe now tracks whether your sessions align with your declared goals. Each distill assesses `goal_alignment` (aligned/partial/drifted) at zero extra API cost. When you drift for 2+ consecutive sessions, a mirror observation is injected passively; after 3+ sessions, a reflection prompt gently asks: "Was this an intentional pivot, or did you lose track?" Session logs now record project, branch, intent, and file directories for richer retrospective analysis. Pattern detection can spot sustained drift trends across your session history.
|
|
52
|
+
* **๐ Provider Relay (v1.3.11):** Use any Anthropic-compatible API relay as your backend โ no file mutation, no invasion. MetaMe injects `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` at spawn time. Separate provider roles for `active`, `distill`, and `daemon` tasks. CLI: `metame provider add/use/remove/test`. Config stored in `~/.metame/providers.yaml`.
|
|
53
53
|
* **๐ Session History Bootstrap (v1.3.12):** Solves the cold-start problem โ MetaMe previously needed 5-7 sessions before producing any visible feedback. Now, on first launch it auto-bootstraps your session history from existing Claude Code JSONL transcripts (zero API cost). Three complementary data layers: **Skeleton** (structural facts extracted locally โ tools, duration, project, branch, intent), **Facets** (interaction quality from `/insights` โ outcome, friction, satisfaction, when available), and **Haiku** (metacognitive judgments โ cognitive load, zones, goal alignment, from the existing distill call). Patterns and mirror observations can appear from your very first MetaMe session.
|
|
54
|
+
* **๐ฅ Emergency Recovery (v1.3.13):** `/doctor` interactive diagnostics with one-tap fix buttons, `/sh` direct shell access from your phone (bypasses Claude entirely โ the lifeline when everything else is broken), automatic config backup before any setting change, `/fix` to restore last known good config. `/model` interactive model switcher with auto-backup.
|
|
55
|
+
* **๐ Browser Automation (v1.3.15):** Native Playwright MCP integration โ auto-registered on first run. Every MetaMe user gets browser control capability out of the box. Combined with Skills, enables workflows like automated podcast publishing, form filling, and web scraping.
|
|
56
|
+
* **๐ Interactive File Browser (v1.3.15):** `/list` shows clickable button cards โ folders expand inline, files download on tap. Folder buttons survive daemon restarts (absolute paths, no expiry). Zero token cost.
|
|
54
57
|
|
|
55
58
|
## ๐ Prerequisites
|
|
56
59
|
|
|
@@ -263,6 +266,9 @@ This resumes the latest session with all mobile messages included. Also works as
|
|
|
263
266
|
๐ Read: ใconfig.yamlใ
|
|
264
267
|
โ๏ธ Edit: ใdaemon.jsใ
|
|
265
268
|
๐ป Bash: ใgit statusใ
|
|
269
|
+
๐ง Skill: ใwechat-publisherใ
|
|
270
|
+
๐ Browser: ใnavigateใ
|
|
271
|
+
๐ MCP:server: ใactionใ
|
|
266
272
|
```
|
|
267
273
|
|
|
268
274
|
**File transfer (v1.3.8):** Seamlessly move files between your phone and computer.
|
|
@@ -325,6 +331,7 @@ Bot: ๅ้ๅฐๅชไธ่ฝฎ๏ผ
|
|
|
325
331
|
| `/tasks` | List scheduled heartbeat tasks |
|
|
326
332
|
| `/run <name>` | Run a task immediately |
|
|
327
333
|
| `/model [name]` | Interactive model switcher with buttons (sonnet, opus, haiku). Auto-backs up config before switching. |
|
|
334
|
+
| `/list` | File browser with clickable buttons โ folders expand, files download. Zero tokens. |
|
|
328
335
|
| `/budget` | Today's token usage |
|
|
329
336
|
| `/quiet` | Silence mirror/reflections for 48h |
|
|
330
337
|
| `/reload` | Manually reload daemon.yaml (also auto-reloads on file change) |
|
|
@@ -386,6 +393,41 @@ Each step runs in the same Claude Code session. Step outputs automatically becom
|
|
|
386
393
|
* `~/.metame/` directory set to mode 700
|
|
387
394
|
* Bot tokens stored locally, never transmitted
|
|
388
395
|
|
|
396
|
+
### Provider Relay โ Third-Party Model Support (v1.3.11)
|
|
397
|
+
|
|
398
|
+
MetaMe supports any Anthropic-compatible API relay as a backend. This means you can route Claude Code through a third-party relay that maps `sonnet`/`opus`/`haiku` to any model (GPT-4, DeepSeek, Gemini, etc.) โ MetaMe passes standard model names and the relay handles translation.
|
|
399
|
+
|
|
400
|
+
**How it works:** At spawn time, MetaMe injects `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` environment variables. Zero file mutation โ `~/.claude/settings.json` is never touched.
|
|
401
|
+
|
|
402
|
+
**CLI commands:**
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
metame provider # List all providers
|
|
406
|
+
metame provider add <name> # Add a relay (prompts for URL & key)
|
|
407
|
+
metame provider use <name> # Switch active provider
|
|
408
|
+
metame provider remove <name> # Remove a provider (can't remove 'anthropic')
|
|
409
|
+
metame provider test [name] # Test connectivity
|
|
410
|
+
metame provider set-role distill <name> # Use a different provider for background distill
|
|
411
|
+
metame provider set-role daemon <name> # Use a different provider for daemon tasks
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Configuration** (`~/.metame/providers.yaml`):
|
|
415
|
+
|
|
416
|
+
```yaml
|
|
417
|
+
active: 'anthropic'
|
|
418
|
+
providers:
|
|
419
|
+
anthropic:
|
|
420
|
+
label: 'Anthropic (Official)'
|
|
421
|
+
my-relay:
|
|
422
|
+
label: 'My Relay'
|
|
423
|
+
base_url: 'https://api.relay.example.com/v1'
|
|
424
|
+
api_key: 'sk-xxx'
|
|
425
|
+
distill_provider: null # null = use active
|
|
426
|
+
daemon_provider: null # null = use active
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Three independent provider roles let you optimize cost: e.g., use an official Anthropic key for active work, a cheaper relay for background distill, and another for daemon heartbeat tasks.
|
|
430
|
+
|
|
389
431
|
### Hot Reload (Refresh)
|
|
390
432
|
|
|
391
433
|
If you update your profile or need to fix a broken context **without restarting your session**:
|
|
@@ -544,6 +586,20 @@ A: No. It *prepends* its meta-cognitive protocol to your existing `CLAUDE.md`. Y
|
|
|
544
586
|
**Q: Is my data sent to a third party?**
|
|
545
587
|
A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passes text to the official Claude Code tool.
|
|
546
588
|
|
|
589
|
+
## ๐ Changelog
|
|
590
|
+
|
|
591
|
+
| Version | Highlights |
|
|
592
|
+
|---------|------------|
|
|
593
|
+
| **v1.3.15** | Native Playwright MCP (browser automation for all users), `/list` interactive file browser with buttons, Feishu image download fix, Skill/MCP/Agent status push, hot restart reliability (single notification, no double instance) |
|
|
594
|
+
| **v1.3.14** | Fix daemon crash on fresh install (missing bundled scripts) |
|
|
595
|
+
| **v1.3.13** | `/doctor` diagnostics, `/sh` direct shell, `/fix` config restore, `/model` interactive switcher with auto-backup, daemon state caching & config backup/restore |
|
|
596
|
+
| **v1.3.12** | Session history bootstrap (cold-start fix), three-layer data architecture (Skeleton + Facets + Haiku), session summary extraction |
|
|
597
|
+
| **v1.3.11** | Goal alignment & drift detection, provider relay system for third-party models, `/insights` facet integration |
|
|
598
|
+
| **v1.3.10** | `/stop`, `/undo` with file restoration, `/model`, concurrent task protection, `metame continue`, daemon auto-restart on code change |
|
|
599
|
+
| **v1.3.8** | Bidirectional file transfer (phone โ computer) |
|
|
600
|
+
| **v1.3.7** | Real-time streaming status on mobile |
|
|
601
|
+
| **v1.3** | Metacognition layer, remote Claude Code (Telegram & Feishu), workflow engine, heartbeat tasks, launchd auto-start |
|
|
602
|
+
|
|
547
603
|
## ๐ License
|
|
548
604
|
|
|
549
605
|
MIT License. Feel free to fork, modify, and evolve your own Meta-Cognition.
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const BRAIN_FILE = path.join(HOME_DIR, '.claude_profile.yaml');
|
|
|
13
13
|
const PROJECT_FILE = path.join(process.cwd(), 'CLAUDE.md');
|
|
14
14
|
const METAME_DIR = path.join(HOME_DIR, '.metame');
|
|
15
15
|
const CLAUDE_SETTINGS = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
16
|
+
const CLAUDE_MCP_CONFIG = path.join(HOME_DIR, '.claude', 'mcp.json'); // legacy, kept for reference
|
|
16
17
|
const SIGNAL_CAPTURE_SCRIPT = path.join(METAME_DIR, 'signal-capture.js');
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------
|
|
@@ -23,7 +24,7 @@ if (!fs.existsSync(METAME_DIR)) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
26
|
-
const BUNDLED_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js'];
|
|
27
|
+
const BUNDLED_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js'];
|
|
27
28
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
28
29
|
|
|
29
30
|
for (const script of BUNDLED_SCRIPTS) {
|
|
@@ -87,6 +88,13 @@ function ensureHookInstalled() {
|
|
|
87
88
|
|
|
88
89
|
ensureHookInstalled();
|
|
89
90
|
|
|
91
|
+
// ---------------------------------------------------------
|
|
92
|
+
// 1.6b ENSURE PROJECT-LEVEL MCP CONFIG
|
|
93
|
+
// ---------------------------------------------------------
|
|
94
|
+
// MCP servers are registered per-project via .mcp.json (not user-scope ~/.claude.json)
|
|
95
|
+
// so they only load when working in projects that need them.
|
|
96
|
+
// The daemon's heartbeat tasks use cwd: ~/AGI/Digital_Me which has its own .mcp.json.
|
|
97
|
+
|
|
90
98
|
// ---------------------------------------------------------
|
|
91
99
|
// 1.7 PASSIVE DISTILLATION (Background, post-launch)
|
|
92
100
|
// ---------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.16",
|
|
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": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node --test scripts/*.test.js",
|
|
15
15
|
"start": "node index.js",
|
|
16
|
-
"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 scripts/providers.js scripts/utils.js plugin/scripts/ && echo 'โ
Plugin scripts synced'",
|
|
16
|
+
"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 scripts/providers.js scripts/utils.js scripts/resolve-yaml.js plugin/scripts/ && echo 'โ
Plugin scripts synced'",
|
|
17
17
|
"restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo 'โ ๏ธ Daemon not running or restart failed'",
|
|
18
18
|
"precommit": "npm run sync:plugin && npm run restart:daemon"
|
|
19
19
|
},
|
package/scripts/daemon.js
CHANGED
|
@@ -640,6 +640,106 @@ async function sendBrowse(bot, chatId, mode, dirPath) {
|
|
|
640
640
|
}
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
const DIR_LIST_TYPE_EMOJI = {
|
|
644
|
+
'.md': '๐', '.txt': '๐', '.pdf': '๐',
|
|
645
|
+
'.js': 'โ๏ธ', '.ts': 'โ๏ธ', '.py': '๐', '.json': '๐', '.yaml': '๐', '.yml': '๐',
|
|
646
|
+
'.png': '๐ผ๏ธ', '.jpg': '๐ผ๏ธ', '.jpeg': '๐ผ๏ธ', '.gif': '๐ผ๏ธ', '.svg': '๐ผ๏ธ', '.webp': '๐ผ๏ธ',
|
|
647
|
+
'.wav': '๐ต', '.mp3': '๐ต', '.m4a': '๐ต', '.flac': '๐ต',
|
|
648
|
+
'.mp4': '๐ฌ', '.mov': '๐ฌ',
|
|
649
|
+
'.csv': '๐', '.xlsx': '๐',
|
|
650
|
+
'.html': '๐', '.css': '๐จ',
|
|
651
|
+
'.sh': '๐ป', '.bash': '๐ป',
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* List directory contents with file info + download buttons + folder nav buttons.
|
|
656
|
+
* Zero token cost โ pure daemon fs operation.
|
|
657
|
+
*/
|
|
658
|
+
async function sendDirListing(bot, chatId, baseDir, arg) {
|
|
659
|
+
let targetDir = baseDir;
|
|
660
|
+
let globFilter = null;
|
|
661
|
+
|
|
662
|
+
if (arg) {
|
|
663
|
+
if (arg.includes('*')) {
|
|
664
|
+
globFilter = arg;
|
|
665
|
+
} else {
|
|
666
|
+
const sub = path.resolve(baseDir, arg);
|
|
667
|
+
if (fs.existsSync(sub) && fs.statSync(sub).isDirectory()) {
|
|
668
|
+
targetDir = sub;
|
|
669
|
+
} else {
|
|
670
|
+
await bot.sendMessage(chatId, `โ Not found: ${arg}`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
let entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
678
|
+
if (globFilter) {
|
|
679
|
+
const pattern = globFilter.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
|
680
|
+
const re = new RegExp('^' + pattern + '$', 'i');
|
|
681
|
+
entries = entries.filter(e => re.test(e.name));
|
|
682
|
+
}
|
|
683
|
+
entries.sort((a, b) => {
|
|
684
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
685
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
686
|
+
return a.name.localeCompare(b.name);
|
|
687
|
+
});
|
|
688
|
+
entries = entries.filter(e => !e.name.startsWith('.'));
|
|
689
|
+
|
|
690
|
+
if (entries.length === 0) {
|
|
691
|
+
await bot.sendMessage(chatId, `๐ ${path.basename(targetDir)}/\n(empty)`);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const allButtons = [];
|
|
696
|
+
const MAX_BUTTONS = 20;
|
|
697
|
+
|
|
698
|
+
for (const entry of entries.slice(0, MAX_BUTTONS)) {
|
|
699
|
+
const fullPath = path.join(targetDir, entry.name);
|
|
700
|
+
if (entry.isDirectory()) {
|
|
701
|
+
// Use absolute path directly for folders (survives daemon restart)
|
|
702
|
+
// Fall back to shortenPath only if path is too long for callback_data (64 byte limit)
|
|
703
|
+
const cbPath = fullPath.length <= 58 ? fullPath : shortenPath(fullPath);
|
|
704
|
+
allButtons.push([{ text: `๐ ${entry.name}/`, callback_data: `/list ${cbPath}` }]);
|
|
705
|
+
} else {
|
|
706
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
707
|
+
const emoji = DIR_LIST_TYPE_EMOJI[ext] || '๐';
|
|
708
|
+
let size = '';
|
|
709
|
+
try {
|
|
710
|
+
const stat = fs.statSync(fullPath);
|
|
711
|
+
const bytes = stat.size;
|
|
712
|
+
if (bytes < 1024) size = ` ${bytes}B`;
|
|
713
|
+
else if (bytes < 1048576) size = ` ${(bytes / 1024).toFixed(0)}KB`;
|
|
714
|
+
else size = ` ${(bytes / 1048576).toFixed(1)}MB`;
|
|
715
|
+
} catch { /* ignore */ }
|
|
716
|
+
if (isContentFile(fullPath)) {
|
|
717
|
+
const shortId = cacheFile(fullPath);
|
|
718
|
+
allButtons.push([{ text: `${emoji} ${entry.name}${size}`, callback_data: `/file ${shortId}` }]);
|
|
719
|
+
} else {
|
|
720
|
+
// Non-downloadable files shown as info-only buttons (no action)
|
|
721
|
+
allButtons.push([{ text: `${emoji} ${entry.name}${size}`, callback_data: 'noop' }]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const header = `๐ ${path.basename(targetDir)}/` + (entries.length > MAX_BUTTONS ? ` (${MAX_BUTTONS}/${entries.length})` : '');
|
|
727
|
+
if (allButtons.length > 0 && bot.sendButtons) {
|
|
728
|
+
await bot.sendButtons(chatId, header, allButtons);
|
|
729
|
+
} else {
|
|
730
|
+
// Fallback for adapters without button support
|
|
731
|
+
const lines = [header];
|
|
732
|
+
for (const entry of entries.slice(0, MAX_BUTTONS)) {
|
|
733
|
+
const isDir = entry.isDirectory();
|
|
734
|
+
lines.push(isDir ? ` ๐ ${entry.name}/` : ` ๐ ${entry.name}`);
|
|
735
|
+
}
|
|
736
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
await bot.sendMessage(chatId, `โ ${e.message}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
643
743
|
/**
|
|
644
744
|
* Unified command handler โ shared by Telegram & Feishu
|
|
645
745
|
*/
|
|
@@ -653,6 +753,9 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
653
753
|
const dirPath = expandPath(parts.slice(1).join(' '));
|
|
654
754
|
if (mode && dirPath && fs.existsSync(dirPath)) {
|
|
655
755
|
await sendBrowse(bot, chatId, mode, dirPath);
|
|
756
|
+
} else if (/^p\d+$/.test(dirPath)) {
|
|
757
|
+
await bot.sendMessage(chatId, 'โ ๏ธ Button expired. Pick again:');
|
|
758
|
+
await sendDirPicker(bot, chatId, mode || 'cd', 'Switch workdir:');
|
|
656
759
|
} else {
|
|
657
760
|
await bot.sendMessage(chatId, 'Invalid browse path.');
|
|
658
761
|
}
|
|
@@ -857,6 +960,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
857
960
|
const name = target.customTitle || target.summary || '';
|
|
858
961
|
const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
|
|
859
962
|
await bot.sendMessage(chatId, `๐ Synced to: ${label}\n๐ ${path.basename(target.projectPath)}`);
|
|
963
|
+
await sendDirListing(bot, chatId, target.projectPath, null);
|
|
860
964
|
return;
|
|
861
965
|
} else {
|
|
862
966
|
await bot.sendMessage(chatId, 'No recent session found.');
|
|
@@ -864,7 +968,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
864
968
|
}
|
|
865
969
|
}
|
|
866
970
|
if (!fs.existsSync(newCwd)) {
|
|
867
|
-
|
|
971
|
+
// Likely an expired path shortcode (e.g. p16) from a daemon restart
|
|
972
|
+
if (/^p\d+$/.test(newCwd)) {
|
|
973
|
+
await bot.sendMessage(chatId, 'โ ๏ธ Button expired (daemon restarted). Pick again:');
|
|
974
|
+
await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
|
|
975
|
+
} else {
|
|
976
|
+
await bot.sendMessage(chatId, `Path not found: ${newCwd}`);
|
|
977
|
+
}
|
|
868
978
|
return;
|
|
869
979
|
}
|
|
870
980
|
const state2 = loadState();
|
|
@@ -889,6 +999,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
889
999
|
saveState(state2);
|
|
890
1000
|
await bot.sendMessage(chatId, `๐ ${path.basename(newCwd)}`);
|
|
891
1001
|
}
|
|
1002
|
+
await sendDirListing(bot, chatId, newCwd, null);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// /list [subdir|glob|fullpath] โ list files (zero token, daemon-only)
|
|
1007
|
+
if (text === '/list' || text.startsWith('/list ')) {
|
|
1008
|
+
const session = getSession(chatId);
|
|
1009
|
+
const cwd = session?.cwd || HOME;
|
|
1010
|
+
const arg = text.slice(5).trim();
|
|
1011
|
+
// If arg is an absolute or ~ path, list that directly
|
|
1012
|
+
const expanded = arg ? expandPath(arg) : null;
|
|
1013
|
+
if (expanded && /^p\d+$/.test(expanded)) {
|
|
1014
|
+
// Expired shortcode from daemon restart
|
|
1015
|
+
await bot.sendMessage(chatId, 'โ ๏ธ Button expired. Refreshing...');
|
|
1016
|
+
await sendDirListing(bot, chatId, cwd, null);
|
|
1017
|
+
} else if (expanded && path.isAbsolute(expanded) && fs.existsSync(expanded) && fs.statSync(expanded).isDirectory()) {
|
|
1018
|
+
await sendDirListing(bot, chatId, expanded, null);
|
|
1019
|
+
} else {
|
|
1020
|
+
await sendDirListing(bot, chatId, cwd, arg || null);
|
|
1021
|
+
}
|
|
892
1022
|
return;
|
|
893
1023
|
}
|
|
894
1024
|
|
|
@@ -1014,6 +1144,12 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1014
1144
|
}
|
|
1015
1145
|
|
|
1016
1146
|
if (text === '/stop') {
|
|
1147
|
+
// Clear message queue (don't process queued messages after stop)
|
|
1148
|
+
if (messageQueue.has(chatId)) {
|
|
1149
|
+
const q = messageQueue.get(chatId);
|
|
1150
|
+
if (q.timer) clearTimeout(q.timer);
|
|
1151
|
+
messageQueue.delete(chatId);
|
|
1152
|
+
}
|
|
1017
1153
|
const proc = activeProcesses.get(chatId);
|
|
1018
1154
|
if (proc && proc.child) {
|
|
1019
1155
|
proc.aborted = true;
|
|
@@ -1025,6 +1161,59 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1025
1161
|
return;
|
|
1026
1162
|
}
|
|
1027
1163
|
|
|
1164
|
+
// /quit โ restart session process (reloads MCP/config, keeps same session)
|
|
1165
|
+
if (text === '/quit') {
|
|
1166
|
+
// Stop running task if any
|
|
1167
|
+
if (messageQueue.has(chatId)) {
|
|
1168
|
+
const q = messageQueue.get(chatId);
|
|
1169
|
+
if (q.timer) clearTimeout(q.timer);
|
|
1170
|
+
messageQueue.delete(chatId);
|
|
1171
|
+
}
|
|
1172
|
+
const proc = activeProcesses.get(chatId);
|
|
1173
|
+
if (proc && proc.child) {
|
|
1174
|
+
proc.aborted = true;
|
|
1175
|
+
proc.child.kill('SIGINT');
|
|
1176
|
+
}
|
|
1177
|
+
const session = getSession(chatId);
|
|
1178
|
+
const name = session ? getSessionName(session.id) : null;
|
|
1179
|
+
const label = name || (session ? session.id.slice(0, 8) : 'none');
|
|
1180
|
+
await bot.sendMessage(chatId, `๐ Session restarted. MCP/config reloaded.\n๐ ${session ? path.basename(session.cwd) : '~'} [${label}]`);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// /publish <otp> โ npm publish with OTP (zero latency, no Claude)
|
|
1185
|
+
if (text.startsWith('/publish ')) {
|
|
1186
|
+
const otp = text.slice(9).trim();
|
|
1187
|
+
if (!otp || !/^\d{6}$/.test(otp)) {
|
|
1188
|
+
await bot.sendMessage(chatId, '็จๆณ: /publish 123456');
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const session = getSession(chatId);
|
|
1192
|
+
const cwd = session?.cwd || HOME;
|
|
1193
|
+
await bot.sendMessage(chatId, `๐ฆ npm publish --otp=${otp} ...`);
|
|
1194
|
+
try {
|
|
1195
|
+
const child = spawn('npm', ['publish', `--otp=${otp}`], { cwd, timeout: 60000 });
|
|
1196
|
+
let stdout = '', stderr = '';
|
|
1197
|
+
child.stdout.on('data', d => { stdout += d; });
|
|
1198
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
1199
|
+
await new Promise((resolve) => {
|
|
1200
|
+
child.on('close', resolve);
|
|
1201
|
+
child.on('error', resolve);
|
|
1202
|
+
});
|
|
1203
|
+
const output = (stdout + stderr).trim();
|
|
1204
|
+
if (output.includes('+ metame-cli@') || output.includes('npm notice')) {
|
|
1205
|
+
const ver = output.match(/metame-cli@([\d.]+)/);
|
|
1206
|
+
await bot.sendMessage(chatId, `โ
Published${ver ? ' v' + ver[1] : ''}!`);
|
|
1207
|
+
} else {
|
|
1208
|
+
let msg = output.slice(0, 2000) || '(no output)';
|
|
1209
|
+
await bot.sendMessage(chatId, `โ ${msg}`);
|
|
1210
|
+
}
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
await bot.sendMessage(chatId, `โ ${e.message}`);
|
|
1213
|
+
}
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1028
1217
|
// /sh [command] โ direct shell execution (emergency lifeline)
|
|
1029
1218
|
if (text === '/sh' || text.startsWith('/sh ')) {
|
|
1030
1219
|
const command = text.slice(3).trim();
|
|
@@ -1058,6 +1247,12 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1058
1247
|
}
|
|
1059
1248
|
|
|
1060
1249
|
if (text === '/undo' || text.startsWith('/undo ')) {
|
|
1250
|
+
// Clear message queue
|
|
1251
|
+
if (messageQueue.has(chatId)) {
|
|
1252
|
+
const q = messageQueue.get(chatId);
|
|
1253
|
+
if (q.timer) clearTimeout(q.timer);
|
|
1254
|
+
messageQueue.delete(chatId);
|
|
1255
|
+
}
|
|
1061
1256
|
// Stop running task first
|
|
1062
1257
|
const proc = activeProcesses.get(chatId);
|
|
1063
1258
|
if (proc && proc.child) {
|
|
@@ -1071,10 +1266,9 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1071
1266
|
return;
|
|
1072
1267
|
}
|
|
1073
1268
|
|
|
1074
|
-
// Find session .jsonl file
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
if (!fs.existsSync(sessionFile)) {
|
|
1269
|
+
// Find session .jsonl file (scan Claude's native projects directory)
|
|
1270
|
+
const sessionFile = findSessionFile(session.id);
|
|
1271
|
+
if (!sessionFile) {
|
|
1078
1272
|
await bot.sendMessage(chatId, 'Session file not found.');
|
|
1079
1273
|
return;
|
|
1080
1274
|
}
|
|
@@ -1226,10 +1420,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1226
1420
|
|
|
1227
1421
|
const turnsRemoved = turns.filter(t => t.lineIdx >= targetLineIdx).length;
|
|
1228
1422
|
const allAffected = [...restored, ...deleted];
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1423
|
+
const turnsMsg = `โช ๅ้ไบ ${turnsRemoved} ่ฝฎๅฏน่ฏ`;
|
|
1424
|
+
if (allAffected.length > 0) {
|
|
1425
|
+
const fileList = allAffected.map(f => path.basename(f)).join(', ');
|
|
1426
|
+
await bot.sendMessage(chatId, `${turnsMsg}\n๐ ๆขๅค ${restored.length} / ๅ ้ค ${deleted.length}: ${fileList}`);
|
|
1427
|
+
} else {
|
|
1428
|
+
await bot.sendMessage(chatId, `${turnsMsg}\n๐ ๆ ๆไปถๅๆด้่ฆๆขๅค`);
|
|
1429
|
+
}
|
|
1233
1430
|
} catch (e) {
|
|
1234
1431
|
await bot.sendMessage(chatId, `โ Undo failed: ${e.message}`);
|
|
1235
1432
|
}
|
|
@@ -1421,6 +1618,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1421
1618
|
'/session โ ๆฅ็ๅฝๅไผ่ฏ',
|
|
1422
1619
|
'/stop โ ไธญๆญๅฝๅไปปๅก (ESC)',
|
|
1423
1620
|
'/undo โ ๅ้ไธไธ่ฝฎๆไฝ (ESCร2)',
|
|
1621
|
+
'/quit โ ็ปๆไผ่ฏ๏ผ้ๆฐๅ ่ฝฝ MCP/้
็ฝฎ',
|
|
1424
1622
|
'',
|
|
1425
1623
|
`โ๏ธ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
|
|
1426
1624
|
'๐ง /doctor /fix /reset /sh <cmd>',
|
|
@@ -1431,9 +1629,42 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1431
1629
|
}
|
|
1432
1630
|
|
|
1433
1631
|
// --- Natural language โ Claude Code session ---
|
|
1434
|
-
//
|
|
1632
|
+
// If a task is running: interrupt + collect + merge
|
|
1435
1633
|
if (activeProcesses.has(chatId)) {
|
|
1436
|
-
|
|
1634
|
+
const isFirst = !messageQueue.has(chatId);
|
|
1635
|
+
if (isFirst) {
|
|
1636
|
+
messageQueue.set(chatId, { messages: [], timer: null });
|
|
1637
|
+
}
|
|
1638
|
+
const q = messageQueue.get(chatId);
|
|
1639
|
+
q.messages.push(text);
|
|
1640
|
+
// Only notify once (first message), subsequent ones silently queue
|
|
1641
|
+
if (isFirst) {
|
|
1642
|
+
await bot.sendMessage(chatId, '๐ ๆถๅฐ๏ผไธญๆญๅฝๅไปปๅกๅไธ่ตทๅค็');
|
|
1643
|
+
}
|
|
1644
|
+
// Interrupt the running Claude process
|
|
1645
|
+
const proc = activeProcesses.get(chatId);
|
|
1646
|
+
if (proc && proc.child && !proc.aborted) {
|
|
1647
|
+
proc.aborted = true;
|
|
1648
|
+
proc.child.kill('SIGINT');
|
|
1649
|
+
}
|
|
1650
|
+
// Debounce: wait 5s for more messages before processing
|
|
1651
|
+
if (q.timer) clearTimeout(q.timer);
|
|
1652
|
+
q.timer = setTimeout(async () => {
|
|
1653
|
+
// Wait for active process to fully exit (up to 10s)
|
|
1654
|
+
for (let i = 0; i < 20 && activeProcesses.has(chatId); i++) {
|
|
1655
|
+
await sleep(500);
|
|
1656
|
+
}
|
|
1657
|
+
const msgs = q.messages.splice(0);
|
|
1658
|
+
messageQueue.delete(chatId);
|
|
1659
|
+
if (msgs.length === 0) return;
|
|
1660
|
+
const combined = msgs.join('\n');
|
|
1661
|
+
log('INFO', `Processing ${msgs.length} queued message(s) for ${chatId}`);
|
|
1662
|
+
try {
|
|
1663
|
+
await handleCommand(bot, chatId, combined, config, executeTaskByName);
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
log('ERROR', `Queue dispatch failed: ${e.message}`);
|
|
1666
|
+
}
|
|
1667
|
+
}, 5000);
|
|
1437
1668
|
return;
|
|
1438
1669
|
}
|
|
1439
1670
|
const cd = checkCooldown(chatId);
|
|
@@ -1451,6 +1682,30 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1451
1682
|
const crypto = require('crypto');
|
|
1452
1683
|
const CLAUDE_PROJECTS_DIR = path.join(HOME, '.claude', 'projects');
|
|
1453
1684
|
|
|
1685
|
+
/**
|
|
1686
|
+
* Find a session's .jsonl file by scanning Claude's native projects directory.
|
|
1687
|
+
* This avoids guessing the directory naming convention โ we just search for the file.
|
|
1688
|
+
* Results cached for 30s to avoid repeated directory scans in loops.
|
|
1689
|
+
*/
|
|
1690
|
+
const _sessionFileCache = new Map(); // sessionId -> { path, ts }
|
|
1691
|
+
function findSessionFile(sessionId) {
|
|
1692
|
+
if (!sessionId || !fs.existsSync(CLAUDE_PROJECTS_DIR)) return null;
|
|
1693
|
+
const cached = _sessionFileCache.get(sessionId);
|
|
1694
|
+
if (cached && Date.now() - cached.ts < 30000) return cached.path;
|
|
1695
|
+
const target = sessionId + '.jsonl';
|
|
1696
|
+
try {
|
|
1697
|
+
for (const proj of fs.readdirSync(CLAUDE_PROJECTS_DIR)) {
|
|
1698
|
+
const candidate = path.join(CLAUDE_PROJECTS_DIR, proj, target);
|
|
1699
|
+
if (fs.existsSync(candidate)) {
|
|
1700
|
+
_sessionFileCache.set(sessionId, { path: candidate, ts: Date.now() });
|
|
1701
|
+
return candidate;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
} catch { /* ignore */ }
|
|
1705
|
+
_sessionFileCache.set(sessionId, { path: null, ts: Date.now() });
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1454
1709
|
/**
|
|
1455
1710
|
* Scan all project session indexes, return most recent N sessions.
|
|
1456
1711
|
* Results cached for 10 seconds to avoid repeated directory scans.
|
|
@@ -1537,10 +1792,9 @@ function listRecentSessions(limit, cwd) {
|
|
|
1537
1792
|
*/
|
|
1538
1793
|
function getSessionFileMtime(sessionId, projectPath) {
|
|
1539
1794
|
try {
|
|
1540
|
-
if (!
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
if (fs.existsSync(sessionFile)) {
|
|
1795
|
+
if (!sessionId) return null;
|
|
1796
|
+
const sessionFile = findSessionFile(sessionId);
|
|
1797
|
+
if (sessionFile) {
|
|
1544
1798
|
return fs.statSync(sessionFile).mtimeMs;
|
|
1545
1799
|
}
|
|
1546
1800
|
} catch { /* ignore */ }
|
|
@@ -1653,12 +1907,10 @@ function getSessionName(sessionId) {
|
|
|
1653
1907
|
*/
|
|
1654
1908
|
function writeSessionName(sessionId, cwd, name) {
|
|
1655
1909
|
try {
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
if (!fs.existsSync(dir)) {
|
|
1661
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1910
|
+
const sessionFile = findSessionFile(sessionId);
|
|
1911
|
+
if (!sessionFile) {
|
|
1912
|
+
log('WARN', `writeSessionName: session file not found for ${sessionId.slice(0, 8)}`);
|
|
1913
|
+
return;
|
|
1662
1914
|
}
|
|
1663
1915
|
const entry = JSON.stringify({ type: 'custom-title', customTitle: name, sessionId }) + '\n';
|
|
1664
1916
|
fs.appendFileSync(sessionFile, entry, 'utf8');
|
|
@@ -1771,6 +2023,9 @@ const TOOL_EMOJI = {
|
|
|
1771
2023
|
WebFetch: '๐',
|
|
1772
2024
|
WebSearch: '๐',
|
|
1773
2025
|
Task: '๐ค',
|
|
2026
|
+
Skill: '๐ง',
|
|
2027
|
+
TodoWrite: '๐',
|
|
2028
|
+
NotebookEdit: '๐',
|
|
1774
2029
|
default: '๐ง',
|
|
1775
2030
|
};
|
|
1776
2031
|
|
|
@@ -1788,6 +2043,9 @@ const CONTENT_EXTENSIONS = new Set([
|
|
|
1788
2043
|
// Active Claude processes per chat (for /stop)
|
|
1789
2044
|
const activeProcesses = new Map(); // chatId -> { child, aborted }
|
|
1790
2045
|
|
|
2046
|
+
// Message queue for messages received while a task is running
|
|
2047
|
+
const messageQueue = new Map(); // chatId -> { messages: string[], notified: false }
|
|
2048
|
+
|
|
1791
2049
|
// File cache for button callbacks (shortId -> fullPath)
|
|
1792
2050
|
const fileCache = new Map();
|
|
1793
2051
|
const FILE_CACHE_TTL = 1800000; // 30 minutes
|
|
@@ -1886,9 +2144,33 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
|
|
|
1886
2144
|
lastStatusTime = now;
|
|
1887
2145
|
const emoji = TOOL_EMOJI[toolName] || TOOL_EMOJI.default;
|
|
1888
2146
|
|
|
1889
|
-
//
|
|
2147
|
+
// Resolve display name and context for MCP/Skill/Task tools
|
|
2148
|
+
let displayName = toolName;
|
|
2149
|
+
let displayEmoji = emoji;
|
|
1890
2150
|
let context = '';
|
|
1891
|
-
|
|
2151
|
+
|
|
2152
|
+
if (toolName === 'Skill' && block.input?.skill) {
|
|
2153
|
+
// Skill invocation: show skill name
|
|
2154
|
+
context = block.input.skill;
|
|
2155
|
+
} else if (toolName === 'Task' && block.input?.description) {
|
|
2156
|
+
// Agent task: show description
|
|
2157
|
+
context = block.input.description.slice(0, 30);
|
|
2158
|
+
} else if (toolName.startsWith('mcp__')) {
|
|
2159
|
+
// MCP tool: mcp__server__action โ "MCP server: action"
|
|
2160
|
+
const parts = toolName.split('__');
|
|
2161
|
+
const server = parts[1] || 'unknown';
|
|
2162
|
+
const action = parts.slice(2).join('_') || '';
|
|
2163
|
+
if (server === 'playwright') {
|
|
2164
|
+
displayEmoji = '๐';
|
|
2165
|
+
displayName = 'Browser';
|
|
2166
|
+
context = action.replace(/_/g, ' ');
|
|
2167
|
+
} else {
|
|
2168
|
+
displayEmoji = '๐';
|
|
2169
|
+
displayName = `MCP:${server}`;
|
|
2170
|
+
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
2171
|
+
}
|
|
2172
|
+
} else if (block.input) {
|
|
2173
|
+
// Standard tools: extract brief context
|
|
1892
2174
|
if (block.input.file_path) {
|
|
1893
2175
|
// Insert zero-width space before extension to prevent link parsing
|
|
1894
2176
|
const basename = path.basename(block.input.file_path);
|
|
@@ -1909,8 +2191,8 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
|
|
|
1909
2191
|
}
|
|
1910
2192
|
|
|
1911
2193
|
const status = context
|
|
1912
|
-
? `${
|
|
1913
|
-
: `${
|
|
2194
|
+
? `${displayEmoji} ${displayName}: ใ${context}ใ`
|
|
2195
|
+
: `${displayEmoji} ${displayName}...`;
|
|
1914
2196
|
|
|
1915
2197
|
if (onStatus) {
|
|
1916
2198
|
onStatus(status).catch(() => {});
|
|
@@ -2041,11 +2323,14 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
2041
2323
|
- Multiple files: use multiple [[FILE:...]] tags]`;
|
|
2042
2324
|
const fullPrompt = prompt + daemonHint;
|
|
2043
2325
|
|
|
2044
|
-
// Use streaming mode to show progress
|
|
2326
|
+
// Use streaming mode to show progress
|
|
2327
|
+
// Telegram: edit status msg in-place; Feishu/others: send new messages
|
|
2045
2328
|
const onStatus = async (status) => {
|
|
2046
2329
|
try {
|
|
2047
2330
|
if (statusMsgId && bot.editMessage) {
|
|
2048
2331
|
await bot.editMessage(chatId, statusMsgId, status);
|
|
2332
|
+
} else {
|
|
2333
|
+
await bot.sendMessage(chatId, status);
|
|
2049
2334
|
}
|
|
2050
2335
|
} catch { /* ignore status update failures */ }
|
|
2051
2336
|
};
|
|
@@ -2137,6 +2422,9 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
2137
2422
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
2138
2423
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
2139
2424
|
}
|
|
2425
|
+
} else if (errMsg === 'Stopped by user' && messageQueue.has(chatId)) {
|
|
2426
|
+
// Interrupted by message queue โ suppress error, queue timer will handle it
|
|
2427
|
+
log('INFO', `Task interrupted by new message for ${chatId}`);
|
|
2140
2428
|
} else {
|
|
2141
2429
|
try { await bot.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`); } catch { /* */ }
|
|
2142
2430
|
}
|
|
@@ -2219,8 +2507,11 @@ function killExistingDaemon() {
|
|
|
2219
2507
|
if (oldPid && oldPid !== process.pid) {
|
|
2220
2508
|
process.kill(oldPid, 'SIGTERM');
|
|
2221
2509
|
log('INFO', `Killed existing daemon (PID: ${oldPid})`);
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2510
|
+
// Wait for old process to actually exit (up to 5s)
|
|
2511
|
+
for (let i = 0; i < 10; i++) {
|
|
2512
|
+
try { process.kill(oldPid, 0); } catch { break; } // throws if process gone
|
|
2513
|
+
require('child_process').execSync('sleep 0.5', { stdio: 'ignore' });
|
|
2514
|
+
}
|
|
2224
2515
|
}
|
|
2225
2516
|
} catch {
|
|
2226
2517
|
// Process doesn't exist or already dead
|
|
@@ -2351,22 +2642,17 @@ async function main() {
|
|
|
2351
2642
|
|
|
2352
2643
|
// Auto-restart: watch daemon.js for code changes (hot restart)
|
|
2353
2644
|
const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
|
|
2645
|
+
const _startTime = Date.now();
|
|
2354
2646
|
let _restartDebounce = null;
|
|
2355
2647
|
fs.watchFile(DAEMON_SCRIPT, { interval: 3000 }, (curr, prev) => {
|
|
2356
2648
|
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
2649
|
+
// Ignore file changes within 10s of startup (avoids restart loop)
|
|
2650
|
+
if (Date.now() - _startTime < 10000) return;
|
|
2357
2651
|
if (_restartDebounce) clearTimeout(_restartDebounce);
|
|
2358
|
-
_restartDebounce = setTimeout(
|
|
2359
|
-
log('INFO', 'daemon.js changed on disk โ
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
const { spawn } = require('child_process');
|
|
2363
|
-
const newDaemon = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
2364
|
-
detached: true,
|
|
2365
|
-
stdio: 'ignore',
|
|
2366
|
-
env: { ...process.env, METAME_ROOT: process.env.METAME_ROOT || path.dirname(__dirname) },
|
|
2367
|
-
});
|
|
2368
|
-
newDaemon.unref();
|
|
2369
|
-
setTimeout(() => process.exit(0), 500);
|
|
2652
|
+
_restartDebounce = setTimeout(() => {
|
|
2653
|
+
log('INFO', 'daemon.js changed on disk โ exiting for restart...');
|
|
2654
|
+
// Don't notify here โ the NEW process will notify after startup
|
|
2655
|
+
process.exit(0);
|
|
2370
2656
|
}, 2000);
|
|
2371
2657
|
});
|
|
2372
2658
|
|
|
@@ -2374,6 +2660,10 @@ async function main() {
|
|
|
2374
2660
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
2375
2661
|
feishuBridge = await startFeishuBridge(config, executeTaskByName);
|
|
2376
2662
|
|
|
2663
|
+
// Notify once on startup (single message, no duplicates)
|
|
2664
|
+
await sleep(1500); // Let polling settle
|
|
2665
|
+
await notifyFn('โ
Daemon ready.').catch(() => {});
|
|
2666
|
+
|
|
2377
2667
|
// Graceful shutdown
|
|
2378
2668
|
const shutdown = () => {
|
|
2379
2669
|
log('INFO', 'Daemon shutting down...');
|
|
@@ -121,18 +121,13 @@ function createBot(config) {
|
|
|
121
121
|
async downloadFile(messageId, fileKey, destPath, msgType = 'file') {
|
|
122
122
|
try {
|
|
123
123
|
let res;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
res = await client.im.messageResource.get({
|
|
132
|
-
path: { message_id: messageId, file_key: fileKey },
|
|
133
|
-
params: { type: 'file' },
|
|
134
|
-
});
|
|
135
|
-
}
|
|
124
|
+
// All message attachments (images, files, media) use messageResource.get
|
|
125
|
+
// im.image.get only works for images uploaded by the app itself
|
|
126
|
+
const resourceType = msgType === 'image' ? 'image' : 'file';
|
|
127
|
+
res = await client.im.messageResource.get({
|
|
128
|
+
path: { message_id: messageId, file_key: fileKey },
|
|
129
|
+
params: { type: resourceType },
|
|
130
|
+
});
|
|
136
131
|
|
|
137
132
|
// SDK returns writeFile method or getReadableStream
|
|
138
133
|
if (res && res.writeFile) {
|
|
@@ -293,7 +288,7 @@ function createBot(config) {
|
|
|
293
288
|
fileInfo = {
|
|
294
289
|
messageId: msg.message_id,
|
|
295
290
|
fileKey: content.file_key || content.image_key,
|
|
296
|
-
fileName: content.file_name || content.image_key
|
|
291
|
+
fileName: content.file_name || (content.image_key ? `image_${Date.now()}.png` : `file_${Date.now()}`),
|
|
297
292
|
msgType: msg.message_type, // 'file', 'image', or 'media'
|
|
298
293
|
};
|
|
299
294
|
} catch {}
|