metame-cli 1.3.1 → 1.3.3
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 +5 -3
- package/index.js +134 -59
- package/package.json +1 -1
- package/scripts/daemon.js +99 -19
- package/scripts/distill.js +52 -1
- package/scripts/feishu-adapter.js +23 -2
- package/scripts/schema.js +2 -0
package/README.md
CHANGED
|
@@ -20,7 +20,6 @@ It is not a memory system; it is a **Cognitive Mirror** .
|
|
|
20
20
|
|
|
21
21
|
* **🧠 Global Brain (`~/.claude_profile.yaml`):** A single, portable source of truth — your identity, cognitive traits, and preferences travel with you across every project.
|
|
22
22
|
* **🧬 Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** — silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** — `!metame evolve` for explicit teaching; (3) **Confidence gates** — strong directives ("always"/"以后一律") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
|
|
23
|
-
* **🤝 Dynamic Handshake:** The "Canary Test." Claude must address you by your **Codename** in the first sentence. If it doesn't, the link is broken.
|
|
24
23
|
* **🛡️ Auto-Lock:** Mark any value with `# [LOCKED]` — treated as a constitution, never auto-modified.
|
|
25
24
|
* **🪞 Metacognition Layer (v1.3):** MetaMe now observes *how* you think, not just *what* you say. Behavioral pattern detection runs inside the existing Haiku distill call (zero extra cost). It tracks decision patterns, cognitive load, comfort zones, and avoidance topics across sessions. When persistent patterns emerge, MetaMe injects a one-line mirror observation — e.g., *"You tend to avoid testing until forced"* — with a 14-day cooldown per pattern. Conditional reflection prompts appear only when triggered (every 7th distill or 3x consecutive comfort zone). All injection logic runs in Node.js; Claude receives only pre-decided directives, never rules to self-evaluate.
|
|
26
25
|
* **📱 Remote Claude Code (v1.3):** Full Claude Code from your phone via Telegram or Feishu (Lark). Stateful sessions with `--resume` — same conversation history, tool use, and file editing as your terminal. Interactive buttons for project/session picking, directory browser, and macOS launchd auto-start.
|
|
@@ -107,6 +106,11 @@ metame set-trait status.focus "Learning Rust"
|
|
|
107
106
|
metame evolve "I prefer functional programming patterns"
|
|
108
107
|
```
|
|
109
108
|
|
|
109
|
+
**Episodic memory (keyframe, not full log):** MetaMe is not a memory system, but it captures two types of experiential "keyframes" that pure personality traits can't replace:
|
|
110
|
+
|
|
111
|
+
* **Anti-patterns** (`context.anti_patterns`, max 5): Cross-project failure lessons — e.g., *"Promise.all rejects all on single failure, use Promise.allSettled"*. Auto-expires after 60 days. Prevents the AI from repeating the same mistakes across sessions.
|
|
112
|
+
* **Milestones** (`context.milestones`, max 3): Recent completed landmarks — e.g., *"MetaMe v1.3 published"*. Provides continuity so Claude knows where you left off without you having to recap.
|
|
113
|
+
|
|
110
114
|
**Anti-bias safeguards:** single observations ≠ traits, contradictions are tracked not overwritten, pending traits expire after 30 days, context fields auto-clear on staleness.
|
|
111
115
|
|
|
112
116
|
**Metacognition controls:**
|
|
@@ -391,8 +395,6 @@ You might worry: *"Does this eat up my context window?"*
|
|
|
391
395
|
**Q: Does this replace `CLAUDE.md`?**
|
|
392
396
|
A: No. It *prepends* its meta-cognitive protocol to your existing `CLAUDE.md`. Your project-specific notes remain intact.
|
|
393
397
|
|
|
394
|
-
**Q: What if Claude stops calling me by my nickname?**
|
|
395
|
-
A: This is the "Canary Test." It means the context window has been compressed or the file link is broken. Run `/compact` in Claude or restart `metame` to fix it.
|
|
396
398
|
|
|
397
399
|
**Q: Is my data sent to a third party?**
|
|
398
400
|
A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passes text to the official Claude Code tool.
|
package/index.js
CHANGED
|
@@ -208,20 +208,13 @@ const CORE_PROTOCOL = `
|
|
|
208
208
|
---
|
|
209
209
|
## 🧠 SYSTEM KERNEL: SHADOW_MODE (Active)
|
|
210
210
|
|
|
211
|
-
**1. THE
|
|
212
|
-
* **SOURCE:** Check \`$HOME/.claude_profile.yaml\`.
|
|
213
|
-
* **RULE:** Look for \`identity.nickname\`.
|
|
214
|
-
* **IF FOUND:** You MUST address the user by that nickname in the very first sentence.
|
|
215
|
-
* **IF NULL/MISSING:** Address user as "User" and immediately trigger Genesis to fix it.
|
|
216
|
-
* **SIGNAL:** This nickname is the "Canary Test". If you omit it, the user assumes the system is broken.
|
|
217
|
-
|
|
218
|
-
**2. THE BRAIN (Source of Truth):**
|
|
211
|
+
**1. THE BRAIN (Source of Truth):**
|
|
219
212
|
* **FILE:** \`$HOME/.claude_profile.yaml\`
|
|
220
213
|
* **RULE:** This file is your Ground Truth. If context is compressed (/compact), **RE-READ** this file immediately.
|
|
221
214
|
* **AUTO-LOCK:** Never edit lines marked \`# [LOCKED]\` (Core Values).
|
|
222
215
|
* **EVOLUTION:** Silently overwrite outdated status/focus.
|
|
223
216
|
|
|
224
|
-
**
|
|
217
|
+
**2. EVOLUTION MECHANISM (Manual Sync):**
|
|
225
218
|
* **PHILOSOPHY:** You respect the User's flow. You do NOT interrupt.
|
|
226
219
|
* **TOOLS:**
|
|
227
220
|
1. **Log Insight:** \`!metame evolve "Insight"\` (For additive knowledge).
|
|
@@ -578,11 +571,12 @@ if (isDaemon) {
|
|
|
578
571
|
const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
|
|
579
572
|
|
|
580
573
|
if (subCmd === 'init') {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
574
|
+
const readline = require('readline');
|
|
575
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
576
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
577
|
+
|
|
578
|
+
// Create config from template if not exists
|
|
579
|
+
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
586
580
|
const templateSrc = fs.existsSync(DAEMON_DEFAULT)
|
|
587
581
|
? DAEMON_DEFAULT
|
|
588
582
|
: path.join(METAME_DIR, 'daemon-default.yaml');
|
|
@@ -592,45 +586,113 @@ if (isDaemon) {
|
|
|
592
586
|
console.error("❌ Template not found. Reinstall MetaMe.");
|
|
593
587
|
process.exit(1);
|
|
594
588
|
}
|
|
595
|
-
// Ensure directory permissions (700)
|
|
596
589
|
try { fs.chmodSync(METAME_DIR, 0o700); } catch { /* ignore on Windows */ }
|
|
597
|
-
console.log("✅
|
|
598
|
-
|
|
590
|
+
console.log("✅ Config created: ~/.metame/daemon.yaml\n");
|
|
591
|
+
} else {
|
|
592
|
+
console.log("✅ Config exists: ~/.metame/daemon.yaml\n");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const yaml = require(path.join(__dirname, 'node_modules', 'js-yaml'));
|
|
596
|
+
let cfg = yaml.load(fs.readFileSync(DAEMON_CONFIG, 'utf8')) || {};
|
|
597
|
+
|
|
598
|
+
// --- Telegram Setup ---
|
|
599
|
+
console.log("━━━ 📱 Telegram Setup ━━━");
|
|
600
|
+
console.log("Steps:");
|
|
601
|
+
console.log(" 1. Open Telegram, search @BotFather");
|
|
602
|
+
console.log(" 2. Send /newbot, follow prompts to create a bot");
|
|
603
|
+
console.log(" 3. Copy the bot token (looks like: 123456:ABC-DEF...)");
|
|
604
|
+
console.log("");
|
|
605
|
+
|
|
606
|
+
const tgToken = (await ask("Paste your Telegram bot token (Enter to skip): ")).trim();
|
|
607
|
+
if (tgToken) {
|
|
608
|
+
if (!cfg.telegram) cfg.telegram = {};
|
|
609
|
+
cfg.telegram.enabled = true;
|
|
610
|
+
cfg.telegram.bot_token = tgToken;
|
|
611
|
+
|
|
612
|
+
console.log("\nFinding your chat ID...");
|
|
613
|
+
console.log(" → Send any message to your bot in Telegram first, then press Enter.");
|
|
614
|
+
await ask("Press Enter after you've messaged your bot: ");
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const https = require('https');
|
|
618
|
+
const chatIds = await new Promise((resolve, reject) => {
|
|
619
|
+
https.get(`https://api.telegram.org/bot${tgToken}/getUpdates`, (res) => {
|
|
620
|
+
let body = '';
|
|
621
|
+
res.on('data', d => body += d);
|
|
622
|
+
res.on('end', () => {
|
|
623
|
+
try {
|
|
624
|
+
const data = JSON.parse(body);
|
|
625
|
+
const ids = new Set();
|
|
626
|
+
if (data.result) {
|
|
627
|
+
for (const u of data.result) {
|
|
628
|
+
if (u.message && u.message.chat) ids.add(u.message.chat.id);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
resolve([...ids]);
|
|
632
|
+
} catch { resolve([]); }
|
|
633
|
+
});
|
|
634
|
+
}).on('error', () => resolve([]));
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (chatIds.length > 0) {
|
|
638
|
+
cfg.telegram.allowed_chat_ids = chatIds;
|
|
639
|
+
console.log(` ✅ Found chat ID(s): ${chatIds.join(', ')}`);
|
|
640
|
+
} else {
|
|
641
|
+
console.log(" ⚠️ No messages found. Make sure you messaged the bot.");
|
|
642
|
+
console.log(" You can set allowed_chat_ids manually in daemon.yaml later.");
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
console.log(" ⚠️ Could not fetch chat ID. Set it manually in daemon.yaml.");
|
|
646
|
+
}
|
|
647
|
+
console.log(" ✅ Telegram configured!\n");
|
|
648
|
+
} else {
|
|
649
|
+
console.log(" Skipped.\n");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// --- Feishu Setup ---
|
|
653
|
+
console.log("━━━ 📘 Feishu (Lark) Setup ━━━");
|
|
654
|
+
console.log("Steps:");
|
|
655
|
+
console.log(" 1. Go to: https://open.feishu.cn/app");
|
|
656
|
+
console.log(" → Create App (企业自建应用)");
|
|
657
|
+
console.log(" 2. In 'Credentials' (凭证与基础信息), copy App ID & App Secret");
|
|
658
|
+
console.log(" 3. In 'Bot' (机器人), enable bot capability");
|
|
659
|
+
console.log(" 4. In 'Event Subscription' (事件订阅):");
|
|
660
|
+
console.log(" → Set mode to 'Long Connection' (使用长连接接收事件)");
|
|
661
|
+
console.log(" → Add event: im.message.receive_v1 (接收消息)");
|
|
662
|
+
console.log(" 5. In 'Permissions' (权限管理), add:");
|
|
663
|
+
console.log(" → im:message, im:message:send_as_bot, im:chat");
|
|
664
|
+
console.log(" 6. Publish the app version (创建版本 → 申请发布)");
|
|
665
|
+
console.log("");
|
|
666
|
+
|
|
667
|
+
const feishuAppId = (await ask("Paste your Feishu App ID (Enter to skip): ")).trim();
|
|
668
|
+
if (feishuAppId) {
|
|
669
|
+
const feishuSecret = (await ask("Paste your Feishu App Secret: ")).trim();
|
|
670
|
+
if (feishuSecret) {
|
|
671
|
+
if (!cfg.feishu) cfg.feishu = {};
|
|
672
|
+
cfg.feishu.enabled = true;
|
|
673
|
+
cfg.feishu.app_id = feishuAppId;
|
|
674
|
+
cfg.feishu.app_secret = feishuSecret;
|
|
675
|
+
if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
|
|
676
|
+
console.log(" ✅ Feishu configured!");
|
|
677
|
+
console.log(" Note: allowed_chat_ids is empty = allow all users.");
|
|
678
|
+
console.log(" To restrict, add chat IDs to daemon.yaml later.\n");
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
console.log(" Skipped.\n");
|
|
599
682
|
}
|
|
600
683
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
console.log("
|
|
604
|
-
console.log(
|
|
605
|
-
console.log("
|
|
606
|
-
console.log("
|
|
607
|
-
console.log("
|
|
608
|
-
console.log(" allowed_chat_ids: [YOUR_CHAT_ID]");
|
|
609
|
-
console.log(" 4. To find your chat_id: message your bot, then run:");
|
|
610
|
-
console.log(" curl https://api.telegram.org/botYOUR_TOKEN/getUpdates");
|
|
611
|
-
console.log("\n📘 Feishu Setup (optional):");
|
|
612
|
-
console.log(" 1. Go to open.feishu.cn → Create App → get app_id & app_secret");
|
|
613
|
-
console.log(" 2. Enable Bot capability + im:message events");
|
|
614
|
-
console.log(" 3. Enable 'Long Connection' (长连接) mode in Event Subscription");
|
|
615
|
-
console.log(" 4. Edit ~/.metame/daemon.yaml:");
|
|
616
|
-
console.log(" feishu:");
|
|
617
|
-
console.log(" enabled: true");
|
|
618
|
-
console.log(" app_id: \"YOUR_APP_ID\"");
|
|
619
|
-
console.log(" app_secret: \"YOUR_APP_SECRET\"");
|
|
620
|
-
console.log(" allowed_chat_ids: [CHAT_ID]");
|
|
621
|
-
|
|
622
|
-
console.log("\n Then: metame daemon start");
|
|
623
|
-
|
|
624
|
-
// Optional launchd setup (macOS only)
|
|
684
|
+
// Write config
|
|
685
|
+
fs.writeFileSync(DAEMON_CONFIG, yaml.dump(cfg, { lineWidth: -1 }), 'utf8');
|
|
686
|
+
console.log("━━━ ✅ Setup Complete ━━━");
|
|
687
|
+
console.log(`Config saved: ${DAEMON_CONFIG}`);
|
|
688
|
+
console.log("\nNext steps:");
|
|
689
|
+
console.log(" metame daemon start — start the daemon");
|
|
690
|
+
console.log(" metame daemon status — check status");
|
|
625
691
|
if (process.platform === 'darwin') {
|
|
626
|
-
|
|
627
|
-
const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
|
|
628
|
-
console.log("\n🍎 Auto-start on macOS (optional):");
|
|
629
|
-
console.log(" To start daemon automatically on login:");
|
|
630
|
-
console.log(` metame daemon start (first time to verify it works)`);
|
|
631
|
-
console.log(` Then create: ${plistPath}`);
|
|
632
|
-
console.log(" Or run: metame daemon install-launchd");
|
|
692
|
+
console.log(" metame daemon install-launchd — auto-start on login");
|
|
633
693
|
}
|
|
694
|
+
|
|
695
|
+
rl.close();
|
|
634
696
|
process.exit(0);
|
|
635
697
|
}
|
|
636
698
|
|
|
@@ -683,18 +745,21 @@ if (isDaemon) {
|
|
|
683
745
|
}
|
|
684
746
|
|
|
685
747
|
if (subCmd === 'start') {
|
|
748
|
+
// Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
|
|
749
|
+
try {
|
|
750
|
+
const { execSync: es } = require('child_process');
|
|
751
|
+
const pids = es("pgrep -f 'node.*daemon\\.js' 2>/dev/null || true", { encoding: 'utf8' }).trim();
|
|
752
|
+
if (pids) {
|
|
753
|
+
for (const p of pids.split('\n').filter(Boolean)) {
|
|
754
|
+
const n = parseInt(p, 10);
|
|
755
|
+
if (n && n !== process.pid) try { process.kill(n, 'SIGKILL'); } catch { /* */ }
|
|
756
|
+
}
|
|
757
|
+
es('sleep 1');
|
|
758
|
+
}
|
|
759
|
+
} catch { /* ignore */ }
|
|
686
760
|
// Check if already running
|
|
687
761
|
if (fs.existsSync(DAEMON_PID)) {
|
|
688
|
-
|
|
689
|
-
try {
|
|
690
|
-
process.kill(existingPid, 0); // test if alive
|
|
691
|
-
console.log(`⚠️ Daemon already running (PID: ${existingPid})`);
|
|
692
|
-
console.log(" Use 'metame daemon stop' first.");
|
|
693
|
-
process.exit(1);
|
|
694
|
-
} catch {
|
|
695
|
-
// Stale PID file — clean up
|
|
696
|
-
fs.unlinkSync(DAEMON_PID);
|
|
697
|
-
}
|
|
762
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
698
763
|
}
|
|
699
764
|
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
700
765
|
console.error("❌ No config found. Run: metame daemon init");
|
|
@@ -724,11 +789,21 @@ if (isDaemon) {
|
|
|
724
789
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
725
790
|
try {
|
|
726
791
|
process.kill(pid, 'SIGTERM');
|
|
792
|
+
// Wait for process to die (up to 3s), then force kill
|
|
793
|
+
let dead = false;
|
|
794
|
+
for (let i = 0; i < 6; i++) {
|
|
795
|
+
const { execSync: es } = require('child_process');
|
|
796
|
+
es('sleep 0.5');
|
|
797
|
+
try { process.kill(pid, 0); } catch { dead = true; break; }
|
|
798
|
+
}
|
|
799
|
+
if (!dead) {
|
|
800
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
|
|
801
|
+
}
|
|
727
802
|
console.log(`✅ Daemon stopped (PID: ${pid})`);
|
|
728
803
|
} catch (e) {
|
|
729
804
|
console.log(`⚠️ Process ${pid} not found (may have already exited).`);
|
|
730
|
-
fs.unlinkSync(DAEMON_PID);
|
|
731
805
|
}
|
|
806
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
|
|
732
807
|
process.exit(0);
|
|
733
808
|
}
|
|
734
809
|
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -256,11 +256,12 @@ function executeTask(task, config) {
|
|
|
256
256
|
}
|
|
257
257
|
const fullPrompt = preamble + taskPrompt;
|
|
258
258
|
|
|
259
|
+
const allowedArgs = (task.allowedTools || []).map(t => `--allowedTools ${t}`).join(' ');
|
|
259
260
|
log('INFO', `Executing task: ${task.name} (model: ${model})`);
|
|
260
261
|
|
|
261
262
|
try {
|
|
262
263
|
const output = execSync(
|
|
263
|
-
`claude -p --model ${model}`,
|
|
264
|
+
`claude -p --model ${model}${allowedArgs ? ' ' + allowedArgs : ''}`,
|
|
264
265
|
{
|
|
265
266
|
input: fullPrompt,
|
|
266
267
|
encoding: 'utf8',
|
|
@@ -335,6 +336,7 @@ function executeWorkflow(task, config) {
|
|
|
335
336
|
const sessionId = crypto.randomUUID();
|
|
336
337
|
const outputs = [];
|
|
337
338
|
let totalTokens = 0;
|
|
339
|
+
const allowed = task.allowedTools || [];
|
|
338
340
|
|
|
339
341
|
log('INFO', `Workflow ${task.name}: ${steps.length} steps, session ${sessionId.slice(0, 8)}`);
|
|
340
342
|
|
|
@@ -343,6 +345,7 @@ function executeWorkflow(task, config) {
|
|
|
343
345
|
let prompt = (step.skill ? `/${step.skill} ` : '') + (step.prompt || '');
|
|
344
346
|
if (i === 0 && precheck.context) prompt += `\n\n相关数据:\n\`\`\`\n${precheck.context}\n\`\`\``;
|
|
345
347
|
const args = ['-p', '--model', model];
|
|
348
|
+
for (const tool of allowed) args.push('--allowedTools', tool);
|
|
346
349
|
args.push(i === 0 ? '--session-id' : '--resume', sessionId);
|
|
347
350
|
|
|
348
351
|
log('INFO', `Workflow ${task.name} step ${i + 1}/${steps.length}: ${step.skill || 'prompt'}`);
|
|
@@ -612,12 +615,27 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
612
615
|
await sendDirPicker(bot, chatId, 'new', 'Pick a workdir:');
|
|
613
616
|
return;
|
|
614
617
|
}
|
|
615
|
-
if
|
|
616
|
-
|
|
617
|
-
|
|
618
|
+
// Parse: /new <path> [name] — if arg contains a space after a valid path, rest is name
|
|
619
|
+
let dirPath = arg;
|
|
620
|
+
let sessionName = '';
|
|
621
|
+
// Try full arg as path first; if not, split on spaces to find path + name
|
|
622
|
+
if (!fs.existsSync(dirPath)) {
|
|
623
|
+
const spaceIdx = arg.indexOf(' ');
|
|
624
|
+
if (spaceIdx > 0) {
|
|
625
|
+
const maybePath = arg.slice(0, spaceIdx);
|
|
626
|
+
if (fs.existsSync(maybePath)) {
|
|
627
|
+
dirPath = maybePath;
|
|
628
|
+
sessionName = arg.slice(spaceIdx + 1).trim();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!fs.existsSync(dirPath)) {
|
|
632
|
+
await bot.sendMessage(chatId, `Path not found: ${dirPath}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
618
635
|
}
|
|
619
|
-
const session = createSession(chatId,
|
|
620
|
-
|
|
636
|
+
const session = createSession(chatId, dirPath, sessionName || '');
|
|
637
|
+
const label = sessionName ? `[${sessionName}]` : '';
|
|
638
|
+
await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
|
|
621
639
|
return;
|
|
622
640
|
}
|
|
623
641
|
|
|
@@ -666,11 +684,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
666
684
|
return;
|
|
667
685
|
}
|
|
668
686
|
|
|
669
|
-
// Argument given → match by
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
687
|
+
// Argument given → match by name, then by session ID prefix
|
|
688
|
+
const allSessions = listRecentSessions(50);
|
|
689
|
+
const argLower = arg.toLowerCase();
|
|
690
|
+
// 1. Match by name (from session_names map)
|
|
691
|
+
let fullMatch = allSessions.find(s => {
|
|
692
|
+
const n = getSessionName(s.sessionId);
|
|
693
|
+
return n && n.toLowerCase() === argLower;
|
|
694
|
+
});
|
|
695
|
+
// 2. Partial name match
|
|
696
|
+
if (!fullMatch) {
|
|
697
|
+
fullMatch = allSessions.find(s => {
|
|
698
|
+
const n = getSessionName(s.sessionId);
|
|
699
|
+
return n && n.toLowerCase().includes(argLower);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
// 3. Session ID prefix match
|
|
703
|
+
if (!fullMatch) {
|
|
704
|
+
fullMatch = recentSessions.find(s => s.sessionId.startsWith(arg))
|
|
705
|
+
|| allSessions.find(s => s.sessionId.startsWith(arg));
|
|
706
|
+
}
|
|
674
707
|
const sessionId = fullMatch ? fullMatch.sessionId : arg;
|
|
675
708
|
const cwd = (fullMatch && fullMatch.projectPath) || (getSession(chatId) && getSession(chatId).cwd) || HOME;
|
|
676
709
|
|
|
@@ -681,8 +714,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
681
714
|
created: new Date().toISOString(),
|
|
682
715
|
started: true,
|
|
683
716
|
};
|
|
717
|
+
if (fullMatch) {
|
|
718
|
+
const n = getSessionName(sessionId);
|
|
719
|
+
if (n) state2.sessions[chatId].name = n;
|
|
720
|
+
}
|
|
684
721
|
saveState(state2);
|
|
685
|
-
const
|
|
722
|
+
const name = getSessionName(sessionId);
|
|
723
|
+
const label = name || (fullMatch ? (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) : sessionId.slice(0, 8));
|
|
686
724
|
await bot.sendMessage(chatId, `Resumed: ${label}\nWorkdir: ${cwd}`);
|
|
687
725
|
return;
|
|
688
726
|
}
|
|
@@ -708,12 +746,32 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
708
746
|
return;
|
|
709
747
|
}
|
|
710
748
|
|
|
749
|
+
if (text.startsWith('/name ')) {
|
|
750
|
+
const name = text.slice(6).trim();
|
|
751
|
+
if (!name) {
|
|
752
|
+
await bot.sendMessage(chatId, 'Usage: /name <session name>');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const state2 = loadState();
|
|
756
|
+
if (!state2.sessions[chatId]) {
|
|
757
|
+
await bot.sendMessage(chatId, 'No active session. Start one first.');
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
state2.sessions[chatId].name = name;
|
|
761
|
+
if (!state2.session_names) state2.session_names = {};
|
|
762
|
+
state2.session_names[state2.sessions[chatId].id] = name;
|
|
763
|
+
saveState(state2);
|
|
764
|
+
await bot.sendMessage(chatId, `Session named: ${name}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
711
768
|
if (text === '/session') {
|
|
712
769
|
const session = getSession(chatId);
|
|
713
770
|
if (!session) {
|
|
714
771
|
await bot.sendMessage(chatId, 'No active session. Send any message to start one.');
|
|
715
772
|
} else {
|
|
716
|
-
|
|
773
|
+
const nameTag = session.name ? ` [${session.name}]` : '';
|
|
774
|
+
await bot.sendMessage(chatId, `Session: ${session.id.slice(0, 8)}...${nameTag}\nWorkdir: ${session.cwd}\nStarted: ${session.created}`);
|
|
717
775
|
}
|
|
718
776
|
return;
|
|
719
777
|
}
|
|
@@ -793,9 +851,10 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
793
851
|
if (text.startsWith('/')) {
|
|
794
852
|
await bot.sendMessage(chatId, [
|
|
795
853
|
'Commands:',
|
|
796
|
-
'/new [path] — new session',
|
|
854
|
+
'/new [path] [name] — new session (optional name)',
|
|
797
855
|
'/continue — resume last computer session',
|
|
798
|
-
'/resume <id> — resume
|
|
856
|
+
'/resume <name|id> — resume by name or session ID',
|
|
857
|
+
'/name <name> — name current session',
|
|
799
858
|
'/cd <path> — change workdir',
|
|
800
859
|
'/session — current session info',
|
|
801
860
|
'/status /tasks /run /budget /quiet /reload',
|
|
@@ -862,8 +921,10 @@ function listRecentSessions(limit, cwd) {
|
|
|
862
921
|
* Format a session entry into a short, readable label for buttons
|
|
863
922
|
*/
|
|
864
923
|
function sessionLabel(s) {
|
|
924
|
+
const name = getSessionName(s.sessionId);
|
|
865
925
|
const proj = s.projectPath ? path.basename(s.projectPath) : '';
|
|
866
926
|
const date = new Date(s.modified).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' });
|
|
927
|
+
if (name) return `${date} [${name}] ${proj}`;
|
|
867
928
|
const title = (s.summary || '').slice(0, 28);
|
|
868
929
|
return `${date} ${proj ? proj + ': ' : ''}${title}`;
|
|
869
930
|
}
|
|
@@ -898,7 +959,7 @@ function getSession(chatId) {
|
|
|
898
959
|
return state.sessions[chatId] || null;
|
|
899
960
|
}
|
|
900
961
|
|
|
901
|
-
function createSession(chatId, cwd) {
|
|
962
|
+
function createSession(chatId, cwd, name) {
|
|
902
963
|
const state = loadState();
|
|
903
964
|
const sessionId = crypto.randomUUID();
|
|
904
965
|
state.sessions[chatId] = {
|
|
@@ -907,11 +968,21 @@ function createSession(chatId, cwd) {
|
|
|
907
968
|
created: new Date().toISOString(),
|
|
908
969
|
started: false, // true after first message sent
|
|
909
970
|
};
|
|
971
|
+
if (name) {
|
|
972
|
+
state.sessions[chatId].name = name;
|
|
973
|
+
if (!state.session_names) state.session_names = {};
|
|
974
|
+
state.session_names[sessionId] = name;
|
|
975
|
+
}
|
|
910
976
|
saveState(state);
|
|
911
|
-
log('INFO', `New session for ${chatId}: ${sessionId} (cwd: ${state.sessions[chatId].cwd})`);
|
|
977
|
+
log('INFO', `New session for ${chatId}: ${sessionId}${name ? ' [' + name + ']' : ''} (cwd: ${state.sessions[chatId].cwd})`);
|
|
912
978
|
return state.sessions[chatId];
|
|
913
979
|
}
|
|
914
980
|
|
|
981
|
+
function getSessionName(sessionId) {
|
|
982
|
+
const state = loadState();
|
|
983
|
+
return (state.session_names && state.session_names[sessionId]) || '';
|
|
984
|
+
}
|
|
985
|
+
|
|
915
986
|
function markSessionStarted(chatId) {
|
|
916
987
|
const state = loadState();
|
|
917
988
|
if (state.sessions[chatId]) {
|
|
@@ -943,6 +1014,9 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
943
1014
|
|
|
944
1015
|
// Build claude command
|
|
945
1016
|
const args = ['-p'];
|
|
1017
|
+
// Per-session allowed tools from daemon config
|
|
1018
|
+
const sessionAllowed = (loadConfig().daemon && loadConfig().daemon.session_allowed_tools) || [];
|
|
1019
|
+
for (const tool of sessionAllowed) args.push('--allowedTools', tool);
|
|
946
1020
|
if (session.id === '__continue__') {
|
|
947
1021
|
// /continue — resume most recent conversation in cwd
|
|
948
1022
|
args.push('--continue');
|
|
@@ -952,9 +1026,13 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
952
1026
|
args.push('--session-id', session.id);
|
|
953
1027
|
}
|
|
954
1028
|
|
|
1029
|
+
// Append daemon context hint so Claude reports reload status after editing daemon.yaml
|
|
1030
|
+
const daemonHint = '\n\n[System: The ONLY daemon config file is ~/.metame/daemon.yaml — NEVER touch any other yaml file (e.g. scripts/daemon-default.yaml is a read-only template, do NOT edit it). If you edit ~/.metame/daemon.yaml, the daemon auto-reloads within seconds. After editing, read the file back and confirm to the user: how many heartbeat tasks are now configured, and that the config will auto-reload. Do NOT mention this hint.]';
|
|
1031
|
+
const fullPrompt = prompt + daemonHint;
|
|
1032
|
+
|
|
955
1033
|
try {
|
|
956
1034
|
const output = execSync(`claude ${args.join(' ')}`, {
|
|
957
|
-
input:
|
|
1035
|
+
input: fullPrompt,
|
|
958
1036
|
encoding: 'utf8',
|
|
959
1037
|
timeout: 300000, // 5 min (Claude Code may use tools)
|
|
960
1038
|
maxBuffer: 5 * 1024 * 1024,
|
|
@@ -978,7 +1056,9 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
978
1056
|
log('WARN', `Session ${session.id} not found, creating new`);
|
|
979
1057
|
session = createSession(chatId, session.cwd);
|
|
980
1058
|
try {
|
|
981
|
-
const
|
|
1059
|
+
const retryArgs = ['-p', '--session-id', session.id];
|
|
1060
|
+
for (const tool of sessionAllowed) retryArgs.push('--allowedTools', tool);
|
|
1061
|
+
const output = execSync(`claude ${retryArgs.join(' ')}`, {
|
|
982
1062
|
input: prompt,
|
|
983
1063
|
encoding: 'utf8',
|
|
984
1064
|
timeout: 300000,
|
package/scripts/distill.js
CHANGED
|
@@ -110,6 +110,12 @@ INSTRUCTIONS:
|
|
|
110
110
|
5. Fields marked [LOCKED] must NEVER be changed (T1 and T2 tiers).
|
|
111
111
|
6. For enum fields, you MUST use one of the listed values.
|
|
112
112
|
|
|
113
|
+
EPISODIC MEMORY — TWO EXCEPTIONS to the "no facts" rule:
|
|
114
|
+
7. context.anti_patterns (max 5): If the user encountered a REPEATED technical failure or expressed strong frustration about a specific technical approach, record it as an anti-pattern. Format: "topic — what failed and why". Only cross-project generalizable lessons, NOT project-specific bugs.
|
|
115
|
+
Example: ["async/await deadlock — Promise.all rejects all on single failure, use Promise.allSettled", "CSS Grid in email templates — no support, use tables"]
|
|
116
|
+
8. context.milestones (max 3): If the user completed a significant milestone or made a key decision, record it. Only the 3 most recent. Format: short description string.
|
|
117
|
+
Example: ["MetaMe v1.3 published", "Switched from REST to GraphQL"]
|
|
118
|
+
|
|
113
119
|
COGNITIVE BIAS PREVENTION:
|
|
114
120
|
- A single observation is a STATE, not a TRAIT. Do NOT infer T3 cognition fields from one message.
|
|
115
121
|
- Never infer cognitive style from identity/demographics.
|
|
@@ -233,6 +239,9 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
233
239
|
|
|
234
240
|
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
235
241
|
|
|
242
|
+
// Auto-expire anti_patterns older than 60 days
|
|
243
|
+
expireAntiPatterns(profile);
|
|
244
|
+
|
|
236
245
|
// Read raw content to find locked lines and comments
|
|
237
246
|
const rawProfile = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
238
247
|
const lockedKeys = extractLockedKeys(rawProfile);
|
|
@@ -415,7 +424,18 @@ function strategicMerge(profile, updates, lockedKeys, pendingTraits, confidenceM
|
|
|
415
424
|
}
|
|
416
425
|
|
|
417
426
|
case 'T4':
|
|
418
|
-
|
|
427
|
+
// Stamp added date on anti_pattern entries for auto-expiry
|
|
428
|
+
if (key === 'context.anti_patterns' && Array.isArray(value)) {
|
|
429
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
430
|
+
const existing = getNested(result, key) || [];
|
|
431
|
+
const existingTexts = new Set(existing.map(e => typeof e === 'string' ? e : e.text));
|
|
432
|
+
const stamped = value
|
|
433
|
+
.filter(v => !existingTexts.has(typeof v === 'string' ? v : v.text))
|
|
434
|
+
.map(v => typeof v === 'string' ? { text: v, added: today } : v);
|
|
435
|
+
setNested(result, key, [...existing, ...stamped].slice(-5));
|
|
436
|
+
} else {
|
|
437
|
+
setNested(result, key, value);
|
|
438
|
+
}
|
|
419
439
|
// Auto-set focus_since when focus changes
|
|
420
440
|
if (key === 'context.focus') {
|
|
421
441
|
setNested(result, 'context.focus_since', new Date().toISOString().slice(0, 10));
|
|
@@ -455,6 +475,19 @@ function flattenObject(obj, parentKey = '', result = {}) {
|
|
|
455
475
|
return result;
|
|
456
476
|
}
|
|
457
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Get a nested property by dot-path key.
|
|
480
|
+
*/
|
|
481
|
+
function getNested(obj, dotPath) {
|
|
482
|
+
const keys = dotPath.split('.');
|
|
483
|
+
let current = obj;
|
|
484
|
+
for (const k of keys) {
|
|
485
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
486
|
+
current = current[k];
|
|
487
|
+
}
|
|
488
|
+
return current;
|
|
489
|
+
}
|
|
490
|
+
|
|
458
491
|
/**
|
|
459
492
|
* Set a nested property by dot-path key.
|
|
460
493
|
*/
|
|
@@ -508,6 +541,24 @@ function truncateArrays(obj) {
|
|
|
508
541
|
}
|
|
509
542
|
}
|
|
510
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Auto-expire anti_patterns older than 60 days.
|
|
546
|
+
* Each entry is stored as { text: "...", added: "2026-01-15" } internally.
|
|
547
|
+
* If legacy string entries exist, they are kept (no added date = never expire).
|
|
548
|
+
*/
|
|
549
|
+
function expireAntiPatterns(profile) {
|
|
550
|
+
if (!profile.context || !Array.isArray(profile.context.anti_patterns)) return;
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;
|
|
553
|
+
profile.context.anti_patterns = profile.context.anti_patterns.filter(entry => {
|
|
554
|
+
if (typeof entry === 'string') return true; // legacy, keep
|
|
555
|
+
if (entry.added) {
|
|
556
|
+
return (now - new Date(entry.added).getTime()) < SIXTY_DAYS;
|
|
557
|
+
}
|
|
558
|
+
return true;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
511
562
|
/**
|
|
512
563
|
* Clean up: remove buffer and lock
|
|
513
564
|
*/
|
|
@@ -121,12 +121,32 @@ function createBot(config) {
|
|
|
121
121
|
loggerLevel: Lark.LoggerLevel.info,
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// Dedup: track recent message_ids (Feishu may redeliver on slow ack)
|
|
125
|
+
const _seenMsgIds = new Map(); // message_id → timestamp
|
|
126
|
+
const DEDUP_TTL = 60000; // 60s window
|
|
127
|
+
function isDuplicate(msgId) {
|
|
128
|
+
if (!msgId) return false;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
// Cleanup old entries
|
|
131
|
+
if (_seenMsgIds.size > 200) {
|
|
132
|
+
for (const [k, t] of _seenMsgIds) {
|
|
133
|
+
if (now - t > DEDUP_TTL) _seenMsgIds.delete(k);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (_seenMsgIds.has(msgId)) return true;
|
|
137
|
+
_seenMsgIds.set(msgId, now);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
const eventDispatcher = new Lark.EventDispatcher({}).register({
|
|
125
142
|
'im.message.receive_v1': async (data) => {
|
|
126
143
|
try {
|
|
127
144
|
const msg = data.message;
|
|
128
145
|
if (!msg) return;
|
|
129
146
|
|
|
147
|
+
// Dedup by message_id
|
|
148
|
+
if (isDuplicate(msg.message_id)) return;
|
|
149
|
+
|
|
130
150
|
const chatId = msg.chat_id;
|
|
131
151
|
let text = '';
|
|
132
152
|
|
|
@@ -143,7 +163,8 @@ function createBot(config) {
|
|
|
143
163
|
text = text.replace(/@_user_\d+\s*/g, '').trim();
|
|
144
164
|
|
|
145
165
|
if (text) {
|
|
146
|
-
|
|
166
|
+
// Fire-and-forget: don't block the event loop (SDK needs fast ack)
|
|
167
|
+
Promise.resolve().then(() => onMessage(chatId, text, data)).catch(() => {});
|
|
147
168
|
}
|
|
148
169
|
} catch (e) {
|
|
149
170
|
// Non-fatal
|
|
@@ -159,7 +180,7 @@ function createBot(config) {
|
|
|
159
180
|
if (action && chatId) {
|
|
160
181
|
const cmd = action.value && action.value.cmd;
|
|
161
182
|
if (cmd) {
|
|
162
|
-
onMessage(chatId, cmd, data);
|
|
183
|
+
Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch(() => {});
|
|
163
184
|
}
|
|
164
185
|
}
|
|
165
186
|
} catch (e) {
|
package/scripts/schema.js
CHANGED
|
@@ -60,6 +60,8 @@ const SCHEMA = {
|
|
|
60
60
|
'context.active_projects': { tier: 'T4', type: 'array', maxItems: 5 },
|
|
61
61
|
'context.blockers': { tier: 'T4', type: 'array', maxItems: 3 },
|
|
62
62
|
'context.energy': { tier: 'T4', type: 'enum', values: ['high', 'medium', 'low', null] },
|
|
63
|
+
'context.milestones': { tier: 'T4', type: 'array', maxItems: 3 },
|
|
64
|
+
'context.anti_patterns': { tier: 'T4', type: 'array', maxItems: 5 },
|
|
63
65
|
'status.focus': { tier: 'T4', type: 'string', maxChars: 80 },
|
|
64
66
|
'status.language': { tier: 'T4', type: 'string' },
|
|
65
67
|
|