metame-cli 1.2.1 → 1.3.0

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
@@ -18,14 +18,11 @@ It is not a memory system; it is a **Cognitive Mirror** .
18
18
 
19
19
  ## ✨ Key Features
20
20
 
21
- * **🧠 Global Brain (`~/.claude_profile.yaml`):** A single source of truth for your identity, storing your nickname, stress status, and cognitive traits.
22
- * **🧬 Evolution Mechanism:** You are in control. Use `!metame evolve` to manually teach Claude about your new preferences or constraints, ensuring it gets smarter with every interaction.
23
- * **🤝 Dynamic Handshake Protocol:** The "Canary Test." MetaMe verifies its connection to your profile by addressing you by your chosen **Codename** in the very first sentence. If it doesn't, you know the link is broken.
24
- * **🛡️ Auto-Lock Mechanism:** Mark any value in your profile with `# [LOCKED]`, and MetaMe will treat it as a constitution that cannot be overwritten.
25
- * **🔌 Smart Injection:** Automatically injects your profile context into the `CLAUDE.md` of any project you enter, ensuring seamless context switching.
26
- * **🧠 Passive Distillation:** MetaMe silently captures your messages via Claude Code hooks and, on next launch, uses a lightweight LLM (Haiku) to extract cognitive traits and preferences — automatically merging them into your profile with confidence-based upsert. Zero manual effort required.
27
- * **📊 Schema-Enforced Profile:** A 41-field whitelist across 5 tiers (T1-T5) prevents profile bloat. Fields have type validation, enum constraints, and token budget limits (800 tokens max).
28
- * **🎯 Confidence-Based Learning:** Strong directives ("always"/"以后一律") write directly. Normal observations accumulate in a pending queue and only promote to the profile after 3 consistent observations — preventing single-session bias.
21
+ * **🧠 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.
23
+ * **🤝 Dynamic Handshake:** The "Canary Test." Claude must address you by your **Codename** in the first sentence. If it doesn't, the link is broken.
24
+ * **🛡️ Auto-Lock:** Mark any value with `# [LOCKED]` treated as a constitution, never auto-modified.
25
+ * **📱 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.
29
26
 
30
27
  ## 🛠 Prerequisites
31
28
 
@@ -91,41 +88,119 @@ metame interview
91
88
  ```
92
89
  (Command to be implemented in v1.3 - currently you can manually edit `~/.claude_profile.yaml` or use `set-trait`)
93
90
 
94
- ### Surgical Update (Manual Override)
91
+ ### Cognitive Evolution
95
92
 
96
- If you need to update a specific trait without editing the file manually:
93
+ MetaMe learns who you are through two paths:
97
94
 
98
- **Bash**
95
+ **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:
99
96
 
100
97
  ```
98
+ 🧠 MetaMe: Distilling 7 moments in background...
99
+ ```
100
+
101
+ **Manual:** Update a specific trait directly:
102
+
103
+ ```bash
101
104
  metame set-trait status.focus "Learning Rust"
105
+ metame evolve "I prefer functional programming patterns"
102
106
  ```
103
107
 
104
- ### Passive Distillation (Automatic)
108
+ **Anti-bias safeguards:** single observations ≠ traits, contradictions are tracked not overwritten, pending traits expire after 30 days, context fields auto-clear on staleness.
105
109
 
106
- MetaMe automatically learns your cognitive patterns from conversations — no action needed.
110
+ ### Remote Claude Code Telegram & Feishu (v1.3)
107
111
 
108
- **How it works:**
112
+ Full Claude Code from your phone — stateful sessions with conversation history, tool use, and file editing. Supports both Telegram and Feishu (Lark).
109
113
 
110
- 1. A global Claude Code hook captures every message, tagging each with a **confidence level** (high for strong directives like "always"/"以后一律", normal otherwise).
111
- 2. On your next `metame` launch, a background Haiku model analyzes the buffer and extracts cognitive traits and preferences.
112
- 3. **High-confidence** traits write directly to your profile. **Normal-confidence** traits enter a pending queue (`~/.metame/pending_traits.yaml`) and only promote after 3+ consistent observations.
113
- 4. All writes are validated against a **41-field schema whitelist** — unknown keys are silently dropped, enum fields are type-checked, and a **token budget** (800 max) prevents bloat.
114
- 5. The buffer is cleared, and Claude starts with a clean context.
114
+ **Setup:**
115
+
116
+ ```bash
117
+ metame daemon init # Create config + setup guide
118
+ ```
115
119
 
116
- **Anti-bias safeguards:**
117
- - Single observations are treated as states, not traits
118
- - Contradictions are tracked, not blindly overwritten
119
- - Pending traits expire after 30 days without re-observation
120
- - Context fields (focus, energy) auto-expire on staleness
120
+ Edit `~/.metame/daemon.yaml`:
121
121
 
122
- You'll see this in the startup log:
122
+ ```yaml
123
+ telegram:
124
+ enabled: true
125
+ bot_token: "YOUR_BOT_TOKEN" # From @BotFather
126
+ allowed_chat_ids:
127
+ - 123456789 # Your Telegram chat ID
123
128
 
