metame-cli 1.5.0 → 1.5.2
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 +60 -132
- package/index.js +193 -140
- package/package.json +4 -3
- package/scripts/agent-layer.js +320 -0
- package/scripts/bin/dispatch_to +45 -4
- package/scripts/daemon-admin-commands.js +80 -25
- package/scripts/daemon-agent-commands.js +81 -0
- package/scripts/daemon-agent-tools.js +145 -10
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +201 -169
- package/scripts/daemon-command-router.js +8 -2
- package/scripts/daemon-engine-runtime.js +74 -21
- package/scripts/daemon-exec-commands.js +5 -3
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +127 -4
- package/scripts/daemon-session-commands.js +23 -36
- package/scripts/daemon-session-store.js +120 -13
- package/scripts/daemon-task-scheduler.js +71 -13
- package/scripts/daemon-user-acl.js +10 -1
- package/scripts/daemon.js +227 -21
- package/scripts/distill.js +81 -0
- package/scripts/docs/maintenance-manual.md +39 -3
- package/scripts/docs/pointer-map.md +23 -1
- package/scripts/feishu-adapter.js +36 -12
- package/scripts/memory-extract.js +5 -1
- package/scripts/memory-nightly-reflect.js +3 -0
- package/scripts/mentor-engine.js +4 -4
- package/scripts/platform.js +22 -0
- package/scripts/providers.js +14 -2
- package/scripts/publish-public.sh +85 -0
- package/scripts/self-reflect.js +286 -0
- package/scripts/telegram-adapter.js +12 -8
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', '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'];
|
|
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,14 +139,37 @@ 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
|
|
@@ -172,19 +224,22 @@ for (const altDir of [
|
|
|
172
224
|
path.join(HOME_DIR, '.agents', 'skills'),
|
|
173
225
|
]) {
|
|
174
226
|
try {
|
|
175
|
-
const parentDir = path.dirname(altDir);
|
|
176
|
-
if (!fs.existsSync(parentDir)) continue; // engine not installed, skip
|
|
177
227
|
const stat = fs.lstatSync(altDir);
|
|
178
228
|
if (stat.isSymbolicLink()) continue; // already a symlink, good
|
|
179
|
-
// Physical directory exists — replace with symlink
|
|
180
|
-
|
|
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}`);
|
|
181
233
|
fs.symlinkSync(CLAUDE_SKILLS_DIR, altDir);
|
|
182
234
|
} catch (e) {
|
|
183
235
|
if (e.code === 'ENOENT') {
|
|
184
|
-
//
|
|
185
|
-
try {
|
|
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 */ }
|
|
186
241
|
}
|
|
187
|
-
// Other errors: non-fatal, skip
|
|
242
|
+
// Other errors (e.g. engine not installed): non-fatal, skip
|
|
188
243
|
}
|
|
189
244
|
}
|
|
190
245
|
|
|
@@ -345,7 +400,7 @@ function needsBootstrap() {
|
|
|
345
400
|
} catch { return true; }
|
|
346
401
|
}
|
|
347
402
|
|
|
348
|
-
function spawnDistillBackground() {
|
|
403
|
+
function spawnDistillBackground(engine) {
|
|
349
404
|
const distillPath = path.join(METAME_DIR, 'distill.js');
|
|
350
405
|
if (!fs.existsSync(distillPath)) return;
|
|
351
406
|
|
|
@@ -384,14 +439,16 @@ function spawnDistillBackground() {
|
|
|
384
439
|
}
|
|
385
440
|
|
|
386
441
|
|
|
387
|
-
// Spawn as detached background process — won't block
|
|
388
|
-
// 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
|
|
389
444
|
const distillEnvClean = { ...process.env };
|
|
390
445
|
delete distillEnvClean.CLAUDECODE;
|
|
446
|
+
if (engine) distillEnvClean.METAME_ENGINE = engine;
|
|
391
447
|
const bg = spawn('node', [distillPath], {
|
|
392
448
|
detached: true,
|
|
393
449
|
stdio: 'ignore',
|
|
394
450
|
env: distillEnvClean,
|
|
451
|
+
windowsHide: true,
|
|
395
452
|
});
|
|
396
453
|
bg.unref();
|
|
397
454
|
}
|
|
@@ -570,18 +627,45 @@ This step connects the bot to the user's PRIVATE chat — this is the admin chan
|
|
|
570
627
|
6. Tell user to run \`metame start\` to activate.
|
|
571
628
|
|
|
572
629
|
- If **Feishu:**
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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:
|
|
579
659
|
\`\`\`bash
|
|
580
|
-
TOKEN=$(curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
|
|
581
|
-
|
|
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}'
|
|
582
665
|
\`\`\`
|
|
583
|
-
|
|
584
|
-
|
|
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.
|
|
585
669
|
|
|
586
670
|
- If **Skip:** Say "No problem. You can run \`metame daemon init\` anytime to set this up later." Then begin normal work.
|
|
587
671
|
|
|
@@ -628,25 +712,39 @@ if (fs.existsSync(PROJECT_FILE)) {
|
|
|
628
712
|
}
|
|
629
713
|
|
|
630
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.
|
|
631
716
|
const yaml = require('js-yaml');
|
|
632
717
|
let isKnownUser = false;
|
|
718
|
+
let _brainDoc = null;
|
|
633
719
|
try {
|
|
634
720
|
if (fs.existsSync(BRAIN_FILE)) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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;
|
|
639
730
|
}
|
|
640
731
|
} catch (e) {
|
|
641
732
|
// Ignore error, treat as unknown
|
|
642
733
|
}
|
|
643
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
|
+
|
|
644
740
|
let finalProtocol;
|
|
645
741
|
if (isKnownUser) {
|
|
646
742
|
finalProtocol = PROTOCOL_NORMAL;
|
|
647
743
|
} else {
|
|
648
744
|
finalProtocol = PROTOCOL_ONBOARDING;
|
|
649
|
-
|
|
745
|
+
if (!_isNonSessionCmd) {
|
|
746
|
+
console.log(`${icon("new")} New user detected — entering Genesis interview mode...`);
|
|
747
|
+
}
|
|
650
748
|
}
|
|
651
749
|
|
|
652
750
|
// ---------------------------------------------------------
|
|
@@ -654,8 +752,8 @@ if (isKnownUser) {
|
|
|
654
752
|
// ---------------------------------------------------------
|
|
655
753
|
let mirrorLine = '';
|
|
656
754
|
try {
|
|
657
|
-
if (isKnownUser &&
|
|
658
|
-
const brainDoc =
|
|
755
|
+
if (isKnownUser && _brainDoc) {
|
|
756
|
+
const brainDoc = _brainDoc;
|
|
659
757
|
|
|
660
758
|
// Check quiet mode
|
|
661
759
|
const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
|
|
@@ -708,73 +806,10 @@ try {
|
|
|
708
806
|
// Non-fatal
|
|
709
807
|
}
|
|
710
808
|
|
|
711
|
-
// ---------------------------------------------------------
|
|
712
|
-
// 4.6 REFLECTION PROMPT (Phase C — conditional, NOT static)
|
|
713
|
-
// ---------------------------------------------------------
|
|
714
|
-
// Only inject when trigger conditions are met at startup.
|
|
715
|
-
// This ensures reflections don't fire every session.
|
|
716
|
-
let reflectionLine = '';
|
|
717
|
-
try {
|
|
718
|
-
if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
|
|
719
|
-
const refDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
720
|
-
|
|
721
|
-
// Check quiet mode
|
|
722
|
-
const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
|
|
723
|
-
const isQuietForRef = quietUntil && new Date(quietUntil).getTime() > Date.now();
|
|
724
|
-
|
|
725
|
-
if (!isQuietForRef) {
|
|
726
|
-
const distillCount = (refDoc.evolution && refDoc.evolution.distill_count) || 0;
|
|
727
|
-
const zoneHistory = (refDoc.growth && refDoc.growth.zone_history) || [];
|
|
728
|
-
|
|
729
|
-
// Trigger 1: Every 7th session
|
|
730
|
-
const trigger7th = distillCount > 0 && distillCount % 7 === 0;
|
|
731
|
-
|
|
732
|
-
// Trigger 2: Three consecutive comfort-zone sessions
|
|
733
|
-
const lastThree = zoneHistory.slice(-3);
|
|
734
|
-
const triggerComfort = lastThree.length === 3 && lastThree.every(z => z === 'C');
|
|
735
|
-
|
|
736
|
-
// Trigger 3: Persistent goal drift (2+ drifted in last 3 sessions)
|
|
737
|
-
let triggerDrift = false;
|
|
738
|
-
let driftDeclaredFocus = null;
|
|
739
|
-
try {
|
|
740
|
-
const sessionLogFile = path.join(METAME_DIR, 'session_log.yaml');
|
|
741
|
-
if (fs.existsSync(sessionLogFile)) {
|
|
742
|
-
const driftLog = yaml.load(fs.readFileSync(sessionLogFile, 'utf8'));
|
|
743
|
-
if (driftLog && Array.isArray(driftLog.sessions)) {
|
|
744
|
-
const recentSessions = driftLog.sessions.slice(-3);
|
|
745
|
-
const driftCount = recentSessions.filter(s =>
|
|
746
|
-
s.goal_alignment === 'drifted' || s.goal_alignment === 'partial'
|
|
747
|
-
).length;
|
|
748
|
-
if (driftCount >= 2 && recentSessions.length >= 2) {
|
|
749
|
-
driftDeclaredFocus = refDoc.status?.focus || refDoc.context?.focus;
|
|
750
|
-
if (driftDeclaredFocus) triggerDrift = true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
} catch { /* non-fatal */ }
|
|
755
|
-
|
|
756
|
-
if (triggerDrift || triggerComfort || trigger7th) {
|
|
757
|
-
let hint = '';
|
|
758
|
-
if (triggerDrift) {
|
|
759
|
-
hint = `最近几个session的方向和"${driftDeclaredFocus}"有偏差。请在对话开始时温和地问:${icon("mirror")} 是方向有意调整了,还是不小心偏了?`;
|
|
760
|
-
} else if (triggerComfort) {
|
|
761
|
-
hint = `连续几次都在熟悉领域。如果用户在session结束时自然停顿,可以温和地问:${icon("mirror")} 准备好探索拉伸区了吗?`;
|
|
762
|
-
} else {
|
|
763
|
-
hint = '这是第' + distillCount + `次session。如果session自然结束,可以附加一句:${icon("mirror")} 一个词形容这次session的感受?`;
|
|
764
|
-
}
|
|
765
|
-
const timing = triggerDrift ? '在对话开始时就问一次' : '只在session即将结束时说一次';
|
|
766
|
-
reflectionLine = `\n[MetaMe reflection: ${hint} ${timing}。如果用户没回应就不要追问。]\n`;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
} catch {
|
|
771
|
-
// Non-fatal
|
|
772
|
-
}
|
|
773
|
-
|
|
774
809
|
// Project-level CLAUDE.md: KERNEL has moved to global ~/.claude/CLAUDE.md.
|
|
775
|
-
// Only inject dynamic per-session observations (mirror
|
|
810
|
+
// Only inject dynamic per-session observations (mirror).
|
|
776
811
|
// If nothing dynamic, write the cleaned file with no METAME block at all.
|
|
777
|
-
const dynamicContent = mirrorLine
|
|
812
|
+
const dynamicContent = mirrorLine;
|
|
778
813
|
const newContent = dynamicContent.trim()
|
|
779
814
|
? METAME_START + '\n' + dynamicContent + METAME_END + '\n' + fileContent
|
|
780
815
|
: fileContent;
|
|
@@ -1450,6 +1485,7 @@ if (isDaemon) {
|
|
|
1450
1485
|
const DAEMON_CONFIG = path.join(METAME_DIR, 'daemon.yaml');
|
|
1451
1486
|
const DAEMON_STATE = path.join(METAME_DIR, 'daemon_state.json');
|
|
1452
1487
|
const DAEMON_PID = path.join(METAME_DIR, 'daemon.pid');
|
|
1488
|
+
const DAEMON_LOCK = path.join(METAME_DIR, 'daemon.lock');
|
|
1453
1489
|
const DAEMON_LOG = path.join(METAME_DIR, 'daemon.log');
|
|
1454
1490
|
const DAEMON_DEFAULT = path.join(__dirname, 'scripts', 'daemon-default.yaml');
|
|
1455
1491
|
const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
|
|
@@ -1780,11 +1816,10 @@ WantedBy=default.target
|
|
|
1780
1816
|
}
|
|
1781
1817
|
// Use caffeinate on macOS to prevent sleep while daemon is running
|
|
1782
1818
|
const isMac = process.platform === 'darwin';
|
|
1783
|
-
const isWin = process.platform === 'win32';
|
|
1784
1819
|
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
1785
1820
|
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
1786
1821
|
const bg = spawn(cmd, args, {
|
|
1787
|
-
detached:
|
|
1822
|
+
detached: true,
|
|
1788
1823
|
stdio: 'ignore',
|
|
1789
1824
|
windowsHide: true,
|
|
1790
1825
|
env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
|
|
@@ -1827,14 +1862,26 @@ WantedBy=default.target
|
|
|
1827
1862
|
|
|
1828
1863
|
// Check if running
|
|
1829
1864
|
let isRunning = false;
|
|
1865
|
+
let runningPid = null;
|
|
1830
1866
|
if (fs.existsSync(DAEMON_PID)) {
|
|
1831
1867
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
1832
|
-
try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
|
|
1868
|
+
try { process.kill(pid, 0); isRunning = true; runningPid = pid; } catch { /* dead */ }
|
|
1869
|
+
}
|
|
1870
|
+
if (!isRunning && fs.existsSync(DAEMON_LOCK)) {
|
|
1871
|
+
try {
|
|
1872
|
+
const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
|
|
1873
|
+
const pid = parseInt(lock && lock.pid, 10);
|
|
1874
|
+
if (pid) {
|
|
1875
|
+
process.kill(pid, 0);
|
|
1876
|
+
isRunning = true;
|
|
1877
|
+
runningPid = pid;
|
|
1878
|
+
}
|
|
1879
|
+
} catch { /* lock stale or invalid */ }
|
|
1833
1880
|
}
|
|
1834
1881
|
|
|
1835
1882
|
console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
|
|
1836
1883
|
if (state.started_at) console.log(` Started: ${state.started_at}`);
|
|
1837
|
-
if (state.pid) console.log(` PID: ${state.pid}`);
|
|
1884
|
+
if (runningPid || state.pid) console.log(` PID: ${runningPid || state.pid}`);
|
|
1838
1885
|
|
|
1839
1886
|
// Budget
|
|
1840
1887
|
const budget = state.budget || {};
|
|
@@ -1926,39 +1973,49 @@ WantedBy=default.target
|
|
|
1926
1973
|
process.exit(0);
|
|
1927
1974
|
}
|
|
1928
1975
|
|
|
1976
|
+
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.';
|
|
1977
|
+
|
|
1929
1978
|
// ---------------------------------------------------------
|
|
1930
1979
|
// 5.8 CODEX — launch Codex with MetaMe initialization
|
|
1931
1980
|
// ---------------------------------------------------------
|
|
1932
1981
|
const isCodex = process.argv[2] === 'codex';
|
|
1933
1982
|
if (isCodex) {
|
|
1934
|
-
// spawn() resolves PATH automatically; error event handles missing binary
|
|
1935
|
-
const codexBin = 'codex';
|
|
1936
|
-
|
|
1937
|
-
// Build codex args: remaining user args after 'codex'
|
|
1938
1983
|
const codexUserArgs = process.argv.slice(3);
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1984
|
+
const codexProviderEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
|
|
1985
|
+
|
|
1986
|
+
// Genesis: new user + interactive mode — trigger profile interview within the same Codex session.
|
|
1987
|
+
// CLAUDE.md (already written to disk above) contains the full genesis protocol; Codex reads it.
|
|
1988
|
+
// We pass the trigger as the opening [PROMPT] argument so genesis flows into normal work seamlessly.
|
|
1989
|
+
const codexArgs = codexUserArgs.length === 0
|
|
1990
|
+
? ['--dangerously-bypass-approvals-and-sandbox']
|
|
1991
|
+
: ['exec', '--dangerously-bypass-approvals-and-sandbox', ...codexUserArgs];
|
|
1992
|
+
|
|
1993
|
+
// Codex reads AGENTS.md (not CLAUDE.md); create symlink so genesis protocol is visible.
|
|
1994
|
+
// Also ensure global ~/AGENTS.md → ~/.claude/CLAUDE.md for identity context.
|
|
1995
|
+
// Use try-catch on symlinkSync directly (avoids TOCTOU race from existsSync pre-check).
|
|
1996
|
+
try {
|
|
1997
|
+
if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')))
|
|
1998
|
+
fs.symlinkSync('CLAUDE.md', path.join(process.cwd(), 'AGENTS.md'));
|
|
1999
|
+
} catch { /* EEXIST or other — non-critical */ }
|
|
2000
|
+
try {
|
|
2001
|
+
const globalClaudeMd = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
|
|
2002
|
+
if (fs.existsSync(globalClaudeMd))
|
|
2003
|
+
fs.symlinkSync(globalClaudeMd, path.join(HOME_DIR, 'AGENTS.md'));
|
|
2004
|
+
} catch { /* EEXIST or other — non-critical */ }
|
|
1947
2005
|
|
|
1948
|
-
const
|
|
2006
|
+
const child = spawnCodex(codexArgs, {
|
|
1949
2007
|
stdio: 'inherit',
|
|
1950
2008
|
cwd: process.cwd(),
|
|
1951
|
-
env: { ...process.env, METAME_ACTIVE_SESSION: 'true' },
|
|
2009
|
+
env: { ...process.env, ...codexProviderEnv, METAME_ACTIVE_SESSION: 'true' },
|
|
1952
2010
|
});
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2011
|
+
let launchError = false;
|
|
2012
|
+
child.on('error', (err) => {
|
|
2013
|
+
launchError = true;
|
|
2014
|
+
console.error(`\n${icon("fail")} Error: Could not launch 'codex': ${err.message}`);
|
|
1956
2015
|
console.error(" Please install: npm install -g @openai/codex");
|
|
1957
2016
|
});
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
// Background distillation
|
|
1961
|
-
spawnDistillBackground();
|
|
2017
|
+
child.on('close', (code) => process.exit(launchError ? 127 : (code || 0)));
|
|
2018
|
+
spawnDistillBackground('codex');
|
|
1962
2019
|
return;
|
|
1963
2020
|
}
|
|
1964
2021
|
|
|
@@ -2049,10 +2106,7 @@ if (daemonCfg.dangerously_skip_permissions && !launchArgs.includes('--dangerousl
|
|
|
2049
2106
|
launchArgs.push('--dangerously-skip-permissions');
|
|
2050
2107
|
}
|
|
2051
2108
|
if (!isKnownUser) {
|
|
2052
|
-
launchArgs.push(
|
|
2053
|
-
'--append-system-prompt',
|
|
2054
|
-
'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.'
|
|
2055
|
-
);
|
|
2109
|
+
launchArgs.push('--append-system-prompt', GENESIS_TRIGGER_PROMPT);
|
|
2056
2110
|
}
|
|
2057
2111
|
|
|
2058
2112
|
// RAG: inject relevant facts based on current project (desktop-side equivalent of daemon RAG)
|
|
@@ -2100,11 +2154,10 @@ try {
|
|
|
2100
2154
|
}
|
|
2101
2155
|
if (!daemonRunning) {
|
|
2102
2156
|
const _isMac = process.platform === 'darwin';
|
|
2103
|
-
const _isWin = process.platform === 'win32';
|
|
2104
2157
|
const dCmd = _isMac ? 'caffeinate' : process.execPath;
|
|
2105
2158
|
const dArgs = _isMac ? ['-i', process.execPath, _daemonScript] : [_daemonScript];
|
|
2106
2159
|
const bg = spawn(dCmd, dArgs, {
|
|
2107
|
-
detached:
|
|
2160
|
+
detached: true,
|
|
2108
2161
|
stdio: 'ignore',
|
|
2109
2162
|
windowsHide: true,
|
|
2110
2163
|
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.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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": {
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test scripts/*.test.js",
|
|
18
|
+
"test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
|
|
18
19
|
"start": "node index.js",
|
|
19
|
-
"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/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/self-reflect.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
|
|
20
21
|
"sync:readme": "node scripts/sync-readme.js",
|
|
21
|
-
"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'",
|
|
22
23
|
"precommit": "npm run sync:plugin && npm run restart:daemon"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|