metame-cli 1.5.10 โ 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 +202 -65
- 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.js +79 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/maintenance-manual.md +55 -2
- package/scripts/docs/pointer-map.md +34 -0
- package/scripts/feishu-adapter.js +25 -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/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');
|
|
@@ -1907,50 +1989,20 @@ if (isDaemon) {
|
|
|
1907
1989
|
console.error(`${icon("fail")} launchd is macOS-only.`);
|
|
1908
1990
|
process.exit(1);
|
|
1909
1991
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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');
|
|
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 */ }
|
|
1951
2002
|
console.log(`${icon("ok")} launchd plist installed: ${plistPath}`);
|
|
1952
|
-
console.log("
|
|
1953
|
-
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);
|
|
1954
2006
|
process.exit(0);
|
|
1955
2007
|
}
|
|
1956
2008
|
|
|
@@ -2051,7 +2103,61 @@ WantedBy=default.target
|
|
|
2051
2103
|
}
|
|
2052
2104
|
|
|
2053
2105
|
if (subCmd === 'start') {
|
|
2054
|
-
|
|
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)
|
|
2055
2161
|
try {
|
|
2056
2162
|
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
2057
2163
|
if (pids.length) {
|
|
@@ -2061,24 +2167,11 @@ WantedBy=default.target
|
|
|
2061
2167
|
sleepSync(1000);
|
|
2062
2168
|
}
|
|
2063
2169
|
} catch { /* ignore */ }
|
|
2064
|
-
// Clean stale PID and lock files before spawning new daemon
|
|
2065
2170
|
if (fs.existsSync(DAEMON_PID)) {
|
|
2066
2171
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2067
2172
|
}
|
|
2068
2173
|
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2069
|
-
|
|
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, {
|
|
2174
|
+
const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
2082
2175
|
detached: true,
|
|
2083
2176
|
stdio: 'ignore',
|
|
2084
2177
|
windowsHide: true,
|
|
@@ -2092,6 +2185,30 @@ WantedBy=default.target
|
|
|
2092
2185
|
}
|
|
2093
2186
|
|
|
2094
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
|
|
2095
2212
|
if (!fs.existsSync(DAEMON_PID)) {
|
|
2096
2213
|
console.log(`${icon("info")} No daemon running (no PID file).`);
|
|
2097
2214
|
process.exit(0);
|
|
@@ -2099,7 +2216,6 @@ WantedBy=default.target
|
|
|
2099
2216
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
2100
2217
|
try {
|
|
2101
2218
|
process.kill(pid, 'SIGTERM');
|
|
2102
|
-
// Wait for process to die (up to 3s), then force kill
|
|
2103
2219
|
let dead = false;
|
|
2104
2220
|
for (let i = 0; i < 6; i++) {
|
|
2105
2221
|
sleepSync(500);
|
|
@@ -2126,6 +2242,30 @@ WantedBy=default.target
|
|
|
2126
2242
|
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
2127
2243
|
process.exit(1);
|
|
2128
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
|
|
2129
2269
|
const result = requestDaemonRestart({
|
|
2130
2270
|
reason: 'cli-restart',
|
|
2131
2271
|
daemonPidFile: DAEMON_PID,
|
|
@@ -2142,10 +2282,7 @@ WantedBy=default.target
|
|
|
2142
2282
|
}
|
|
2143
2283
|
if (result.status === 'not_running') {
|
|
2144
2284
|
console.log(`${icon("info")} No daemon running. Starting a fresh daemon instead.`);
|
|
2145
|
-
const
|
|
2146
|
-
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
2147
|
-
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
2148
|
-
const bg = spawn(cmd, args, {
|
|
2285
|
+
const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
2149
2286
|
detached: true,
|
|
2150
2287
|
stdio: 'ignore',
|
|
2151
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:
|