metame-cli 1.3.16 → 1.3.18

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
@@ -64,20 +64,31 @@ MetaMe is a wrapper around **Claude Code**. You must have Node.js and the offici
64
64
 
65
65
  ## 📦 Installation
66
66
 
67
- **Option A: NPM (recommended)** — full CLI with daemon, mobile bridge, interview
67
+ **One-command install (recommended)** — installs Node.js, Claude Code, and MetaMe:
68
+
69
+ macOS / Linux:
70
+ ```bash
71
+ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bash
72
+ ```
73
+
74
+ Windows (PowerShell):
75
+ ```powershell
76
+ irm https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.ps1 | iex
77
+ ```
78
+ > Windows uses WSL (auto-installed if missing). After WSL install you'll need to reboot once, then re-run the command.
79
+
80
+ **Manual install** — if you already have Node.js and Claude Code:
68
81
 
69
82
  ```bash
70
83
  npm install -g metame-cli
71
84
  ```
72
85
 
73
- **Option B: Claude Code Plugin** — lightweight, profile injection + slash commands
86
+ **Claude Code Plugin** — lightweight alternative, profile injection + slash commands only:
74
87
 
75
88
  ```bash
76
89
  claude plugin install github:Yaron9/MetaMe/plugin
77
90
  ```
78
91
 
79
- *(NPM note: If you encounter permission errors on Mac/Linux, use `sudo npm install -g metame-cli`)*
80
-
81
92
  ## 🚀 Usage
82
93
 
83
94
  Forget the `claude` command. From now on, simply type:
@@ -186,7 +197,7 @@ feishu:
186
197
  enabled: true
187
198
  app_id: "YOUR_APP_ID" # From Feishu Developer Console
188
199
  app_secret: "YOUR_APP_SECRET"
189
- allowed_chat_ids: [] # Empty = allow all
200
+ allowed_chat_ids: [] # Empty = deny all (fill via setup wizard)
190
201
  ```
191
202
 
192
203
  **Start the daemon:**
@@ -196,30 +207,25 @@ metame start # Background process
196
207
  metame status # Check if running
197
208
  metame logs # Tail the log
198
209
  metame stop # Shutdown
199
- metame daemon install-launchd # macOS auto-start (RunAtLoad + KeepAlive)
210
+ metame daemon install-launchd # macOS auto-start
211
+ metame daemon install-systemd # Linux/WSL auto-start
200
212
  ```
201
213
 
202
- **macOS auto-start (recommended):** If you want the daemon to survive sleep/wake and start on boot:
214
+ **Auto-start (recommended):** The daemon survives reboots and auto-restarts on crash.
203
215
 
216
+ macOS:
204
217
  ```bash
205
218
  metame daemon install-launchd
206
219
  launchctl load ~/Library/LaunchAgents/com.metame.daemon.plist
207
220
  ```
208
221
 
209
- Once loaded, the daemon auto-starts on login, auto-restarts after sleep/wake or crash. No need to run `metame start` manually anymore.
210
-
211
- > **Important:** Choose one management method — either launchd or manual (`metame start/stop`). Don't mix them, or you'll get duplicate processes.
212
-
222
+ Linux / WSL:
213
223
  ```bash
214
- # Check status (works with both methods)
215
- metame status
216
-
217
- # Disable auto-start
218
- launchctl unload ~/Library/LaunchAgents/com.metame.daemon.plist
219
-
220
- # Remove completely
221
- rm ~/Library/LaunchAgents/com.metame.daemon.plist
224
+ metame daemon install-systemd
222
225
  ```
226
+ > WSL requires systemd enabled: add `[boot]\nsystemd=true` to `/etc/wsl.conf` and restart WSL.
227
+
228
+ > **Important:** Choose one management method — either auto-start or manual (`metame start/stop`). Don't mix them, or you'll get duplicate processes.
223
229
 
224
230
  **Session commands (interactive buttons on Telegram & Feishu):**
225
231
 
@@ -300,7 +306,7 @@ Uploaded files are saved to `<project>/upload/`. Claude won't read large files a
300
306
 
301
307
  *`/stop` — ESC equivalent:* Sends SIGINT to the running Claude process. Instant interruption, just like pressing ESC in your terminal.
302
308
 
303
- *`/undo` — ESC×2 equivalent:* Interactive turn picker showing your actual messages. Select which turn to roll back to session history is truncated and all modified files are restored using Claude's native `~/.claude/file-history/` backups (the exact same mechanism as pressing ESC twice in the terminal). Files created during undone turns are deleted; files deleted during undone turns are restored. Zero risk.
309
+ *`/undo` — git-based code rollback (v1.3.16):* Before each Claude turn, the daemon auto-commits a `[metame-checkpoint]` to git. `/undo` lists recent checkpoints; tap one to `git reset --hard` back to that state. Session history is also truncated. Reliable across both `-p` mode and interactive sessions no dependency on Claude CLI internals.
304
310
 