129
+ feishu:
130
+ enabled: true
131
+ app_id: "YOUR_APP_ID" # From Feishu Developer Console
132
+ app_secret: "YOUR_APP_SECRET"
133
+ allowed_chat_ids: [] # Empty = allow all
124
134
  ```
125
- 🧠 MetaMe: Distilling 7 moments in background...
135
+
136
+ **Start the daemon:**
137
+
138
+ ```bash
139
+ metame daemon start # Background process
140
+ metame daemon status # Check if running
141
+ metame daemon logs # Tail the log
142
+ metame daemon stop # Shutdown
143
+ metame daemon install-launchd # macOS auto-start (RunAtLoad + KeepAlive)
144
+ ```
145
+
146
+ **Session commands (interactive buttons on Telegram & Feishu):**
147
+
148
+ | Command | Description |
149
+ |---------|-------------|
150
+ | `/new` | Start new session — pick project directory from button list |
151
+ | `/resume` | Resume a session — clickable list scoped to current workdir |
152
+ | `/continue` | Continue the most recent terminal session |
153
+ | `/cd` | Change working directory — with directory browser |
154
+ | `/session` | Current session info |
155
+
156
+ Just type naturally for conversation — every message stays in the same Claude Code session with full context.
157
+
158
+ **How it works:**
159
+
160
+ Each chat gets a persistent session via `claude -p --resume <session-id>`. This is the same Claude Code engine as your terminal — same tools (file editing, bash, code search), same conversation history. You can start work on your computer and `/resume` from your phone, or vice versa.
161
+
162
+ **Other commands:**
163
+
164
+ | Command | Description |
165
+ |---------|-------------|
166
+ | `/status` | Daemon status + profile summary |
167
+ | `/tasks` | List scheduled heartbeat tasks |
168
+ | `/run <name>` | Run a task immediately |
169
+ | `/budget` | Today's token usage |
170
+ | `/quiet` | Silence mirror/reflections for 48h |
171
+
172
+ **Heartbeat Tasks:**
173
+
174
+ Define scheduled tasks in `daemon.yaml`:
175
+
176
+ ```yaml
177
+ heartbeat:
178
+ tasks:
179
+ - name: "morning-news"
180
+ prompt: "Summarize today's top 3 AI news stories."
181
+ interval: "24h"
182
+ model: "haiku"
183
+ notify: true
184
+ precondition: "curl -s -o /dev/null -w '%{http_code}' https://news.ycombinator.com | grep 200"
126
185
  ```
127
186
 
128
- The hook is installed automatically on first run to `~/.claude/settings.json` (global scope — works across all projects).
187
+ * `precondition`: Shell command empty output task skipped, zero tokens.
188
+ * `type: "script"`: Run a local script directly instead of `claude -p`.
189
+ * `notify: true`: Push results to Telegram/Feishu.
190
+
191
+ **Token efficiency:**
192
+
193
+ * Polling, slash commands, directory browsing: **zero tokens**
194
+ * Stateful sessions: same cost as using Claude Code in terminal (conversation history managed by Claude CLI)
195
+ * Budget tracking with daily limit (default 50k tokens)
196
+ * 10-second cooldown between Claude calls
197
+
198
+ **Security:**
199
+
200
+ * `allowed_chat_ids` whitelist — unauthorized users silently ignored
201
+ * No `--dangerously-skip-permissions` — standard `-p` mode permissions
202
+ * `~/.metame/` directory set to mode 700
203
+ * Bot tokens stored locally, never transmitted
129
204
 
130
205
  ### Hot Reload (Refresh)
131
206
 
@@ -226,7 +301,15 @@ If you want to delete your stored profile data:
226
301
  rm ~/.claude_profile.yaml
227
302
  ```
