metame-cli 1.3.17 โ†’ 1.3.20

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
@@ -54,6 +54,10 @@
54
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
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
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.
57
+ * **๐Ÿ”€ Parallel Multi-Agent Chats (v1.3.19):** Assign each agent its own dedicated Feishu/Telegram group. Messages in different groups execute in parallel โ€” no waiting for each other. Route `chatId โ†’ agent` via `chat_agent_map` in `daemon.yaml`. Create a new group, send `/bind <name>` to register it instantly.
58
+ * **๐Ÿ”ง Config Hot-Reload Fix (v1.3.19):** `allowed_chat_ids` is now read dynamically on every message โ€” no restart needed after editing `daemon.yaml`. `/fix` config restore now merges current `chatId` settings so manually-added groups are never lost.
59
+ * **๐Ÿ›ก๏ธ Daemon Auto-Restart via LaunchAgent (v1.3.19):** MetaMe's npm daemon is now managed by macOS launchd. Crashes or unexpected exits trigger an automatic restart after 5 seconds.
60
+ * **๐Ÿ‘ฅ Operator Permissions & Read-Only Mode (v1.3.19):** Add `operator_ids` to restrict who can execute Claude commands in shared groups. Non-operators can still chat and query (read/search/web only) โ€” they just can't edit files, run bash, or trigger slash commands. Use `/myid` to discover any user's Feishu open_id.
57
61
 
58
62
  ## ๐Ÿ›  Prerequisites
59
63
 
@@ -341,6 +345,9 @@ Bot: ๅ›ž้€€ๅˆฐๅ“ชไธ€่ฝฎ๏ผŸ
341
345
  | `/budget` | Today's token usage |
342
346
  | `/quiet` | Silence mirror/reflections for 48h |
343
347
  | `/reload` | Manually reload daemon.yaml (also auto-reloads on file change) |
348
+ | `/bind <name>` | Register current group as a dedicated agent chat โ€” opens directory browser to pick working directory |
349
+ | `/chatid` | Show the current group's chat ID |
350
+ | `/myid` | Show your own Feishu sender open_id (for configuring `operator_ids`) |
344
351
 
345
352
  **Heartbeat Tasks:**
346
353
 
@@ -395,10 +402,126 @@ Each step runs in the same Claude Code session. Step outputs automatically becom
395
402
  **Security:**
396
403
 
397
404
  * `allowed_chat_ids` whitelist โ€” unauthorized users silently ignored (empty = deny all)
