metame-cli 1.5.10 → 1.5.12

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.
Files changed (67) hide show
  1. package/README.md +49 -6
  2. package/index.js +266 -72
  3. package/package.json +7 -3
  4. package/scripts/daemon-admin-commands.js +34 -0
  5. package/scripts/daemon-agent-commands.js +6 -2
  6. package/scripts/daemon-bridges.js +41 -10
  7. package/scripts/daemon-claude-engine.js +128 -29
  8. package/scripts/daemon-command-router.js +16 -0
  9. package/scripts/daemon-command-session-route.js +3 -1
  10. package/scripts/daemon-default.yaml +3 -1
  11. package/scripts/daemon-engine-runtime.js +1 -5
  12. package/scripts/daemon-message-pipeline.js +113 -44
  13. package/scripts/daemon-ops-commands.js +25 -11
  14. package/scripts/daemon-reactive-lifecycle.js +757 -76
  15. package/scripts/daemon-session-commands.js +3 -2
  16. package/scripts/daemon-session-store.js +82 -27
  17. package/scripts/daemon-team-dispatch.js +21 -5
  18. package/scripts/daemon-utils.js +3 -1
  19. package/scripts/daemon.js +80 -2
  20. package/scripts/distill.js +1 -1
  21. package/scripts/docs/file-transfer.md +1 -0
  22. package/scripts/docs/maintenance-manual.md +55 -2
  23. package/scripts/docs/pointer-map.md +34 -0
  24. package/scripts/feishu-adapter.js +25 -0
  25. package/scripts/hooks/intent-file-transfer.js +2 -1
  26. package/scripts/hooks/intent-perpetual.js +109 -0
  27. package/scripts/hooks/intent-research.js +112 -0
  28. package/scripts/intent-registry.js +4 -0
  29. package/scripts/memory-extract.js +29 -1
  30. package/scripts/memory-nightly-reflect.js +104 -0
  31. package/scripts/ops-mission-queue.js +258 -0
  32. package/scripts/ops-verifier.js +197 -0
  33. package/scripts/signal-capture.js +3 -3
  34. package/scripts/skill-evolution.js +11 -2
  35. package/skills/agent-browser/SKILL.md +153 -0
  36. package/skills/agent-reach/SKILL.md +66 -0
  37. package/skills/agent-reach/evolution.json +13 -0
  38. package/skills/deep-research/SKILL.md +77 -0
  39. package/skills/find-skills/SKILL.md +133 -0
  40. package/skills/heartbeat-task-manager/SKILL.md +63 -0
  41. package/skills/macos-local-orchestrator/SKILL.md +192 -0
  42. package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
  43. package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
  44. package/skills/macos-mail-calendar/SKILL.md +394 -0
  45. package/skills/mcp-installer/SKILL.md +138 -0
  46. package/skills/skill-creator/LICENSE.txt +202 -0
  47. package/skills/skill-creator/README.md +72 -0
  48. package/skills/skill-creator/SKILL.md +96 -0
  49. package/skills/skill-creator/evolution.json +6 -0
  50. package/skills/skill-creator/references/creation-guide.md +116 -0
  51. package/skills/skill-creator/references/evolution-guide.md +74 -0
  52. package/skills/skill-creator/references/output-patterns.md +82 -0
  53. package/skills/skill-creator/references/workflows.md +28 -0
  54. package/skills/skill-creator/scripts/align_all.py +32 -0
  55. package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
  56. package/skills/skill-creator/scripts/init_skill.py +303 -0
  57. package/skills/skill-creator/scripts/merge_evolution.py +70 -0
  58. package/skills/skill-creator/scripts/package_skill.py +110 -0
  59. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  60. package/skills/skill-creator/scripts/setup.py +141 -0
  61. package/skills/skill-creator/scripts/smart_stitch.py +82 -0
  62. package/skills/skill-manager/SKILL.md +112 -0
  63. package/skills/skill-manager/scripts/delete_skill.py +31 -0
  64. package/skills/skill-manager/scripts/list_skills.py +61 -0
  65. package/skills/skill-manager/scripts/scan_and_check.py +125 -0
  66. package/skills/skill-manager/scripts/sync_index.py +144 -0
  67. package/skills/skill-manager/scripts/update_helper.py +39 -0
package/README.md CHANGED
@@ -26,8 +26,9 @@ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bas
26
26
 
27
27
  ---
28
28
 
29
- > ### 🚀 v1.5.4 — Agent Soul Layer Auto-Repair & Intent Engine
29
+ > ### 🚀 v1.5.10 — Perpetual Task Engine & Agent Soul Layer
30
30
  >
