metame-cli 1.5.9 โ 1.5.11
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 +49 -6
- package/index.js +218 -64
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +34 -0
- package/scripts/daemon-bridges.js +18 -1
- package/scripts/daemon-claude-engine.js +41 -1
- package/scripts/daemon-default.yaml +3 -1
- package/scripts/daemon-ops-commands.js +25 -11
- package/scripts/daemon-reactive-lifecycle.js +355 -70
- package/scripts/daemon-utils.js +55 -0
- package/scripts/daemon.js +79 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/maintenance-manual.md +55 -2
- package/scripts/docs/orphan-files-review.md +72 -0
- package/scripts/docs/pointer-map.md +34 -0
- package/scripts/feishu-adapter.js +25 -0
- package/scripts/hooks/intent-auto-rules.js +50 -0
- package/scripts/memory-extract.js +29 -1
- package/scripts/memory-nightly-reflect.js +104 -0
- package/scripts/signal-capture.js +3 -3
- package/scripts/skill-evolution.js +11 -2
- package/scripts/daemon.yaml +0 -444
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.
|
|
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
|
|
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.
|
|
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:
|
|
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)
|
|
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).
|
|
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
|
-
|
|
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');
|
|
@@ -305,7 +387,7 @@ function requestDaemonRestart({
|
|
|
305
387
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
306
388
|
// Auto-detect ALL runtime scripts: daemon-*.js + all other non-test, non-utility .js/.yaml/.sh files.
|
|
307
389
|
// This prevents "missing module" crashes when new files are added without updating a manual list.
|
|
308
|
-
const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js']);
|
|
390
|
+
const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js', 'daemon.yaml']);
|
|
309
391
|
const BUNDLED_SCRIPTS = (() => {
|
|
310
392
|
try {
|
|
311
393
|
return fs.readdirSync(scriptsDir).filter((f) => {
|
|
@@ -330,6 +412,19 @@ try {
|
|
|
330
412
|
}
|
|
331
413
|
} catch { /* non-fatal */ }
|
|
332
414
|
|
|
415
|
+
// Worktree guard: team members running in worktrees must NEVER deploy to ~/.metame/
|
|
416
|
+
// Their worktree is an isolated sandbox โ deploying would overwrite production symlinks.
|
|
417
|
+
// Detect any .worktrees/ parent in the path (covers both ~/.metame/worktrees/ and repo-local .worktrees/).
|
|
418
|
+
const _isInWorktree = __dirname.split(path.sep).includes('.worktrees') ||
|
|
419
|
+
__dirname.startsWith(path.join(HOME_DIR, '.metame', 'worktrees'));
|
|
420
|
+
if (_isInWorktree) {
|
|
421
|
+
console.error(`\n${icon("stop")} ACTION BLOCKED: Worktree Deploy Prevented`);
|
|
422
|
+
console.error(` You are running from a worktree (${path.basename(__dirname)}).`);
|
|
423
|
+
console.error(' Deploying from a worktree would overwrite production daemon code.');
|
|
424
|
+
console.error(' Use \x1b[36mtouch ~/.metame/daemon.js\x1b[0m to hot-reload instead.\n');
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
333
428
|
// Pre-deploy syntax validation: check all .js files before syncing to ~/.metame/
|
|
334
429
|
// Catches bad merges and careless agent edits BEFORE they can crash the daemon.
|
|
335
430
|
const { execSync: _execSync } = require('child_process');
|
|
@@ -1894,48 +1989,20 @@ if (isDaemon) {
|
|
|
1894
1989
|
console.error(`${icon("fail")} launchd is macOS-only.`);
|
|
1895
1990
|
process.exit(1);
|
|
1896
1991
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
<key>Label</key>
|
|
1908
|
-
<string>com.metame.daemon</string>
|
|
1909
|
-
<key>ProgramArguments</key>
|
|
1910
|
-
<array>
|
|
1911
|
-
<string>/usr/bin/caffeinate</string>
|
|
1912
|
-
<string>-i</string>
|
|
1913
|
-
<string>${nodePath}</string>
|
|
1914
|
-
<string>${DAEMON_SCRIPT}</string>
|
|
1915
|
-
</array>
|
|
1916
|
-
<key>RunAtLoad</key>
|
|
1917
|
-
<true/>
|
|
1918
|
-
<key>KeepAlive</key>
|
|
1919
|
-
<true/>
|
|
1920
|
-
<key>StandardOutPath</key>
|
|
1921
|
-
<string>${DAEMON_LOG}</string>
|
|
1922
|
-
<key>StandardErrorPath</key>
|
|
1923
|
-
<string>${DAEMON_LOG}</string>
|
|
1924
|
-
<key>EnvironmentVariables</key>
|
|
1925
|
-
<dict>
|
|
1926
|
-
<key>METAME_ROOT</key>
|
|
1927
|
-
<string>${__dirname}</string>
|
|
1928
|
-
<key>PATH</key>
|
|
1929
|
-
<string>${currentPath}</string>
|
|
1930
|
-
<key>HOME</key>
|
|
1931
|
-
<string>${HOME_DIR}</string>
|
|
1932
|
-
</dict>
|
|
1933
|
-
</dict>
|
|
1934
|
-
</plist>`;
|
|
1935
|
-
fs.writeFileSync(plistPath, plistContent, 'utf8');
|
|
1992
|
+
// Clean up legacy plist with different label if exists
|
|
1993
|
+
const legacyPlist = path.join(HOME_DIR, 'Library', 'LaunchAgents', 'com.metame.daemon.plist');
|
|
1994
|
+
if (fs.existsSync(legacyPlist)) {
|
|
1995
|
+
try { execSync(`launchctl bootout gui/$(id -u) ${legacyPlist} 2>/dev/null`); } catch { /* */ }
|
|
1996
|
+
try { fs.unlinkSync(legacyPlist); } catch { /* */ }
|
|
1997
|
+
}
|
|
1998
|
+
const plistPath = ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
|
|
1999
|
+
try {
|
|
2000
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath} 2>/dev/null`);
|
|
2001
|
+
} catch { /* already bootstrapped */ }
|
|
1936
2002
|
console.log(`${icon("ok")} launchd plist installed: ${plistPath}`);
|
|
1937
|
-
console.log("
|
|
1938
|
-
console.log("
|
|
2003
|
+
console.log(" The daemon will auto-start at login and restart if it crashes.");
|
|
2004
|
+
console.log(" Start now: metame start");
|
|
2005
|
+
console.log(" Remove: launchctl bootout gui/$(id -u)/" + LAUNCHD_LABEL);
|
|
1939
2006
|
process.exit(0);
|
|
1940
2007
|
}
|
|
1941
2008
|
|
|
@@ -2036,7 +2103,61 @@ WantedBy=default.target
|
|
|
2036
2103
|
}
|
|
2037
2104
|
|
|
2038
2105
|
if (subCmd === 'start') {
|
|
2039
|
-
|
|
2106
|
+
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
2107
|
+
console.error(`${icon("fail")} No config found. Run: metame daemon init`);
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
if (!fs.existsSync(DAEMON_SCRIPT)) {
|
|
2111
|
+
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
2112
|
+
process.exit(1);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (process.platform === 'darwin') {
|
|
2116
|
+
// macOS: delegate to launchd for auto-restart and boot persistence
|
|
2117
|
+
// Kill any orphan daemon processes NOT managed by launchd
|
|
2118
|
+
try {
|
|
2119
|
+
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
2120
|
+
const launchdPid = launchdIsRunning();
|
|
2121
|
+
for (const n of pids) {
|
|
2122
|
+
if (n !== launchdPid) {
|
|
2123
|
+
try { process.kill(n, 'SIGTERM'); } catch { /* */ }
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
} catch { /* ignore */ }
|
|
2127
|
+
// Clean stale lock/pid from orphan processes
|
|
2128
|
+
if (fs.existsSync(DAEMON_LOCK)) {
|
|
2129
|
+
try {
|
|
2130
|
+
const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
|
|
2131
|
+
const pid = parseInt(lock && lock.pid, 10);
|
|
2132
|
+
if (pid) { process.kill(pid, 0); } // throws if dead
|
|
2133
|
+
} catch {
|
|
2134
|
+
// Owner is dead โ clean stale files
|
|
2135
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2136
|
+
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
|
|
2140
|
+
try {
|
|
2141
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
|
|
2142
|
+
} catch { /* already bootstrapped */ }
|
|
2143
|
+
// kickstart ensures the process is actually running now
|
|
2144
|
+
try {
|
|
2145
|
+
execSync(`launchctl kickstart gui/$(id -u)/${LAUNCHD_LABEL}`);
|
|
2146
|
+
} catch { /* already running */ }
|
|
2147
|
+
sleepSync(1500);
|
|
2148
|
+
const pid = launchdIsRunning();
|
|
2149
|
+
if (pid) {
|
|
2150
|
+
console.log(`${icon("ok")} MetaMe daemon started via launchd (PID: ${pid})`);
|
|
2151
|
+
} else {
|
|
2152
|
+
console.log(`${icon("ok")} MetaMe daemon starting via launchd...`);
|
|
2153
|
+
}
|
|
2154
|
+
console.log(" Auto-restart: enabled (KeepAlive)");
|
|
2155
|
+
console.log(" Logs: metame logs");
|
|
2156
|
+
console.log(" Stop: metame stop");
|
|
2157
|
+
process.exit(0);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// Non-macOS: direct spawn (original behavior)
|
|
2040
2161
|
try {
|
|
2041
2162
|
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
2042
2163
|
if (pids.length) {
|
|
@@ -2046,23 +2167,11 @@ WantedBy=default.target
|
|
|
2046
2167
|
sleepSync(1000);
|
|
2047
2168
|
}
|
|
2048
2169
|
} catch { /* ignore */ }
|
|
2049
|
-
// Check if already running
|
|
2050
2170
|
if (fs.existsSync(DAEMON_PID)) {
|
|
2051
2171
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2052
2172
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
process.exit(1);
|
|
2056
|
-
}
|
|
2057
|
-
if (!fs.existsSync(DAEMON_SCRIPT)) {
|
|
2058
|
-
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
2059
|
-
process.exit(1);
|
|
2060
|
-
}
|
|
2061
|
-
// Use caffeinate on macOS to prevent sleep while daemon is running
|
|
2062
|
-
const isMac = process.platform === 'darwin';
|
|
2063
|
-
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
2064
|
-
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
2065
|
-
const bg = spawn(cmd, args, {
|
|
2173
|
+
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2174
|
+
const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
2066
2175
|
detached: true,
|
|
2067
2176
|
stdio: 'ignore',
|
|
2068
2177
|
windowsHide: true,
|
|
@@ -2076,6 +2185,30 @@ WantedBy=default.target
|
|
|
2076
2185
|
}
|
|
2077
2186
|
|
|
2078
2187
|
if (subCmd === 'stop') {
|
|
2188
|
+
if (process.platform === 'darwin') {
|
|
2189
|
+
// macOS: bootout the launchd job (stops process + prevents auto-restart)
|
|
2190
|
+
try {
|
|
2191
|
+
execSync(`launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
|
|
2192
|
+
} catch { /* not loaded */ }
|
|
2193
|
+
// Also kill any orphan daemon processes not managed by launchd
|
|
2194
|
+
try {
|
|
2195
|
+
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
2196
|
+
for (const n of pids) {
|
|
2197
|
+
try { process.kill(n, 'SIGTERM'); } catch { /* */ }
|
|
2198
|
+
}
|
|
2199
|
+
if (pids.length) sleepSync(2000);
|
|
2200
|
+
for (const n of pids) {
|
|
2201
|
+
try { process.kill(n, 'SIGKILL'); } catch { /* already gone */ }
|
|
2202
|
+
}
|
|
2203
|
+
} catch { /* */ }
|
|
2204
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2205
|
+
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2206
|
+
console.log(`${icon("ok")} Daemon stopped. launchd auto-restart disabled.`);
|
|
2207
|
+
console.log(` To re-enable: metame start`);
|
|
2208
|
+
process.exit(0);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// Non-macOS: original behavior
|
|
2079
2212
|
if (!fs.existsSync(DAEMON_PID)) {
|
|
2080
2213
|
console.log(`${icon("info")} No daemon running (no PID file).`);
|
|
2081
2214
|
process.exit(0);
|
|
@@ -2083,7 +2216,6 @@ WantedBy=default.target
|
|
|
2083
2216
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
2084
2217
|
try {
|
|
2085
2218
|
process.kill(pid, 'SIGTERM');
|
|
2086
|
-
// Wait for process to die (up to 3s), then force kill
|
|
2087
2219
|
let dead = false;
|
|
2088
2220
|
for (let i = 0; i < 6; i++) {
|
|
2089
2221
|
sleepSync(500);
|
|
@@ -2097,6 +2229,7 @@ WantedBy=default.target
|
|
|
2097
2229
|
console.log(`${icon("warn")} Process ${pid} not found (may have already exited).`);
|
|
2098
2230
|
}
|
|
2099
2231
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
|
|
2232
|
+
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* ignore */ }
|
|
2100
2233
|
process.exit(0);
|
|
2101
2234
|
}
|
|
2102
2235
|
|
|
@@ -2109,6 +2242,30 @@ WantedBy=default.target
|
|
|
2109
2242
|
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
2110
2243
|
process.exit(1);
|
|
2111
2244
|
}
|
|
2245
|
+
|
|
2246
|
+
if (process.platform === 'darwin') {
|
|
2247
|
+
// macOS: use launchctl kickstart -k (kills + restarts in one atomic op)
|
|
2248
|
+
ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
|
|
2249
|
+
try {
|
|
2250
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
|
|
2251
|
+
} catch { /* already bootstrapped */ }
|
|
2252
|
+
try {
|
|
2253
|
+
execSync(`launchctl kickstart -k gui/$(id -u)/${LAUNCHD_LABEL}`);
|
|
2254
|
+
sleepSync(1500);
|
|
2255
|
+
const pid = launchdIsRunning();
|
|
2256
|
+
if (pid) {
|
|
2257
|
+
console.log(`${icon("ok")} Daemon restarted via launchd (PID: ${pid})`);
|
|
2258
|
+
} else {
|
|
2259
|
+
console.log(`${icon("ok")} Daemon restart requested via launchd...`);
|
|
2260
|
+
}
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
console.error(`${icon("fail")} launchctl kickstart failed: ${e.message}`);
|
|
2263
|
+
process.exit(1);
|
|
2264
|
+
}
|
|
2265
|
+
process.exit(0);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Non-macOS: original SIGUSR2 / respawn logic
|
|
2112
2269
|
const result = requestDaemonRestart({
|
|
2113
2270
|
reason: 'cli-restart',
|
|
2114
2271
|
daemonPidFile: DAEMON_PID,
|
|
@@ -2125,10 +2282,7 @@ WantedBy=default.target
|
|
|
2125
2282
|
}
|
|
2126
2283
|
if (result.status === 'not_running') {
|
|
2127
2284
|
console.log(`${icon("info")} No daemon running. Starting a fresh daemon instead.`);
|
|
2128
|
-
const
|
|
2129
|
-
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
2130
|
-
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
2131
|
-
const bg = spawn(cmd, args, {
|
|
2285
|
+
const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
2132
2286
|
detached: true,
|
|
2133
2287
|
stdio: 'ignore',
|
|
2134
2288
|
windowsHide: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11",
|
|
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,19 @@
|
|
|
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"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"test": "node --test scripts/*.test.js",
|
|
18
20
|
"test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
|
|
19
21
|
"start": "node index.js",
|
|
20
22
|
"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'",
|
|
23
|
+
"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
24
|
"sync:readme": "node scripts/sync-readme.js",
|
|
23
25
|
"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'",
|
|
26
|
+
"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
27
|
"precommit": "npm run sync:plugin && npm run restart:daemon"
|
|
25
28
|
},
|
|
26
29
|
"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`;
|
|
@@ -652,13 +652,30 @@ function createBridgeStarter(deps) {
|
|
|
652
652
|
async function startFeishuBridge(config, executeTaskByName) {
|
|
653
653
|
if (!config.feishu || !config.feishu.enabled) return null;
|
|
654
654
|
if (!config.feishu.app_id || !config.feishu.app_secret) {
|
|
655
|
-
log('
|
|
655
|
+
log('ERROR', 'Feishu enabled but app_id/app_secret missing โ bridge will NOT start. Check ~/.metame/daemon.yaml');
|
|
656
656
|
return null;
|
|
657
657
|
}
|
|
658
658
|
|
|
659
659
|
const { createBot } = require('./feishu-adapter.js');
|
|
660
660
|
const bot = createBot(config.feishu);
|
|
661
661
|
|
|
662
|
+
// Validate credentials before starting WebSocket โ fail loud, not silent
|
|
663
|
+
try {
|
|
664
|
+
const validation = await bot.validateCredentials();
|
|
665
|
+
if (!validation.ok) {
|
|
666
|
+
log('ERROR', `Feishu credential check FAILED: ${validation.error}`);
|
|
667
|
+
if (validation.isAuthError) {
|
|
668
|
+
log('ERROR', 'Feishu bridge will NOT start โ fix app_id/app_secret in ~/.metame/daemon.yaml and restart daemon');
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
log('WARN', 'Feishu credential check failed (possibly network issue) โ attempting to start anyway');
|
|
672
|
+
} else {
|
|
673
|
+
log('INFO', 'Feishu credentials validated OK');
|
|
674
|
+
}
|
|
675
|
+
} catch (e) {
|
|
676
|
+
log('WARN', `Feishu credential pre-check error: ${e.message} โ attempting to start anyway`);
|
|
677
|
+
}
|
|
678
|
+
|
|
662
679
|
try {
|
|
663
680
|
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
|
|
664
681
|
const liveCfg = loadConfig();
|
|
@@ -1605,6 +1605,33 @@ function createClaudeEngine(deps) {
|
|
|
1605
1605
|
}
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
|
+
// Inject latest nightly insight (decisions/lessons) โ one-liner per file, ~100 tokens
|
|
1609
|
+
if (!session.started) {
|
|
1610
|
+
try {
|
|
1611
|
+
const reflectDirs = [
|
|
1612
|
+
path.join(HOME, '.metame', 'memory', 'decisions'),
|
|
1613
|
+
path.join(HOME, '.metame', 'memory', 'lessons'),
|
|
1614
|
+
];
|
|
1615
|
+
const reflectItems = [];
|
|
1616
|
+
for (const dir of reflectDirs) {
|
|
1617
|
+
if (!fs.existsSync(dir)) continue;
|
|
1618
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort();
|
|
1619
|
+
const latest = files[files.length - 1];
|
|
1620
|
+
if (!latest) continue;
|
|
1621
|
+
const content = fs.readFileSync(path.join(dir, latest), 'utf8');
|
|
1622
|
+
// Extract ## headings as one-line summaries (skip frontmatter)
|
|
1623
|
+
const headings = content.match(/^## .+$/gm);
|
|
1624
|
+
if (headings && headings.length > 0) {
|
|
1625
|
+
const type = dir.endsWith('decisions') ? 'decision' : 'lesson';
|
|
1626
|
+
reflectItems.push(...headings.slice(0, 2).map(h => `- [${type}] ${h.replace(/^## /, '')}`));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (reflectItems.length > 0) {
|
|
1630
|
+
memoryHint += `\n\n[Recent insights:\n${reflectItems.join('\n')}]`;
|
|
1631
|
+
}
|
|
1632
|
+
} catch { /* non-critical */ }
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1608
1635
|
memory.close();
|
|
1609
1636
|
} catch (e) {
|
|
1610
1637
|
if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
|
|
@@ -1636,6 +1663,19 @@ function createClaudeEngine(deps) {
|
|
|
1636
1663
|
} catch { /* ignore */ }
|
|
1637
1664
|
}
|
|
1638
1665
|
|
|
1666
|
+
// Self-reflection patterns: behavioral guardrails distilled from past mistakes
|
|
1667
|
+
let reflectHint = '';
|
|
1668
|
+
if (!session.started && brainDoc) {
|
|
1669
|
+
try {
|
|
1670
|
+
const patterns = (brainDoc.growth && Array.isArray(brainDoc.growth.self_reflection_patterns))
|
|
1671
|
+
? brainDoc.growth.self_reflection_patterns.filter(p => p && p.summary).slice(0, 3)
|
|
1672
|
+
: [];
|
|
1673
|
+
if (patterns.length > 0) {
|
|
1674
|
+
reflectHint = `\n- Self-correction patterns (avoid repeating these mistakes):\n${patterns.map(p => ` - ${String(p.summary).slice(0, 150)}`).join('\n')}`;
|
|
1675
|
+
}
|
|
1676
|
+
} catch { /* non-critical */ }
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1639
1679
|
// Inject daemon hints only on first message of a session
|
|
1640
1680
|
// Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
1641
1681
|
let daemonHint = '';
|
|
@@ -1654,7 +1694,7 @@ ${mentorRadarHint}
|
|
|
1654
1694
|
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
1655
1695
|
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
1656
1696
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
1657
|
-
2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${taskRules}]`;
|
|
1697
|
+
2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${reflectHint}${taskRules}]`;
|
|
1658
1698
|
}
|
|
1659
1699
|
|
|
1660
1700
|
daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
|
|
@@ -45,6 +45,7 @@ heartbeat:
|
|
|
45
45
|
type: script
|
|
46
46
|
command: node ~/.metame/distill.js
|
|
47
47
|
interval: 4h
|
|
48
|
+
timeout: 5m
|
|
48
49
|
precondition: "test -s ~/.metame/raw_signals.jsonl"
|
|
49
50
|
require_idle: true
|
|
50
51
|
notify: false
|
|
@@ -74,6 +75,7 @@ heartbeat:
|
|
|
74
75
|
type: script
|
|
75
76
|
command: node ~/.metame/skill-evolution.js
|
|
76
77
|
interval: 12h
|
|
78
|
+
timeout: 5m
|
|
77
79
|
precondition: "test -s ~/.metame/skill_signals.jsonl"
|
|
78
80
|
require_idle: true
|
|
79
81
|
notify: false
|
|
@@ -104,7 +106,7 @@ heartbeat:
|
|
|
104
106
|
at: "01:30"
|
|
105
107
|
require_idle: true
|
|
106
108
|
notify: false
|
|
107
|
-
enabled:
|
|
109
|
+
enabled: false
|
|
108
110
|
|
|
109
111
|
# Legacy flat tasks (no project isolation). New tasks should go under projects: above.
|
|
110
112
|
# Examples โ uncomment or add your own:
|