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.
- package/README.md +49 -6
- package/index.js +266 -72
- package/package.json +7 -3
- package/scripts/daemon-admin-commands.js +34 -0
- package/scripts/daemon-agent-commands.js +6 -2
- package/scripts/daemon-bridges.js +41 -10
- package/scripts/daemon-claude-engine.js +128 -29
- package/scripts/daemon-command-router.js +16 -0
- package/scripts/daemon-command-session-route.js +3 -1
- package/scripts/daemon-default.yaml +3 -1
- package/scripts/daemon-engine-runtime.js +1 -5
- package/scripts/daemon-message-pipeline.js +113 -44
- package/scripts/daemon-ops-commands.js +25 -11
- package/scripts/daemon-reactive-lifecycle.js +757 -76
- package/scripts/daemon-session-commands.js +3 -2
- package/scripts/daemon-session-store.js +82 -27
- package/scripts/daemon-team-dispatch.js +21 -5
- package/scripts/daemon-utils.js +3 -1
- package/scripts/daemon.js +80 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/file-transfer.md +1 -0
- 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/hooks/intent-file-transfer.js +2 -1
- package/scripts/hooks/intent-perpetual.js +109 -0
- package/scripts/hooks/intent-research.js +112 -0
- package/scripts/intent-registry.js +4 -0
- package/scripts/memory-extract.js +29 -1
- package/scripts/memory-nightly-reflect.js +104 -0
- package/scripts/ops-mission-queue.js +258 -0
- package/scripts/ops-verifier.js +197 -0
- package/scripts/signal-capture.js +3 -3
- package/scripts/skill-evolution.js +11 -2
- package/skills/agent-browser/SKILL.md +153 -0
- package/skills/agent-reach/SKILL.md +66 -0
- package/skills/agent-reach/evolution.json +13 -0
- package/skills/deep-research/SKILL.md +77 -0
- package/skills/find-skills/SKILL.md +133 -0
- package/skills/heartbeat-task-manager/SKILL.md +63 -0
- package/skills/macos-local-orchestrator/SKILL.md +192 -0
- package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
- package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
- package/skills/macos-mail-calendar/SKILL.md +394 -0
- package/skills/mcp-installer/SKILL.md +138 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +72 -0
- package/skills/skill-creator/SKILL.md +96 -0
- package/skills/skill-creator/evolution.json +6 -0
- package/skills/skill-creator/references/creation-guide.md +116 -0
- package/skills/skill-creator/references/evolution-guide.md +74 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/align_all.py +32 -0
- package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/merge_evolution.py +70 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/setup.py +141 -0
- package/skills/skill-creator/scripts/smart_stitch.py +82 -0
- package/skills/skill-manager/SKILL.md +112 -0
- package/skills/skill-manager/scripts/delete_skill.py +31 -0
- package/skills/skill-manager/scripts/list_skills.py +61 -0
- package/skills/skill-manager/scripts/scan_and_check.py +125 -0
- package/skills/skill-manager/scripts/sync_index.py +144 -0
- 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.
|
|
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');
|
|
@@ -330,16 +412,21 @@ try {
|
|
|
330
412
|
}
|
|
331
413
|
} catch { /* non-fatal */ }
|
|
332
414
|
|
|
333
|
-
// Worktree guard:
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
const
|
|
337
|
-
|
|
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('
|
|
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
|
-
|
|
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');
|
|
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("
|
|
1953
|
-
console.log("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|