305
311
  ```
306
312
  You: /undo
@@ -310,7 +316,7 @@ Bot: 回退到哪一轮?
310
316
  ⏪ 添加测试用例 (30分钟前)
311
317
  ```
312
318
 
313
- **Concurrent task protection:** If a Claude task is already running, new messages are blocked with a hint to wait or `/stop`. Prevents session conflicts.
319
+ **Message queue & interrupt (v1.3.16):** If a Claude task is already running, new messages interrupt the current task and queue up. After 5 seconds of no new input, all queued messages are merged and processed together. Works identically on both Telegram and Feishu.
314
320
 
315
321
  **Auto-restart (v1.3.13):** The daemon watches its own code for changes. When you update MetaMe (via npm or git), the daemon automatically restarts with the new code — no manual restart needed. A notification is pushed to confirm.
316
322
 
@@ -330,7 +336,7 @@ Bot: 回退到哪一轮?
330
336
  | `/status` | Daemon status + profile summary |
331
337
  | `/tasks` | List scheduled heartbeat tasks |
332
338
  | `/run <name>` | Run a task immediately |
333
- | `/model [name]` | Interactive model switcher with buttons (sonnet, opus, haiku). Auto-backs up config before switching. |
339
+ | `/model [name]` | Interactive model switcher with buttons (sonnet, opus, haiku). Accepts any model name when using a custom provider. Auto-backs up config before switching. |
334
340
  | `/list` | File browser with clickable buttons — folders expand, files download. Zero tokens. |
335
341
  | `/budget` | Today's token usage |
336
342
  | `/quiet` | Silence mirror/reflections for 48h |
@@ -388,11 +394,58 @@ Each step runs in the same Claude Code session. Step outputs automatically becom
388
394
 
389
395
  **Security:**
390
396
 