228
303
 
229
- ### 3. Remove Passive Distillation Data (Optional)
304
+ ### 3. Stop the Daemon (if running)
305
+
306
+ ```bash
307
+ metame daemon stop
308
+ launchctl unload ~/Library/LaunchAgents/com.metame.daemon.plist 2>/dev/null
309
+ rm -f ~/Library/LaunchAgents/com.metame.daemon.plist
310
+ ```
311
+
312
+ ### 4. Remove Passive Distillation Data (Optional)
230
313
 
231
314
  Remove the signal capture scripts:
232
315
 
@@ -236,7 +319,7 @@ Remove the signal capture scripts:
236
319
  rm -rf ~/.metame
237
320
  ```
238
321
 
239
- ### 4. Remove the Signal Capture Hook (Optional)
322
+ ### 5. Remove the Signal Capture Hook (Optional)
240
323
 
241
324
  MetaMe installs a global hook in `~/.claude/settings.json`. To remove it, edit the file and delete the `UserPromptSubmit` entry under `hooks`, or run:
242
325
 
@@ -253,7 +336,7 @@ console.log('Hook removed.');
253
336
  "
254
337
  ```
255
338
 
256
- ### 5. Cleanup Project Files (Optional)
339
+ ### 6. Cleanup Project Files (Optional)
257
340
 
258
341
  MetaMe adds a header to `CLAUDE.md` files in your projects. To restore them to their original state (if you have many), you can use a text editor to remove the block starting with `## 🧠 SYSTEM KERNEL`.
259
342
 
package/index.js CHANGED
@@ -23,7 +23,7 @@ if (!fs.existsSync(METAME_DIR)) {
23
23
  }
24
24
 
25
25
  // Auto-deploy bundled scripts to ~/.metame/
26
- const BUNDLED_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js'];
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'];
27
27
  const scriptsDir = path.join(__dirname, 'scripts');
28
28
 
29
29
  for (const script of BUNDLED_SCRIPTS) {
@@ -294,8 +294,86 @@ if (!isKnownUser) {
294
294
  console.log("🆕 User Unknown: Injecting Deep Genesis Protocol...");
295
295
  }
296
296
 
297
+ // ---------------------------------------------------------
298
+ // 4.5 MIRROR INJECTION (Phase C — metacognition observation)
299
+ // ---------------------------------------------------------
300
+ let mirrorLine = '';
301
+ try {
302
+ if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
303
+ const brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
304
+
305
+ // Check quiet mode
306
+ const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
307
+ const isQuiet = quietUntil && new Date(quietUntil).getTime() > Date.now();
308
+
309
+ // Check mirror enabled (default: true)
310
+ const mirrorEnabled = !(brainDoc.growth && brainDoc.growth.mirror_enabled === false);
311
+
312
+ if (!isQuiet && mirrorEnabled && brainDoc.growth && Array.isArray(brainDoc.growth.patterns)) {
313
+ const now = Date.now();
314
+ const COOLDOWN_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
315
+
316
+ // Find a pattern that hasn't been surfaced in 14 days
317
+ const candidate = brainDoc.growth.patterns.find(p => {
318
+ if (!p.surfaced) return true;
319
+ return (now - new Date(p.surfaced).getTime()) > COOLDOWN_MS;
320
+ });
321
+
322
+ if (candidate) {
323
+ mirrorLine = `\n[MetaMe observation: ${candidate.summary} 不要主动提起,只在用户自然提到相关话题时温和回应。]\n`;
324
+
325
+ // Mark as surfaced
326
+ candidate.surfaced = new Date().toISOString().slice(0, 10);
327
+ fs.writeFileSync(BRAIN_FILE, yaml.dump(brainDoc, { lineWidth: -1 }), 'utf8');
328
+ }
329
+ }
330
+ }
331
+ } catch {
332
+ // Non-fatal
333
+ }
334
+
335
+ // ---------------------------------------------------------
336
+ // 4.6 REFLECTION PROMPT (Phase C — conditional, NOT static)
337
+ // ---------------------------------------------------------
338
+ // Only inject when trigger conditions are met at startup.
339
+ // This ensures reflections don't fire every session.
340
+ let reflectionLine = '';
341
+ try {
342
+ if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
343
+ const refDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
344
+
345
+ // Check quiet mode
346
+ const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
347
+ const isQuietForRef = quietUntil && new Date(quietUntil).getTime() > Date.now();
348
+
349
+ if (!isQuietForRef) {
350
+ const distillCount = (refDoc.evolution && refDoc.evolution.distill_count) || 0;
351
+ const zoneHistory = (refDoc.growth && refDoc.growth.zone_history) || [];
352
+
353
+ // Trigger 1: Every 7th session
354
+ const trigger7th = distillCount > 0 && distillCount % 7 === 0;
355
+
356
+ // Trigger 2: Three consecutive comfort-zone sessions
357
+ const lastThree = zoneHistory.slice(-3);
358
+ const triggerComfort = lastThree.length === 3 && lastThree.every(z => z === 'C');
359
+
360
+ if (trigger7th || triggerComfort) {
361
+ let hint = '';
362
+ if (triggerComfort) {
363
+ hint = '连续几次都在熟悉领域。如果用户在session结束时自然停顿,可以温和地问:🪞 准备好探索拉伸区了吗?';
364
+ } else {
365
+ hint = '这是第' + distillCount + '次session。如果session自然结束,可以附加一句:🪞 一个词形容这次session的感受?';
366
+ }
367
+ reflectionLine = `\n[MetaMe reflection: ${hint} 只在session即将结束时说一次。如果用户没回应就不要追问。]\n`;
368
+ }
369
+ }
370
+ }
371
+ } catch {
372
+ // Non-fatal
373
+ }
374
+
297
375
  // Prepend the new Protocol to the top