31
+ > - **Perpetual task engine**: Any project can run as a 24/7 autonomous agent loop with event sourcing, verifier gates, budget/depth control, and reconciliation. Domain-agnostic — works for research, code auditing, documentation, anything. Configure with `reactive: true` and an optional `perpetual.yaml`.
31
32
  > - **Cross-device dispatch**: Team members can run on remote machines. Add `peer: windows` to a member and messages route automatically via a Feishu relay chat — HMAC-signed, dedup-protected, zero manual routing.
32
33
  > - **`/dispatch peers`**: View remote dispatch config, relay chat, and all remote team members from mobile.
33
34
  > - **`dispatch_to peer:project`**: Dispatch tasks to remote peers from CLI, admin commands, or Claude sessions.
@@ -185,9 +186,51 @@ Chain skills into multi-step workflows — research → write → publish — fu
185
186
  | `cwd` | Working directory for the task |
186
187
  | `timeout` | Max run time |
187
188
 
188
- > **Scheduled tasks require system registration.** Run `metame daemon install-launchd` (macOS) or `metame daemon install-task-scheduler` (Windows) and tasks fire on schedule even with the screen locked — as long as the machine is on.
189
+ > **Scheduled tasks require the daemon to be running.** On macOS, `metame start` auto-registers with launchd (auto-restart on crash/reboot). On Windows, run `metame daemon install-task-scheduler`. Tasks fire on schedule even with the screen locked — as long as the machine is on.
189
190
 
190
- ### 5. Skills That Evolve Themselves
191
+ ### 5. Perpetual Task Engine — Agents That Never Stop
192
+
193
+ Any project can run as an autonomous perpetual loop. The daemon drives the cycle: agent acts → verifier gates → event log records → next dispatch. Domain-agnostic — research, code auditing, documentation, anything.
194
+
195
+ **How it works:**
196
+
197
+ ```yaml
198
+ # daemon.yaml — register a perpetual project
199
+ scientist:
200
+ name: Research Director
201
+ reactive: true # enables perpetual lifecycle
202
+ cwd: ~/AGI/AgentScientist
203
+ team:
204
+ - key: sci_scout
205
+ name: Literature Scout
206
+ cwd: ~/AGI/AgentScientist/team/scout
207
+ ```
208
+
209
+ **What the platform provides (zero project-specific code in MetaMe):**
210
+ - **Event sourcing**: All state changes logged to `~/.metame/events/<key>.jsonl` — single source of truth, daemon-exclusive writes
211
+ - **Budget & depth gates**: Auto-pause when token budget or loop depth exceeded
212
+ - **Verifier hooks**: Project scripts validate phase completion with objective checks (file existence → structure checks → API verification)
213
+ - **Reconciliation**: Heartbeat detects stale projects and notifies you
214
+ - **`/status perpetual`**: See all running projects — phase, depth, mission, last activity
215
+
216
+ **Convention over configuration**: Drop a `scripts/verifier.js` in your project and it just works. Need custom signals? Add a `perpetual.yaml`:
217
+
218
+ ```yaml
219
+ # perpetual.yaml — optional, override defaults
220
+ completion_signal: RESEARCH_COMPLETE
221
+ verifier: scripts/research-verifier.js
222
+ max_depth: 50
223
+ ```
224
+
225
+ ```
226
+ Agent output → daemon parses signals
227
+ → budget gate → depth gate → verifier gate
228
+ → event logged → state file regenerated
229
+ → next dispatch (fresh session) OR mission complete
230
+ → archive → next mission from queue → repeat
231
+ ```
232
+
233
+ ### 6. Skills That Evolve Themselves
191
234
 
192
235
  MetaMe's current skill loop is queue-driven and reviewable (not magic black-box automation).
193
236
 
@@ -231,7 +274,7 @@ MetaMe is the orchestration layer. Claude Code and Codex are the engines. You in
231
274
  | 2. Genesis Interview | Just chat — MetaMe auto-starts a deep soul interview on first run → builds `~/.claude_profile.yaml` |
232
275
  | 3. Connect phone | Say "help me set up mobile access" → interactive Telegram/Feishu bot wizard |
233
276
  | 4. Start daemon | `metame start` → background daemon launches, bot goes online |
234
- | 5. Register with OS | macOS: `metame daemon install-launchd` · Windows: `metame daemon install-task-scheduler` |
277
+ | 5. Register with OS | macOS: automatic (step 4 registers with launchd) · Windows: `metame daemon install-task-scheduler` |
235
278
 