405
+ * `operator_ids` โ€” within an allowed group, restrict command execution to specific users; non-operators get read-only chat mode
398
406
  * `dangerously_skip_permissions` enabled by default for mobile (users can't click "allow" on phone โ€” security relies on the chat ID whitelist)
399
407
  * `~/.metame/` directory set to mode 700
400
408
  * Bot tokens stored locally, never transmitted
401
409
 
410
+ ### Multi-Agent Projects โ€” Context Isolation & Nickname Routing (v1.3.18)
411
+
412
+ 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.
413
+
414
+ **How it works:**
415
+
416
+ 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.
417
+
418
+ **Setup via conversation:**
419
+
420
+ The easiest way to add an agent is to tell the bot:
421
+
422
+ > *"Add a project called 'work' pointing to ~/my-project, nickname is 'ๅทฅไฝœ'"*
423
+
424
+ Or edit `~/.metame/daemon.yaml` directly:
425
+
426
+ ```yaml
427
+ projects:
428
+ my_agent:
429
+ name: "My Agent" # Display name in notifications
430
+ icon: "๐Ÿค–" # Emoji shown in Feishu cards
431
+ color: "blue" # Feishu card color: blue|orange|green|purple|red|grey
432
+ cwd: "~/my-project" # Working directory for this agent
433
+ nicknames: [nickname1, nickname2] # Wake words (matched at message start)
434
+ heartbeat_tasks: [] # Scheduled tasks for this project (optional)
435
+ ```
436
+
437
+ **Available colors:** `blue` ยท `orange` ยท `green` ยท `purple` ยท `red` ยท `grey` ยท `turquoise`
438
+
439
+ **Phone commands:**
440
+
441
+ | Action | How |
442
+ |--------|-----|
443
+ | Switch agent | Send the nickname alone: `่ดพ็ปดๆ–ฏ` โ†’ `๐Ÿค– Jarvis ๅœจ็บฟ` |
444
+ | Switch + ask | `่ดพ็ปดๆ–ฏ, what's the status?` โ†’ switches then asks Claude |
445
+ | Pick from list | `/agent` โ†’ tap button to switch |
446
+ | Continue a reply | Reply to any bot message โ†’ session auto-restored |
447
+ | View all tasks | `/tasks` โ†’ grouped by project |
448
+ | Run a task | `/run <task-name>` |
449
+
450
+ **Nickname routing rules:**
451
+ - Matched at **message start only** โ€” mentioning a nickname mid-sentence never triggers a switch
452
+ - Pure nickname (no content after) โ†’ instant switch, zero token cost, bypasses cooldown
453
+ - Nickname + content โ†’ switch then send content to Claude
454
+
455
+ **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.
456
+
457
+ ### Parallel Multi-Agent Chats & `/bind` Command (v1.3.19)
458
+
459
+ Give each agent its own dedicated group chat โ€” messages to different groups execute simultaneously without blocking each other.
460
+
461
+ **How it works:**
462
+
463
+ Each group is mapped to a specific agent via `chat_agent_map` in `daemon.yaml`. When a message arrives in a group, the daemon looks up which agent owns that `chatId` and dispatches the Claude call to that agent's working directory โ€” fully parallel.
464
+
465
+ **Setup โ€” `/bind` command (recommended):**
466
+
467
+ 1. Create a new Feishu or Telegram group and add your bot.
468
+ 2. In the group, send `/bind <name>` (e.g., `/bind backend`).
469
+ 3. The bot opens a Finder-style directory browser โ€” tap folders to navigate, tap a folder name to select it as the working directory.
470
+ 4. Done. The bot automatically:
471
+ - Adds the group's `chatId` to `allowed_chat_ids`
472
+ - Creates a `chat_agent_map` entry routing this group to the agent
473
+ - Creates a `projects` entry for the agent
474
+ - Sends a welcome card
475
+
476
+ > **No whitelist required:** `/bind` works in any group โ€” the new group self-registers without needing to be pre-approved in `allowed_chat_ids`.
477
+
478
+ > **Re-binding:** Send `/bind <name>` again in the same group to overwrite the previous configuration.
479
+
480
+ **Manual setup** (`~/.metame/daemon.yaml`):
481
+
482
+ ```yaml
483
+ chat_agent_map:
484
+ "oc_abc123": "backend" # chatId โ†’ project key
485
+ "oc_def456": "frontend"
486
+
487
+ projects:
488
+ backend:
489
+ name: "Backend API"
490
+ cwd: "~/projects/api"
491
+ frontend:
492
+ name: "Frontend App"
493
+ cwd: "~/projects/app"
494
+ ```
495
+
496
+ **`/chatid` command:**
497
+
498
+ In any authorized group, send `/chatid` and the bot replies with the current group's `chatId`. Useful for manual configuration.
499
+
500
+ | Command | Description |
501
+ |---------|-------------|
502
+ | `/bind <name>` | Register current group as a dedicated agent chat โ€” opens directory browser to pick working directory |
503
+ | `/chatid` | Show the current group's chat ID |
504
+ | `/myid` | Show your own Feishu sender open_id |
505
+
506
+ **Operator Permissions (`operator_ids`):**
507
+
508
+ In shared groups (e.g., a group with a colleague or tester), you can restrict who can execute Claude commands. Non-operators get a read-only chat mode โ€” they can ask questions and search, but can't edit files, run bash, or trigger slash commands.
509
+
510
+ ```yaml
511
+ feishu:
512
+ operator_ids:
513
+ - "ou_abc123yourid" # Only these users can execute commands
514
+ ```
515
+
516
+ Use `/myid` in any Feishu group to get a user's open_id. Then add it to `operator_ids` to grant full access.
517
+
518
+ | User type | Chat & query | Slash commands | Write / Edit / Bash |
519
+ |-----------|:---:|:---:|:---:|
520
+ | Operator | โœ… | โœ… | โœ… |
521
+ | Non-operator | โœ… | โŒ | โŒ |
522
+
523
+ > If `operator_ids` is empty, all whitelisted users have full access (default behavior).
524
+
402
525
  ### Provider Relay โ€” Third-Party Model Support (v1.3.11)
403
526
 
404
527
  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.
@@ -602,6 +725,8 @@ A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passe
602
725
 
603
726
  | Version | Highlights |
604
727
  |---------|------------|
728
+ | **v1.3.19** | **Parallel multi-agent group chats** โ€” `chat_agent_map` routes chatId โ†’ agent for true parallel execution; `/bind` command for one-tap group registration with Finder-style directory browser; `/chatid` to look up group ID; `allowed_chat_ids` hot-reload fix (read per message, no restart); `/fix` now merges current chatId config; daemon auto-restart via macOS LaunchAgent (5-second recovery); **operator_ids** permission layer โ€” non-operators get read-only chat mode (query/search, no write/execute); `/myid` command to retrieve Feishu open_id |
729
+ | **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 |
605
730
  | **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 |
606
731
  | **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 |
607
732
  | **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) |
