metame-cli 1.4.31 → 1.4.33
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 +42 -19
- package/index.js +237 -149
- package/package.json +3 -2
- package/scripts/bin/dispatch_to +92 -0
- package/scripts/daemon-claude-engine.js +5 -2
- package/scripts/daemon-runtime-lifecycle.js +4 -2
- package/scripts/daemon-task-scheduler.js +28 -3
- package/scripts/daemon.js +40 -3
- package/scripts/docs/agent-guide.md +50 -0
- package/scripts/docs/file-transfer.md +32 -0
- package/scripts/feishu-adapter.js +6 -2
- package/scripts/memory-extract.js +2 -2
- package/scripts/memory-gc.js +2 -2
- package/scripts/memory-write.js +0 -1
- package/scripts/memory.js +1 -1
- package/scripts/platform.js +172 -0
- package/scripts/reliability-core.test.js +15 -3
- package/scripts/schema.js +43 -10
- package/scripts/skill-evolution.test.js +7 -1
- package/scripts/sync-readme.js +64 -0
- package/scripts/templates/default-global-claude.md +19 -0
- package/scripts/utils.test.js +12 -5
- package/scripts/migrate-v2.js +0 -112
- package/scripts/self-reflect.js +0 -285
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const { spawn, execSync } = require('child_process');
|
|
10
|
+
const { sleepSync, findProcessesByPattern, icon } = require('./scripts/platform');
|
|
10
11
|
|
|
11
12
|
// On Windows, .cmd files (like claude.cmd from npm global) need shell:true to spawn.
|
|
12
13
|
// We use COMSPEC to avoid conda/PATH issues where cmd.exe can't be found.
|
|
@@ -49,10 +50,45 @@ if (!fs.existsSync(METAME_DIR)) {
|
|
|
49
50
|
fs.mkdirSync(METAME_DIR, { recursive: true });
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// ---------------------------------------------------------
|
|
54
|
+
// DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
|
|
55
|
+
// ---------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sync files from srcDir to destDir. Only writes when content differs.
|
|
59
|
+
* @param {string} srcDir - source directory
|
|
60
|
+
* @param {string} destDir - destination directory
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {string[]} [opts.fileList] - explicit file list (skip readdirSync)
|
|
63
|
+
* @param {number} [opts.chmod] - chmod after write (e.g. 0o755)
|
|
64
|
+
* @returns {boolean} true if any file was updated
|
|
65
|
+
*/
|
|
66
|
+
function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
|
|
67
|
+
if (!fs.existsSync(srcDir)) return false;
|
|
68
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
69
|
+
let updated = false;
|
|
70
|
+
const files = fileList || fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
|
|
71
|
+
for (const f of files) {
|
|
72
|
+
const src = path.join(srcDir, f);
|
|
73
|
+
const dest = path.join(destDir, f);
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(src)) continue;
|
|
76
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
77
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
|
|
78
|
+
if (srcContent !== destContent) {
|
|
79
|
+
fs.writeFileSync(dest, srcContent, 'utf8');
|
|
80
|
+
if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
|
|
81
|
+
updated = true;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* non-fatal per file */ }
|
|
84
|
+
}
|
|
85
|
+
return updated;
|
|
86
|
+
}
|
|
87
|
+
|
|
52
88
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
53
89
|
// IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
|
|
54
90
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
55
|
-
const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.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-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
|
|
91
|
+
const BUNDLED_BASE_SCRIPTS = ['platform.js', 'signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.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-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
|
|
56
92
|
const DAEMON_MODULE_SCRIPTS = (() => {
|
|
57
93
|
try {
|
|
58
94
|
return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
|
|
@@ -74,32 +110,21 @@ try {
|
|
|
74
110
|
}
|
|
75
111
|
} catch { /* non-fatal */ }
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
for (const script of BUNDLED_SCRIPTS) {
|
|
79
|
-
const src = path.join(scriptsDir, script);
|
|
80
|
-
const dest = path.join(METAME_DIR, script);
|
|
81
|
-
try {
|
|
82
|
-
if (fs.existsSync(src)) {
|
|
83
|
-
const srcContent = fs.readFileSync(src, 'utf8');
|
|
84
|
-
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
|
|
85
|
-
if (srcContent !== destContent) {
|
|
86
|
-
fs.writeFileSync(dest, srcContent, 'utf8');
|
|
87
|
-
scriptsUpdated = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// Non-fatal
|
|
92
|
-
}
|
|
93
|
-
}
|
|
113
|
+
const scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
|
|
94
114
|
|
|
95
115
|
// Daemon restart on script update:
|
|
96
116
|
// Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
|
|
97
117
|
// and has defer logic (waits for active Claude tasks to finish before restarting).
|
|
98
118
|
// Killing here bypasses that and interrupts ongoing conversations.
|
|
99
119
|
if (scriptsUpdated) {
|
|
100
|
-
console.log(
|
|
120
|
+
console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
// Docs: lazy-load references for CLAUDE.md pointer instructions
|
|
124
|
+
syncDirFiles(path.join(__dirname, 'scripts', 'docs'), path.join(METAME_DIR, 'docs'));
|
|
125
|
+
// Bin: CLI tools (dispatch_to etc.)
|
|
126
|
+
syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.join(METAME_DIR, 'bin'), { chmod: 0o755 });
|
|
127
|
+
|
|
103
128
|
// ---------------------------------------------------------
|
|
104
129
|
// Deploy bundled skills to ~/.claude/skills/
|
|
105
130
|
// Only installs if not already present — never overwrites user customizations.
|
|
@@ -131,7 +156,7 @@ if (fs.existsSync(bundledSkillsDir)) {
|
|
|
131
156
|
skillsInstalled.push(skillName);
|
|
132
157
|
}
|
|
133
158
|
if (skillsInstalled.length > 0) {
|
|
134
|
-
console.log(
|
|
159
|
+
console.log(`${icon("brain")} Skills installed: ${skillsInstalled.join(', ')}`);
|
|
135
160
|
}
|
|
136
161
|
} catch {
|
|
137
162
|
// Non-fatal
|
|
@@ -153,7 +178,7 @@ if (!fs.existsSync(DAEMON_CONFIG_FILE)) {
|
|
|
153
178
|
if (fs.existsSync(DAEMON_YAML_BACKUP)) {
|
|
154
179
|
// Restore from backup — user had real config that was lost
|
|
155
180
|
fs.copyFileSync(DAEMON_YAML_BACKUP, DAEMON_CONFIG_FILE);
|
|
156
|
-
console.log(
|
|
181
|
+
console.log(`${icon("warn")} daemon.yaml was missing — restored from backup.`);
|
|
157
182
|
} else {
|
|
158
183
|
const daemonTemplate = path.join(scriptsDir, 'daemon-default.yaml');
|
|
159
184
|
if (fs.existsSync(daemonTemplate)) {
|
|
@@ -179,13 +204,34 @@ function ensureHookInstalled() {
|
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
// Check if our hook is already configured
|
|
182
|
-
|
|
207
|
+
// Use forward slashes + quotes — Claude Code runs hooks via bash even on Windows
|
|
208
|
+
const scriptPathForHook = SIGNAL_CAPTURE_SCRIPT.replace(/\\/g, '/');
|
|
209
|
+
const hookCommand = `node "${scriptPathForHook}"`;
|
|
183
210
|
const existing = settings.hooks?.UserPromptSubmit || [];
|
|
184
211
|
const alreadyInstalled = existing.some(entry =>
|
|
185
|
-
entry.hooks?.some(h => h.command
|
|
212
|
+
entry.hooks?.some(h => h.command && h.command.includes('signal-capture.js'))
|
|
186
213
|
);
|
|
187
214
|
|
|
188
|
-
|
|
215
|
+
// Remove stale hooks with backslash paths (old Windows format)
|
|
216
|
+
if (settings.hooks?.UserPromptSubmit) {
|
|
217
|
+
for (const entry of settings.hooks.UserPromptSubmit) {
|
|
218
|
+
if (entry.hooks) {
|
|
219
|
+
entry.hooks = entry.hooks.filter(h =>
|
|
220
|
+
!(h.command && h.command.includes('signal-capture.js') && h.command.includes('\\'))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
225
|
+
entry => entry.hooks && entry.hooks.length > 0
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Re-check after cleanup
|
|
230
|
+
const stillInstalled = (settings.hooks?.UserPromptSubmit || []).some(entry =>
|
|
231
|
+
entry.hooks?.some(h => h.command && h.command.includes('signal-capture.js'))
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!stillInstalled) {
|
|
189
235
|
if (!settings.hooks) settings.hooks = {};
|
|
190
236
|
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
191
237
|
|
|
@@ -197,11 +243,11 @@ function ensureHookInstalled() {
|
|
|
197
243
|
});
|
|
198
244
|
|
|
199
245
|
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
|
|
200
|
-
console.log("
|
|
246
|
+
console.log(`${icon("hook")} MetaMe: Signal capture hook installed.`);
|
|
201
247
|
}
|
|
202
248
|
} catch (e) {
|
|
203
249
|
// Non-fatal: hook install failure shouldn't block launch
|
|
204
|
-
console.error("
|
|
250
|
+
console.error(`${icon("warn")} Hook install skipped:`, e.message);
|
|
205
251
|
}
|
|
206
252
|
}
|
|
207
253
|
|
|
@@ -264,7 +310,7 @@ function spawnDistillBackground() {
|
|
|
264
310
|
// 4-hour cooldown: check last distill timestamp from profile
|
|
265
311
|
const cooldownMs = 4 * 60 * 60 * 1000;
|
|
266
312
|
try {
|
|
267
|
-
const profilePath = path.join(
|
|
313
|
+
const profilePath = path.join(HOME_DIR, '.claude_profile.yaml');
|
|
268
314
|
if (fs.existsSync(profilePath)) {
|
|
269
315
|
const yaml = require('js-yaml');
|
|
270
316
|
const profile = yaml.load(fs.readFileSync(profilePath, 'utf8'));
|
|
@@ -391,7 +437,7 @@ status:
|
|
|
391
437
|
// ---------------------------------------------------------
|
|
392
438
|
const PROTOCOL_NORMAL = `${METAME_START}
|
|
393
439
|
---
|
|
394
|
-
##
|
|
440
|
+
## ${icon("brain")} SYSTEM KERNEL: SHADOW_MODE (Active)
|
|
395
441
|
|
|
396
442
|
**1. THE BRAIN (Source of Truth):**
|
|
397
443
|
* **FILE:** \`$HOME/.claude_profile.yaml\`
|
|
@@ -417,7 +463,7 @@ const PROTOCOL_NORMAL = `${METAME_START}
|
|
|
417
463
|
|
|
418
464
|
const PROTOCOL_ONBOARDING = `${METAME_START}
|
|
419
465
|
---
|
|
420
|
-
##
|
|
466
|
+
## ${icon("brain")} SYSTEM KERNEL: SHADOW_MODE (Active)
|
|
421
467
|
|
|
422
468
|
**1. THE BRAIN (Source of Truth):**
|
|
423
469
|
* **FILE:** \`$HOME/.claude_profile.yaml\`
|
|
@@ -474,7 +520,7 @@ This step connects the bot to the user's PRIVATE chat — this is the admin chan
|
|
|
474
520
|
|
|
475
521
|
- If **Feishu:**
|
|
476
522
|
1. Guide through: open.feishu.cn/app → create app → get App ID + Secret → enable bot → add event subscription (long connection mode) → add permissions (im:message, im:message.p2p_msg:readonly, im:message.group_at_msg:readonly, im:message:send_as_bot, im:resource) → publish.
|
|
477
|
-
|
|
523
|
+
**${icon("warn")} 重要:** 在「事件订阅」页面,必须开启「接收消息 im.message.receive_v1」事件。然后在该事件的配置中,勾选「获取群组中所有消息」(否则 bot 在群聊中只能收到 @它 的消息,无法接收普通群消息)。
|
|
478
524
|
2. Ask user to paste App ID and App Secret.
|
|
479
525
|
3. Write \`app_id\` and \`app_secret\` into \`~/.metame/daemon.yaml\` under \`feishu:\` section, set \`enabled: true\`.
|
|
480
526
|
4. Tell user: "Now open Feishu and send any message to your new bot (private chat), then tell me you're done."
|
|
@@ -549,7 +595,7 @@ if (isKnownUser) {
|
|
|
549
595
|
finalProtocol = PROTOCOL_NORMAL;
|
|
550
596
|
} else {
|
|
551
597
|
finalProtocol = PROTOCOL_ONBOARDING;
|
|
552
|
-
console.log("
|
|
598
|
+
console.log(`${icon("new")} New user detected — entering Genesis interview mode...`);
|
|
553
599
|
}
|
|
554
600
|
|
|
555
601
|
// ---------------------------------------------------------
|
|
@@ -659,11 +705,11 @@ try {
|
|
|
659
705
|
if (triggerDrift || triggerComfort || trigger7th) {
|
|
660
706
|
let hint = '';
|
|
661
707
|
if (triggerDrift) {
|
|
662
|
-
hint = `最近几个session的方向和"${driftDeclaredFocus}"
|
|
708
|
+
hint = `最近几个session的方向和"${driftDeclaredFocus}"有偏差。请在对话开始时温和地问:${icon("mirror")} 是方向有意调整了,还是不小心偏了?`;
|
|
663
709
|
} else if (triggerComfort) {
|
|
664
|
-
hint =
|
|
710
|
+
hint = `连续几次都在熟悉领域。如果用户在session结束时自然停顿,可以温和地问:${icon("mirror")} 准备好探索拉伸区了吗?`;
|
|
665
711
|
} else {
|
|
666
|
-
hint = '这是第' + distillCount +
|
|
712
|
+
hint = '这是第' + distillCount + `次session。如果session自然结束,可以附加一句:${icon("mirror")} 一个词形容这次session的感受?`;
|
|
667
713
|
}
|
|
668
714
|
const timing = triggerDrift ? '在对话开始时就问一次' : '只在session即将结束时说一次';
|
|
669
715
|
reflectionLine = `\n[MetaMe reflection: ${hint} ${timing}。如果用户没回应就不要追问。]\n`;
|
|
@@ -728,16 +774,20 @@ const CAPABILITY_SECTIONS = [
|
|
|
728
774
|
`"告诉X/让X" → \`~/.metame/bin/dispatch_to <project_key> "内容"\`,手机端 \`/dispatch to <key> <消息>\`。` + dispatchTable,
|
|
729
775
|
'新增 Agent:`/agent bind <名称> <工作目录>`',
|
|
730
776
|
'',
|
|
777
|
+
'## Agent 创建与管理',
|
|
778
|
+
'用户问创建/管理/绑定 Agent 时 → 先 `cat ~/.metame/docs/agent-guide.md` 再回答。',
|
|
779
|
+
'',
|
|
780
|
+
'## 手机端文件交互',
|
|
781
|
+
'用户要文件("发给我"/"发过来"/"导出")→ 先 `cat ~/.metame/docs/file-transfer.md` 再执行。',
|
|
782
|
+
'**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
|
|
783
|
+
'**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
|
|
784
|
+
'',
|
|
731
785
|
'## 跨会话记忆',
|
|
732
786
|
'用户提"上次/之前"时搜索:`node ~/.metame/memory-search.js "关键词1" "keyword2"`',
|
|
733
787
|
'一次传 3-4 个关键词(中文+英文+函数名),`--facts` 只搜事实,`--sessions` 只搜会话。',
|
|
734
788
|
'',
|
|
735
789
|
'## Skills',
|
|
736
790
|
'能力不足/工具缺失/任务失败 → 先查 `cat ~/.claude/skills/skill-manager/SKILL.md`,不要自己猜。',
|
|
737
|
-
'',
|
|
738
|
-
'## 手机端文件交互',
|
|
739
|
-
'**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
|
|
740
|
-
'**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
|
|
741
791
|
|
|
742
792
|
].join('\n');
|
|
743
793
|
|
|
@@ -756,6 +806,14 @@ try {
|
|
|
756
806
|
), '');
|
|
757
807
|
}
|
|
758
808
|
|
|
809
|
+
// New user: seed with default template if CLAUDE.md is empty or missing
|
|
810
|
+
if (!globalContent.trim()) {
|
|
811
|
+
const tplPath = path.join(__dirname, 'scripts', 'templates', 'default-global-claude.md');
|
|
812
|
+
if (fs.existsSync(tplPath)) {
|
|
813
|
+
globalContent = fs.readFileSync(tplPath, 'utf8');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
759
817
|
const injection =
|
|
760
818
|
GLOBAL_MARKER_START + '\n' +
|
|
761
819
|
KERNEL_BODY + '\n\n' +
|
|
@@ -767,13 +825,13 @@ try {
|
|
|
767
825
|
fs.writeFileSync(GLOBAL_CLAUDE_MD, finalGlobal, 'utf8');
|
|
768
826
|
} catch (e) {
|
|
769
827
|
// Non-fatal: global CLAUDE.md injection is best-effort
|
|
770
|
-
console.error(
|
|
828
|
+
console.error(`${icon("warn")} Failed to inject global CLAUDE.md: ${e.message}`);
|
|
771
829
|
}
|
|
772
830
|
|
|
773
831
|
|
|
774
832
|
|
|
775
833
|
|
|
776
|
-
console.log(
|
|
834
|
+
console.log(`${icon("magic")} MetaMe v${pkgVersion}: Link Established.`);
|
|
777
835
|
|
|
778
836
|
// Memory system status — show live stats without blocking launch
|
|
779
837
|
try {
|
|
@@ -789,7 +847,7 @@ try {
|
|
|
789
847
|
memMod.close();
|
|
790
848
|
} catch { /* memory.js not available or DB not ready */ }
|
|
791
849
|
if (factCount > 0 || tagCount > 0) {
|
|
792
|
-
console.log(
|
|
850
|
+
console.log(`${icon("brain")} Memory: ${factCount} facts · ${tagCount} sessions tagged`);
|
|
793
851
|
}
|
|
794
852
|
} catch { /* non-fatal */ }
|
|
795
853
|
|
|
@@ -801,12 +859,12 @@ try {
|
|
|
801
859
|
: 0;
|
|
802
860
|
|
|
803
861
|
if (pendingCount > 0) {
|
|
804
|
-
console.log(
|
|
862
|
+
console.log(`${icon("dna")} Cognition: ${pendingCount} moment${pendingCount > 1 ? 's' : ''} pending distillation`);
|
|
805
863
|
} else {
|
|
806
864
|
// Show last distill time
|
|
807
865
|
let lastDistillStr = '从未';
|
|
808
866
|
try {
|
|
809
|
-
const profilePath = path.join(
|
|
867
|
+
const profilePath = path.join(HOME_DIR, '.claude_profile.yaml');
|
|
810
868
|
if (fs.existsSync(profilePath)) {
|
|
811
869
|
const _yaml = require('js-yaml');
|
|
812
870
|
const profile = _yaml.load(fs.readFileSync(profilePath, 'utf8'));
|
|
@@ -820,7 +878,7 @@ try {
|
|
|
820
878
|
}
|
|
821
879
|
}
|
|
822
880
|
} catch { /* non-fatal */ }
|
|
823
|
-
console.log(
|
|
881
|
+
console.log(`${icon("dna")} Cognition: 无新信号 · 上次蒸馏 ${lastDistillStr}`);
|
|
824
882
|
}
|
|
825
883
|
} catch { /* non-fatal */ }
|
|
826
884
|
|
|
@@ -846,7 +904,7 @@ const CURRENT_VERSION = pkgVersion;
|
|
|
846
904
|
});
|
|
847
905
|
|
|
848
906
|
if (latest && latest !== CURRENT_VERSION) {
|
|
849
|
-
console.log(
|
|
907
|
+
console.log(`${icon("pkg")} MetaMe ${latest} available (current ${CURRENT_VERSION}), updating...`);
|
|
850
908
|
const { execSync } = require('child_process');
|
|
851
909
|
try {
|
|
852
910
|
execSync('npm install -g metame-cli@latest', {
|
|
@@ -854,10 +912,10 @@ const CURRENT_VERSION = pkgVersion;
|
|
|
854
912
|
timeout: 60000,
|
|
855
913
|
...(process.platform === 'win32' ? { shell: process.env.COMSPEC || true } : {}),
|
|
856
914
|
});
|
|
857
|
-
console.log(
|
|
915
|
+
console.log(`${icon("ok")} Updated to ${latest}. Restart metame to use the new version.`);
|
|
858
916
|
} catch (e) {
|
|
859
917
|
const msg = e.stderr ? e.stderr.toString().trim().split('\n').pop() : '';
|
|
860
|
-
console.log(
|
|
918
|
+
console.log(`${icon("warn")} Auto-update failed${msg ? ': ' + msg : ''}. Run manually: npm install -g metame-cli`);
|
|
861
919
|
}
|
|
862
920
|
}
|
|
863
921
|
} catch { /* network unavailable, skip silently */ }
|
|
@@ -887,14 +945,14 @@ const CURRENT_VERSION = pkgVersion;
|
|
|
887
945
|
try { execSync(`${whichCmd} bun`, { stdio: 'pipe', timeout: 2000 }); bunAvailable = true; } catch { }
|
|
888
946
|
|
|
889
947
|
console.log('');
|
|
890
|
-
console.log(
|
|
948
|
+
console.log(`┌─ ${icon("search")} 记忆搜索增强(可选,免费)`);
|
|
891
949
|
console.log('│');
|
|
892
950
|
console.log('│ 当前模式:基础全文搜索(FTS5)');
|
|
893
951
|
console.log('│ 安装 QMD 后:BM25 + 向量语义 + 重排序 混合搜索');
|
|
894
952
|
console.log('│ 效果:召回质量约 5x,模糊描述也能精准命中历史记忆');
|
|
895
953
|
if (!bunAvailable) {
|
|
896
954
|
console.log('│');
|
|
897
|
-
console.log(
|
|
955
|
+
console.log(`│ ${icon("warn")} 未检测到 bun,无法自动安装。`);
|
|
898
956
|
console.log('│ 手动安装:curl -fsSL https://bun.sh/install | bash');
|
|
899
957
|
console.log('│ bun install -g github:tobi/qmd');
|
|
900
958
|
console.log('└────────────────────────────────────────────────');
|
|
@@ -913,12 +971,12 @@ const CURRENT_VERSION = pkgVersion;
|
|
|
913
971
|
process.stdout.write('\n');
|
|
914
972
|
|
|
915
973
|
if (answer === 'y' || answer === 'yes') {
|
|
916
|
-
console.log(
|
|
974
|
+
console.log(` ${icon("down")} 正在安装 QMD...`);
|
|
917
975
|
try {
|
|
918
976
|
execSync('bun install -g github:tobi/qmd', { stdio: 'inherit', timeout: 120000 });
|
|
919
|
-
console.log(
|
|
977
|
+
console.log(` ${icon("ok")} QMD 已安装,下次记忆搜索自动启用向量模式。`);
|
|
920
978
|
} catch {
|
|
921
|
-
console.log(
|
|
979
|
+
console.log(` ${icon("warn")} 安装失败,可手动执行:bun install -g github:tobi/qmd`);
|
|
922
980
|
}
|
|
923
981
|
} else {
|
|
924
982
|
console.log(' 跳过。如需日后安装:bun install -g github:tobi/qmd');
|
|
@@ -937,7 +995,7 @@ const CURRENT_VERSION = pkgVersion;
|
|
|
937
995
|
const isRefresh = process.argv.includes('refresh') || process.argv.includes('--refresh');
|
|
938
996
|
|
|
939
997
|
if (isRefresh) {
|
|
940
|
-
console.log("
|
|
998
|
+
console.log(`${icon("ok")} MetaMe configuration re-injected.`);
|
|
941
999
|
console.log(" Ask Claude to 'read CLAUDE.md' to apply the changes.");
|
|
942
1000
|
process.exit(0);
|
|
943
1001
|
}
|
|
@@ -953,7 +1011,7 @@ if (isEvolve) {
|
|
|
953
1011
|
const insight = process.argv.slice(evolveIndex + 1).join(' ').trim();
|
|
954
1012
|
|
|
955
1013
|
if (!insight) {
|
|
956
|
-
console.error("
|
|
1014
|
+
console.error(`${icon("fail")} Error: Missing insight.`);
|
|
957
1015
|
console.error(" Usage: metame evolve \"I realized I prefer functional programming\"");
|
|
958
1016
|
process.exit(1);
|
|
959
1017
|
}
|
|
@@ -975,14 +1033,14 @@ if (isEvolve) {
|
|
|
975
1033
|
// Save back to file
|
|
976
1034
|
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc), 'utf8');
|
|
977
1035
|
|
|
978
|
-
console.log("
|
|
1036
|
+
console.log(`${icon("brain")} MetaMe Brain Updated.`);
|
|
979
1037
|
console.log(` Added insight: "${insight}"`);
|
|
980
1038
|
console.log(" (Run 'metame refresh' to apply this to the current session)");
|
|
981
1039
|
} else {
|
|
982
|
-
console.error("
|
|
1040
|
+
console.error(`${icon("fail")} Error: No profile found. Run 'metame' first to initialize.`);
|
|
983
1041
|
}
|
|
984
1042
|
} catch (e) {
|
|
985
|
-
console.error("
|
|
1043
|
+
console.error(`${icon("fail")} Error updating profile:`, e.message);
|
|
986
1044
|
}
|
|
987
1045
|
process.exit(0);
|
|
988
1046
|
}
|
|
@@ -1002,7 +1060,7 @@ if (isSetTrait) {
|
|
|
1002
1060
|
const value = process.argv.slice(setIndex + 2).join(' ').trim();
|
|
1003
1061
|
|
|
1004
1062
|
if (!key || !value) {
|
|
1005
|
-
console.error("
|
|
1063
|
+
console.error(`${icon("fail")} Error: Missing key or value.`);
|
|
1006
1064
|
console.error(" Usage: metame set-trait identity.role \"New Role\"");
|
|
1007
1065
|
process.exit(1);
|
|
1008
1066
|
}
|
|
@@ -1028,14 +1086,14 @@ if (isSetTrait) {
|
|
|
1028
1086
|
|
|
1029
1087
|
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc), 'utf8');
|
|
1030
1088
|
|
|
1031
|
-
console.log(
|
|
1089
|
+
console.log(`${icon("brain")} MetaMe Brain Surgically Updated.`);
|
|
1032
1090
|
console.log(` Set \`${key}\` = "${value}"`);
|
|
1033
1091
|
console.log(" (Run 'metame refresh' to apply this to the current session)");
|
|
1034
1092
|
} else {
|
|
1035
|
-
console.error("
|
|
1093
|
+
console.error(`${icon("fail")} Error: No profile found.`);
|
|
1036
1094
|
}
|
|
1037
1095
|
} catch (e) {
|
|
1038
|
-
console.error("
|
|
1096
|
+
console.error(`${icon("fail")} Error updating profile:`, e.message);
|
|
1039
1097
|
}
|
|
1040
1098
|
process.exit(0);
|
|
1041
1099
|
}
|
|
@@ -1052,9 +1110,9 @@ if (isQuiet) {
|
|
|
1052
1110
|
if (!doc.growth) doc.growth = {};
|
|
1053
1111
|
doc.growth.quiet_until = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString();
|
|
1054
1112
|
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
1055
|
-
console.log("
|
|
1113
|
+
console.log(`${icon("brain")} MetaMe: Mirror & reflections silenced for 48 hours.`);
|
|
1056
1114
|
} catch (e) {
|
|
1057
|
-
console.error("
|
|
1115
|
+
console.error(`${icon("fail")} Error:`, e.message);
|
|
1058
1116
|
}
|
|
1059
1117
|
process.exit(0);
|
|
1060
1118
|
}
|
|
@@ -1068,26 +1126,26 @@ if (isInsights) {
|
|
|
1068
1126
|
const zoneHistory = (doc.growth && doc.growth.zone_history) || [];
|
|
1069
1127
|
|
|
1070
1128
|
if (patterns.length === 0) {
|
|
1071
|
-
console.log("
|
|
1129
|
+
console.log(`${icon("search")} MetaMe: No patterns detected yet. Keep using MetaMe and patterns will emerge after ~5 sessions.`);
|
|
1072
1130
|
} else {
|
|
1073
|
-
console.log("
|
|
1131
|
+
console.log(`${icon("mirror")} MetaMe Insights:\n`);
|
|
1074
1132
|
patterns.forEach((p, i) => {
|
|
1075
|
-
const
|
|
1076
|
-
console.log(` ${
|
|
1133
|
+
const sym = p.type === 'avoidance' ? icon("warn") : p.type === 'growth' ? '+' : p.type === 'energy' ? '*' : icon("reload");
|
|
1134
|
+
console.log(` ${sym} [${p.type}] ${p.summary} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
|
|
1077
1135
|
console.log(` Detected: ${p.detected}${p.surfaced ? `, Last shown: ${p.surfaced}` : ''}`);
|
|
1078
1136
|
});
|
|
1079
1137
|
if (zoneHistory.length > 0) {
|
|
1080
|
-
console.log(`\n
|
|
1138
|
+
console.log(`\n ${icon("chart")} Recent zone history: ${zoneHistory.join(' → ')}`);
|
|
1081
1139
|
console.log(` (C=Comfort, S=Stretch, P=Panic)`);
|
|
1082
1140
|
}
|
|
1083
1141
|
const answered = (doc.growth && doc.growth.reflections_answered) || 0;
|
|
1084
1142
|
const skipped = (doc.growth && doc.growth.reflections_skipped) || 0;
|
|
1085
1143
|
if (answered + skipped > 0) {
|
|
1086
|
-
console.log(`\n
|
|
1144
|
+
console.log(`\n ${icon("thought")} Reflections: ${answered} answered, ${skipped} skipped`);
|
|
1087
1145
|
}
|
|
1088
1146
|
}
|
|
1089
1147
|
} catch (e) {
|
|
1090
|
-
console.error("
|
|
1148
|
+
console.error(`${icon("fail")} Error:`, e.message);
|
|
1091
1149
|
}
|
|
1092
1150
|
process.exit(0);
|
|
1093
1151
|
}
|
|
@@ -1098,7 +1156,7 @@ if (isMirror) {
|
|
|
1098
1156
|
const mirrorIndex = process.argv.indexOf('mirror');
|
|
1099
1157
|
const toggle = process.argv[mirrorIndex + 1];
|
|
1100
1158
|
if (toggle !== 'on' && toggle !== 'off') {
|
|
1101
|
-
console.error("
|
|
1159
|
+
console.error(`${icon("fail")} Usage: metame mirror on|off`);
|
|
1102
1160
|
process.exit(1);
|
|
1103
1161
|
}
|
|
1104
1162
|
try {
|
|
@@ -1106,9 +1164,9 @@ if (isMirror) {
|
|
|
1106
1164
|
if (!doc.growth) doc.growth = {};
|
|
1107
1165
|
doc.growth.mirror_enabled = (toggle === 'on');
|
|
1108
1166
|
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
1109
|
-
console.log(
|
|
1167
|
+
console.log(`${icon("mirror")} MetaMe: Mirror ${toggle === 'on' ? 'enabled' : 'disabled'}.`);
|
|
1110
1168
|
} catch (e) {
|
|
1111
|
-
console.error("
|
|
1169
|
+
console.error(`${icon("fail")} Error:`, e.message);
|
|
1112
1170
|
}
|
|
1113
1171
|
process.exit(0);
|
|
1114
1172
|
}
|
|
@@ -1124,7 +1182,7 @@ if (isProvider) {
|
|
|
1124
1182
|
|
|
1125
1183
|
if (!subCmd || subCmd === 'list') {
|
|
1126
1184
|
const active = providers.getActiveProvider();
|
|
1127
|
-
console.log(
|
|
1185
|
+
console.log(`${icon("plug")} MetaMe Providers (active: ${active ? active.name : 'anthropic'})`);
|
|
1128
1186
|
console.log(providers.listFormatted());
|
|
1129
1187
|
process.exit(0);
|
|
1130
1188
|
}
|
|
@@ -1132,18 +1190,18 @@ if (isProvider) {
|
|
|
1132
1190
|
if (subCmd === 'use') {
|
|
1133
1191
|
const name = process.argv[providerIndex + 2];
|
|
1134
1192
|
if (!name) {
|
|
1135
|
-
console.error("
|
|
1193
|
+
console.error(`${icon("fail")} Usage: metame provider use <name>`);
|
|
1136
1194
|
process.exit(1);
|
|
1137
1195
|
}
|
|
1138
1196
|
try {
|
|
1139
1197
|
providers.setActive(name);
|
|
1140
1198
|
const p = providers.getActiveProvider();
|
|
1141
|
-
console.log(
|
|
1199
|
+
console.log(`${icon("ok")} Provider switched → ${name} (${p.label || name})`);
|
|
1142
1200
|
if (name !== 'anthropic') {
|
|
1143
1201
|
console.log(` Base URL: ${p.base_url || 'not set'}`);
|
|
1144
1202
|
}
|
|
1145
1203
|
} catch (e) {
|
|
1146
|
-
console.error(
|
|
1204
|
+
console.error(`${icon("fail")} ${e.message}`);
|
|
1147
1205
|
process.exit(1);
|
|
1148
1206
|
}
|
|
1149
1207
|
process.exit(0);
|
|
@@ -1152,7 +1210,7 @@ if (isProvider) {
|
|
|
1152
1210
|
if (subCmd === 'add') {
|
|
1153
1211
|
const name = process.argv[providerIndex + 2];
|
|
1154
1212
|
if (!name) {
|
|
1155
|
-
console.error("
|
|
1213
|
+
console.error(`${icon("fail")} Usage: metame provider add <name>`);
|
|
1156
1214
|
process.exit(1);
|
|
1157
1215
|
}
|
|
1158
1216
|
const readline = require('readline');
|
|
@@ -1160,7 +1218,7 @@ if (isProvider) {
|
|
|
1160
1218
|
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
1161
1219
|
|
|
1162
1220
|
(async () => {
|
|
1163
|
-
console.log(`\n
|
|
1221
|
+
console.log(`\n${icon("plug")} Add Provider: ${name}\n`);
|
|
1164
1222
|
console.log("The relay must accept Anthropic Messages API format.");
|
|
1165
1223
|
console.log("(Most quality relays like OpenRouter, OneAPI, etc. support this.)\n");
|
|
1166
1224
|
|
|
@@ -1169,7 +1227,7 @@ if (isProvider) {
|
|
|
1169
1227
|
const api_key = (await ask("API Key: ")).trim();
|
|
1170
1228
|
|
|
1171
1229
|
if (!base_url) {
|
|
1172
|
-
console.error("
|
|
1230
|
+
console.error(`${icon("fail")} Base URL is required.`);
|
|
1173
1231
|
rl.close();
|
|
1174
1232
|
process.exit(1);
|
|
1175
1233
|
}
|
|
@@ -1180,10 +1238,10 @@ if (isProvider) {
|
|
|
1180
1238
|
|
|
1181
1239
|
try {
|
|
1182
1240
|
providers.addProvider(name, config);
|
|
1183
|
-
console.log(`\n
|
|
1241
|
+
console.log(`\n${icon("ok")} Provider "${name}" added.`);
|
|
1184
1242
|
console.log(` Switch to it: metame provider use ${name}`);
|
|
1185
1243
|
} catch (e) {
|
|
1186
|
-
console.error(
|
|
1244
|
+
console.error(`${icon("fail")} ${e.message}`);
|
|
1187
1245
|
}
|
|
1188
1246
|
rl.close();
|
|
1189
1247
|
process.exit(0);
|
|
@@ -1194,14 +1252,14 @@ if (isProvider) {
|
|
|
1194
1252
|
if (subCmd === 'remove') {
|
|
1195
1253
|
const name = process.argv[providerIndex + 2];
|
|
1196
1254
|
if (!name) {
|
|
1197
|
-
console.error("
|
|
1255
|
+
console.error(`${icon("fail")} Usage: metame provider remove <name>`);
|
|
1198
1256
|
process.exit(1);
|
|
1199
1257
|
}
|
|
1200
1258
|
try {
|
|
1201
1259
|
providers.removeProvider(name);
|
|
1202
|
-
console.log(
|
|
1260
|
+
console.log(`${icon("ok")} Provider "${name}" removed.`);
|
|
1203
1261
|
} catch (e) {
|
|
1204
|
-
console.error(
|
|
1262
|
+
console.error(`${icon("fail")} ${e.message}`);
|
|
1205
1263
|
}
|
|
1206
1264
|
process.exit(0);
|
|
1207
1265
|
}
|
|
@@ -1210,15 +1268,15 @@ if (isProvider) {
|
|
|
1210
1268
|
const role = process.argv[providerIndex + 2]; // distill | daemon
|
|
1211
1269
|
const name = process.argv[providerIndex + 3]; // provider name or empty to clear
|
|
1212
1270
|
if (!role) {
|
|
1213
|
-
console.error("
|
|
1271
|
+
console.error(`${icon("fail")} Usage: metame provider set-role <distill|daemon> [provider-name]`);
|
|
1214
1272
|
console.error(" Omit provider name to reset to active provider.");
|
|
1215
1273
|
process.exit(1);
|
|
1216
1274
|
}
|
|
1217
1275
|
try {
|
|
1218
1276
|
providers.setRole(role, name || null);
|
|
1219
|
-
console.log(
|
|
1277
|
+
console.log(`${icon("ok")} ${role} provider ${name ? `set to "${name}"` : 'reset to active'}.`);
|
|
1220
1278
|
} catch (e) {
|
|
1221
|
-
console.error(
|
|
1279
|
+
console.error(`${icon("fail")} ${e.message}`);
|
|
1222
1280
|
}
|
|
1223
1281
|
process.exit(0);
|
|
1224
1282
|
}
|
|
@@ -1229,11 +1287,11 @@ if (isProvider) {
|
|
|
1229
1287
|
const name = targetName || prov.active;
|
|
1230
1288
|
const p = prov.providers[name];
|
|
1231
1289
|
if (!p) {
|
|
1232
|
-
console.error(
|
|
1290
|
+
console.error(`${icon("fail")} Provider "${name}" not found.`);
|
|
1233
1291
|
process.exit(1);
|
|
1234
1292
|
}
|
|
1235
1293
|
|
|
1236
|
-
console.log(
|
|
1294
|
+
console.log(`${icon("search")} Testing provider: ${name} (${p.label || name})`);
|
|
1237
1295
|
if (name === 'anthropic') {
|
|
1238
1296
|
console.log(" Using official Anthropic endpoint — testing via claude CLI...");
|
|
1239
1297
|
} else {
|
|
@@ -1257,18 +1315,18 @@ if (isProvider) {
|
|
|
1257
1315
|
const elapsed = Date.now() - start;
|
|
1258
1316
|
|
|
1259
1317
|
if (result.includes('PROVIDER_OK')) {
|
|
1260
|
-
console.log(`
|
|
1318
|
+
console.log(` ${icon("ok")} Connected (${elapsed}ms)`);
|
|
1261
1319
|
} else {
|
|
1262
|
-
console.log(`
|
|
1320
|
+
console.log(` ${icon("warn")} Response received (${elapsed}ms) but unexpected: ${result.slice(0, 80)}`);
|
|
1263
1321
|
}
|
|
1264
1322
|
} catch (e) {
|
|
1265
|
-
console.error(`
|
|
1323
|
+
console.error(` ${icon("fail")} Failed: ${e.message.split('\n')[0]}`);
|
|
1266
1324
|
}
|
|
1267
1325
|
process.exit(0);
|
|
1268
1326
|
}
|
|
1269
1327
|
|
|
1270
1328
|
// Unknown subcommand — show help
|
|
1271
|
-
console.log("
|
|
1329
|
+
console.log(`${icon("plug")} MetaMe Provider Commands:`);
|
|
1272
1330
|
console.log(" metame provider — list providers");
|
|
1273
1331
|
console.log(" metame provider use <name> — switch active provider");
|
|
1274
1332
|
console.log(" metame provider add <name> — add a new provider");
|
|
@@ -1312,20 +1370,20 @@ if (isDaemon) {
|
|
|
1312
1370
|
if (fs.existsSync(templateSrc)) {
|
|
1313
1371
|
fs.copyFileSync(templateSrc, DAEMON_CONFIG);
|
|
1314
1372
|
} else {
|
|
1315
|
-
console.error("
|
|
1373
|
+
console.error(`${icon("fail")} Template not found. Reinstall MetaMe.`);
|
|
1316
1374
|
process.exit(1);
|
|
1317
1375
|
}
|
|
1318
1376
|
try { fs.chmodSync(METAME_DIR, 0o700); } catch { /* ignore on Windows */ }
|
|
1319
|
-
console.log("
|
|
1377
|
+
console.log(`${icon("ok")} Config created: ~/.metame/daemon.yaml\n`);
|
|
1320
1378
|
} else {
|
|
1321
|
-
console.log("
|
|
1379
|
+
console.log(`${icon("ok")} Config exists: ~/.metame/daemon.yaml\n`);
|
|
1322
1380
|
}
|
|
1323
1381
|
|
|
1324
1382
|
const yaml = require(path.join(__dirname, 'node_modules', 'js-yaml'));
|
|
1325
1383
|
let cfg = yaml.load(fs.readFileSync(DAEMON_CONFIG, 'utf8')) || {};
|
|
1326
1384
|
|
|
1327
1385
|
// --- Telegram Setup ---
|
|
1328
|
-
console.log("
|
|
1386
|
+
console.log(`━━━ ${icon("phone")} Telegram Setup ━━━`);
|
|
1329
1387
|
console.log("");
|
|
1330
1388
|
console.log("Step 1: Create a Bot");
|
|
1331
1389
|
console.log(" • Open Telegram app on your phone or desktop");
|
|
@@ -1370,21 +1428,21 @@ if (isDaemon) {
|
|
|
1370
1428
|
|
|
1371
1429
|
if (chatIds.length > 0) {
|
|
1372
1430
|
cfg.telegram.allowed_chat_ids = chatIds;
|
|
1373
|
-
console.log(`
|
|
1431
|
+
console.log(` ${icon("ok")} Found chat ID(s): ${chatIds.join(', ')}`);
|
|
1374
1432
|
} else {
|
|
1375
|
-
console.log("
|
|
1433
|
+
console.log(` ${icon("warn")} No messages found. Make sure you messaged the bot.`);
|
|
1376
1434
|
console.log(" You can set allowed_chat_ids manually in daemon.yaml later.");
|
|
1377
1435
|
}
|
|
1378
1436
|
} catch {
|
|
1379
|
-
console.log("
|
|
1437
|
+
console.log(` ${icon("warn")} Could not fetch chat ID. Set it manually in daemon.yaml.`);
|
|
1380
1438
|
}
|
|
1381
|
-
console.log("
|
|
1439
|
+
console.log(` ${icon("ok")} Telegram configured!\n`);
|
|
1382
1440
|
} else {
|
|
1383
1441
|
console.log(" Skipped.\n");
|
|
1384
1442
|
}
|
|
1385
1443
|
|
|
1386
1444
|
// --- Feishu Setup ---
|
|
1387
|
-
console.log("
|
|
1445
|
+
console.log(`━━━ ${icon("feishu")} Feishu (Lark) Setup ━━━`);
|
|
1388
1446
|
console.log("");
|
|
1389
1447
|
console.log("Step 1: Create an App");
|
|
1390
1448
|
console.log(" • Go to: https://open.feishu.cn/app");
|
|
@@ -1428,7 +1486,7 @@ if (isDaemon) {
|
|
|
1428
1486
|
cfg.feishu.app_id = feishuAppId;
|
|
1429
1487
|
cfg.feishu.app_secret = feishuSecret;
|
|
1430
1488
|
if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
|
|
1431
|
-
console.log("
|
|
1489
|
+
console.log(` ${icon("ok")} Feishu configured!`);
|
|
1432
1490
|
console.log(" Note: allowed_chat_ids is empty = deny all users.");
|
|
1433
1491
|
console.log(" Add chat IDs to daemon.yaml or use /agent bind from target chat.\n");
|
|
1434
1492
|
}
|
|
@@ -1438,7 +1496,7 @@ if (isDaemon) {
|
|
|
1438
1496
|
|
|
1439
1497
|
// Write config
|
|
1440
1498
|
fs.writeFileSync(DAEMON_CONFIG, yaml.dump(cfg, { lineWidth: -1 }), 'utf8');
|
|
1441
|
-
console.log("
|
|
1499
|
+
console.log(`━━━ ${icon("ok")} Setup Complete ━━━`);
|
|
1442
1500
|
console.log(`Config saved: ${DAEMON_CONFIG}`);
|
|
1443
1501
|
console.log("\nNext steps:");
|
|
1444
1502
|
console.log(" metame start — start the daemon");
|
|
@@ -1455,7 +1513,7 @@ if (isDaemon) {
|
|
|
1455
1513
|
|
|
1456
1514
|
if (subCmd === 'install-launchd') {
|
|
1457
1515
|
if (process.platform !== 'darwin') {
|
|
1458
|
-
console.error("
|
|
1516
|
+
console.error(`${icon("fail")} launchd is macOS-only.`);
|
|
1459
1517
|
process.exit(1);
|
|
1460
1518
|
}
|
|
1461
1519
|
const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
|
|
@@ -1497,7 +1555,7 @@ if (isDaemon) {
|
|
|
1497
1555
|
</dict>
|
|
1498
1556
|
</plist>`;
|
|
1499
1557
|
fs.writeFileSync(plistPath, plistContent, 'utf8');
|
|
1500
|
-
console.log(
|
|
1558
|
+
console.log(`${icon("ok")} launchd plist installed: ${plistPath}`);
|
|
1501
1559
|
console.log(" Load now: launchctl load " + plistPath);
|
|
1502
1560
|
console.log(" Unload: launchctl unload " + plistPath);
|
|
1503
1561
|
process.exit(0);
|
|
@@ -1505,7 +1563,7 @@ if (isDaemon) {
|
|
|
1505
1563
|
|
|
1506
1564
|
if (subCmd === 'install-systemd') {
|
|
1507
1565
|
if (process.platform === 'darwin') {
|
|
1508
|
-
console.error("
|
|
1566
|
+
console.error(`${icon("fail")} Use 'metame daemon install-launchd' on macOS.`);
|
|
1509
1567
|
process.exit(1);
|
|
1510
1568
|
}
|
|
1511
1569
|
|
|
@@ -1513,7 +1571,7 @@ if (isDaemon) {
|
|
|
1513
1571
|
try {
|
|
1514
1572
|
require('child_process').execSync('systemctl --user --no-pager status 2>/dev/null || true');
|
|
1515
1573
|
} catch {
|
|
1516
|
-
console.error("
|
|
1574
|
+
console.error(`${icon("fail")} systemd not available.`);
|
|
1517
1575
|
console.error(" WSL users: add [boot]\\nsystemd=true to /etc/wsl.conf, then restart WSL.");
|
|
1518
1576
|
process.exit(1);
|
|
1519
1577
|
}
|
|
@@ -1553,7 +1611,7 @@ WantedBy=default.target
|
|
|
1553
1611
|
// Enable lingering so service runs even when user is not logged in
|
|
1554
1612
|
try { es(`loginctl enable-linger ${process.env.USER || ''}`); } catch { /* may need root */ }
|
|
1555
1613
|
|
|
1556
|
-
console.log(
|
|
1614
|
+
console.log(`${icon("ok")} systemd service installed: ${servicePath}`);
|
|
1557
1615
|
console.log(" Status: systemctl --user status metame-daemon");
|
|
1558
1616
|
console.log(" Logs: journalctl --user -u metame-daemon -f");
|
|
1559
1617
|
console.log(" Disable: systemctl --user disable metame-daemon");
|
|
@@ -1562,7 +1620,7 @@ WantedBy=default.target
|
|
|
1562
1620
|
const isWSL = fs.existsSync('/proc/version') &&
|
|
1563
1621
|
fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
|
|
1564
1622
|
if (isWSL) {
|
|
1565
|
-
console.log(
|
|
1623
|
+
console.log(`\n ${icon("pin")} WSL auto-boot tip:`);
|
|
1566
1624
|
console.log(" Add this to Windows Task Scheduler (run at login):");
|
|
1567
1625
|
console.log(` wsl -d ${process.env.WSL_DISTRO_NAME || 'Ubuntu'} -- sh -c 'nohup sleep infinity &'`);
|
|
1568
1626
|
console.log(" This keeps WSL alive so the daemon stays running.");
|
|
@@ -1570,17 +1628,44 @@ WantedBy=default.target
|
|
|
1570
1628
|
process.exit(0);
|
|
1571
1629
|
}
|
|
1572
1630
|
|
|
1631
|
+
if (subCmd === 'install-task-scheduler') {
|
|
1632
|
+
if (process.platform !== 'win32') {
|
|
1633
|
+
console.error("Task Scheduler is Windows-only. Use install-launchd (macOS) or install-systemd (Linux).");
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
const nodePath = process.execPath;
|
|
1637
|
+
const taskName = 'MetaMe-Daemon';
|
|
1638
|
+
const scriptPath = DAEMON_SCRIPT.replace(/\//g, '\\');
|
|
1639
|
+
const nodePathWin = nodePath.replace(/\//g, '\\');
|
|
1640
|
+
try {
|
|
1641
|
+
try {
|
|
1642
|
+
execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'ignore' });
|
|
1643
|
+
} catch { /* task may not exist yet */ }
|
|
1644
|
+
execSync(
|
|
1645
|
+
`schtasks /create /tn "${taskName}" /tr "\\"${nodePathWin}\\" \\"${scriptPath}\\"" /sc onlogon /rl limited /f`,
|
|
1646
|
+
{ stdio: 'inherit' }
|
|
1647
|
+
);
|
|
1648
|
+
console.log(`Task Scheduler task "${taskName}" installed.`);
|
|
1649
|
+
console.log(` The daemon will auto-start at login.`);
|
|
1650
|
+
console.log(` Remove: schtasks /delete /tn "${taskName}" /f`);
|
|
1651
|
+
console.log(` Query: schtasks /query /tn "${taskName}"`);
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
console.error(`Failed to create scheduled task: ${e.message}`);
|
|
1654
|
+
console.error(" Try running as Administrator, or create it manually in Task Scheduler.");
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
process.exit(0);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1573
1660
|
if (subCmd === 'start') {
|
|
1574
1661
|
// Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
|
|
1575
1662
|
try {
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const n = parseInt(p, 10);
|
|
1581
|
-
if (n && n !== process.pid) try { process.kill(n, 'SIGKILL'); } catch { /* */ }
|
|
1663
|
+
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
1664
|
+
if (pids.length) {
|
|
1665
|
+
for (const n of pids) {
|
|
1666
|
+
try { process.kill(n, 'SIGKILL'); } catch { /* */ }
|
|
1582
1667
|
}
|
|
1583
|
-
|
|
1668
|
+
sleepSync(1000);
|
|
1584
1669
|
}
|
|
1585
1670
|
} catch { /* ignore */ }
|
|
1586
1671
|
// Check if already running
|
|
@@ -1588,25 +1673,26 @@ WantedBy=default.target
|
|
|
1588
1673
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
1589
1674
|
}
|
|
1590
1675
|
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
1591
|
-
console.error("
|
|
1676
|
+
console.error(`${icon("fail")} No config found. Run: metame daemon init`);
|
|
1592
1677
|
process.exit(1);
|
|
1593
1678
|
}
|
|
1594
1679
|
if (!fs.existsSync(DAEMON_SCRIPT)) {
|
|
1595
|
-
console.error("
|
|
1680
|
+
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
1596
1681
|
process.exit(1);
|
|
1597
1682
|
}
|
|
1598
|
-
// Use caffeinate on macOS
|
|
1599
|
-
const
|
|
1600
|
-
const
|
|
1601
|
-
const
|
|
1683
|
+
// Use caffeinate on macOS to prevent sleep while daemon is running
|
|
1684
|
+
const isMac = process.platform === 'darwin';
|
|
1685
|
+
const isWin = process.platform === 'win32';
|
|
1686
|
+
const cmd = isMac ? 'caffeinate' : process.execPath;
|
|
1687
|
+
const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
|
|
1602
1688
|
const bg = spawn(cmd, args, {
|
|
1603
|
-
detached:
|
|
1689
|
+
detached: !isWin,
|
|
1604
1690
|
stdio: 'ignore',
|
|
1605
1691
|
windowsHide: true,
|
|
1606
1692
|
env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
|
|
1607
1693
|
});
|
|
1608
1694
|
bg.unref();
|
|
1609
|
-
console.log(
|
|
1695
|
+
console.log(`${icon("ok")} MetaMe daemon started (PID: ${bg.pid})`);
|
|
1610
1696
|
console.log(" Logs: metame logs");
|
|
1611
1697
|
console.log(" Stop: metame stop");
|
|
1612
1698
|
process.exit(0);
|
|
@@ -1614,7 +1700,7 @@ WantedBy=default.target
|
|
|
1614
1700
|
|
|
1615
1701
|
if (subCmd === 'stop') {
|
|
1616
1702
|
if (!fs.existsSync(DAEMON_PID)) {
|
|
1617
|
-
console.log("
|
|
1703
|
+
console.log(`${icon("info")} No daemon running (no PID file).`);
|
|
1618
1704
|
process.exit(0);
|
|
1619
1705
|
}
|
|
1620
1706
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
@@ -1623,16 +1709,15 @@ WantedBy=default.target
|
|
|
1623
1709
|
// Wait for process to die (up to 3s), then force kill
|
|
1624
1710
|
let dead = false;
|
|
1625
1711
|
for (let i = 0; i < 6; i++) {
|
|
1626
|
-
|
|
1627
|
-
es('sleep 0.5');
|
|
1712
|
+
sleepSync(500);
|
|
1628
1713
|
try { process.kill(pid, 0); } catch { dead = true; break; }
|
|
1629
1714
|
}
|
|
1630
1715
|
if (!dead) {
|
|
1631
1716
|
try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
|
|
1632
1717
|
}
|
|
1633
|
-
console.log(
|
|
1718
|
+
console.log(`${icon("ok")} Daemon stopped (PID: ${pid})`);
|
|
1634
1719
|
} catch (e) {
|
|
1635
|
-
console.log(
|
|
1720
|
+
console.log(`${icon("warn")} Process ${pid} not found (may have already exited).`);
|
|
1636
1721
|
}
|
|
1637
1722
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
|
|
1638
1723
|
process.exit(0);
|
|
@@ -1649,7 +1734,7 @@ WantedBy=default.target
|
|
|
1649
1734
|
try { process.kill(pid, 0); isRunning = true; } catch { /* dead */ }
|
|
1650
1735
|
}
|
|
1651
1736
|
|
|
1652
|
-
console.log(
|
|
1737
|
+
console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
|
|
1653
1738
|
if (state.started_at) console.log(` Started: ${state.started_at}`);
|
|
1654
1739
|
if (state.pid) console.log(` PID: ${state.pid}`);
|
|
1655
1740
|
|
|
@@ -1677,8 +1762,8 @@ WantedBy=default.target
|
|
|
1677
1762
|
if (taskEntries.length > 0) {
|
|
1678
1763
|
console.log(" Recent tasks:");
|
|
1679
1764
|
for (const [name, info] of taskEntries) {
|
|
1680
|
-
const
|
|
1681
|
-
console.log(` ${
|
|
1765
|
+
const sym = info.status === 'success' ? icon("ok") : icon("fail");
|
|
1766
|
+
console.log(` ${sym} ${name}: ${info.last_run || 'unknown'}`);
|
|
1682
1767
|
if (info.output_preview) console.log(` ${info.output_preview.slice(0, 80)}...`);
|
|
1683
1768
|
}
|
|
1684
1769
|
const hiddenStale = Object.keys(tasks).length - taskEntries.length;
|
|
@@ -1691,7 +1776,7 @@ WantedBy=default.target
|
|
|
1691
1776
|
|
|
1692
1777
|
if (subCmd === 'logs') {
|
|
1693
1778
|
if (!fs.existsSync(DAEMON_LOG)) {
|
|
1694
|
-
console.log("
|
|
1779
|
+
console.log(`${icon("info")} No log file yet. Start the daemon first.`);
|
|
1695
1780
|
process.exit(0);
|
|
1696
1781
|
}
|
|
1697
1782
|
const content = fs.readFileSync(DAEMON_LOG, 'utf8');
|
|
@@ -1704,11 +1789,11 @@ WantedBy=default.target
|
|
|
1704
1789
|
if (subCmd === 'run') {
|
|
1705
1790
|
const taskName = process.argv[daemonIndex + 2];
|
|
1706
1791
|
if (!taskName) {
|
|
1707
|
-
console.error("
|
|
1792
|
+
console.error(`${icon("fail")} Usage: metame daemon run <task-name>`);
|
|
1708
1793
|
process.exit(1);
|
|
1709
1794
|
}
|
|
1710
1795
|
if (!fs.existsSync(DAEMON_SCRIPT)) {
|
|
1711
|
-
console.error("
|
|
1796
|
+
console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
|
|
1712
1797
|
process.exit(1);
|
|
1713
1798
|
}
|
|
1714
1799
|
// Run in foreground using daemon.js --run
|
|
@@ -1721,7 +1806,7 @@ WantedBy=default.target
|
|
|
1721
1806
|
}
|
|
1722
1807
|
|
|
1723
1808
|
// Unknown subcommand
|
|
1724
|
-
console.log("
|
|
1809
|
+
console.log(`${icon("book")} MetaMe Daemon Commands:`);
|
|
1725
1810
|
console.log(" metame start — start background daemon");
|
|
1726
1811
|
console.log(" metame stop — stop daemon");
|
|
1727
1812
|
console.log(" metame status — show status & budget");
|
|
@@ -1729,9 +1814,11 @@ WantedBy=default.target
|
|
|
1729
1814
|
console.log(" metame daemon init — initialize config");
|
|
1730
1815
|
console.log(" metame daemon run <name> — run a task once");
|
|
1731
1816
|
if (process.platform === 'darwin') {
|
|
1732
|
-
console.log(" metame daemon install-launchd
|
|
1817
|
+
console.log(" metame daemon install-launchd — auto-start on macOS");
|
|
1818
|
+
} else if (process.platform === 'win32') {
|
|
1819
|
+
console.log(" metame daemon install-task-scheduler — auto-start on Windows");
|
|
1733
1820
|
} else {
|
|
1734
|
-
console.log(" metame daemon install-systemd
|
|
1821
|
+
console.log(" metame daemon install-systemd — auto-start on Linux/WSL");
|
|
1735
1822
|
}
|
|
1736
1823
|
process.exit(0);
|
|
1737
1824
|
}
|
|
@@ -1780,7 +1867,7 @@ if (isSync) {
|
|
|
1780
1867
|
process.exit(1);
|
|
1781
1868
|
}
|
|
1782
1869
|
|
|
1783
|
-
console.log(`\n
|
|
1870
|
+
console.log(`\n${icon("reload")} Resuming session ${bestSession.id.slice(0, 8)}...\n`);
|
|
1784
1871
|
const providerEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
|
|
1785
1872
|
const resumeArgs = ['--resume', bestSession.id];
|
|
1786
1873
|
if (daemonCfg.dangerously_skip_permissions) resumeArgs.push('--dangerously-skip-permissions');
|
|
@@ -1801,7 +1888,7 @@ if (isSync) {
|
|
|
1801
1888
|
// We rely on our own scoped variable to detect nesting,
|
|
1802
1889
|
// ignoring the leaky CLAUDE_CODE_SSE_PORT from IDEs.
|
|
1803
1890
|
if (process.env.METAME_ACTIVE_SESSION === 'true') {
|
|
1804
|
-
console.error("
|
|
1891
|
+
console.error(`\n${icon("stop")} ACTION BLOCKED: Nested Session Detected`);
|
|
1805
1892
|
console.error(" You are actively running inside a MetaMe session.");
|
|
1806
1893
|
console.error(" To reload configuration, use: \x1b[36m!metame refresh\x1b[0m\n");
|
|
1807
1894
|
process.exit(1);
|
|
@@ -1814,7 +1901,7 @@ if (process.env.METAME_ACTIVE_SESSION === 'true') {
|
|
|
1814
1901
|
const activeProviderEnv = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).buildActiveEnv(); } catch { return {}; } })();
|
|
1815
1902
|
const activeProviderName = (() => { try { return require(path.join(__dirname, 'scripts', 'providers.js')).getActiveName(); } catch { return 'anthropic'; } })();
|
|
1816
1903
|
if (activeProviderName !== 'anthropic') {
|
|
1817
|
-
console.log(
|
|
1904
|
+
console.log(`${icon("plug")} Provider: ${activeProviderName}`);
|
|
1818
1905
|
}
|
|
1819
1906
|
|
|
1820
1907
|
// Build launch args — inject system prompt for new users
|
|
@@ -1873,17 +1960,18 @@ try {
|
|
|
1873
1960
|
} catch { /* PID file stale, daemon not running */ }
|
|
1874
1961
|
}
|
|
1875
1962
|
if (!daemonRunning) {
|
|
1876
|
-
const
|
|
1877
|
-
const
|
|
1878
|
-
const
|
|
1963
|
+
const _isMac = process.platform === 'darwin';
|
|
1964
|
+
const _isWin = process.platform === 'win32';
|
|
1965
|
+
const dCmd = _isMac ? 'caffeinate' : process.execPath;
|
|
1966
|
+
const dArgs = _isMac ? ['-i', process.execPath, _daemonScript] : [_daemonScript];
|
|
1879
1967
|
const bg = spawn(dCmd, dArgs, {
|
|
1880
|
-
detached:
|
|
1968
|
+
detached: !_isWin,
|
|
1881
1969
|
stdio: 'ignore',
|
|
1882
1970
|
windowsHide: true,
|
|
1883
1971
|
env: { ...process.env, HOME: HOME_DIR, METAME_ROOT: __dirname },
|
|
1884
1972
|
});
|
|
1885
1973
|
bg.unref();
|
|
1886
|
-
console.log(
|
|
1974
|
+
console.log(`${icon("bot")} Daemon auto-started (PID: ${bg.pid})`);
|
|
1887
1975
|
}
|
|
1888
1976
|
}
|
|
1889
1977
|
} catch { /* non-fatal */ }
|
|
@@ -1895,7 +1983,7 @@ const child = spawnClaude(launchArgs, {
|
|
|
1895
1983
|
});
|
|
1896
1984
|
|
|
1897
1985
|
child.on('error', () => {
|
|
1898
|
-
console.error("
|
|
1986
|
+
console.error(`\n${icon("fail")} Error: Could not launch 'claude'.`);
|
|
1899
1987
|
console.error(" Please make sure Claude Code is installed globally:");
|
|
1900
1988
|
console.error(" npm install -g @anthropic-ai/claude-code");
|
|
1901
1989
|
});
|