236
279
  > **First time?** Just run `metame` and talk naturally. Everything is conversational.
237
280
 
@@ -257,7 +300,7 @@ rm -rf ~/.metame ~/.claude_profile.yaml
257
300
  ```
258
301
 
259
302
  Optional service cleanup:
260
- - macOS: `launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.metame.daemon.plist && rm -f ~/Library/LaunchAgents/com.metame.daemon.plist`
303
+ - macOS: `launchctl bootout gui/$(id -u)/com.metame.npm-daemon && rm -f ~/Library/LaunchAgents/com.metame.npm-daemon.plist`
261
304
  - Windows: `schtasks /delete /tn "MetaMe-Daemon" /f`
262
305
  - Linux/WSL: `systemctl --user disable --now metame && rm -f ~/.config/systemd/user/metame.service`
263
306
 
@@ -625,7 +668,7 @@ For day-2 operations and troubleshooting (engine routing, codex login/rate-limit
625
668
 
626
669
  Install directly into Claude Code without npm: `claude plugin install github:Yaron9/MetaMe/plugin`
627
670
 
628
- All features included. Plugin auto-starts daemon on `SessionStart` (if `daemon.yaml` exists). Does **not** auto-register OS service — after reboot, open Claude once or start daemon manually.
671
+ All features included. Plugin auto-starts daemon on `SessionStart` (if `daemon.yaml` exists). On macOS, `metame start` auto-registers with launchd — daemon auto-restarts on crash/reboot without opening Claude.
629
672
 
630
673
  ## Contributing
631
674
 
package/index.js CHANGED
@@ -173,10 +173,18 @@ if (!fs.existsSync(METAME_DIR)) {
173
173
  // DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
174
174
  // ---------------------------------------------------------
175
175
 
176
- // Dev mode: when running from git repo, symlink instead of copy.
176
+ // Dev mode: when running from the real git repo, symlink instead of copy.
177
177
  // This ensures source files and runtime files are always the same,
178
178
  // preventing agents from accidentally editing copies instead of source.
179
- const IS_DEV_MODE = fs.existsSync(path.join(__dirname, '.git'));
179
+ // IMPORTANT: git worktrees have a `.git` FILE (not directory) pointing to the main repo.
180
+ // They must NOT be treated as dev mode — deploying from a worktree would overwrite
181
+ // production symlinks with stale code. Only a real .git directory qualifies.
182
+ const IS_DEV_MODE = (() => {
183
+ const dotGit = path.join(__dirname, '.git');
184
+ try {
185
+ return fs.statSync(dotGit).isDirectory();
186
+ } catch { return false; }
187
+ })();
180
188
 
181
189
  /**
182
190
  * Sync files from srcDir to destDir.
@@ -257,6 +265,70 @@ function readRunningDaemonPid({ pidFile, lockFile }) {
257
265
  return null;
258
266
  }
259
267
 
268
+ // --- macOS launchd integration ---
269
+ const LAUNCHD_LABEL = 'com.metame.npm-daemon';
270
+ const LAUNCHD_PLIST = path.join(HOME_DIR, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
271
+
272
+ function ensureLaunchdPlist({ daemonScript, daemonLog }) {
273
+ const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
274
+ if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
275
+ const nodePath = process.execPath;
276
+ const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
277
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
278
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
279
+ <plist version="1.0">
280
+ <dict>
281
+ <key>Label</key>
282
+ <string>${LAUNCHD_LABEL}</string>
283
+ <key>ProgramArguments</key>
284
+ <array>
285
+ <string>/usr/bin/caffeinate</string>
286
+ <string>-i</string>
287
+ <string>${nodePath}</string>
288
+ <string>${daemonScript}</string>
289
+ </array>
290
+ <key>RunAtLoad</key>
291
+ <true/>
292
+ <key>KeepAlive</key>
293
+ <true/>
294
+ <key>ThrottleInterval</key>
295
+ <integer>30</integer>
296
+ <key>StandardOutPath</key>
297
+ <string>${daemonLog}</string>
298
+ <key>StandardErrorPath</key>
299
+ <string>${daemonLog}</string>
300
+ <key>WorkingDirectory</key>
301
+ <string>${HOME_DIR}</string>
302
+ <key>EnvironmentVariables</key>
303
+ <dict>
304
+ <key>PATH</key>
305
+ <string>${currentPath}</string>
306
+ <key>HOME</key>
307
+ <string>${HOME_DIR}</string>
308
+ <key>METAME_ROOT</key>
309
+ <string>${__dirname}</string>
310
+ <key>LAUNCHED_BY_LAUNCHD</key>
311
+ <string>1</string>
312
+ </dict>
313
+ </dict>
314
+ </plist>`;
315
+ const existing = fs.existsSync(LAUNCHD_PLIST) ? fs.readFileSync(LAUNCHD_PLIST, 'utf8') : '';
316
+ if (existing !== plist) {
317
+ // Bootout old version if loaded, ignore errors
318
+ try { execSync(`launchctl bootout gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`); } catch { /* not loaded */ }
319
+ fs.writeFileSync(LAUNCHD_PLIST, plist, 'utf8');
320
+ }
321
+ return LAUNCHD_PLIST;
322
+ }
323
+
324
+ function launchdIsRunning() {
325
+ try {
326
+ const out = execSync(`launchctl list ${LAUNCHD_LABEL} 2>/dev/null`, { encoding: 'utf8' });
327
+ const m = out.match(/"PID"\s*=\s*(\d+)/);
328
+ return m ? parseInt(m[1], 10) : null;
329
+ } catch { return null; }
330
+ }
331
+
260
332
  function requestDaemonRestart({
261
333
  reason = 'manual-restart',
262
334
  daemonPidFile = path.join(METAME_DIR, 'daemon.pid'),
@@ -266,6 +338,16 @@ function requestDaemonRestart({
266
338
  const pid = readRunningDaemonPid({ pidFile: daemonPidFile, lockFile: daemonLockFile });
267
339
  if (!pid) return { ok: false, status: 'not_running' };
268
340
 
341
+ // macOS: use launchctl kickstart -k for atomic restart (no orphan/race)
342
+ if (process.platform === 'darwin') {
343
+ try {
344
+ execSync(`launchctl kickstart -k gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
345
+ return { ok: true, status: 'signaled', pid };
346
+ } catch {
347
+ // launchd job not loaded — fall through to SIGUSR2
348
+ }
349
+ }
350
+
269
351
  if (process.platform !== 'win32') {
270
352
  try {
271
353
  process.kill(pid, 'SIGUSR2');
@@ -330,16 +412,21 @@ try {
330
412
  }
331
413
  } catch { /* non-fatal */ }
332
414
 
333
- // Worktree guard: team members running in worktrees must NEVER deploy to ~/.metame/
334
- // Their worktree is an isolated sandbox — deploying would overwrite production symlinks.
335
- // Detect any .worktrees/ parent in the path (covers both ~/.metame/worktrees/ and repo-local .worktrees/).
336
- const _isInWorktree = __dirname.split(path.sep).includes('.worktrees') ||
337
- __dirname.startsWith(path.join(HOME_DIR, '.metame', 'worktrees'));
415
+ // Worktree guard: worktrees must NEVER deploy to ~/.metame/ — they are isolated sandboxes.
416
+ // Detection: git worktrees have a .git FILE (pointing to main repo), not a .git DIRECTORY.
417
+ // This is reliable regardless of worktree path conventions.
418
+ const _dotGitPath = path.join(__dirname, '.git');
419
+ const _isInWorktree = (() => {
420
+ try {
421
+ const stat = fs.statSync(_dotGitPath);
422
+ return stat.isFile(); // .git is a file → worktree; directory → main repo; missing → npm install
423
+ } catch { return false; }
424
+ })();
338
425
  if (_isInWorktree) {
339
426
  console.error(`\n${icon("stop")} ACTION BLOCKED: Worktree Deploy Prevented`);
340
- console.error(` You are running from a worktree (${path.basename(__dirname)}).`);
427
+ console.error(` You are running from a git worktree (${path.basename(__dirname)}).`);
341
428
  console.error(' Deploying from a worktree would overwrite production daemon code.');
342
- console.error(' Use \x1b[36mtouch ~/.metame/daemon.js\x1b[0m to hot-reload instead.\n');
429
+ console.error(' Commit your changes, then deploy from the main repo.\n');
343
430
  process.exit(1);
344
431
  }
345
432
 
@@ -613,6 +700,58 @@ function ensureHookInstalled() {
613
700
 
614
701
  ensureHookInstalled();
615
702
 
703
+ // ---------------------------------------------------------
704
+ // 1.6a AUTO-ENABLE BUNDLED PLUGINS
705
+ // ---------------------------------------------------------
706
+ function ensurePluginsEnabled() {
707
+ try {
708
+ let settings = {};
709
+ if (fs.existsSync(CLAUDE_SETTINGS)) {
710
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf8'));
711
+ }
712
+
713
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
714
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
715
+
716
+ const bundledPlugins = {
717
+ 'example-skills@anthropic-agent-skills': true,
718
+ 'ralph-loop@claude-plugins-official': true,
719
+ 'planning-with-files@planning-with-files': true,
720
+ };
721
+
722
+ const bundledMarketplaces = {
723
+ 'planning-with-files': {
724
+ source: { source: 'github', repo: 'OthmanAdi/planning-with-files' },
725
+ },
726
+ };
727
+
728
+ let modified = false;
729
+
730
+ for (const [key, val] of Object.entries(bundledPlugins)) {
731
+ if (!(key in settings.enabledPlugins)) {
732
+ settings.enabledPlugins[key] = val;
733
+ modified = true;
734
+ }
735
+ }
736
+
737
+ for (const [key, val] of Object.entries(bundledMarketplaces)) {
738
+ if (!(key in settings.extraKnownMarketplaces)) {
739
+ settings.extraKnownMarketplaces[key] = val;
740
+ modified = true;
741
+ }
742
+ }
743
+
744
+ if (modified) {
745
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
746
+ console.log(`${icon("brain")} MetaMe: Bundled plugins enabled.`);
747
+ }
748
+ } catch {
749
+ // Non-fatal
750
+ }
751
+ }
752
+
753
+ ensurePluginsEnabled();
754
+
616
755
  // ---------------------------------------------------------
617
756
  // 1.6b LOCAL ACTIVITY HEARTBEAT
618
757
  // ---------------------------------------------------------
@@ -1907,50 +2046,20 @@ if (isDaemon) {
1907
2046
  console.error(`${icon("fail")} launchd is macOS-only.`);
1908
2047
  process.exit(1);
1909
2048
  }
1910
- const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
1911
- if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
1912
- const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
1913
- const nodePath = process.execPath;
1914
- // Capture current PATH so launchd can find `claude` and other tools
1915
- const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
1916
- const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
1917
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1918
- <plist version="1.0">
1919
- <dict>
1920
- <key>Label</key>
1921
- <string>com.metame.daemon</string>
1922
- <key>ProgramArguments</key>
1923
- <array>
1924
- <string>/usr/bin/caffeinate</string>
1925
- <string>-i</string>
1926
- <string>${nodePath}</string>
1927
- <string>${DAEMON_SCRIPT}</string>
1928
- </array>
1929
- <key>RunAtLoad</key>
1930
- <true/>
1931
- <key>KeepAlive</key>
1932
- <true/>
1933
- <key>StandardOutPath</key>
1934
- <string>${DAEMON_LOG}</string>
1935
- <key>StandardErrorPath</key>
1936
- <string>${DAEMON_LOG}</string>
1937
- <key>EnvironmentVariables</key>
1938
- <dict>
1939
- <key>METAME_ROOT</key>
1940
- <string>${__dirname}</string>
1941
- <key>PATH</key>
1942
- <string>${currentPath}</string>
1943
- <key>HOME</key>
1944
- <string>${HOME_DIR}</string>
1945
- <key>LAUNCHED_BY_LAUNCHD</key>
1946
- <string>1</string>
1947
- </dict>
1948
- </dict>
1949
- </plist>`;
1950
- fs.writeFileSync(plistPath, plistContent, 'utf8');
2049
+ // Clean up legacy plist with different label if exists
2050
+ const legacyPlist = path.join(HOME_DIR, 'Library', 'LaunchAgents', 'com.metame.daemon.plist');
2051
+ if (fs.existsSync(legacyPlist)) {
2052
+ try { execSync(`launchctl bootout gui/$(id -u) ${legacyPlist} 2>/dev/null`); } catch { /* */ }
2053
+ try { fs.unlinkSync(legacyPlist); } catch { /* */ }
2054
+ }
2055
+ const plistPath = ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
2056
+ try {
2057
+ execSync(`launchctl bootstrap gui/$(id -u) ${plistPath} 2>/dev/null`);
2058
+ } catch { /* already bootstrapped */ }
1951
2059
  console.log(`${icon("ok")} launchd plist installed: ${plistPath}`);
1952
- console.log(" Load now: launchctl load " + plistPath);
1953
- console.log(" Unload: launchctl unload " + plistPath);
2060
+ console.log(" The daemon will auto-start at login and restart if it crashes.");
2061
+ console.log(" Start now: metame start");
2062
+ console.log(" Remove: launchctl bootout gui/$(id -u)/" + LAUNCHD_LABEL);
1954
2063
  process.exit(0);
1955
2064
  }
1956
2065
 
@@ -2051,7 +2160,61 @@ WantedBy=default.target
2051
2160
  }
2052
2161
 
2053
2162
  if (subCmd === 'start') {
2054
- // Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
2163
+ if (!fs.existsSync(DAEMON_CONFIG)) {
2164
+ console.error(`${icon("fail")} No config found. Run: metame daemon init`);
2165
+ process.exit(1);
2166
+ }
2167
+ if (!fs.existsSync(DAEMON_SCRIPT)) {
2168
+ console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2169
+ process.exit(1);
2170
+ }
2171
+
2172
+ if (process.platform === 'darwin') {
2173
+ // macOS: delegate to launchd for auto-restart and boot persistence
2174
+ // Kill any orphan daemon processes NOT managed by launchd
2175
+ try {
2176
+ const pids = findProcessesByPattern('node.*daemon\\.js');
2177
+ const launchdPid = launchdIsRunning();
2178
+ for (const n of pids) {
2179
+ if (n !== launchdPid) {
2180
+ try { process.kill(n, 'SIGTERM'); } catch { /* */ }
2181
+ }
2182
+ }
2183
+ } catch { /* ignore */ }
2184
+ // Clean stale lock/pid from orphan processes
2185
+ if (fs.existsSync(DAEMON_LOCK)) {
2186
+ try {
2187
+ const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
2188
+ const pid = parseInt(lock && lock.pid, 10);
2189
+ if (pid) { process.kill(pid, 0); } // throws if dead
2190
+ } catch {
2191
+ // Owner is dead — clean stale files
2192
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2193
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2194
+ }
2195
+ }
2196
+ ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
2197
+ try {
2198
+ execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
2199
+ } catch { /* already bootstrapped */ }
2200
+ // kickstart ensures the process is actually running now
2201
+ try {
2202
+ execSync(`launchctl kickstart gui/$(id -u)/${LAUNCHD_LABEL}`);
2203
+ } catch { /* already running */ }
2204
+ sleepSync(1500);
2205
+ const pid = launchdIsRunning();
2206
+ if (pid) {
2207
+ console.log(`${icon("ok")} MetaMe daemon started via launchd (PID: ${pid})`);
2208
+ } else {
2209
+ console.log(`${icon("ok")} MetaMe daemon starting via launchd...`);
2210
+ }
2211
+ console.log(" Auto-restart: enabled (KeepAlive)");
2212
+ console.log(" Logs: metame logs");
2213
+ console.log(" Stop: metame stop");
2214
+ process.exit(0);
2215
+ }
2216
+
2217
+ // Non-macOS: direct spawn (original behavior)
2055
2218
  try {
2056
2219
  const pids = findProcessesByPattern('node.*daemon\\.js');
2057
2220
  if (pids.length) {
@@ -2061,24 +2224,11 @@ WantedBy=default.target
2061
2224
  sleepSync(1000);
2062
2225
  }
2063
2226
  } catch { /* ignore */ }
2064
- // Clean stale PID and lock files before spawning new daemon
2065
2227
  if (fs.existsSync(DAEMON_PID)) {
2066
2228
  try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2067
2229
  }
2068
2230
  try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2069
- if (!fs.existsSync(DAEMON_CONFIG)) {
2070
- console.error(`${icon("fail")} No config found. Run: metame daemon init`);
2071
- process.exit(1);
2072
- }
2073
- if (!fs.existsSync(DAEMON_SCRIPT)) {
2074
- console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2075
- process.exit(1);
2076
- }
2077
- // Use caffeinate on macOS to prevent sleep while daemon is running
2078
- const isMac = process.platform === 'darwin';
2079
- const cmd = isMac ? 'caffeinate' : process.execPath;
2080
- const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
2081
- const bg = spawn(cmd, args, {
2231
+ const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
2082
2232
  detached: true,
2083
2233
  stdio: 'ignore',
2084
2234
  windowsHide: true,
@@ -2092,6 +2242,30 @@ WantedBy=default.target
2092
2242
  }
2093
2243
 
2094
2244
  if (subCmd === 'stop') {
2245
+ if (process.platform === 'darwin') {
2246
+ // macOS: bootout the launchd job (stops process + prevents auto-restart)
2247
+ try {
2248
+ execSync(`launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
2249
+ } catch { /* not loaded */ }
2250
+ // Also kill any orphan daemon processes not managed by launchd
2251
+ try {
2252
+ const pids = findProcessesByPattern('node.*daemon\\.js');
2253
+ for (const n of pids) {
2254
+ try { process.kill(n, 'SIGTERM'); } catch { /* */ }
2255
+ }
2256
+ if (pids.length) sleepSync(2000);
2257
+ for (const n of pids) {
2258
+ try { process.kill(n, 'SIGKILL'); } catch { /* already gone */ }
2259
+ }
2260
+ } catch { /* */ }
2261
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2262
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2263
+ console.log(`${icon("ok")} Daemon stopped. launchd auto-restart disabled.`);
2264
+ console.log(` To re-enable: metame start`);
2265
+ process.exit(0);
2266
+ }
2267
+
2268
+ // Non-macOS: original behavior
2095
2269
  if (!fs.existsSync(DAEMON_PID)) {
2096
2270
  console.log(`${icon("info")} No daemon running (no PID file).`);
2097
2271
  process.exit(0);
@@ -2099,7 +2273,6 @@ WantedBy=default.target
2099
2273
  const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
2100
2274
  try {
2101
2275
  process.kill(pid, 'SIGTERM');
2102
- // Wait for process to die (up to 3s), then force kill
2103
2276
  let dead = false;
2104
2277
  for (let i = 0; i < 6; i++) {
2105
2278
  sleepSync(500);
@@ -2126,6 +2299,30 @@ WantedBy=default.target
2126
2299
  console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2127
2300
  process.exit(1);
2128
2301
  }
2302
+
2303
+ if (process.platform === 'darwin') {
2304
+ // macOS: use launchctl kickstart -k (kills + restarts in one atomic op)
2305
+ ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
2306
+ try {
2307
+ execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
2308
+ } catch { /* already bootstrapped */ }
2309
+ try {
2310
+ execSync(`launchctl kickstart -k gui/$(id -u)/${LAUNCHD_LABEL}`);
2311
+ sleepSync(1500);
2312
+ const pid = launchdIsRunning();
2313
+ if (pid) {
2314
+ console.log(`${icon("ok")} Daemon restarted via launchd (PID: ${pid})`);
2315
+ } else {
2316
+ console.log(`${icon("ok")} Daemon restart requested via launchd...`);
2317
+ }
2318
+ } catch (e) {
2319
+ console.error(`${icon("fail")} launchctl kickstart failed: ${e.message}`);
2320
+ process.exit(1);
2321
+ }
2322
+ process.exit(0);
2323
+ }
2324
+
2325
+ // Non-macOS: original SIGUSR2 / respawn logic
2129
2326
  const result = requestDaemonRestart({
2130
2327
  reason: 'cli-restart',
2131
2328
  daemonPidFile: DAEMON_PID,
@@ -2142,10 +2339,7 @@ WantedBy=default.target
2142
2339
  }
2143
2340
  if (result.status === 'not_running') {
2144
2341
  console.log(`${icon("info")} No daemon running. Starting a fresh daemon instead.`);
2145
- const isMac = process.platform === 'darwin';
2146
- const cmd = isMac ? 'caffeinate' : process.execPath;
2147
- const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
2148
- const bg = spawn(cmd, args, {
2342
+ const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
2149
2343
  detached: true,
2150
2344
  stdio: 'ignore',
2151
2345
  windowsHide: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
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": {
@@ -11,16 +11,20 @@
11
11
  "scripts/",
12
12
  "!scripts/*.test.js",
13
13
  "!scripts/test_daemon.js",
14
- "!scripts/hooks/test-*.js"
14
+ "!scripts/hooks/test-*.js",
15
+ "!scripts/daemon.yaml",
16
+ "!scripts/daemon.yaml.bak",
17
+ "skills/"
15
18
  ],
16
19
  "scripts": {
17
20
  "test": "node --test scripts/*.test.js",
18
21
  "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
19
22
  "start": "node index.js",
20
23
  "push": "bash scripts/bin/push-clean.sh",
21
- "sync:plugin": "node -e \"const fs=require('fs'),path=require('path');const ex=new Set(['sync-readme.js','test_daemon.js']);const files=fs.readdirSync('scripts').filter(f=>!ex.has(f)&&!/\\.test\\.js$/.test(f)&&/\\.(js|yaml|sh)$/.test(f));files.forEach(f=>fs.copyFileSync('scripts/'+f,'plugin/scripts/'+f));\" && mkdir -p plugin/scripts/hooks && cp scripts/hooks/*.js plugin/scripts/hooks/ && echo 'Plugin scripts synced'",
24
+ "sync:plugin": "node -e \"const fs=require('fs'),path=require('path');const ex=new Set(['sync-readme.js','test_daemon.js','daemon.yaml']);const files=fs.readdirSync('scripts').filter(f=>!ex.has(f)&&!/\\.test\\.js$/.test(f)&&/\\.(js|yaml|sh)$/.test(f));files.forEach(f=>fs.copyFileSync('scripts/'+f,'plugin/scripts/'+f));\" && mkdir -p plugin/scripts/hooks && cp scripts/hooks/*.js plugin/scripts/hooks/ && echo 'Plugin scripts synced'",
22
25
  "sync:readme": "node scripts/sync-readme.js",
23
26
  "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'",
27
+ "prepublishOnly": "node -e \"const fs=require('fs');['scripts/daemon.yaml','plugin/scripts/daemon.yaml'].forEach(f=>{if(fs.existsSync(f)){const c=fs.readFileSync(f,'utf8');if(/bot_token:\\s*[^n]|app_secret:\\s*[^n]|enabled:\\s*true/.test(c)){console.error('ABORT: Real credentials found in '+f);process.exit(1)}}})\"",
24
28
  "precommit": "npm run sync:plugin && npm run restart:daemon"
25
29
  },
26
30
  "keywords": [
@@ -289,6 +289,40 @@ function createAdminCommandHandler(deps) {
289
289
  const state = ctx.state || {};
290
290
  let config = ctx.config || {};
291
291
 
292
+ if (text === '/status perpetual' || text === '/status reactive') {
293
+ const { replayEventLog } = require('./daemon-reactive-lifecycle');
294
+ const projects = config.projects || {};
295
+ const lines = ['**Perpetual Projects**\n'];
296
+ let found = false;
297
+
298
+ for (const [key, proj] of Object.entries(projects)) {
299
+ if (!proj.reactive) continue;
300
+ found = true;
301
+
302
+ const rs = (state.reactive && state.reactive[key]) || {};
303
+ const { phase, mission } = replayEventLog(key, { log: () => {} });
304
+
305
+ const icon = proj.icon || '🔄';
306
+ const name = proj.name || key;
307
+ const status = rs.status || 'idle';
308
+ const depth = rs.depth || 0;
309
+ const maxDepth = rs.max_depth || 50;
310
+ const lastSignal = rs.last_signal || '-';
311
+ const updatedAt = rs.updated_at ? new Date(rs.updated_at).toLocaleString() : '-';
312
+
313
+ lines.push(`${icon} **${name}** (\`${key}\`)`);
314
+ lines.push(` Status: ${status} | Phase: ${phase || '-'} | Depth: ${depth}/${maxDepth}`);
315
+ if (mission) lines.push(` Mission: ${mission.title}`);
316
+ lines.push(` Last signal: ${lastSignal} | Updated: ${updatedAt}`);
317
+ lines.push('');
318
+ }
319
+
320
+ if (!found) lines.push('No reactive projects configured.');
321
+
322
+ await bot.sendMessage(chatId, lines.join('\n'));
323
+ return { handled: true, config };
324
+ }
325
+
292
326
  if (text === '/status') {
293
327
  const session = getSession(chatId);
294
328
  let msg = `MetaMe Daemon\nStatus: Running\nStarted: ${state.started_at || 'unknown'}\n`;
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+
3
5
  function createAgentCommandHandler(deps) {
4
6
  const {
5
7
  fs,
@@ -34,11 +36,11 @@ function createAgentCommandHandler(deps) {
34
36
  agentFlowTtlMs,
35
37
  agentBindTtlMs,
36
38
  getDefaultEngine = () => 'claude',
39
+ log = () => {},
37
40
  } = deps;
38
41
 
39
42
  function normalizeEngineName(name) {
40
- const n = String(name || '').trim().toLowerCase();
41
- return n === 'codex' ? 'codex' : getDefaultEngine();
43
+ return _normalizeEngine(name, getDefaultEngine);
42
44
  }
43
45
 
44
46
  function inferStoredEngine(rawSession) {
@@ -358,7 +360,9 @@ function createAgentCommandHandler(deps) {
358
360
  const curSession = getSession(route.sessionChatId) || getSession(chatId);
359
361
  const curCwd = route.cwd || (curSession ? curSession.cwd : null);
360
362
  const currentEngine = getCurrentEngine(chatId);
363
+ log('DEBUG', `[/resume] chatId=${chatId} curCwd=${curCwd} engine=${currentEngine} route.sessionChatId=${route.sessionChatId}`);
361
364
  const recentSessions = listRecentSessions(5, curCwd, currentEngine);
365
+ log('DEBUG', `[/resume] recentSessions=${recentSessions.length} ids=[${recentSessions.map(s=>s.sessionId.slice(0,8)).join(',')}]`);
362
366
  const resumeChoices = buildResumeChoices({
363
367
  recentSessions,
364
368
  currentLogical,