298
- const newContent = finalProtocol + "\n" + fileContent;
376
+ const newContent = finalProtocol + mirrorLine + reflectionLine + "\n" + fileContent;
299
377
  fs.writeFileSync(PROJECT_FILE, newContent, 'utf8');
300
378
 
301
379
  console.log("🔮 MetaMe: Link Established.");
@@ -412,6 +490,328 @@ if (isSetTrait) {
412
490
  process.exit(0);
413
491
  }
414
492
 
493
+ // ---------------------------------------------------------
494
+ // 5.5 METACOGNITION CONTROL COMMANDS (Phase C)
495
+ // ---------------------------------------------------------
496
+
497
+ // metame quiet — silence mirror + reflections for 48 hours
498
+ const isQuiet = process.argv.includes('quiet');
499
+ if (isQuiet) {
500
+ try {
501
+ const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
502
+ if (!doc.growth) doc.growth = {};
503
+ doc.growth.quiet_until = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString();
504
+ fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
505
+ console.log("🤫 MetaMe: Mirror & reflections silenced for 48 hours.");
506
+ } catch (e) {
507
+ console.error("❌ Error:", e.message);
508
+ }
509
+ process.exit(0);
510
+ }
511
+
512
+ // metame insights — show detected patterns
513
+ const isInsights = process.argv.includes('insights');
514
+ if (isInsights) {
515
+ try {
516
+ const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
517
+ const patterns = (doc.growth && doc.growth.patterns) || [];
518
+ const zoneHistory = (doc.growth && doc.growth.zone_history) || [];
519
+
520
+ if (patterns.length === 0) {
521
+ console.log("🔍 MetaMe: No patterns detected yet. Keep using MetaMe and patterns will emerge after ~5 sessions.");
522
+ } else {
523
+ console.log("🪞 MetaMe Insights:\n");
524
+ patterns.forEach((p, i) => {
525
+ const icon = p.type === 'avoidance' ? '⚠️' : p.type === 'growth' ? '🌱' : p.type === 'energy' ? '⚡' : '🔄';
526
+ console.log(` ${icon} [${p.type}] ${p.summary} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
527
+ console.log(` Detected: ${p.detected}${p.surfaced ? `, Last shown: ${p.surfaced}` : ''}`);
528
+ });
529
+ if (zoneHistory.length > 0) {
530
+ console.log(`\n 📊 Recent zone history: ${zoneHistory.join(' → ')}`);
531
+ console.log(` (C=Comfort, S=Stretch, P=Panic)`);
532
+ }
533
+ const answered = (doc.growth && doc.growth.reflections_answered) || 0;
534
+ const skipped = (doc.growth && doc.growth.reflections_skipped) || 0;
535
+ if (answered + skipped > 0) {
536
+ console.log(`\n 💭 Reflections: ${answered} answered, ${skipped} skipped`);
537
+ }
538
+ }
539
+ } catch (e) {
540
+ console.error("❌ Error:", e.message);
541
+ }
542
+ process.exit(0);
543
+ }
544
+
545
+ // metame mirror on/off — toggle mirror injection
546
+ const isMirror = process.argv.includes('mirror');
547
+ if (isMirror) {
548
+ const mirrorIndex = process.argv.indexOf('mirror');
549
+ const toggle = process.argv[mirrorIndex + 1];
550
+ if (toggle !== 'on' && toggle !== 'off') {
551
+ console.error("❌ Usage: metame mirror on|off");
552
+ process.exit(1);
553
+ }
554
+ try {
555
+ const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
556
+ if (!doc.growth) doc.growth = {};
557
+ doc.growth.mirror_enabled = (toggle === 'on');
558
+ fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
559
+ console.log(`🪞 MetaMe: Mirror ${toggle === 'on' ? 'enabled' : 'disabled'}.`);
560
+ } catch (e) {
561
+ console.error("❌ Error:", e.message);
562
+ }
563
+ process.exit(0);
564
+ }
565
+
566
+ // ---------------------------------------------------------
567
+ // 5.6 DAEMON SUBCOMMANDS
568
+ // ---------------------------------------------------------
569
+ const isDaemon = process.argv.includes('daemon');
570
+ if (isDaemon) {
571
+ const daemonIndex = process.argv.indexOf('daemon');
572
+ const subCmd = process.argv[daemonIndex + 1];
573
+ const DAEMON_CONFIG = path.join(METAME_DIR, 'daemon.yaml');
574
+ const DAEMON_STATE = path.join(METAME_DIR, 'daemon_state.json');
575
+ const DAEMON_PID = path.join(METAME_DIR, 'daemon.pid');
576
+ const DAEMON_LOG = path.join(METAME_DIR, 'daemon.log');
577
+ const DAEMON_DEFAULT = path.join(__dirname, 'scripts', 'daemon-default.yaml');
578
+ const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
579
+
580
+ if (subCmd === 'init') {
581
+ // Create config from template
582
+ if (fs.existsSync(DAEMON_CONFIG)) {
583
+ console.log("⚠️ daemon.yaml already exists at ~/.metame/daemon.yaml");
584
+ console.log(" Delete it first if you want to re-initialize.");
585
+ } else {
586
+ const templateSrc = fs.existsSync(DAEMON_DEFAULT)
587
+ ? DAEMON_DEFAULT
588
+ : path.join(METAME_DIR, 'daemon-default.yaml');
589
+ if (fs.existsSync(templateSrc)) {
590
+ fs.copyFileSync(templateSrc, DAEMON_CONFIG);
591
+ } else {
592
+ console.error("❌ Template not found. Reinstall MetaMe.");
593
+ process.exit(1);
594
+ }
595
+ // Ensure directory permissions (700)
596
+ try { fs.chmodSync(METAME_DIR, 0o700); } catch { /* ignore on Windows */ }
597
+ console.log("✅ MetaMe daemon initialized.");
598
+ console.log(` Config: ${DAEMON_CONFIG}`);
599
+ }
600
+
601
+ console.log("\n📱 Telegram Setup (optional):");
602
+ console.log(" 1. Message @BotFather on Telegram → /newbot");
603
+ console.log(" 2. Copy the bot token");
604
+ console.log(" 3. Edit ~/.metame/daemon.yaml:");
605
+ console.log(" telegram:");
606
+ console.log(" enabled: true");
607
+ console.log(" bot_token: \"YOUR_TOKEN\"");
608
+ console.log(" allowed_chat_ids: [YOUR_CHAT_ID]");
609
+ console.log(" 4. To find your chat_id: message your bot, then run:");
610
+ console.log(" curl https://api.telegram.org/botYOUR_TOKEN/getUpdates");
611
+ console.log("\n📘 Feishu Setup (optional):");
612
+ console.log(" 1. Go to open.feishu.cn → Create App → get app_id & app_secret");
613
+ console.log(" 2. Enable Bot capability + im:message events");
614
+ console.log(" 3. Enable 'Long Connection' (长连接) mode in Event Subscription");
615
+ console.log(" 4. Edit ~/.metame/daemon.yaml:");
616
+ console.log(" feishu:");
617
+ console.log(" enabled: true");
618
+ console.log(" app_id: \"YOUR_APP_ID\"");
619
+ console.log(" app_secret: \"YOUR_APP_SECRET\"");
620
+ console.log(" allowed_chat_ids: [CHAT_ID]");
621
+
622
+ console.log("\n Then: metame daemon start");
623
+
624
+ // Optional launchd setup (macOS only)
625
+ if (process.platform === 'darwin') {
626
+ const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
627
+ const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
628
+ console.log("\n🍎 Auto-start on macOS (optional):");
629
+ console.log(" To start daemon automatically on login:");
630
+ console.log(` metame daemon start (first time to verify it works)`);
631
+ console.log(` Then create: ${plistPath}`);
632
+ console.log(" Or run: metame daemon install-launchd");
633
+ }
634
+ process.exit(0);
635
+ }
636
+
637
+ if (subCmd === 'install-launchd') {
638
+ if (process.platform !== 'darwin') {
639
+ console.error("❌ launchd is macOS-only.");
640
+ process.exit(1);
641
+ }
642
+ const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
643
+ if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
644
+ const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
645
+ const nodePath = process.execPath;
646
+ // Capture current PATH so launchd can find `claude` and other tools
647
+ const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
648
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
649
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
650
+ <plist version="1.0">
651
+ <dict>
652
+ <key>Label</key>
653
+ <string>com.metame.daemon</string>
654
+ <key>ProgramArguments</key>
655
+ <array>
656
+ <string>${nodePath}</string>
657
+ <string>${DAEMON_SCRIPT}</string>
658
+ </array>
659
+ <key>RunAtLoad</key>
660
+ <true/>
661
+ <key>KeepAlive</key>
662
+ <true/>
663
+ <key>StandardOutPath</key>
664
+ <string>${DAEMON_LOG}</string>
665
+ <key>StandardErrorPath</key>
666
+ <string>${DAEMON_LOG}</string>
667
+ <key>EnvironmentVariables</key>
668
+ <dict>
669
+ <key>METAME_ROOT</key>
670
+ <string>${__dirname}</string>
671
+ <key>PATH</key>
672
+ <string>${currentPath}</string>
673
+ <key>HOME</key>
674
+ <string>${HOME_DIR}</string>
675
+ </dict>
676
+ </dict>
677
+ </plist>`;
678
+ fs.writeFileSync(plistPath, plistContent, 'utf8');
679
+ console.log(`✅ launchd plist installed: ${plistPath}`);
680
+ console.log(" Load now: launchctl load " + plistPath);
681
+ console.log(" Unload: launchctl unload " + plistPath);
682
+ process.exit(0);
683
+ }
684
+
685
+ if (subCmd === 'start') {
686
+ // Check if already running
687
+ if (fs.existsSync(DAEMON_PID)) {
688
+ const existingPid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
689
+ try {
690
+ process.kill(existingPid, 0); // test if alive
691
+ console.log(`⚠️ Daemon already running (PID: ${existingPid})`);
692
+ console.log(" Use 'metame daemon stop' first.");
693
+ process.exit(1);
694
+ } catch {
695
+ // Stale PID file — clean up
696
+ fs.unlinkSync(DAEMON_PID);
697
+ }
698
+ }
699
+ if (!fs.existsSync(DAEMON_CONFIG)) {
700
+ console.error("❌ No config found. Run: metame daemon init");
701
+ process.exit(1);
702
+ }
703
+ if (!fs.existsSync(DAEMON_SCRIPT)) {
704
+ console.error("❌ daemon.js not found. Reinstall MetaMe.");
705
+ process.exit(1);
706
+ }
707
+ const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
708
+ detached: true,
709
+ stdio: 'ignore',
710
+ env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
711
+ });
712
+ bg.unref();
713
+ console.log(`✅ MetaMe daemon started (PID: ${bg.pid})`);
714
+ console.log(" Logs: metame daemon logs");
715
+ console.log(" Stop: metame daemon stop");
716
+ process.exit(0);
717
+ }
718
+
719
+ if (subCmd === 'stop') {
720
+ if (!fs.existsSync(DAEMON_PID)) {
721
+ console.log("ℹ️ No daemon running (no PID file).");
722
+ process.exit(0);
723
+ }
724
+ const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
725
+ try {
726
+ process.kill(pid, 'SIGTERM');
727
+ console.log(`✅ Daemon stopped (PID: ${pid})`);
728
+ } catch (e) {
729
+ console.log(`⚠️ Process ${pid} not found (may have already exited).`);
730
+ fs.unlinkSync(DAEMON_PID);
731
+ }
732
+ process.exit(0);
733
+ }
734
+
735
+ if (subCmd === 'status') {
736
+ let state = {};
737
+ try { state = JSON.parse(fs.readFileSync(DAEMON_STATE, 'utf8')); } catch { /* empty */ }
738
+
739
+ // Check if running
740
+ let isRunning = false;
741
+ if (fs.existsSync(DAEMON_PID)) {
742
+ const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
743
+ try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
744
+ }
745
+
746
+ console.log(`🤖 MetaMe Daemon: ${isRunning ? '🟢 Running' : '🔴 Stopped'}`);
747
+ if (state.started_at) console.log(` Started: ${state.started_at}`);
748
+ if (state.pid) console.log(` PID: ${state.pid}`);
749
+
750
+ // Budget
751
+ const budget = state.budget || {};
752
+ const config = {};
753
+ try { Object.assign(config, yaml.load(fs.readFileSync(DAEMON_CONFIG, 'utf8'))); } catch { /* empty */ }
754
+ const limit = (config.budget && config.budget.daily_limit) || 50000;
755
+ console.log(` Budget: ${budget.tokens_used || 0}/${limit} tokens (${budget.date || 'no data'})`);
756
+
757
+ // Tasks
758
+ const tasks = state.tasks || {};
759
+ if (Object.keys(tasks).length > 0) {
760
+ console.log(" Recent tasks:");
761
+ for (const [name, info] of Object.entries(tasks)) {
762
+ const icon = info.status === 'success' ? '✅' : '❌';
763
+ console.log(` ${icon} ${name}: ${info.last_run || 'unknown'}`);
764
+ if (info.output_preview) console.log(` ${info.output_preview.slice(0, 80)}...`);
765
+ }
766
+ }
767
+ process.exit(0);
768
+ }
769
+
770
+ if (subCmd === 'logs') {
771
+ if (!fs.existsSync(DAEMON_LOG)) {
772
+ console.log("ℹ️ No log file yet. Start the daemon first.");
773
+ process.exit(0);
774
+ }
775
+ const content = fs.readFileSync(DAEMON_LOG, 'utf8');
776
+ const lines = content.split('\n');
777
+ const tail = lines.slice(-50).join('\n');
778
+ console.log(tail);
779
+ process.exit(0);
780
+ }
781
+
782
+ if (subCmd === 'run') {
783
+ const taskName = process.argv[daemonIndex + 2];
784
+ if (!taskName) {
785
+ console.error("❌ Usage: metame daemon run <task-name>");
786
+ process.exit(1);
787
+ }
788
+ if (!fs.existsSync(DAEMON_SCRIPT)) {
789
+ console.error("❌ daemon.js not found. Reinstall MetaMe.");
790
+ process.exit(1);
791
+ }
792
+ // Run in foreground using daemon.js --run
793
+ const result = require('child_process').spawnSync(
794
+ process.execPath,
795
+ [DAEMON_SCRIPT, '--run', taskName],
796
+ { stdio: 'inherit', env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname } }
797
+ );
798
+ process.exit(result.status || 0);
799
+ }
800
+
801
+ // Unknown subcommand
802
+ console.log("📖 MetaMe Daemon Commands:");
803
+ console.log(" metame daemon init — initialize config");
804
+ console.log(" metame daemon start — start background daemon");
805
+ console.log(" metame daemon stop — stop daemon");
806
+ console.log(" metame daemon status — show status & budget");
807
+ console.log(" metame daemon logs — tail log file");
808
+ console.log(" metame daemon run <name> — run a task once");
809
+ if (process.platform === 'darwin') {
810
+ console.log(" metame daemon install-launchd — auto-start on macOS");
811
+ }
812
+ process.exit(0);
813
+ }
814
+
415
815
  // ---------------------------------------------------------
416
816
  // ---------------------------------------------------------
417
817
  // 6. SAFETY GUARD: RECURSION PREVENTION (v2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
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": {
@@ -23,9 +23,10 @@
23
23
  "author": "Yaron",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
+ "@larksuiteoapi/node-sdk": "^1.58.0",
26
27
  "js-yaml": "^4.1.1"
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=14"
30
31
  }
31
- }
32
+ }