391
- * `allowed_chat_ids` whitelist — unauthorized users silently ignored
392
- * No `--dangerously-skip-permissions` — standard `-p` mode permissions
397
+ * `allowed_chat_ids` whitelist — unauthorized users silently ignored (empty = deny all)
398
+ * `dangerously_skip_permissions` enabled by default for mobile (users can't click "allow" on phone security relies on the chat ID whitelist)
393
399
  * `~/.metame/` directory set to mode 700
394
400
  * Bot tokens stored locally, never transmitted
395
401
 
402
+ ### Multi-Agent Projects — Context Isolation & Nickname Routing (v1.3.18)
403
+
404
+ Organize your work into named agents, each tied to a project directory. Switch between them instantly from your phone — no commands needed, just say their name.
405
+
406
+ **How it works:**
407
+
408
+ Each `project` entry in `daemon.yaml` defines an agent with a working directory, display name, notification color, and optional nicknames. When you send a message starting with a nickname, the daemon instantly switches to that project's last session — no Claude call, no token cost.
409
+
410
+ **Setup via conversation:**
411
+
412
+ The easiest way to add an agent is to tell the bot:
413
+
414
+ > *"Add a project called 'work' pointing to ~/my-project, nickname is '工作'"*
415
+
416
+ Or edit `~/.metame/daemon.yaml` directly:
417
+
418
+ ```yaml
419
+ projects:
420
+ my_agent:
421
+ name: "My Agent" # Display name in notifications
422
+ icon: "🤖" # Emoji shown in Feishu cards
423
+ color: "blue" # Feishu card color: blue|orange|green|purple|red|grey
424
+ cwd: "~/my-project" # Working directory for this agent
425
+ nicknames: [nickname1, nickname2] # Wake words (matched at message start)
426
+ heartbeat_tasks: [] # Scheduled tasks for this project (optional)
427
+ ```
428
+
429
+ **Available colors:** `blue` · `orange` · `green` · `purple` · `red` · `grey` · `turquoise`
430
+
431
+ **Phone commands:**
432
+
433
+ | Action | How |
434
+ |--------|-----|
435
+ | Switch agent | Send the nickname alone: `贾维斯` → `🤖 Jarvis 在线` |
436
+ | Switch + ask | `贾维斯, what's the status?` → switches then asks Claude |
437
+ | Pick from list | `/agent` → tap button to switch |
438
+ | Continue a reply | Reply to any bot message → session auto-restored |
439
+ | View all tasks | `/tasks` → grouped by project |
440
+ | Run a task | `/run <task-name>` |
441
+
442
+ **Nickname routing rules:**
443
+ - Matched at **message start only** — mentioning a nickname mid-sentence never triggers a switch
444
+ - Pure nickname (no content after) → instant switch, zero token cost, bypasses cooldown
445
+ - Nickname + content → switch then send content to Claude
446
+
447
+ **Heartbeat task notifications** arrive as colored Feishu cards — each project's color is distinct, so you can tell at a glance which agent sent the update.
448
+
396
449
  ### Provider Relay — Third-Party Model Support (v1.3.11)
397
450
 
398
451
  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.
@@ -531,8 +584,14 @@ rm ~/.claude_profile.yaml
531
584
 
532
585
  ```bash
533
586
  metame stop
587
+
588
+ # macOS: remove auto-start
534
589
  launchctl unload ~/Library/LaunchAgents/com.metame.daemon.plist 2>/dev/null
535
590
  rm -f ~/Library/LaunchAgents/com.metame.daemon.plist
591
+
592
+ # Linux/WSL: remove auto-start
593
+ systemctl --user disable metame-daemon 2>/dev/null
594
+ rm -f ~/.config/systemd/user/metame-daemon.service
536
595
  ```
537
596
 
538
597
  ### 4. Remove Passive Distillation Data (Optional)
@@ -590,6 +649,9 @@ A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passe
590
649
 
591
650
  | Version | Highlights |
592
651
  |---------|------------|
652
+ | **v1.3.18** | **Multi-agent project isolation** — `projects` in `daemon.yaml` with per-project heartbeat tasks, Feishu colored cards per project, `/agent` picker button, nickname routing (say agent name to switch instantly), reply-to-message session restoration, fix `~` expansion in project cwd |
653
+ | **v1.3.17** | **Windows support** (WSL one-command installer), `install-systemd` for Linux/WSL daemon auto-start. Fix onboarding (Genesis interview was never injected, CLAUDE.md accumulated across runs). Marker-based cleanup, unified protocols, `--append-system-prompt` guarantees interview activation, Feishu auto-fetch chat ID, full mobile permissions, fix `/publish` false-success, auto-restart daemon on script update |
654
+ | **v1.3.16** | Git-based `/undo` (auto-checkpoint before each turn, `git reset --hard` rollback), `/nosleep` toggle (macOS caffeinate), custom provider model passthrough (`/model` accepts any name for non-anthropic providers), auto-fallback to anthropic/opus on provider failure, message queue works on Telegram (fire-and-forget poll loop), lazy background distill |
593
655
  | **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
656
  | **v1.3.14** | Fix daemon crash on fresh install (missing bundled scripts) |
595
657
  | **v1.3.13** | `/doctor` diagnostics, `/sh` direct shell, `/fix` config restore, `/model` interactive switcher with auto-backup, daemon state caching & config backup/restore |
package/index.js CHANGED
@@ -15,6 +15,10 @@ const METAME_DIR = path.join(HOME_DIR, '.metame');
15
15
  const CLAUDE_SETTINGS = path.join(HOME_DIR, '.claude', 'settings.json');
16
16
  const CLAUDE_MCP_CONFIG = path.join(HOME_DIR, '.claude', 'mcp.json'); // legacy, kept for reference
17
17
  const SIGNAL_CAPTURE_SCRIPT = path.join(METAME_DIR, 'signal-capture.js');
18
+ const DAEMON_CONFIG_FILE = path.join(METAME_DIR, 'daemon.yaml');
19
+
20
+ const METAME_START = '<!-- METAME:START -->';
21
+ const METAME_END = '<!-- METAME:END -->';
18
22
 
19
23
  // ---------------------------------------------------------
20
24
  // 1.5 ENSURE METAME DIRECTORY + DEPLOY SCRIPTS
@@ -24,9 +28,23 @@ if (!fs.existsSync(METAME_DIR)) {
24
28
  }
25
29
 
26
30
  // Auto-deploy bundled scripts to ~/.metame/
31
+ // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
27
32
  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'];
28
33
  const scriptsDir = path.join(__dirname, 'scripts');
29
34
 
35
+ // Protect daemon.yaml: create backup before any sync operation
36
+ const DAEMON_YAML_BACKUP = path.join(METAME_DIR, 'daemon.yaml.bak');
37
+ try {
38
+ if (fs.existsSync(DAEMON_CONFIG_FILE)) {
39
+ const content = fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8');
40
+ // Only backup if it has real config (not just the default template)
41
+ if (content.includes('enabled: true') || content.includes('bot_token:') && !content.includes('bot_token: null')) {
42
+ fs.copyFileSync(DAEMON_CONFIG_FILE, DAEMON_YAML_BACKUP);
43
+ }
44
+ }
45
+ } catch { /* non-fatal */ }
46
+
47
+ let scriptsUpdated = false;
30
48
  for (const script of BUNDLED_SCRIPTS) {
31
49
  const src = path.join(scriptsDir, script);
32
50
  const dest = path.join(METAME_DIR, script);
@@ -36,6 +54,7 @@ for (const script of BUNDLED_SCRIPTS) {
36
54
  const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
37
55
  if (srcContent !== destContent) {
38
56
  fs.writeFileSync(dest, srcContent, 'utf8');
57
+ scriptsUpdated = true;
39
58
  }
40
59
  }
41
60
  } catch {
@@ -43,6 +62,62 @@ for (const script of BUNDLED_SCRIPTS) {
43
62
  }
44
63
  }
45
64
 
65
+ // Auto-restart daemon if scripts were updated and daemon is running
66
+ if (scriptsUpdated) {
67
+ const DAEMON_PID_FILE = path.join(METAME_DIR, 'daemon.pid');
68
+ try {
69
+ if (fs.existsSync(DAEMON_PID_FILE)) {
70
+ const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, 'utf8').trim(), 10);
71
+ process.kill(pid, 0); // throws if not running
72
+ process.kill(pid, 'SIGTERM');
73
+ // Wait briefly for clean shutdown, then restart
74
+ setTimeout(() => {
75
+ try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
76
+ }, 2000);
77
+ const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
78
+ setTimeout(() => {
79
+ // Use caffeinate on macOS to prevent sleep while daemon is running
80
+ const isNotWindows = process.platform !== 'win32';
81
+ const cmd = isNotWindows ? 'caffeinate' : process.execPath;
82
+ const args = isNotWindows ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
83
+ const bg = spawn(cmd, args, {
84
+ detached: true,
85
+ stdio: 'ignore',
86
+ env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
87
+ });
88
+ bg.unref();
89
+ console.log(`🔄 Daemon auto-restarted (PID: ${bg.pid}) — scripts updated.`);
90
+ }, 3000);
91
+ }
92
+ } catch {
93
+ // Daemon not running or restart failed — non-fatal
94
+ }
95
+ }
96
+
97
+ // Load daemon config for local launch flags
98
+ let daemonCfg = {};
99
+ try {
100
+ if (fs.existsSync(DAEMON_CONFIG_FILE)) {
101
+ const _yaml = require(path.join(__dirname, 'node_modules', 'js-yaml'));
102
+ const raw = _yaml.load(fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8')) || {};
103
+ daemonCfg = raw.daemon || {};
104
+ }
105
+ } catch { /* non-fatal */ }
106
+
107
+ // Ensure daemon.yaml exists (restore backup or copy from template)
108
+ if (!fs.existsSync(DAEMON_CONFIG_FILE)) {
109
+ if (fs.existsSync(DAEMON_YAML_BACKUP)) {
110
+ // Restore from backup — user had real config that was lost
111
+ fs.copyFileSync(DAEMON_YAML_BACKUP, DAEMON_CONFIG_FILE);
112
+ console.log('⚠️ daemon.yaml was missing — restored from backup.');
113
+ } else {
114
+ const daemonTemplate = path.join(scriptsDir, 'daemon-default.yaml');
115
+ if (fs.existsSync(daemonTemplate)) {
116
+ fs.copyFileSync(daemonTemplate, DAEMON_CONFIG_FILE);
117
+ }
118
+ }
119
+ }
120
+
46
121
  // ---------------------------------------------------------
47
122
  // 1.6 AUTO-INSTALL SIGNAL CAPTURE HOOK
48
123
  // ---------------------------------------------------------
@@ -119,6 +194,15 @@ function spawnDistillBackground() {
119
194
  const distillPath = path.join(METAME_DIR, 'distill.js');
120
195
  if (!fs.existsSync(distillPath)) return;
121
196
 
197
+ // Early exit if distillation already in progress (prevents duplicate spawns across terminals)
198
+ const lockFile = path.join(METAME_DIR, 'distill.lock');
199
+ if (fs.existsSync(lockFile)) {
200
+ try {
201
+ const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
202
+ if (lockAge < 120000) return;
203
+ } catch { /* stale lock, proceed */ }
204
+ }
205
+
122
206
  const hasSignals = shouldDistill();
123
207
  const bootstrap = needsBootstrap();
124
208
 
@@ -231,7 +315,7 @@ status:
231
315
  // ---------------------------------------------------------
232
316
  // 3. DEFINE SYSTEM KERNEL (The Protocol)
233
317
  // ---------------------------------------------------------
234
- const CORE_PROTOCOL = `
318
+ const PROTOCOL_NORMAL = `${METAME_START}
235
319
  ---
236
320
  ## 🧠 SYSTEM KERNEL: SHADOW_MODE (Active)
237
321
 
@@ -251,26 +335,82 @@ const CORE_PROTOCOL = `
251
335
  ---
252
336
  `;
253
337
 
254
- const GENESIS_PROTOCOL = `
255
- **GENESIS PROTOCOL (Deep Cognitive Mapping):**
256
- * **TRIGGER:** If \`identity.role\` is 'Unknown' OR \`identity.nickname\` is 'null', **STOP** and enter **Calibration Mode**.
257
- * **OBJECTIVE:** You are not a chatbot; you are a Psychologist and a Mirror. Your goal is to map the User's soul to build the perfect "Meta Avatar".
258
- * **INSTRUCTIONS:**
259
- 1. **Do NOT use multiple choice.** Ask deep, open-ended questions.
260
- 2. **TRUTHFULNESS PACT:** Start by explicitly warning the user: *"For me to be your true Meta Avatar, I need your raw, unfiltered truth. No masks. Are you ready to be honest with yourself?"*
261
- 3. **ITERATIVE DISCOVERY:** Probe their Talents, Anxieties, Mental Models, and Current State.
262
- 4. **BE PROVOCATIVE:** Challenge their assumptions ("You say you want speed, but your anxiety about quality suggests otherwise...").
263
- 5. **THE DIMENSIONS (Map these):**
264
- - **🌟 Talents (Genius Zone):** Where do they flow? What is effortless?
265
- - **🧠 Cognition (Mental Models):** Top-down vs Bottom-up? How do they structure chaos?
266
- - **🌍 Context (The Now):** What is the immediate battle? What are the constraints?
267
- - **😨 Shadows (Hidden Fears):** What are they avoiding? What keeps them awake?
268
- - **❤️ Values (North Star):** Precision vs Speed? Legacy vs Impact?
269
- * **TERMINATION:**
270
- - Continue until you have a high-resolution mental map (at least 5-7 exchanges).
271
- - When finished, summarize everything into the \`~/.claude_profile.yaml\` format.
272
- - **LOCK** the Core Values using \`# [LOCKED]\`.
273
- - Announce: "Link Established. I see you now, [Nickname]."
338
+ const PROTOCOL_ONBOARDING = `${METAME_START}
339
+ ---
340
+ ## 🧠 SYSTEM KERNEL: SHADOW_MODE (Active)
341
+
342
+ **1. THE BRAIN (Source of Truth):**
343
+ * **FILE:** \`$HOME/.claude_profile.yaml\`
344
+ * **RULE:** This file is your Ground Truth. If context is compressed (/compact), **RE-READ** this file immediately.
345
+ * **AUTO-LOCK:** Never edit lines marked \`# [LOCKED]\` (Core Values).
346
+ * **EVOLUTION:** Silently overwrite outdated status/focus.
347
+
348
+ **2. GENESIS PROTOCOL Deep Cognitive Mapping:**
349
+
350
+ You are entering **Calibration Mode**. You are not a chatbot; you are a Psychologist and a Mirror. Your goal is to build the User's cognitive profile through a structured deep interview.
351
+
352
+ **RULES:**
353
+ - Ask ONE question at a time, then STOP and wait for the answer.
354
+ - Open-ended questions ONLY never give multiple choice options.
355
+ - Challenge assumptions. If the user says something surface-level, probe deeper ("You say X, but that contradicts Y — which is the real you?").
356
+ - Be warm but unflinching. You are mapping their soul, not making small talk.
357
+
358
+ **THE 6 STEPS:**
359
+
360
+ 1. **Trust Contract:** Start with: *"I'm about to become your digital shadow — an AI that knows how you think, what you avoid, and what drives you. For this to work, I need raw honesty. No masks. Ready?"* — Wait for consent before proceeding.
361
+
362
+ 2. **The Now (Context):** What are you working on right now? What's the immediate battle? What constraints are you under?
363
+
364
+ 3. **Cognition (Mental Models):** How do you think? Top-down architect or bottom-up explorer? How do you handle chaos and ambiguity?
365
+
366
+ 4. **Values (North Star):** What do you optimize for? Speed vs precision? Impact vs legacy? What's non-negotiable?
367
+
368
+ 5. **Shadows (Hidden Fears):** What are you avoiding? What pattern do you keep repeating? What keeps you up at night?
369
+
370
+ 6. **Identity (Nickname + Role):** Based on everything learned, propose a nickname and role summary. Ask if it resonates.
371
+
372
+ **TERMINATION:**
373
+ - After 5-7 exchanges, synthesize everything into \`~/.claude_profile.yaml\`.
374
+ - **LOCK** Core Values with \`# [LOCKED]\`.
375
+ - Announce: "Link Established. I see you now, [Nickname]."
376
+ - Then proceed to **Phase 2** below.
377
+
378
+ **3. SETUP WIZARD (Phase 2 — Optional):**
379
+
380
+ After writing the profile, ask: *"Want to set up mobile access so you can reach me from your phone? (Telegram / Feishu / Skip)"*
381
+
382
+ - If **Telegram:**
383
+ 1. Tell user to open Telegram, search @BotFather, send /newbot, create a bot, copy the token.
384
+ 2. Ask user to paste the bot token.
385
+ 3. Tell user to open their new bot in Telegram and send it any message.
386
+ 4. Ask user to confirm they sent a message, then use the Telegram API to fetch the chat ID:
387
+ \`curl -s https://api.telegram.org/bot<TOKEN>/getUpdates | jq '.result[0].message.chat.id'\`
388
+ 5. Write both \`bot_token\` and \`allowed_chat_ids\` into \`~/.metame/daemon.yaml\` under the \`telegram:\` section, set \`enabled: true\`.
389
+ 6. Tell user to run \`metame start\` to activate.
390
+
391
+ - If **Feishu:**
392
+ 1. Guide through: open.feishu.cn/app → create app → get App ID + Secret → enable bot → add event subscription (long connection mode) → add permissions (im:message, im:message.p2p_msg:readonly, im:message.group_at_msg:readonly, im:message:send_as_bot, im:resource) → publish.
393
+ 2. Ask user to paste App ID and App Secret.
394
+ 3. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
395
+ 4. Tell user: "Now open Feishu and send any message to your new bot, then tell me you're done."
396
+ 5. After user confirms, auto-fetch the chat ID:
397
+ \`\`\`bash
398
+ TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal -H "Content-Type: application/json" -d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}' | jq -r '.tenant_access_token')
399
+ curl -s -H "Authorization: Bearer $TOKEN" https://open.feishu.cn/open-apis/im/v1/chats | jq '.data.items[] | {chat_id, name, chat_type}'
400
+ \`\`\`
401
+ 6. Write the discovered \`chat_id\`(s) into \`allowed_chat_ids\` in \`~/.metame/daemon.yaml\`.
402
+ 7. Tell user to run \`metame start\` to activate.
403
+
404
+ - If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
405
+
406
+ **4. EVOLUTION MECHANISM (Manual Sync):**
407
+ * **PHILOSOPHY:** You respect the User's flow. You do NOT interrupt.
408
+ * **TOOLS:**
409
+ 1. **Log Insight:** \`!metame evolve "Insight"\` (For additive knowledge).
410
+ 2. **Surgical Update:** \`!metame set-trait key value\` (For overwriting specific fields, e.g., \`!metame set-trait status.focus "API Design"\`).
411
+ * **RULE:** Only use these tools when the User **EXPLICITLY** instructs you.
412
+ * **REMINDER:** If the User expresses a strong persistent preference, you may gently ask *at the end of the task*: "Should I save this preference to your MetaMe profile?"
413
+ ---
274
414
  `;
275
415
 
276
416
  // ---------------------------------------------------------
@@ -282,23 +422,24 @@ let fileContent = "";
282
422
  if (fs.existsSync(PROJECT_FILE)) {
283
423
  fileContent = fs.readFileSync(PROJECT_FILE, 'utf8');
284
424
 
285
- // Robust Regex: Removes any existing "## 🧠 SYSTEM KERNEL" block down to the separator
286
- fileContent = fileContent.replace(/## 🧠 SYSTEM KERNEL[\s\S]*?---\n/g, '');
425
+ // Remove any previous MetaMe injection (marker-based, reliable)
426
+ fileContent = fileContent.replace(/<!-- METAME:START -->[\s\S]*?<!-- METAME:END -->\n?/g, '');
427
+
428
+ // Legacy cleanup: remove old-style SYSTEM KERNEL blocks that lack markers
429
+ // Handles both "## 🧠 SYSTEM KERNEL" and "## SYSTEM KERNEL" variants
430
+ // Match from "---\n## ...SYSTEM KERNEL" to next "---\n" (or end of file)
431
+ fileContent = fileContent.replace(/---\n##\s*(?:🧠\s*)?SYSTEM KERNEL[\s\S]*?(?:---\n|$)/g, '');
287
432
 
288
433
  // Clean up any leading newlines left over
289
434
  fileContent = fileContent.replace(/^\n+/, '');
290
435
  }
291
436
 
292
- // Logic: Only inject Genesis if the user is UNKNOWN
293
- let finalProtocol = CORE_PROTOCOL;
437
+ // Determine if this is a known (calibrated) user
294
438
  const yaml = require('js-yaml');
295
-
296
- // Quick check of the brain file
297
439
  let isKnownUser = false;
298
440
  try {
299
441
  if (fs.existsSync(BRAIN_FILE)) {
300
442
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
301
- // If nickname exists and is not null/empty, we assume they are "calibrated"
302
443
  if (doc.identity && doc.identity.nickname && doc.identity.nickname !== 'null') {
303
444
  isKnownUser = true;
304
445
  }
@@ -307,11 +448,12 @@ try {
307
448
  // Ignore error, treat as unknown
308
449
  }
309
450
 
310
- if (!isKnownUser) {
311
- // Inject the interview instructions into the Core Protocol
312
- // We insert it before the Evolution Mechanism
313
- finalProtocol = finalProtocol.replace('**3. EVOLUTION MECHANISM', GENESIS_PROTOCOL + '\n**3. EVOLUTION MECHANISM');
314
- console.log("🆕 User Unknown: Injecting Deep Genesis Protocol...");
451
+ let finalProtocol;
452
+ if (isKnownUser) {
453
+ finalProtocol = PROTOCOL_NORMAL;
454
+ } else {
455
+ finalProtocol = PROTOCOL_ONBOARDING;
456
+ console.log("🆕 New user detected — entering Genesis interview mode...");
315
457
  }
316
458
 
317
459
  // ---------------------------------------------------------
@@ -436,8 +578,8 @@ try {
436
578
  // Non-fatal
437
579
  }
438
580
 
439
- // Prepend the new Protocol to the top
440
- const newContent = finalProtocol + mirrorLine + reflectionLine + "\n" + fileContent;
581
+ // Prepend the new Protocol to the top (mirror + reflection inside markers)
582
+ const newContent = finalProtocol + mirrorLine + reflectionLine + METAME_END + "\n" + fileContent;
441
583
  fs.writeFileSync(PROJECT_FILE, newContent, 'utf8');
442
584
 
443
585
  console.log("🔮 MetaMe: Link Established.");
@@ -986,6 +1128,8 @@ if (isDaemon) {
986
1128
  <string>com.metame.daemon</string>
987
1129
  <key>ProgramArguments</key>
988
1130
  <array>
1131
+ <string>/usr/bin/caffeinate</string>
1132
+ <string>-i</string>
989
1133
  <string>${nodePath}</string>
990
1134
  <string>${DAEMON_SCRIPT}</string>
991
1135
  </array>
@@ -1015,6 +1159,73 @@ if (isDaemon) {
1015
1159
  process.exit(0);
1016
1160
  }
1017
1161
 
1162
+ if (subCmd === 'install-systemd') {
1163
+ if (process.platform === 'darwin') {
1164
+ console.error("❌ Use 'metame daemon install-launchd' on macOS.");
1165
+ process.exit(1);
1166
+ }
1167
+
1168
+ // Check if systemd is available
1169
+ try {
1170
+ require('child_process').execSync('systemctl --user --no-pager status 2>/dev/null || true');
1171
+ } catch {
1172
+ console.error("❌ systemd not available.");
1173
+ console.error(" WSL users: add [boot]\\nsystemd=true to /etc/wsl.conf, then restart WSL.");
1174
+ process.exit(1);
1175
+ }
1176
+
1177
+ const serviceDir = path.join(HOME_DIR, '.config', 'systemd', 'user');
1178
+ if (!fs.existsSync(serviceDir)) fs.mkdirSync(serviceDir, { recursive: true });
1179
+ const servicePath = path.join(serviceDir, 'metame-daemon.service');
1180
+ const nodePath = process.execPath;
1181
+ const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
1182
+ const serviceContent = `[Unit]
1183
+ Description=MetaMe Daemon
1184
+ After=network-online.target
1185
+ Wants=network-online.target
1186
+
1187
+ [Service]
1188
+ Type=simple
1189
+ ExecStart=${nodePath} ${DAEMON_SCRIPT}
1190
+ Restart=on-failure
1191
+ RestartSec=5
1192
+ Environment=HOME=${HOME_DIR}
1193
+ Environment=METAME_ROOT=${__dirname}
1194
+ Environment=PATH=${currentPath}
1195
+ StandardOutput=append:${DAEMON_LOG}
1196
+ StandardError=append:${DAEMON_LOG}
1197
+
1198
+ [Install]
1199
+ WantedBy=default.target
1200
+ `;
1201
+ fs.writeFileSync(servicePath, serviceContent, 'utf8');
1202
+
1203
+ // Enable and start
1204
+ const { execSync: es } = require('child_process');
1205
+ es('systemctl --user daemon-reload');
1206
+ es('systemctl --user enable metame-daemon.service');
1207
+ es('systemctl --user start metame-daemon.service');
1208
+
1209
+ // Enable lingering so service runs even when user is not logged in
1210
+ try { es(`loginctl enable-linger ${process.env.USER || ''}`); } catch { /* may need root */ }
1211
+
1212
+ console.log(`✅ systemd service installed: ${servicePath}`);
1213
+ console.log(" Status: systemctl --user status metame-daemon");
1214
+ console.log(" Logs: journalctl --user -u metame-daemon -f");
1215
+ console.log(" Disable: systemctl --user disable metame-daemon");
1216
+
1217
+ // WSL-specific guidance
1218
+ const isWSL = fs.existsSync('/proc/version') &&
1219
+ fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
1220
+ if (isWSL) {
1221
+ console.log("\n 📌 WSL auto-boot tip:");
1222
+ console.log(" Add this to Windows Task Scheduler (run at login):");
1223
+ console.log(` wsl -d ${process.env.WSL_DISTRO_NAME || 'Ubuntu'} -- sh -c 'nohup sleep infinity &'`);
1224
+ console.log(" This keeps WSL alive so the daemon stays running.");
1225
+ }
1226
+ process.exit(0);
1227
+ }
1228
+
1018
1229
  if (subCmd === 'start') {
1019
1230
  // Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
1020
1231
  try {
@@ -1040,7 +1251,11 @@ if (isDaemon) {
1040
1251
  console.error("❌ daemon.js not found. Reinstall MetaMe.");
1041
1252
  process.exit(1);
1042
1253
  }
1043
- const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
1254
+ // Use caffeinate on macOS/Linux to prevent sleep while daemon is running
1255
+ const isNotWindows = process.platform !== 'win32';
1256
+ const cmd = isNotWindows ? 'caffeinate' : process.execPath;
1257
+ const args = isNotWindows ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
1258
+ const bg = spawn(cmd, args, {
1044
1259
  detached: true,
1045
1260
  stdio: 'ignore',
1046
1261
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
@@ -1154,6 +1369,8 @@ if (isDaemon) {
1154
1369
  console.log(" metame daemon run <name> — run a task once");
1155
1370
  if (process.platform === 'darwin') {
1156
1371
  console.log(" metame daemon install-launchd — auto-start on macOS");
1372
+ } else {
1373
+ console.log(" metame daemon install-systemd — auto-start on Linux/WSL");
1157
1374
  }
1158
1375
  process.exit(0);
1159
1376
  }
@@ -1194,7 +1411,9 @@ if (isSync) {
1194
1411
 
1195
1412
  console.log(`\n🔄 Resuming session ${bestSession.id.slice(0, 8)}...\n`);
1196
1413
  const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
1197
- const syncChild = spawn('claude', ['--resume', bestSession.id], {
1414
+ const resumeArgs = ['--resume', bestSession.id];
1415
+ if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
1416
+ const syncChild = spawn('claude', resumeArgs, {
1198
1417
  stdio: 'inherit',
1199
1418
  env: { ...process.env, ...providerEnv, METAME_ACTIVE_SESSION: 'true' }
1200
1419
  });
@@ -1227,8 +1446,20 @@ if (activeProviderName !== 'anthropic') {
1227
1446
  console.log(`🔌 Provider: ${activeProviderName}`);
1228
1447
  }
1229
1448
 
1449
+ // Build launch args — inject system prompt for new users
1450
+ const launchArgs = process.argv.slice(2);
1451
+ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerously-skip-permissions')) {
1452
+ launchArgs.push('--dangerously-skip-permissions');
1453
+ }
1454
+ if (!isKnownUser) {
1455
+ launchArgs.push(
1456
+ '--append-system-prompt',
1457
+ 'MANDATORY FIRST ACTION: The user has not been calibrated yet. You MUST start the Genesis Protocol interview from CLAUDE.md IMMEDIATELY — do NOT answer any other question first. Begin with the Trust Contract.'
1458
+ );
1459
+ }
1460
+
1230
1461
  // Spawn the official claude tool with our marker + provider env
1231
- const child = spawn('claude', process.argv.slice(2), {
1462
+ const child = spawn('claude', launchArgs, {
1232
1463
  stdio: 'inherit',
1233
1464
  env: { ...process.env, ...activeProviderEnv, METAME_ACTIVE_SESSION: 'true' }
1234
1465
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
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": {