metame-cli 1.4.34 → 1.5.1
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 +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/telegram-adapter.js +12 -8
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
package/index.js
CHANGED
|
@@ -9,16 +9,45 @@ const os = require('os');
|
|
|
9
9
|
const { spawn, execSync } = require('child_process');
|
|
10
10
|
const { sleepSync, findProcessesByPattern, icon } = require('./scripts/platform');
|
|
11
11
|
|
|
12
|
-
// On Windows, .cmd
|
|
13
|
-
//
|
|
12
|
+
// On Windows, resolve .cmd wrapper → actual Node.js entry and spawn node directly.
|
|
13
|
+
// Completely bypasses cmd.exe, eliminating terminal flash.
|
|
14
|
+
function resolveNodeEntry(cmdPath) {
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(cmdPath, 'utf8');
|
|
17
|
+
const m = content.match(/"([^"]+\.js)"\s*%\*\s*$/m);
|
|
18
|
+
if (m) {
|
|
19
|
+
const entry = m[1].replace(/%dp0%/gi, path.dirname(cmdPath) + path.sep);
|
|
20
|
+
if (fs.existsSync(entry)) return entry;
|
|
21
|
+
}
|
|
22
|
+
} catch { /* ignore */ }
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function spawnViaNode(cmd, args, options) {
|
|
27
|
+
if (process.platform !== 'win32') return spawn(cmd, args, options);
|
|
28
|
+
try {
|
|
29
|
+
const { execSync: _es } = require('child_process');
|
|
30
|
+
const lines = _es(`where ${cmd}`, { encoding: 'utf8', timeout: 3000 })
|
|
31
|
+
.split('\n').map(l => l.trim()).filter(Boolean);
|
|
32
|
+
const cmdFile = lines.find(l => l.toLowerCase().endsWith(`${cmd}.cmd`)) || lines[0];
|
|
33
|
+
if (cmdFile) {
|
|
34
|
+
const entry = resolveNodeEntry(cmdFile);
|
|
35
|
+
if (entry) return spawn(process.execPath, [entry, ...args], { ...options, windowsHide: true });
|
|
36
|
+
return spawn(cmdFile, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
return spawn(cmd, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
function spawnClaude(args, options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
43
|
+
return spawnViaNode('claude', args, options);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function spawnCodex(args, options) {
|
|
47
|
+
// Sanitize env: unset CODEX_HOME if it points to a non-existent path (corrupted registry value)
|
|
48
|
+
const env = { ...(options && options.env ? options.env : process.env) };
|
|
49
|
+
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
50
|
+
return spawnViaNode('codex', args, { ...options, env });
|
|
22
51
|
}
|
|
23
52
|
|
|
24
53
|
// Quick flags (before heavy init)
|
|
@@ -88,7 +117,7 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
|
|
|
88
117
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
89
118
|
// IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
|
|
90
119
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
91
|
-
const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', '
|
|
120
|
+
const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-write.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'mentor-engine.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js', 'skill-changelog.js', 'agent-layer.js'];
|
|
92
121
|
const DAEMON_MODULE_SCRIPTS = (() => {
|
|
93
122
|
try {
|
|
94
123
|
return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
|
|
@@ -110,20 +139,45 @@ try {
|
|
|
110
139
|
}
|
|
111
140
|
} catch { /* non-fatal */ }
|
|
112
141
|
|
|
113
|
-
|
|
142
|
+
// Pre-deploy syntax validation: check all .js files before syncing to ~/.metame/
|
|
143
|
+
// Catches bad merges and careless agent edits BEFORE they can crash the daemon.
|
|
144
|
+
const { execSync: _execSync } = require('child_process');
|
|
145
|
+
const syntaxErrors = [];
|
|
146
|
+
for (const f of BUNDLED_SCRIPTS) {
|
|
147
|
+
if (!f.endsWith('.js')) continue;
|
|
148
|
+
const fp = path.join(scriptsDir, f);
|
|
149
|
+
if (!fs.existsSync(fp)) continue;
|
|
150
|
+
try {
|
|
151
|
+
_execSync(`"${process.execPath}" -c "${fp}"`, { timeout: 5000, stdio: 'pipe', windowsHide: true });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = (e.stderr ? e.stderr.toString().trim() : e.message).split('\n')[0];
|
|
154
|
+
syntaxErrors.push(`${f}: ${msg}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
114
157
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
let scriptsUpdated = false;
|
|
159
|
+
if (syntaxErrors.length > 0) {
|
|
160
|
+
console.error(`${icon("warn")} DEPLOY BLOCKED — syntax errors in ${syntaxErrors.length} file(s):`);
|
|
161
|
+
for (const err of syntaxErrors) console.error(` ${err}`);
|
|
162
|
+
console.error('Fix the errors before deploying. Daemon continues running with old code.');
|
|
163
|
+
} else {
|
|
164
|
+
scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
|
|
165
|
+
|
|
166
|
+
// Daemon restart on script update:
|
|
167
|
+
// Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
|
|
168
|
+
// and has defer logic (waits for active Claude tasks to finish before restarting).
|
|
169
|
+
// Killing here bypasses that and interrupts ongoing conversations.
|
|
170
|
+
if (scriptsUpdated) {
|
|
171
|
+
console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
|
|
172
|
+
}
|
|
121
173
|
}
|
|
122
174
|
|
|
123
175
|
// Docs: lazy-load references for CLAUDE.md pointer instructions
|
|
124
176
|
syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
|
|
125
177
|
// Bin: CLI tools (dispatch_to etc.)
|
|
126
178
|
syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
|
|
179
|
+
// Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
|
|
180
|
+
syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
|
|
127
181
|
|
|
128
182
|
// ---------------------------------------------------------
|
|
129
183
|
// Deploy bundled skills to ~/.claude/skills/
|
|
@@ -163,6 +217,32 @@ if (fs.existsSync(bundledSkillsDir)) {
|
|
|
163
217
|
}
|
|
164
218
|
}
|
|
165
219
|
|
|
220
|
+
// Ensure ~/.codex/skills and ~/.agents/skills are symlinks to ~/.claude/skills
|
|
221
|
+
// This keeps skill evolution unified across all engines.
|
|
222
|
+
for (const altDir of [
|
|
223
|
+
path.join(HOME_DIR, '.codex', 'skills'),
|
|
224
|
+
path.join(HOME_DIR, '.agents', 'skills'),
|
|
225
|
+
]) {
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.lstatSync(altDir);
|
|
228
|
+
if (stat.isSymbolicLink()) continue; // already a symlink, good
|
|
229
|
+
// Physical directory exists — back it up, then replace with symlink
|
|
230
|
+
const backupDir = altDir + '.bak.' + Date.now();
|
|
231
|
+
fs.renameSync(altDir, backupDir);
|
|
232
|
+
console.log(`[metame] Backed up existing ${altDir} → ${backupDir}`);
|
|
233
|
+
fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
if (e.code === 'ENOENT') {
|
|
236
|
+
// Parent dir or target doesn't exist — try creating symlink
|
|
237
|
+
try {
|
|
238
|
+
fs.mkdirSync(path.dirname(altDir), { recursive: true });
|
|
239
|
+
fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
|
|
240
|
+
} catch { /* non-fatal */ }
|
|
241
|
+
}
|
|
242
|
+
// Other errors (e.g. engine not installed): non-fatal, skip
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
166
246
|
// Load daemon config for local launch flags
|
|
167
247
|
let daemonCfg = {};
|
|
168
248
|
try {
|
|
@@ -231,6 +311,8 @@ function ensureHookInstalled() {
|
|
|
231
311
|
entry.hooks?.some(h => h.command && h.command.includes('signal-capture.js'))
|
|
232
312
|
);
|
|
233
313
|
|
|
314
|
+
let modified = false;
|
|
315
|
+
|
|
234
316
|
if (!stillInstalled) {
|
|
235
317
|
if (!settings.hooks) settings.hooks = {};
|
|
236
318
|
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
@@ -241,9 +323,33 @@ function ensureHookInstalled() {
|
|
|
241
323
|
command: hookCommand
|
|
242
324
|
}]
|
|
243
325
|
});
|
|
326
|
+
modified = true;
|
|
327
|
+
console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Ensure Stop hook (session-logger + tool-failure capture) is installed
|
|
331
|
+
const stopHookScript = path.join(METAME_DIR, 'hooks', 'stop-session-capture.js').replace(/\\/g, '/');
|
|
332
|
+
const stopHookCommand = `node "${stopHookScript}"`;
|
|
333
|
+
const stopHookInstalled = (settings.hooks?.Stop || []).some(entry =>
|
|
334
|
+
entry.hooks?.some(h => h.command && h.command.includes('stop-session-capture.js'))
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (!stopHookInstalled) {
|
|
338
|
+
if (!settings.hooks) settings.hooks = {};
|
|
339
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
340
|
+
|
|
341
|
+
settings.hooks.Stop.push({
|
|
342
|
+
hooks: [{
|
|
343
|
+
type: 'command',
|
|
344
|
+
command: stopHookCommand
|
|
345
|
+
}]
|
|
346
|
+
});
|
|
347
|
+
modified = true;
|
|
348
|
+
console.log(`${icon("hook")} MetaMe: Stop session capture hook installed.`);
|
|
349
|
+
}
|
|
244
350
|
|
|
351
|
+
if (modified) {
|
|
245
352
|
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
|
|
246
|
-
console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
|
|
247
353
|
}
|
|
248
354
|
} catch (e) {
|
|
249
355
|
// Non-fatal: hook install failure shouldn't block launch
|
|
@@ -294,7 +400,7 @@ function needsBootstrap() {
|
|
|
294
400
|
} catch { return true; }
|
|
295
401
|
}
|
|
296
402
|
|
|
297
|
-
function spawnDistillBackground() {
|
|
403
|
+
function spawnDistillBackground(engine) {
|
|
298
404
|
const distillPath = path.join(METAME_DIR, 'distill.js');
|
|
299
405
|
if (!fs.existsSync(distillPath)) return;
|
|
300
406
|
|
|
@@ -333,14 +439,16 @@ function spawnDistillBackground() {
|
|
|
333
439
|
}
|
|
334
440
|
|
|
335
441
|
|
|
336
|
-
// Spawn as detached background process — won't block
|
|
337
|
-
// Remove CLAUDECODE env var so distill.js can call
|
|
442
|
+
// Spawn as detached background process — won't block session launch
|
|
443
|
+
// Remove CLAUDECODE env var so distill.js can call the engine without nested-session rejection
|
|
338
444
|
const distillEnvClean = { ...process.env };
|
|
339
445
|
delete distillEnvClean.CLAUDECODE;
|
|
446
|
+
if (engine) distillEnvClean.METAME_ENGINE = engine;
|
|
340
447
|
const bg = spawn('node', [distillPath], {
|
|
341
448
|
detached: true,
|
|
342
449
|
stdio: 'ignore',
|
|
343
450
|
env: distillEnvClean,
|
|
451
|
+
windowsHide: true,
|
|
344
452
|
});
|
|
345
453
|
bg.unref();
|
|
346
454
|
}
|
|
@@ -519,18 +627,45 @@ This step connects the bot to the user's PRIVATE chat — this is the admin chan
|
|
|
519
627
|
6. Tell user to run \`metame start\` to activate.
|
|
520
628
|
|
|
521
629
|
- If **Feishu:**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
630
|
+
Walk the user through these steps IN ORDER. Confirm each step before proceeding to the next.
|
|
631
|
+
|
|
632
|
+
**阶段一:创建应用,获取凭证(先拿到钥匙)**
|
|
633
|
+
1. 打开 open.feishu.cn → 开发者后台 → 创建企业自建应用,填写名称和描述(随意)。
|
|
634
|
+
2. 进入应用 →「凭证与基础信息」→ 复制 App ID 和 App Secret。
|
|
635
|
+
3. 进入「应用功能」→「机器人」→ 开启机器人功能(点击启用)。
|
|
636
|
+
4. 进入「权限管理」→ 依次搜索并开通以下权限:
|
|
637
|
+
- \`im:message\`
|
|
638
|
+
- \`im:message.p2p_msg:readonly\`
|
|
639
|
+
- \`im:message.group_at_msg:readonly\`
|
|
640
|
+
- \`im:message:send_as_bot\`
|
|
641
|
+
- \`im:resource\`
|
|
642
|
+
5. Ask user to paste their App ID and App Secret.
|
|
643
|
+
6. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
|
|
644
|
+
|
|
645
|
+
**阶段二:启动 daemon,建立长连接(必须先跑起来)**
|
|
646
|
+
7. Tell user to run \`metame start\`.
|
|
647
|
+
8. Run \`metame status\` and confirm the output contains "Feishu bot connected". **${icon("warn")} 必须看到这行才能继续** — 飞书控制台只有在检测到活跃连接后才允许保存事件配置。
|
|
648
|
+
|
|
649
|
+
**阶段三:飞书控制台完成事件订阅(回去点保存)**
|
|
650
|
+
9. 回到飞书开放平台 →「事件与回调」→「事件配置」→ 选择「使用长连接接收事件」。
|
|
651
|
+
10. 点击「添加事件」→ 搜索并添加「接收消息 im.message.receive_v1」。
|
|
652
|
+
11. **${icon("warn")} 关键:** 点击该事件右侧「申请权限」→ 勾选「获取群组中所有消息」。不勾选则 bot 在群聊中只能收到 @ 它的消息。
|
|
653
|
+
12. 点击「保存配置」。此时控制台检测长连接,daemon 已在线,保存会通过。
|
|
654
|
+
13. 进入「版本管理与发布」→ 创建版本 → 申请发布(企业自建应用可直接发布,无需审核)。
|
|
655
|
+
|
|
656
|
+
**阶段四:获取 chat_id,完成私聊绑定**
|
|
657
|
+
14. Tell user: 在飞书里搜索刚创建的机器人名称,打开私聊,发送任意一条消息(如"你好")。
|
|
658
|
+
15. After user confirms, auto-fetch the private chat ID:
|
|
528
659
|
\`\`\`bash
|
|
529
|
-
TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
|
|
530
|
-
|
|
660
|
+
TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal \\
|
|
661
|
+
-H "Content-Type: application/json" \\
|
|
662
|
+
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}' | jq -r '.tenant_access_token')
|
|
663
|
+
curl -s -H "Authorization: Bearer $TOKEN" \\
|
|
664
|
+
"https://open.feishu.cn/open-apis/im/v1/chats?chat_type=p2p" | jq '.data.items[] | {chat_id, name}'
|
|
531
665
|
\`\`\`
|
|
532
|
-
|
|
533
|
-
|
|
666
|
+
16. Write the discovered \`chat_id\` into \`allowed_chat_ids\` in \`~/.metame/daemon.yaml\`.
|
|
667
|
+
17. Run \`metame stop && metame start\` to reload config.
|
|
668
|
+
18. Tell user to send a message in the Feishu private chat — they should receive a reply from MetaMe. Setup complete.
|
|
534
669
|
|
|
535
670
|
- If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
|
|
536
671
|
|
|
@@ -577,25 +712,39 @@ if (fs.existsSync(PROJECT_FILE)) {
|
|
|
577
712
|
}
|
|
578
713
|
|
|
579
714
|
// Determine if this is a known (calibrated) user
|
|
715
|
+
// Cache the parsed doc to avoid re-reading BRAIN_FILE in mirror/reflection sections below.
|
|
580
716
|
const yaml = require('js-yaml');
|
|
581
717
|
let isKnownUser = false;
|
|
718
|
+
let _brainDoc = null;
|
|
582
719
|
try {
|
|
583
720
|
if (fs.existsSync(BRAIN_FILE)) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
721
|
+
_brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
722
|
+
const id = _brainDoc.identity || {};
|
|
723
|
+
const hasLocale = id.locale && id.locale !== 'null' && id.locale !== null;
|
|
724
|
+
// Exclude default placeholder values written by genesis scaffolding
|
|
725
|
+
const hasName = id.name && id.name !== 'Unknown' && id.name !== 'null';
|
|
726
|
+
const hasRole = id.role && id.role !== 'Unknown' && id.role !== 'null';
|
|
727
|
+
const hasOtherFields = hasName || hasRole || id.timezone ||
|
|
728
|
+
(_brainDoc.status && _brainDoc.status.focus && _brainDoc.status.focus !== 'Initializing');
|
|
729
|
+
if (hasLocale || hasOtherFields) isKnownUser = true;
|
|
588
730
|
}
|
|
589
731
|
} catch (e) {
|
|
590
732
|
// Ignore error, treat as unknown
|
|
591
733
|
}
|
|
592
734
|
|
|
735
|
+
// Non-session commands (daemon ops, version, help) should not show genesis message
|
|
736
|
+
const _arg2 = process.argv[2];
|
|
737
|
+
const _isNonSessionCmd = ['daemon', 'start', 'stop', 'status', 'logs', 'codex',
|
|
738
|
+
'sync', 'continue', '-v', '--version', '-h', '--help', 'distill', 'evolve'].includes(_arg2);
|
|
739
|
+
|
|
593
740
|
let finalProtocol;
|
|
594
741
|
if (isKnownUser) {
|
|
595
742
|
finalProtocol = PROTOCOL_NORMAL;
|
|
596
743
|
} else {
|
|
597
744
|
finalProtocol = PROTOCOL_ONBOARDING;
|
|
598
|
-
|
|
745
|
+
if (!_isNonSessionCmd) {
|
|
746
|
+
console.log(`${icon("new")} New user detected — entering Genesis interview mode...`);
|
|
747
|
+
}
|
|
599
748
|
}
|
|
600
749
|
|
|
601
750
|
// ---------------------------------------------------------
|
|
@@ -603,8 +752,8 @@ if (isKnownUser) {
|
|
|
603
752
|
// ---------------------------------------------------------
|
|
604
753
|
let mirrorLine = '';
|
|
605
754
|
try {
|
|
606
|
-
if (isKnownUser &&
|
|
607
|
-
const brainDoc =
|
|
755
|
+
if (isKnownUser && _brainDoc) {
|
|
756
|
+
const brainDoc = _brainDoc;
|
|
608
757
|
|
|
609
758
|
// Check quiet mode
|
|
610
759
|
const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
|
|
@@ -664,8 +813,8 @@ try {
|
|
|
664
813
|
// This ensures reflections don't fire every session.
|
|
665
814
|
let reflectionLine = '';
|
|
666
815
|
try {
|
|
667
|
-
if (isKnownUser &&
|
|
668
|
-
const refDoc =
|
|
816
|
+
if (isKnownUser && _brainDoc) {
|
|
817
|
+
const refDoc = _brainDoc;
|
|
669
818
|
|
|
670
819
|
// Check quiet mode
|
|
671
820
|
const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
|
|
@@ -743,7 +892,8 @@ const GLOBAL_MARKER_END = '<!-- METAME-GLOBAL:END -->';
|
|
|
743
892
|
// Build dynamic Agent dispatch table from daemon.yaml projects.
|
|
744
893
|
// Only include agents whose cwd actually exists on disk — test/stale agents
|
|
745
894
|
// with deleted paths are automatically excluded, no manual cleanup needed.
|
|
746
|
-
|
|
895
|
+
// The table is written to ~/.metame/docs/dispatch-table.md (NOT inlined into CLAUDE.md).
|
|
896
|
+
const DISPATCH_TABLE_PATH = path.join(METAME_DIR, 'docs', 'dispatch-table.md');
|
|
747
897
|
try {
|
|
748
898
|
const daemonYamlPath = path.join(os.homedir(), '.metame', 'daemon.yaml');
|
|
749
899
|
if (fs.existsSync(daemonYamlPath)) {
|
|
@@ -752,13 +902,29 @@ try {
|
|
|
752
902
|
const rows = Object.entries(projects)
|
|
753
903
|
.filter(([, p]) => {
|
|
754
904
|
if (!p || !p.name || !p.cwd) return false;
|
|
755
|
-
// Expand ~ to home directory
|
|
756
905
|
const expandedCwd = String(p.cwd).replace(/^~/, os.homedir());
|
|
757
906
|
return fs.existsSync(expandedCwd);
|
|
758
907
|
})
|
|
759
908
|
.map(([key, p]) => `| \`${key}\` | ${p.name} |`);
|
|
760
909
|
if (rows.length > 0) {
|
|
761
|
-
|
|
910
|
+
const tableContent = [
|
|
911
|
+
'# Agent Dispatch 路由表',
|
|
912
|
+
'',
|
|
913
|
+
'> 自动生成,来源:daemon.yaml。勿手动编辑。',
|
|
914
|
+
'',
|
|
915
|
+
'| project_key | 昵称 |',
|
|
916
|
+
'|-------------|------|',
|
|
917
|
+
...rows,
|
|
918
|
+
'',
|
|
919
|
+
'## 使用方法',
|
|
920
|
+
'```bash',
|
|
921
|
+
'~/.metame/bin/dispatch_to [--new] <project_key> "内容"',
|
|
922
|
+
'```',
|
|
923
|
+
'`--new` 强制新建会话(用户说"新开会话"时加此参数)。',
|
|
924
|
+
'新增 Agent:`/agent bind <名称> <工作目录>`',
|
|
925
|
+
].join('\n') + '\n';
|
|
926
|
+
fs.mkdirSync(path.dirname(DISPATCH_TABLE_PATH), { recursive: true });
|
|
927
|
+
fs.writeFileSync(DISPATCH_TABLE_PATH, tableContent);
|
|
762
928
|
}
|
|
763
929
|
}
|
|
764
930
|
} catch { /* daemon.yaml missing or invalid — skip dispatch table */ }
|
|
@@ -771,11 +937,11 @@ const KERNEL_BODY = PROTOCOL_NORMAL
|
|
|
771
937
|
|
|
772
938
|
const CAPABILITY_SECTIONS = [
|
|
773
939
|
'## Agent Dispatch',
|
|
774
|
-
|
|
775
|
-
'新增 Agent:`/agent bind <名称> <工作目录>`',
|
|
940
|
+
'识别到"告诉X/让X/通知X"等转发意图时 → 先 `cat ~/.metame/docs/dispatch-table.md` 获取路由表(昵称→project_key),再执行转发。不要凭记忆猜测昵称对应关系。',
|
|
776
941
|
'',
|
|
777
942
|
'## Agent 创建与管理',
|
|
778
943
|
'用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
|
|
944
|
+
'用户问代码结构/升级进度/脚本入口时 → 先 `cat ~/.metame/docs/pointer-map.md` 再回答。',
|
|
779
945
|
'',
|
|
780
946
|
'## 手机端文件交互',
|
|
781
947
|
'用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
|
|
@@ -882,7 +1048,37 @@ try {
|
|
|
882
1048
|
}
|
|
883
1049
|
} catch { /* non-fatal */ }
|
|
884
1050
|
|
|
1051
|
+
// Skill evolution status
|
|
1052
|
+
try {
|
|
1053
|
+
const skillChangelog = require('./scripts/skill-changelog');
|
|
1054
|
+
const skillCount = skillChangelog.countInstalledSkills();
|
|
1055
|
+
const lastSession = skillChangelog.getLastSessionStart();
|
|
1056
|
+
const recentChanges = skillChangelog.getRecentChanges(lastSession);
|
|
885
1057
|
|
|
1058
|
+
if (recentChanges.length === 0) {
|
|
1059
|
+
console.log(`${icon("tool")} Skills: ${skillCount} installed · 无新变更`);
|
|
1060
|
+
} else {
|
|
1061
|
+
const evolved = recentChanges.filter(c => c.action === 'evolved');
|
|
1062
|
+
const others = recentChanges.filter(c => c.action !== 'evolved');
|
|
1063
|
+
const parts = [`${skillCount} installed`];
|
|
1064
|
+
if (evolved.length > 0) parts.push(`${evolved.length} evolved since last session`);
|
|
1065
|
+
if (others.length > 0) parts.push(`${others.length} other event${others.length > 1 ? 's' : ''}`);
|
|
1066
|
+
console.log(`${icon("tool")} Skills: ${parts.join(' · ')}`);
|
|
1067
|
+
|
|
1068
|
+
// Show up to 3 details
|
|
1069
|
+
const shown = recentChanges.slice(0, 3);
|
|
1070
|
+
for (const c of shown) {
|
|
1071
|
+
const actionIcon = skillChangelog.getActionIcon(c.action);
|
|
1072
|
+
console.log(` ${actionIcon} ${c.skill || 'system'}: ${c.summary}`);
|
|
1073
|
+
}
|
|
1074
|
+
if (recentChanges.length > 3) {
|
|
1075
|
+
console.log(` +${recentChanges.length - 3} more`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Write session start marker for next time
|
|
1080
|
+
skillChangelog.writeSessionStart();
|
|
1081
|
+
} catch { /* non-fatal */ }
|
|
886
1082
|
|
|
887
1083
|
// ---------------------------------------------------------
|
|
888
1084
|
// 4.9 AUTO-UPDATE CHECK (non-blocking)
|
|
@@ -1352,6 +1548,7 @@ if (isDaemon) {
|
|
|
1352
1548
|
const DAEMON_CONFIG = path.join(METAME_DIR, 'daemon.yaml');
|
|
1353
1549
|
const DAEMON_STATE = path.join(METAME_DIR, 'daemon_state.json');
|
|
1354
1550
|
const DAEMON_PID = path.join(METAME_DIR, 'daemon.pid');
|
|
1551
|
+
const DAEMON_LOCK = path.join(METAME_DIR, 'daemon.lock');
|
|
1355
1552
|
const DAEMON_LOG = path.join(METAME_DIR, 'daemon.log');
|
|
1356
1553
|
const DAEMON_DEFAULT = path.join(__dirname, 'scripts', 'daemon-default.yaml');
|
|
1357
1554
|
const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
|
|
@@ -1682,11 +1879,10 @@ WantedBy=default.target
|
|
|
1682
1879
|
}
|
|
1683
1880
|
// Use caffeinate on macOS to prevent sleep while daemon is running
|
|
1684
1881
|
const isMac = process.platform === 'darwin';
|
|
1685
|
-
const isWin = process.platform === 'win32';
|
|
1686
1882
|
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
1687
1883
|
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
1688
1884
|
const bg = spawn(cmd, args, {
|
|
1689
|
-
detached:
|
|
1885
|
+
detached: true,
|
|
1690
1886
|
stdio: 'ignore',
|
|
1691
1887
|
windowsHide: true,
|
|
1692
1888
|
env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
|
|
@@ -1729,14 +1925,26 @@ WantedBy=default.target
|
|
|
1729
1925
|
|
|
1730
1926
|
// Check if running
|
|
1731
1927
|
let isRunning = false;
|
|
1928
|
+
let runningPid = null;
|
|
1732
1929
|
if (fs.existsSync(DAEMON_PID)) {
|
|
1733
1930
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
1734
|
-
try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
|
|
1931
|
+
try { process.kill(pid, 0); isRunning = true; runningPid = pid; } catch { /* dead */ }
|
|
1932
|
+
}
|
|
1933
|
+
if (!isRunning && fs.existsSync(DAEMON_LOCK)) {
|
|
1934
|
+
try {
|
|
1935
|
+
const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
|
|
1936
|
+
const pid = parseInt(lock && lock.pid, 10);
|
|
1937
|
+
if (pid) {
|
|
1938
|
+
process.kill(pid, 0);
|
|
1939
|
+
isRunning = true;
|
|
1940
|
+
runningPid = pid;
|
|
1941
|
+
}
|
|
1942
|
+
} catch { /* lock stale or invalid */ }
|
|
1735
1943
|
}
|
|
1736
1944
|
|
|
1737
1945
|
console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
|
|
1738
1946
|
if (state.started_at) console.log(` Started: ${state.started_at}`);
|
|
1739
|
-
if (state.pid) console.log(` PID: ${state.pid}`);
|
|
1947
|
+
if (runningPid || state.pid) console.log(` PID: ${runningPid || state.pid}`);
|
|
1740
1948
|
|
|
1741
1949
|
// Budget
|
|
1742
1950
|
const budget = state.budget || {};
|
|
@@ -1806,7 +2014,12 @@ WantedBy=default.target
|
|
|
1806
2014
|
}
|
|
1807
2015
|
|
|
1808
2016
|
// Unknown subcommand
|
|
1809
|
-
console.log(`${icon("book")} MetaMe
|
|
2017
|
+
console.log(`${icon("book")} MetaMe Commands:`);
|
|
2018
|
+
console.log(" metame — launch Claude with MetaMe init");
|
|
2019
|
+
console.log(" metame codex [args] — launch Codex with MetaMe init");
|
|
2020
|
+
console.log(" metame continue — resume latest session");
|
|
2021
|
+
console.log("");
|
|
2022
|
+
console.log(`${icon("book")} Daemon Commands:`);
|
|
1810
2023
|
console.log(" metame start — start background daemon");
|
|
1811
2024
|
console.log(" metame stop — stop daemon");
|
|
1812
2025
|
console.log(" metame status — show status & budget");
|
|
@@ -1823,8 +2036,54 @@ WantedBy=default.target
|
|
|
1823
2036
|
process.exit(0);
|
|
1824
2037
|
}
|
|
1825
2038
|
|
|
2039
|
+
const GENESIS_TRIGGER_PROMPT = 'MANDATORY FIRST ACTION: The user has not been calibrated yet. You MUST start the Genesis Protocol interview from CLAUDE.md IMMEDIATELY — do NOT answer any other question first. Begin with the Trust Contract.';
|
|
2040
|
+
|
|
2041
|
+
// ---------------------------------------------------------
|
|
2042
|
+
// 5.8 CODEX — launch Codex with MetaMe initialization
|
|
2043
|
+
// ---------------------------------------------------------
|
|
2044
|
+
const isCodex = process.argv[2] === 'codex';
|
|
2045
|
+
if (isCodex) {
|
|
2046
|
+
const codexUserArgs = process.argv.slice(3);
|
|
2047
|
+
const codexProviderEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
|
|
2048
|
+
|
|
2049
|
+
// Genesis: new user + interactive mode — trigger profile interview within the same Codex session.
|
|
2050
|
+
// CLAUDE.md (already written to disk above) contains the full genesis protocol; Codex reads it.
|
|
2051
|
+
// We pass the trigger as the opening [PROMPT] argument so genesis flows into normal work seamlessly.
|
|
2052
|
+
const codexArgs = codexUserArgs.length === 0
|
|
2053
|
+
? ['--dangerously-bypass-approvals-and-sandbox']
|
|
2054
|
+
: ['exec', '--dangerously-bypass-approvals-and-sandbox', ...codexUserArgs];
|
|
2055
|
+
|
|
2056
|
+
// Codex reads AGENTS.md (not CLAUDE.md); create symlink so genesis protocol is visible.
|
|
2057
|
+
// Also ensure global ~/AGENTS.md → ~/.claude/CLAUDE.md for identity context.
|
|
2058
|
+
// Use try-catch on symlinkSync directly (avoids TOCTOU race from existsSync pre-check).
|
|
2059
|
+
try {
|
|
2060
|
+
if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')))
|
|
2061
|
+
fs.symlinkSync('CLAUDE.md', path.join(process.cwd(), 'AGENTS.md'));
|
|
2062
|
+
} catch { /* EEXIST or other — non-critical */ }
|
|
2063
|
+
try {
|
|
2064
|
+
const globalClaudeMd = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
|
|
2065
|
+
if (fs.existsSync(globalClaudeMd))
|
|
2066
|
+
fs.symlinkSync(globalClaudeMd, path.join(HOME_DIR, 'AGENTS.md'));
|
|
2067
|
+
} catch { /* EEXIST or other — non-critical */ }
|
|
2068
|
+
|
|
2069
|
+
const child = spawnCodex(codexArgs, {
|
|
2070
|
+
stdio: 'inherit',
|
|
2071
|
+
cwd: process.cwd(),
|
|
2072
|
+
env: { ...process.env, ...codexProviderEnv, METAME_ACTIVE_SESSION: 'true' },
|
|
2073
|
+
});
|
|
2074
|
+
let launchError = false;
|
|
2075
|
+
child.on('error', (err) => {
|
|
2076
|
+
launchError = true;
|
|
2077
|
+
console.error(`\n${icon("fail")} Error: Could not launch 'codex': ${err.message}`);
|
|
2078
|
+
console.error(" Please install: npm install -g @openai/codex");
|
|
2079
|
+
});
|
|
2080
|
+
child.on('close', (code) => process.exit(launchError ? 127 : (code || 0)));
|
|
2081
|
+
spawnDistillBackground('codex');
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1826
2085
|
// ---------------------------------------------------------
|
|
1827
|
-
// 5.
|
|
2086
|
+
// 5.9 CONTINUE/SYNC — resume latest session from terminal
|
|
1828
2087
|
// ---------------------------------------------------------
|
|
1829
2088
|
// Usage: exit Claude first, then run `metame continue` from terminal.
|
|
1830
2089
|
// Finds the most recent session and launches Claude with --resume.
|
|
@@ -1910,10 +2169,7 @@ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerousl
|
|
|
1910
2169
|
launchArgs.push('--dangerously-skip-permissions');
|
|
1911
2170
|
}
|
|
1912
2171
|
if (!isKnownUser) {
|
|
1913
|
-
launchArgs.push(
|
|
1914
|
-
'--append-system-prompt',
|
|
1915
|
-
'MANDATORY FIRST ACTION: The user has not been calibrated yet. You MUST start the Genesis Protocol interview from CLAUDE.md IMMEDIATELY — do NOT answer any other question first. Begin with the Trust Contract.'
|
|
1916
|
-
);
|
|
2172
|
+
launchArgs.push('--append-system-prompt', GENESIS_TRIGGER_PROMPT);
|
|
1917
2173
|
}
|
|
1918
2174
|
|
|
1919
2175
|
// RAG: inject relevant facts based on current project (desktop-side equivalent of daemon RAG)
|
|
@@ -1961,11 +2217,10 @@ try {
|
|
|
1961
2217
|
}
|
|
1962
2218
|
if (!daemonRunning) {
|
|
1963
2219
|
const _isMac = process.platform === 'darwin';
|
|
1964
|
-
const _isWin = process.platform === 'win32';
|
|
1965
2220
|
const dCmd = _isMac ? 'caffeinate' : process.execPath;
|
|
1966
2221
|
const dArgs = _isMac ? ['-i', process.execPath, _daemonScript] : [_daemonScript];
|
|
1967
2222
|
const bg = spawn(dCmd, dArgs, {
|
|
1968
|
-
detached:
|
|
2223
|
+
detached: true,
|
|
1969
2224
|
stdio: 'ignore',
|
|
1970
2225
|
windowsHide: true,
|
|
1971
2226
|
env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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": {
|
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
|
-
"scripts/"
|
|
11
|
+
"scripts/",
|
|
12
|
+
"!scripts/*.test.js",
|
|
13
|
+
"!scripts/test_daemon.js",
|
|
14
|
+
"!scripts/hooks/test-*.js"
|
|
12
15
|
],
|
|
13
16
|
"scripts": {
|
|
14
17
|
"test": "node --test scripts/*.test.js",
|
|
18
|
+
"test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
|
|
15
19
|
"start": "node index.js",
|
|
16
|
-
"sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '
|
|
20
|
+
"sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-engine-runtime.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-write.js scripts/memory-extract.js scripts/memory-search.js scripts/memory-gc.js scripts/memory-nightly-reflect.js scripts/memory-index.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/mentor-engine.js scripts/skill-evolution.js scripts/skill-changelog.js scripts/agent-layer.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
|
|
17
21
|
"sync:readme": "node scripts/sync-readme.js",
|
|
18
|
-
"restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '
|
|
22
|
+
"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'",
|
|
19
23
|
"precommit": "npm run sync:plugin && npm run restart:daemon"
|
|
20
24
|
},
|
|
21
25
|
"keywords": [
|