package/index.js CHANGED
@@ -28,9 +28,22 @@ if (!fs.existsSync(METAME_DIR)) {
28
28
  }
29
29
 
30
30
  // Auto-deploy bundled scripts to ~/.metame/
31
+ // IMPORTANT: daemon.yaml is USER CONFIG โ€” never overwrite it. Only daemon-default.yaml (template) is synced.
31
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'];
32
33
  const scriptsDir = path.join(__dirname, 'scripts');
33
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
+
34
47
  let scriptsUpdated = false;
35
48
  for (const script of BUNDLED_SCRIPTS) {
36
49
  const src = path.join(scriptsDir, script);
@@ -63,7 +76,11 @@ if (scriptsUpdated) {
63
76
  }, 2000);
64
77
  const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
65
78
  setTimeout(() => {
66
- const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
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, {
67
84
  detached: true,
68
85
  stdio: 'ignore',
69
86
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
@@ -77,11 +94,27 @@ if (scriptsUpdated) {
77
94
  }
78
95
  }
79
96
 
80
- // Ensure daemon.yaml exists (copy from template if missing)
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)
81
108
  if (!fs.existsSync(DAEMON_CONFIG_FILE)) {
82
- const daemonTemplate = path.join(scriptsDir, 'daemon-default.yaml');
83
- if (fs.existsSync(daemonTemplate)) {
84
- fs.copyFileSync(daemonTemplate, 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
+ }
85
118
  }
86
119
  }
87
120
 
@@ -161,6 +194,30 @@ function spawnDistillBackground() {
161
194
  const distillPath = path.join(METAME_DIR, 'distill.js');
162
195
  if (!fs.existsSync(distillPath)) return;
163
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
+
206
+ // 4-hour cooldown: check last distill timestamp from profile
207
+ const cooldownMs = 4 * 60 * 60 * 1000;
208
+ try {
209
+ const profilePath = path.join(process.env.HOME || '', '.claude_profile.yaml');
210
+ if (fs.existsSync(profilePath)) {
211
+ const yaml = require('js-yaml');
212
+ const profile = yaml.load(fs.readFileSync(profilePath, 'utf8'));
213
+ const distillLog = profile && profile.evolution && profile.evolution.auto_distill;
214
+ if (Array.isArray(distillLog) && distillLog.length > 0) {
215
+ const lastTs = new Date(distillLog[distillLog.length - 1].ts).getTime();
216
+ if (Date.now() - lastTs < cooldownMs) return;
217
+ }
218
+ }
219
+ } catch { /* non-fatal, proceed */ }
220
+
164
221
  const hasSignals = shouldDistill();
165
222
  const bootstrap = needsBootstrap();
166
223
 
@@ -263,7 +320,7 @@ runExpiryCleanup();
263
320
  if (!fs.existsSync(BRAIN_FILE)) {
264
321
  const initialProfile = `identity:
265
322
  role: Unknown
266
- nickname: null
323
+ locale: null
267
324
  status:
268
325
  focus: Initializing
269
326
  `;
@@ -325,12 +382,12 @@ You are entering **Calibration Mode**. You are not a chatbot; you are a Psycholo
325
382
 
326
383
  5. **Shadows (Hidden Fears):** What are you avoiding? What pattern do you keep repeating? What keeps you up at night?
327
384
 
328
- 6. **Identity (Nickname + Role):** Based on everything learned, propose a nickname and role summary. Ask if it resonates.
385
+ 6. **Identity (Role + Locale):** Based on everything learned, propose a role summary and confirm their preferred language (locale). Ask if it resonates.
329
386
 
330
387
  **TERMINATION:**
331
388
  - After 5-7 exchanges, synthesize everything into \`~/.claude_profile.yaml\`.
332
389
  - **LOCK** Core Values with \`# [LOCKED]\`.
333
- - Announce: "Link Established. I see you now, [Nickname]."
390
+ - Announce: "Link Established. Profile calibrated."
334
391
  - Then proceed to **Phase 2** below.
335
392
 
336
393
  **3. SETUP WIZARD (Phase 2 โ€” Optional):**
@@ -398,7 +455,7 @@ let isKnownUser = false;
398
455
  try {
399
456
  if (fs.existsSync(BRAIN_FILE)) {
400
457
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
401
- if (doc.identity && doc.identity.nickname && doc.identity.nickname !== 'null') {
458
+ if (doc.identity && doc.identity.locale && doc.identity.locale !== 'null') {
402
459
  isKnownUser = true;
403
460
  }
404
461
  }
@@ -1086,6 +1143,8 @@ if (isDaemon) {
1086
1143
  <string>com.metame.daemon</string>
1087
1144
  <key>ProgramArguments</key>
1088
1145
  <array>
1146
+ <string>/usr/bin/caffeinate</string>
1147
+ <string>-i</string>
1089
1148
  <string>${nodePath}</string>
1090
1149
  <string>${DAEMON_SCRIPT}</string>
1091
1150
  </array>
@@ -1207,7 +1266,11 @@ WantedBy=default.target
1207
1266
  console.error("โŒ daemon.js not found. Reinstall MetaMe.");
1208
1267
  process.exit(1);
1209
1268
  }
1210
- const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
1269
+ // Use caffeinate on macOS/Linux to prevent sleep while daemon is running
1270
+ const isNotWindows = process.platform !== 'win32';
1271
+ const cmd = isNotWindows ? 'caffeinate' : process.execPath;
1272
+ const args = isNotWindows ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
1273
+ const bg = spawn(cmd, args, {
1211
1274
  detached: true,
1212
1275
  stdio: 'ignore',
1213
1276
  env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
@@ -1363,7 +1426,9 @@ if (isSync) {
1363
1426
 
1364
1427
  console.log(`\n๐Ÿ”„ Resuming session ${bestSession.id.slice(0, 8)}...\n`);
1365
1428
  const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
1366
- const syncChild = spawn('claude', ['--resume', bestSession.id], {
1429
+ const resumeArgs = ['--resume', bestSession.id];
1430
+ if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
1431
+ const syncChild = spawn('claude', resumeArgs, {
1367
1432
  stdio: 'inherit',
1368
1433
  env: { ...process.env, ...providerEnv, METAME_ACTIVE_SESSION: 'true' }
1369
1434
  });
@@ -1398,6 +1463,9 @@ if (activeProviderName !== 'anthropic') {
1398
1463
 
1399
1464
  // Build launch args โ€” inject system prompt for new users
1400
1465
  const launchArgs = process.argv.slice(2);
1466
+ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerously-skip-permissions')) {
1467
+ launchArgs.push('--dangerously-skip-permissions');
1468
+ }
1401
1469
  if (!isKnownUser) {
1402
1470
  launchArgs.push(
1403
1471
  '--append-system-prompt',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.17",
3
+ "version": "1.3.20",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,8 +12,27 @@ feishu:
12
12
  app_secret: null
13
13
  allowed_chat_ids: []
14
14
 
15
+ projects:
16
+ # Per-project heartbeat tasks. Each project's tasks are isolated and
17
+ # notifications arrive as colored Feishu cards (visually distinct).
18
+ #
19
+ # example_project:
20
+ # name: "My Project"
21
+ # icon: "๐ŸŽฌ"
22
+ # color: "orange" # blue|orange|green|red|grey|purple|turquoise
23
+ # heartbeat_tasks:
24
+ # - name: "daily-task"
25
+ # cwd: "~/AGI/MyProject"
26
+ # model: "sonnet"
27
+ # interval: "24h"
28
+ # notify: true
29
+ # enabled: true
30
+ # prompt: "..."
31
+ # allowedTools: [Read, Write, WebSearch]
32
+
15
33
  heartbeat:
16
34
  tasks: []
35
+ # Legacy flat tasks (no project isolation). New tasks should go under projects: above.
17
36
  # Examples โ€” uncomment or add your own:
18
37
  #
19
38
  # Scheduled task (calls claude -p with your profile context):