metame-cli 1.4.32 → 1.4.34

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # MetaMe
2
2
 
3
3
  <p align="center">
4
- <img src="./logo.png" alt="MetaMe Logo" width="200"/>
4
+ <img src="./logo_high_contrast.png" alt="MetaMe Logo" width="200"/>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -24,7 +24,7 @@ No cloud. Your machine, your data.
24
24
  curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bash
25
25
  ```
26
26
 
27
- **Already have Node.js ≥ 18:**
27
+ **Already have Node.js ≥ 22.5:**
28
28
  ```bash
29
29
  npm install -g metame-cli
30
30
  metame
@@ -38,13 +38,15 @@ metame
38
38
 
39
39
  ---
40
40
 
41
- > ### 🚀 v1.4.19Multi-User ACL + Session Context Preview
41
+ > ### 🚀 v1.4.336-Dimension Soul Schema + Nightly Reflection + Auto-Provisioning
42
42
  >
43
- > - **Multi-user permission system**: role-based ACL (admin / member / stranger) share your bots with teammates without giving them full access. Manage users with `/user` commands.
44
- > - **Session context preview**: `/resume` and `/sessions` now show the last message snippet so you know exactly what to pick up.
45
- > - **Team Task protocol**: multi-agent task board for cross-agent collaboration. Agents can dispatch and track tasks across workspaces.
46
- > - **Layered Memory Architecture**: three-layer memory (long-term facts, session summaries, session index)all automatic.
47
- > - **Unix Socket IPC**: dispatch latency <100ms.
43
+ > - **6-dimension soul schema**: cognitive profile upgraded from key-value pairs to a structured 67-field model covering Values, Drive, Cognition Style, Stress & Shadow, Relational, and Identity Narrative with tier-based lock protection.
44
+ > - **Nightly reflection**: every night, hot-zone facts are distilled into searchable decision logs and lessons learned.
45
+ > - **Memory index**: auto-generated global index of all memory documents for instant retrieval.
46
+ > - **Auto-provisioning**: first run automatically deploys default CLAUDE.md, docs, and `dispatch_to`zero manual setup.
47
+ > - **Token budget tracking**: daily token usage monitoring with per-category breakdown and 80% warning threshold.
48
+ > - **Multi-user ACL**: role-based permissions (admin / member / stranger) with binding protection.
49
+ > - **Windows native support**: cross-platform path handling, Named Pipes IPC, GBK-safe encoding.
48
50
  >
49
51
  > Zero configuration. It just works.
50
52
 
@@ -56,7 +58,9 @@ metame
56
58
 
57
59
  Claude Code forgets you every time you switch folders. MetaMe doesn't.
58
60
 
59
- A cognitive profile (`~/.claude_profile.yaml`) follows you everywhere — not just facts like "user prefers TypeScript", but *how you think*: your decision style, cognitive load preferences, communication patterns. It learns silently from your conversations via background distillation, no effort required.
61
+ A cognitive profile (`~/.claude_profile.yaml`) follows you everywhere — not just facts like "user prefers TypeScript", but *how you think*. The profile is built on a **6-dimension soul schema**: Values, Drive, Cognition Style, Stress & Shadow, Relational, and Identity Narrative — 67 fields organized into 4 tiers (T1 locked identity → T5 system-managed evolution). First-time users go through a **Genesis Interview**: a structured deep conversation that builds your cognitive fingerprint from scratch.
62
+
63
+ Once established, the profile updates silently from your conversations via background distillation — no effort required.
60
64
 
61
65
  ```
62
66
  $ metame
@@ -80,7 +84,7 @@ Start on your laptop, continue on the train. `/stop` to interrupt, `/undo` to ro
80
84
 
81
85
  ### 3. Layered Memory That Works While You Sleep
82
86
 
83
- MetaMe's memory system runs automatically in the background — no prompts, no manual saves.
87
+ MetaMe's memory system runs automatically in the background — no prompts, no manual saves. Five layers, fully autonomous.
84
88
 
85
89
  **Layer 1 — Long-term Facts**
86
90
  When you go idle, MetaMe runs memory consolidation: extracts key decisions, patterns, and knowledge from your sessions into a persistent facts store. These are semantically recalled on every session start.
@@ -91,12 +95,20 @@ Resuming a conversation after 2+ hours? MetaMe injects a brief summary of what y
91
95
  **Layer 3 — Session Index**
92
96
  Every session gets tagged with topics and intent. This powers future session routing: when you reference "that thing we worked on last week", MetaMe knows where to look.
93
97
 
98
+ **Layer 4 — Nightly Reflection**
99
+ Every night at 01:00, MetaMe reviews your most-accessed facts from the past week and distills them into high-level decision logs and operational lessons — searchable documents that grow your personal knowledge base over time.
100
+
101
+ **Layer 5 — Memory Index**
102
+ At 01:30, an auto-generated global index (`INDEX.md`) maps every memory document across all categories. This serves as a fast lookup table so MetaMe always knows where to find relevant context.
103
+
94
104
  ```
95
105
  [Background, while you sleep]
96
106
  idle 30min → memory consolidation triggered
97
107
  → session_tags.json updated (topics indexed)
98
108
  → facts extracted → ~/.metame/memory.db
99
109
  → session summary cached → daemon_state.json
110
+ 01:00 → nightly reflection: hot facts → decisions + lessons
111
+ 01:30 → memory index regenerated
100
112
 
101
113
  [Next morning, when you resume]
102
114
  "continue from yesterday" →
@@ -117,12 +129,14 @@ Built into the daemon. Runs every 60 seconds regardless of what's in your config
117
129
  - Detects when you go idle → generates session continuity summaries
118
130
 
119
131
  **Layer 1 — System Evolution (built-in defaults)**
120
- Three tasks shipped out of the box. They are precondition-gated and run only when useful:
132
+ Five tasks shipped out of the box. They are precondition-gated and run only when useful:
121
133
 
122
134
  ```yaml
123
135
  - cognitive-distill # 4h · has signals? → distill preferences into profile
124
136
  - memory-extract # 4h · scan sessions → extract long-term facts + topic tags
125
137
  - skill-evolve # 6h · has signals? → evolve skills from task outcomes
138
+ - nightly-reflect # 01:00 daily · hot facts → decision logs + lessons learned
139
+ - memory-index # 01:30 daily · regenerate global memory index
126
140
  ```
127
141
 
128
142
  `precondition` guards mean zero tokens burned when there's nothing to process.
@@ -203,19 +217,21 @@ Task fails → skill-scout finds a skill → installs → retries → succeeds
203
217
  curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bash
204
218
  ```
205
219
 
206
- **Already have Node.js ≥ 18:**
220
+ **Already have Node.js ≥ 22.5:**
207
221
  ```bash
208
222
  npm install -g metame-cli
209
223
  metame
210
224
  ```
211
225
 
226
+ > **First run auto-provisioning:** MetaMe automatically deploys a default `CLAUDE.md`, documentation files, and the `dispatch_to` tool to `~/.metame/`. No manual setup needed.
227
+
212
228
  **Setup guide (3 minutes):**
213
229
 
214
230
  | Step | What to do | What happens |
215
231
  |------|-----------|-------------|
216
232
  | 1. Log in to Claude | Run `claude` and complete the login (Anthropic account or API key) | Claude Code is ready to use |
217
233
  | 2. Launch MetaMe | Run `metame` | Opens a Claude session with MetaMe loaded |
218
- | 3. Cognitive interview | Just chat — MetaMe will automatically start a deep interview on first run | Builds `~/.claude_profile.yaml` (your digital twin's brain) |
234
+ | 3. Genesis Interview | Just chat — MetaMe will automatically start a deep soul interview on first run | Builds `~/.claude_profile.yaml` (6-dimension cognitive profile) |
219
235
  | 4. Connect phone | Say "help me set up mobile access" or "connect my phone" | Interactive wizard for Telegram/Feishu bot setup → `~/.metame/daemon.yaml` |
220
236
  | 5. Start daemon | `metame start` | Background daemon launches, bot goes online |
221
237
  | 6. Register with system | macOS: `metame daemon install-launchd` · Windows: `metame daemon install-task-scheduler` · Linux: see below | Always-on, crash recovery |
@@ -281,11 +297,13 @@ systemctl --user start metame
281
297
 
282
298
  | Capability | What It Does |
283
299
  |-----------|-------------|
284
- | **Cognitive Profile** | Learns how you think across sessions. Schema-enforced, 800-token budget, auto-distilled via Haiku. Lock any value with `# [LOCKED]`. |
285
- | **Layered Memory** | Three-tier memory: long-term facts (semantic recall), session summaries (continuity bridge), session index (topic tags). All automatic. |
300
+ | **Cognitive Profile** | 6-dimension soul schema (Values, Drive, Cognition Style, Stress & Shadow, Relational, Identity Narrative). 67 fields, tier-locked, 800-token budget. First-time Genesis Interview builds your profile from scratch. |
301
+ | **Layered Memory** | Five-tier memory: long-term facts (semantic recall), session summaries (continuity bridge), session index (topic tags), nightly reflection (decision/lesson distillation), memory index (global lookup). All automatic. |
286
302
  | **Mobile Bridge** | Full Claude Code via Telegram/Feishu. Stateful sessions, file transfer both ways, real-time streaming status. |
287
303
  | **Skill Evolution** | Self-healing skill system. Auto-discovers missing skills, learns from browser recordings, evolves after every task. Skills get smarter over time. |
288
- | **Heartbeat System** | Three-layer programmable nervous system. Layer 0 kernel always-on (zero config). Layer 1 system evolution built-in (distill + memory + skills). Layer 2 your custom scheduled tasks with `require_idle`, `precondition`, `notify`, workflows. |
304
+ | **Token Budget** | Daily token usage tracking with per-category breakdown. Configurable daily limit, automatic 80% warning threshold, usage history with rollover. |
305
+ | **Auto-Provisioning** | First run deploys default CLAUDE.md, documentation, and `dispatch_to` to `~/.metame/`. Subsequent runs sync scripts without overwriting user config. |
306
+ | **Heartbeat System** | Three-layer programmable nervous system. Layer 0 kernel always-on (zero config). Layer 1 system evolution built-in (5 tasks: distill + memory + skills + nightly reflection + memory index). Layer 2 your custom scheduled tasks with `require_idle`, `precondition`, `notify`, workflows. |
289
307
  | **Multi-Agent** | Multiple projects with dedicated chat groups. `/agent bind` for one-tap setup. True parallel execution. |
290
308
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
291
309
  | **Cross-Platform** | Native support for macOS and Windows. Platform abstraction layer handles spawn, IPC, process management, and terminal encoding automatically. |
@@ -429,27 +447,34 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
429
447
  │ └──────────────┘ │
430
448
  │ │
431
449
  │ ~/.claude_profile │
432
- │ (cognitive layer)
450
+ │ (6-dim soul schema)
433
451
  │ │
434
452
  │ ~/.metame/memory.db │
435
453
  │ session_tags.json │
436
- │ (memory layer) ← NEW
454
+ │ (5-layer memory)
455
+ │ │
456
+ │ dispatch_to (auto-deployed)│
437
457
  └──────────────────────────────┘
438
458
 
439
- idle mode → summaries + background memory tasks
440
- (automatic, precondition-gated)
459
+ idle → summaries + memory tasks
460
+ 01:00 → nightly reflection
461
+ 01:30 → memory index rebuild
441
462
  ```
442
463
 
443
- - **Profile** (`~/.claude_profile.yaml`): Your cognitive fingerprint. Injected into every Claude session via `CLAUDE.md`.
444
- - **Daemon** (`scripts/daemon.js`): Background process handling Telegram/Feishu messages, heartbeat tasks, Unix socket dispatch, and idle/sleep transitions.
445
- - **Distillation** (`scripts/distill.js`): Heartbeat task (default 4h, signal-gated) that updates your profile.
446
- - **Memory Extract** (`scripts/memory-extract.js`): Heartbeat task (default 4h, idle-gated) that extracts long-term facts and session topic tags.
447
- - **Session Summarize** (`scripts/session-summarize.js`): Generates a 2-4 sentence summary for idle sessions. Injected as context when resuming after a 2h+ gap.
464
+ - **Profile** (`~/.claude_profile.yaml`): 6-dimension soul schema. Injected into every Claude session via `CLAUDE.md`.
465
+ - **Daemon**: Background process handling Telegram/Feishu messages, heartbeat tasks, Unix socket dispatch, and idle/sleep transitions.
466
+ - **Distillation**: Heartbeat task (4h, signal-gated) that updates your cognitive profile.
467
+ - **Memory Extract**: Heartbeat task (4h, idle-gated) that extracts long-term facts and session topic tags.
468
+ - **Nightly Reflection**: Daily at 01:00. Distills hot-zone facts into decision logs and operational lessons.
469
+ - **Memory Index**: Daily at 01:30. Regenerates the global memory index for fast retrieval.
470
+ - **Session Summarize**: Generates a brief summary for idle sessions. Injected as context when resuming after a 2h+ gap.
448
471
 
449
472
  ## Security
450
473
 
451
474
  - All data stays on your machine. No cloud, no telemetry.
452
475
  - `allowed_chat_ids` whitelist — new groups get a smart prompt: if a pending agent activation exists, they're guided to send `/activate`; otherwise they receive setup instructions.
476
+ - **Multi-user ACL**: role-based permissions (admin / member / stranger). Admins manage access via `/user` commands with hot-reload config.
477
+ - **Binding protection**: each group can only be bound to one agent. Existing bindings cannot be overwritten without explicit `force:true`.
453
478
  - `operator_ids` for shared groups — non-operators get read-only mode.
454
479
  - `~/.metame/` directory is mode 700.
455
480
  - Bot tokens stored locally, never transmitted.
@@ -460,7 +485,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
460
485
  |--------|-------|
461
486
  | Daemon memory (idle) | ~100 MB RSS — standard Node.js process baseline |
462
487
  | Daemon CPU (idle, between heartbeats) | ~0% — event-loop sleeping |
463
- | Cognitive profile injection | ~600 tokens/session (0.3% of 200k context) |
488
+ | Cognitive profile injection | ~800 tokens/session (0.4% of 200k context) |
464
489
  | Dispatch latency (Unix socket) | <100ms |
465
490
  | Memory consolidation (per session) | ~1,500–2,000 tokens input + ~50–300 tokens output (Haiku) |
466
491
  | Session summary (per session) | ~400–900 tokens input + ≤250 tokens output (Haiku) |
package/index.js CHANGED
@@ -50,6 +50,41 @@ if (!fs.existsSync(METAME_DIR)) {
50
50
  fs.mkdirSync(METAME_DIR, { recursive: true });
51
51
  }
52
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
+
53
88
  // Auto-deploy bundled scripts to ~/.metame/
54
89
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
55
90
  const scriptsDir = path.join(__dirname, 'scripts');
@@ -75,23 +110,7 @@ try {
75
110
  }
76
111
  } catch { /* non-fatal */ }
77
112
 
78
- let scriptsUpdated = false;
79
- for (const script of BUNDLED_SCRIPTS) {
80
- const src = path.join(scriptsDir, script);
81
- const dest = path.join(METAME_DIR, script);
82
- try {
83
- if (fs.existsSync(src)) {
84
- const srcContent = fs.readFileSync(src, 'utf8');
85
- const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
86
- if (srcContent !== destContent) {
87
- fs.writeFileSync(dest, srcContent, 'utf8');
88
- scriptsUpdated = true;
89
- }
90
- }
91
- } catch {
92
- // Non-fatal
93
- }
94
- }
113
+ const scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
95
114
 
96
115
  // Daemon restart on script update:
97
116
  // Don't kill daemon here — daemon's own file watcher detects ~/.metame/daemon.js changes
@@ -101,6 +120,11 @@ if (scriptsUpdated) {
101
120
  console.log(`${icon("pkg")} Scripts synced to ~/.metame/ — daemon will auto-restart when idle.`);
102
121
  }
103
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
+
104
128
  // ---------------------------------------------------------
105
129
  // Deploy bundled skills to ~/.claude/skills/
106
130
  // Only installs if not already present — never overwrites user customizations.
@@ -750,16 +774,20 @@ const CAPABILITY_SECTIONS = [
750
774
  `"告诉X/让X" → \`~/.metame/bin/dispatch_to <project_key> "内容"\`,手机端 \`/dispatch to <key> <消息>\`。` + dispatchTable,
751
775
  '新增 Agent:`/agent bind <名称> <工作目录>`',
752
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
+ '',
753
785
  '## 跨会话记忆',
754
786
  '用户提"上次/之前"时搜索:`node ~/.metame/memory-search.js "关键词1" "keyword2"`',
755
787
  '一次传 3-4 个关键词(中文+英文+函数名),`--facts` 只搜事实,`--sessions` 只搜会话。',
756
788
  '',
757
789
  '## Skills',
758
790
  '能力不足/工具缺失/任务失败 → 先查 `cat ~/.claude/skills/skill-manager/SKILL.md`,不要自己猜。',
759
- '',
760
- '## 手机端文件交互',
761
- '**收**:用户发图片/文件自动存到 `upload/`,用 Read 查看。',
762
- '**发**:回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发手机。不要读内容再复述。',
763
791
 
764
792
  ].join('\n');
765
793
 
@@ -778,6 +806,14 @@ try {
778
806
  ), '');
779
807
  }
780
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
+
781
817
  const injection =
782
818
  GLOBAL_MARKER_START + '\n' +
783
819
  KERNEL_BODY + '\n\n' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.32",
3
+ "version": "1.4.34",
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": {
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dispatch_to [--new] <project_key> "<prompt>"
4
+ * Tries Unix socket / Named Pipe first (low-latency), falls back to pending.jsonl.
5
+ */
6
+ 'use strict';
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const net = require('net');
10
+ const crypto = require('crypto');
11
+ const os = require('os');
12
+ const { socketPath } = require('../platform');
13
+
14
+ const args = process.argv.slice(2);
15
+ const newSession = args[0] === '--new' ? (args.shift(), true) : false;
16
+ const [target, ...rest] = args;
17
+ const prompt = rest.join(' ').replace(/^["']|["']$/g, '');
18
+ if (!target || !prompt) {
19
+ console.error('Usage: dispatch_to [--new] <project_key> "<prompt>"');
20
+ process.exit(1);
21
+ }
22
+
23
+ const METAME_DIR = path.join(os.homedir(), '.metame');
24
+ const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
25
+ const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
26
+ const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
27
+ const SOCK_PATH = socketPath(METAME_DIR);
28
+
29
+ function getDispatchSecret() {
30
+ try {
31
+ if (fs.existsSync(DISPATCH_SECRET_FILE)) {
32
+ return fs.readFileSync(DISPATCH_SECRET_FILE, 'utf8').trim();
33
+ }
34
+ } catch { /* fall through to generate */ }
35
+ const secret = crypto.randomBytes(32).toString('hex');
36
+ try {
37
+ fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 });
38
+ } catch { /* ignore write errors */ }
39
+ return secret;
40
+ }
41
+
42
+ const ts = new Date().toISOString();
43
+ const secret = getDispatchSecret();
44
+ const sigPayload = JSON.stringify({ target, prompt, ts });
45
+ const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
46
+
47
+ const msg = { target, prompt, from: '_claude_session', new_session: newSession, created_at: ts, ts, sig };
48
+
49
+ function fallbackToFile() {
50
+ fs.mkdirSync(DISPATCH_DIR, { recursive: true });
51
+ fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
52
+ console.log(`DISPATCH_OK(file): ${target} → ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
53
+ }
54
+
55
+ const sock = net.createConnection({ path: SOCK_PATH });
56
+ let done = false;
57
+
58
+ const timer = setTimeout(() => {
59
+ if (done) return;
60
+ done = true;
61
+ sock.destroy();
62
+ fallbackToFile();
63
+ }, 2000);
64
+
65
+ sock.on('connect', () => {
66
+ sock.write(JSON.stringify(msg));
67
+ sock.end();
68
+ });
69
+
70
+ sock.on('data', (data) => {
71
+ if (done) return;
72
+ done = true;
73
+ clearTimeout(timer);
74
+ try {
75
+ const res = JSON.parse(data.toString().trim());
76
+ if (res.ok) {
77
+ console.log(`DISPATCH_OK(socket): ${target} → ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
78
+ } else {
79
+ fallbackToFile();
80
+ }
81
+ } catch {
82
+ fallbackToFile();
83
+ }
84
+ sock.destroy();
85
+ });
86
+
87
+ sock.on('error', () => {
88
+ if (done) return;
89
+ done = true;
90
+ clearTimeout(timer);
91
+ fallbackToFile();
92
+ });
@@ -653,41 +653,18 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
653
653
  }
654
654
 
655
655
  // Skill routing: detect skill first, then decide session
656
- // BUT: if agent was explicitly addressed by nickname, don't let skill routing hijack the session
657
- const skill = agentMatch ? null : routeSkill(prompt);
656
+ // BUT: skip skill routing if agent addressed by nickname OR chat already has an active session
657
+ // (active conversation should never be hijacked by keyword-based skill matching)
658
+ let session = getSession(chatId);
659
+ const hasActiveSession = session && session.started;
660
+ const skill = (agentMatch || hasActiveSession) ? null : routeSkill(prompt);
658
661
  const chatIdStr = String(chatId);
659
662
  const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
660
663
  const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
661
664
  const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
662
665
  const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
663
666
 
664
- // Skills with dedicated pinned sessions (reused across days, no re-injection needed)
665
- const PINNED_SKILL_SESSIONS = new Set(['skill-manager']);
666
- const usePinnedSkillSession = !!(skill && PINNED_SKILL_SESSIONS.has(skill));
667
-
668
- let session = getSession(chatId);
669
-
670
- if (usePinnedSkillSession) {
671
- // Use a dedicated long-lived session per skill
672
- const state = loadState();
673
- if (!state.pinned_sessions) state.pinned_sessions = {};
674
- const pinned = state.pinned_sessions[skill];
675
- if (pinned) {
676
- // Reuse existing pinned session
677
- state.sessions[chatId] = { id: pinned.id, cwd: pinned.cwd, started: true };
678
- saveState(state);
679
- session = state.sessions[chatId];
680
- log('INFO', `Pinned session reused for skill ${skill}: ${pinned.id.slice(0, 8)}`);
681
- } else {
682
- // First time — create session and pin it
683
- session = createSession(chatId, HOME, skill);
684
- const st2 = loadState();
685
- if (!st2.pinned_sessions) st2.pinned_sessions = {};
686
- st2.pinned_sessions[skill] = { id: session.id, cwd: session.cwd };
687
- saveState(st2);
688
- log('INFO', `Pinned session created for skill ${skill}: ${session.id.slice(0, 8)}`);
689
- }
690
- } else if (!session) {
667
+ if (!session) {
691
668
  if (boundCwd) {
692
669
  // Agent-bound chats must stay in their own workspace: never attach to another project's session.
693
670
  const recentInBound = listRecentSessions(1, boundCwd);
@@ -95,6 +95,23 @@ function nextClockRunAfter(schedule, fromMs) {
95
95
  return baseMs + 24 * 60 * 60 * 1000;
96
96
  }
97
97
 
98
+ // Map short aliases and full model IDs to what Claude CLI accepts.
99
+ // Claude CLI 2.x accepts both 'sonnet' and 'claude-sonnet-4-6'.
100
+ // This normalization keeps daemon.yaml configs forward-compatible.
101
+ const MODEL_ALIASES = {
102
+ haiku: 'claude-haiku-4-5-20251001',
103
+ sonnet: 'claude-sonnet-4-6',
104
+ opus: 'claude-opus-4-6',
105
+ };
106
+
107
+ function normalizeModel(raw) {
108
+ if (!raw || typeof raw !== 'string') return MODEL_ALIASES.haiku;
109
+ const lower = raw.trim().toLowerCase();
110
+ if (Object.prototype.hasOwnProperty.call(MODEL_ALIASES, lower)) return MODEL_ALIASES[lower];
111
+ // Already a full model ID (e.g. 'claude-sonnet-4-6') — pass through
112
+ return raw.trim();
113
+ }
114
+
98
115
  function buildTaskSchedule(task, parseInterval) {
99
116
  const atRaw = typeof task.at === 'string' ? task.at.trim() : '';
100
117
  if (atRaw) {
@@ -358,7 +375,7 @@ function createTaskScheduler(deps) {
358
375
  }
359
376
 
360
377
  const preamble = buildProfilePreamble();
361
- const model = task.model || 'haiku';
378
+ const model = normalizeModel(task.model || 'haiku');
362
379
  // If precondition returned context data, append it to the prompt
363
380
  let taskPrompt = task.prompt;
364
381
  if (precheck.context) {
@@ -533,7 +550,7 @@ function createTaskScheduler(deps) {
533
550
  const steps = task.steps || [];
534
551
  if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
535
552
 
536
- const model = task.model || 'sonnet';
553
+ const model = normalizeModel(task.model || 'sonnet');
537
554
  const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
538
555
  const sessionId = crypto.randomUUID();
539
556
  const outputs = [];
@@ -686,12 +703,24 @@ function createTaskScheduler(deps) {
686
703
 
687
704
  if (runningTasks.has(task.name)) {
688
705
  // Task is still running; skip this cycle and keep full interval cadence.
689
- nextRun[task.name] = nextRunAfter(schedule, currentTime);
706
+ try {
707
+ nextRun[task.name] = nextRunAfter(schedule, currentTime);
708
+ } catch (schedErr) {
709
+ nextRun[task.name] = currentTime + checkIntervalSec * 2 * 1000;
710
+ log('ERROR', `nextRunAfter (running guard) failed for "${task.name}": ${schedErr.message}`);
711
+ }
690
712
  log('WARN', `Task ${task.name} still running — skipping this interval`);
691
713
  continue;
692
714
  }
693
715
 
694
- nextRun[task.name] = nextRunAfter(schedule, currentTime);
716
+ try {
717
+ nextRun[task.name] = nextRunAfter(schedule, currentTime);
718
+ } catch (schedErr) {
719
+ // If next-run calculation fails, back off by at least 2 ticks to prevent infinite loop
720
+ nextRun[task.name] = currentTime + checkIntervalSec * 2 * 1000;
721
+ log('ERROR', `nextRunAfter failed for "${task.name}": ${schedErr.message} — backing off`);
722
+ continue;
723
+ }
695
724
  runningTasks.add(task.name);
696
725
  // executeTask now returns a Promise (async, non-blocking, process-group kill)
697
726
  Promise.resolve(executeTask(task, config))
@@ -0,0 +1,50 @@
1
+ # Agent 创建与管理指南
2
+
3
+ ## 创建 Agent(完整流程)
4
+
5
+ 用户说"创建agent"、"新建agent"、"帮我建个agent"时,按此流程引导:
6
+
7
+ ### Step 1: 收集信息
8
+ 需要两个必要信息:
9
+ - **工作目录**:Agent 的代码/项目目录(如 `~/projects/my-bot`)
10
+ - **角色描述**(可选):Agent 的职责定义
11
+
12
+ 如果用户没给目录,提示:
13
+ > 请告诉我 Agent 的工作目录,例如 `~/projects/my-bot`
14
+
15
+ ### Step 2: 执行创建
16
+ 在手机端(飞书/Telegram),直接说即可,daemon 会自动处理:
17
+ > 创建一个 Agent,目录是 ~/projects/my-bot
18
+
19
+ 在桌面 Claude Code 终端,需要手动操作:
20
+ 1. 创建项目目录和 CLAUDE.md(角色定义)
21
+ 2. 编辑 `~/.metame/daemon.yaml`,在 `projects` 下新增:
22
+ ```yaml
23
+ projects:
24
+ my_bot:
25
+ name: "我的机器人"
26
+ cwd: "~/projects/my-bot"
27
+ icon: "🤖"
28
+ ```
29
+ 3. 运行 `touch ~/.metame/daemon.js` 触发热重载
30
+
31
+ ### Step 3: 绑定群聊
32
+ 告知用户:
33
+ > 请在飞书/Telegram 新建群组,把 bot 加进去,发送 `/activate` 完成绑定。
34
+
35
+ `/activate` 会自动将群与最近创建的 Agent 绑定(30分钟内有效)。
36
+
37
+ ## 常用命令速查
38
+
39
+ | 操作 | 手机端命令 |
40
+ |------|-----------|
41
+ | 新建 Agent | `/agent new` 或自然语言"创建agent" |
42
+ | 绑定群 | `/activate` 或 `/agent bind <名称> [目录]` |
43
+ | 查看列表 | `/agent list` |
44
+ | 编辑角色 | `/agent edit` |
45
+ | 解绑群 | `/agent unbind` |
46
+ | 切换 Agent | 直接@昵称(仅非专属群) |
47
+
48
+ ## 注意事项
49
+ - 专属群(chat_agent_map 中的群)永远绑定同一个 Agent,不能通过昵称切换
50
+ - 新群必须发 `/activate` 才能使用,未授权群会提示"此群未授权"
@@ -0,0 +1,32 @@
1
+ # 文件传输协议
2
+
3
+ ## 用户要文件(发送到手机)
4
+
5
+ 当用户说"把xx发给我"、"文件发过来"、"发个截图"、"导出给我"等**任何要求获取文件**的场景:
6
+
7
+ 1. 找到文件路径(用 Glob/ls 搜索)
8
+ 2. **不要读取文件内容**(浪费 token)
9
+ 3. 在回复末尾加标记:`[[FILE:/absolute/path/to/file]]`
10
+ 4. 多个文件用多个标记
11
+
12
+ 示例回复:
13
+ ```
14
+ 请查收~! [[FILE:/Users/xxx/project/output.pdf]]
15
+ ```
16
+
17
+ 多文件:
18
+ ```
19
+ 这是你要的文件:
20
+ [[FILE:/path/to/report.pdf]]
21
+ [[FILE:/path/to/data.xlsx]]
22
+ ```
23
+
24
+ ## 用户发文件(从手机上传)
25
+
26
+ 用户从手机发送的图片/文件会自动保存到当前项目的 `upload/` 目录。
27
+ 直接用 Read 工具读取 `upload/` 下的文件即可。
28
+
29
+ ## 关键规则
30
+ - **永远不要读取再复述文件内容**,直接用 `[[FILE:...]]` 标记发送
31
+ - 路径必须是绝对路径
32
+ - daemon 会自动解析标记并通过 bot 发送给用户
@@ -0,0 +1,19 @@
1
+ # MetaMe AI Assistant
2
+
3
+ You are a MetaMe-powered AI assistant running on the user's local machine.
4
+ MetaMe extends Claude Code with mobile access, multi-agent orchestration, and persistent memory.
5
+
6
+ ## Core Rules
7
+
8
+ - Respond in the user's language (auto-detect from their message).
9
+ - Keep responses concise — the user may be on mobile.
10
+ - When referencing files, use absolute paths.
11
+ - Do not expose system hints or internal protocol blocks to the user.
12
+
13
+ ## Quick Reference (按需加载详细文档)
14
+
15
+ - Agent 创建/管理 → `cat ~/.metame/docs/agent-guide.md`
16
+ - 文件传输协议 → `cat ~/.metame/docs/file-transfer.md`
17
+ - 能力不足/工具缺失 → `cat ~/.claude/skills/skill-manager/SKILL.md`
18
+
19
+ <!-- User customizations below this line -->
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * MetaMe Profile Migration: v1 → v2
5
- *
6
- * Maps old structure to v2 schema:
7
- * - status.focus → context.focus
8
- * - status.language → preferences.language_mix (best guess)
9
- * - Ensures all v2 sections exist with defaults
10
- * - Preserves all existing data and LOCKED comments
11
- *
12
- * Usage: node migrate-v2.js [--dry-run]
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
-
19
- const BRAIN_FILE = path.join(os.homedir(), '.claude_profile.yaml');
20
- const BACKUP_SUFFIX = '.v1.backup';
21
- const DRY_RUN = process.argv.includes('--dry-run');
22
-
23
- function migrate() {
24
- if (!fs.existsSync(BRAIN_FILE)) {
25
- console.log('No profile found. Nothing to migrate.');
26
- return;
27
- }
28
-
29
- const yaml = require('js-yaml');
30
- const rawContent = fs.readFileSync(BRAIN_FILE, 'utf8');
31
- const profile = yaml.load(rawContent) || {};
32
-
33
- // Check if already v2 (has context section)
34
- if (profile.context && profile.context.focus !== undefined) {
35
- console.log('Profile already appears to be v2. Skipping migration.');
36
- return;
37
- }
38
-
39
- console.log('Migrating profile from v1 to v2...');
40
-
41
- // --- Backup ---
42
- if (!DRY_RUN) {
43
- const backupPath = BRAIN_FILE + BACKUP_SUFFIX;
44
- fs.writeFileSync(backupPath, rawContent, 'utf8');
45
- console.log(` Backup saved to: ${backupPath}`);
46
- }
47
-
48
- // --- Migration rules ---
49
-
50
- // 1. status.focus → context.focus
51
- if (profile.status && profile.status.focus) {
52
- if (!profile.context) profile.context = {};
53
- profile.context.focus = profile.status.focus;
54
- profile.context.focus_since = new Date().toISOString().slice(0, 10);
55
- delete profile.status.focus;
56
- }
57
-
58
- // 2. status.language → status.language (keep, it's in schema)
59
- // No change needed, status.language is valid in v2
60
-
61
- // 3. Clean up empty status object
62
- if (profile.status && Object.keys(profile.status).length === 0) {
63
- delete profile.status;
64
- }
65
-
66
- // 4. Ensure context section exists with defaults
67
- if (!profile.context) profile.context = {};
68
- if (profile.context.focus === undefined) profile.context.focus = null;
69
- if (profile.context.focus_since === undefined) profile.context.focus_since = null;
70
- if (profile.context.active_projects === undefined) profile.context.active_projects = [];
71
- if (profile.context.blockers === undefined) profile.context.blockers = [];
72
- if (profile.context.energy === undefined) profile.context.energy = null;
73
-
74
- // 5. Ensure evolution section exists
75
- if (!profile.evolution) profile.evolution = {};
76
- if (profile.evolution.last_distill === undefined) profile.evolution.last_distill = null;
77
- if (profile.evolution.distill_count === undefined) profile.evolution.distill_count = 0;
78
- if (profile.evolution.recent_changes === undefined) profile.evolution.recent_changes = [];
79
-
80
- // 6. Ensure preferences section exists (don't overwrite existing values)
81
- if (!profile.preferences) profile.preferences = {};
82
-
83
- // --- Output ---
84
- const dumped = yaml.dump(profile, { lineWidth: -1 });
85
-
86
- // Restore LOCKED comments from original
87
- const lockedLines = rawContent.split('\n').filter(l => l.includes('# [LOCKED]'));
88
- let restored = dumped;
89
- for (const lockedLine of lockedLines) {
90
- const match = lockedLine.match(/^\s*([\w_]+)\s*:\s*(.+?)\s+(#.+)$/);
91
- if (match) {
92
- const key = match[1];
93
- const comment = match[3];
94
- // Find the corresponding line in dumped output and append comment
95
- restored = restored.replace(
96
- new RegExp(`^(\\s*${key}\\s*:.+)$`, 'm'),
97
- (line) => line.includes('#') ? line : `${line} ${comment}`
98
- );
99
- }
100
- }
101
-
102
- if (DRY_RUN) {
103
- console.log('\n--- DRY RUN (would write): ---');
104
- console.log(restored);
105
- console.log('--- END DRY RUN ---');
106
- } else {
107
- fs.writeFileSync(BRAIN_FILE, restored, 'utf8');
108
- console.log(' Migration complete. Profile is now v2.');
109
- }
110
- }
111
-
112
- migrate();
@@ -1,285 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * self-reflect.js — Daily Self-Reflection Task
5
- *
6
- * Scans correction/metacognitive signals from the past 7 days,
7
- * aggregates "where did the AI get it wrong", and writes a brief
8
- * self-critique pattern into growth.patterns in ~/.claude_profile.yaml.
9
- *
10
- * Also distills correction signals into lessons/ SOP markdown files.
11
- *
12
- * Heartbeat: nightly at 23:00, require_idle, non-blocking.
13
- */
14
-
15
- 'use strict';
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
- const os = require('os');
20
- const { callHaiku, buildDistillEnv } = require('./providers');
21
- const { writeBrainFileSafe } = require('./utils');
22
-
23
- const HOME = os.homedir();
24
- const SIGNAL_FILE = path.join(HOME, '.metame', 'raw_signals.jsonl');
25
- const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
26
- const LOCK_FILE = path.join(HOME, '.metame', 'self-reflect.lock');
27
- const LESSONS_DIR = path.join(HOME, '.metame', 'memory', 'lessons');
28
- const WINDOW_DAYS = 7;
29
-
30
- /**
31
- * Distill correction signals into reusable SOP markdown files.
32
- * Each run produces at most one lesson file per unique slug.
33
- * Returns the number of lesson files actually written.
34
- *
35
- * @param {Array} signals - all recent signals (will filter to 'correction' type internally)
36
- * @param {string} lessonsDir - absolute path where lesson .md files are written
37
- */
38
- async function generateLessons(signals, lessonsDir) {
39
- // Only process correction signals that carry explicit feedback
40
- const corrections = signals.filter(s => s.type === 'correction' && s.feedback);
41
- if (corrections.length < 2) {
42
- console.log(`[self-reflect] Only ${corrections.length} correction signal(s) with feedback, skipping lessons.`);
43
- return 0;
44
- }
45
-
46
- fs.mkdirSync(lessonsDir, { recursive: true });
47
-
48
- const correctionText = corrections
49
- .slice(-15) // cap to avoid prompt bloat
50
- .map(c => `- Prompt: ${(c.prompt || '').slice(0, 100)}\n Feedback: ${(c.feedback || '').slice(0, 150)}`)
51
- .join('\n');
52
-
53
- const prompt = `You are distilling correction signals into a reusable SOP for an AI assistant.
54
-
55
- Corrections (JSON):
56
- ${correctionText}
57
-
58
- Generate ONE actionable lesson in this JSON format:
59
- {
60
- "title": "简短标题(中文,10字以内)",
61
- "slug": "kebab-case-english-slug",
62
- "content": "## 问题\\n...\\n## 根因\\n...\\n## 操作手册\\n1. ...\\n2. ...\\n3. ..."
63
- }
64
-
65
- Rules: content must be in 中文, concrete and actionable, 100-300 chars total.
66
- Only output the JSON object, no explanation.`;
67
-
68
- let distillEnv = {};
69
- try { distillEnv = buildDistillEnv(); } catch {}
70
-
71
- let result;
72
- try {
73
- result = await Promise.race([
74
- callHaiku(prompt, distillEnv, 60000),
75
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 90000)),
76
- ]);
77
- } catch (e) {
78
- console.log(`[self-reflect] generateLessons Haiku call failed: ${e.message}`);
79
- return 0;
80
- }
81
-
82
- let lesson;
83
- try {
84
- const cleaned = result.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
85
- lesson = JSON.parse(cleaned);
86
- if (!lesson.title || !lesson.slug || !lesson.content) throw new Error('missing fields');
87
- } catch (e) {
88
- console.log(`[self-reflect] Failed to parse lesson JSON: ${e.message}`);
89
- return 0;
90
- }
91
-
92
- // Sanitize slug: only lowercase alphanumeric and hyphens
93
- const slug = (lesson.slug || '').toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
94
- if (!slug) {
95
- console.log('[self-reflect] generateLessons: empty slug, skipping');
96
- return 0;
97
- }
98
-
99
- // Prevent duplicates: skip if any existing file already uses this slug
100
- const existing = fs.readdirSync(lessonsDir).filter(f => f.endsWith(`-${slug}.md`));
101
- if (existing.length > 0) {
102
- console.log(`[self-reflect] Lesson '${slug}' already exists (${existing[0]}), skipping.`);
103
- return 0;
104
- }
105
-
106
- const today = new Date().toISOString().slice(0, 10);
107
- const filename = `${today}-${slug}.md`;
108
- const filepath = path.join(lessonsDir, filename);
109
-
110
- const fileContent = `---
111
- date: ${today}
112
- source: self-reflect
113
- corrections: ${corrections.length}
114
- ---
115
-
116
- # ${lesson.title}
117
-
118
- ${lesson.content}
119
- `;
120
-
121
- fs.writeFileSync(filepath, fileContent, 'utf8');
122
- console.log(`[self-reflect] Lesson written: ${filepath}`);
123
- return 1;
124
- }
125
-
126
- async function run() {
127
- // Atomic lock
128
- let lockFd;
129
- try {
130
- lockFd = fs.openSync(LOCK_FILE, 'wx');
131
- fs.writeSync(lockFd, process.pid.toString());
132
- fs.closeSync(lockFd);
133
- } catch (e) {
134
- if (e.code === 'EEXIST') {
135
- const age = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
136
- if (age < 300000) { console.log('[self-reflect] Already running.'); return; }
137
- fs.unlinkSync(LOCK_FILE);
138
- try {
139
- lockFd = fs.openSync(LOCK_FILE, 'wx');
140
- } catch {
141
- // Another process acquired the lock
142
- return;
143
- }
144
- fs.writeSync(lockFd, process.pid.toString());
145
- fs.closeSync(lockFd);
146
- } else throw e;
147
- }
148
-
149
- try {
150
- // Read signals from last WINDOW_DAYS days
151
- if (!fs.existsSync(SIGNAL_FILE)) {
152
- console.log('[self-reflect] No signal file, skipping.');
153
- return;
154
- }
155
-
156
- const cutoff = Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000;
157
- const lines = fs.readFileSync(SIGNAL_FILE, 'utf8').trim().split('\n').filter(Boolean);
158
- const recentSignals = lines
159
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
160
- .filter(s => s && s.ts && new Date(s.ts).getTime() > cutoff);
161
-
162
- // Filter to correction + metacognitive signals only
163
- const correctionSignals = recentSignals.filter(s =>
164
- s.type === 'correction' || s.type === 'metacognitive'
165
- );
166
-
167
- if (correctionSignals.length < 2) {
168
- console.log(`[self-reflect] Only ${correctionSignals.length} correction signals this week, skipping.`);
169
- return;
170
- }
171
-
172
- // Read current profile for context
173
- let currentPatterns = '';
174
- try {
175
- const yaml = require('js-yaml');
176
- const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
177
- const existing = (profile.growth && profile.growth.patterns) || [];
178
- if (existing.length > 0) {
179
- currentPatterns = `Current growth.patterns (avoid repeating):\n${existing.map(p => `- ${p}`).join('\n')}\n\n`;
180
- }
181
- } catch { /* non-fatal */ }
182
-
183
- const signalText = correctionSignals
184
- .slice(-20) // cap at 20 signals
185
- .map((s, i) => `${i + 1}. [${s.type}] "${s.prompt}"`)
186
- .join('\n');
187
-
188
- const prompt = `你是一个AI自我审视引擎。分析以下用户纠正/元认知信号,找出AI(即你)**系统性**犯错的模式。
189
-
190
- ${currentPatterns}用户纠正信号(最近7天):
191
- ${signalText}
192
-
193
- 任务:找出1-2条AI的系统性问题(不是偶发错误),例如:
194
- - "经常过度简化用户的技术问题,忽略背景细节"
195
- - "倾向于在用户还没说完就开始行动,导致方向偏差"
196
- - "在不确定时倾向于肯定用户,而非直接说不知道"
197
-
198
- 输出格式(JSON数组,最多2条,每条≤40字中文):
199
- ["模式1描述", "模式2描述"]
200
-
201
- 注意:
202
- - 只输出有充分证据支持的系统性模式
203
- - 如果证据不足,输出 []
204
- - 只输出JSON,不要解释`;
205
-
206
- let distillEnv = {};
207
- try { distillEnv = buildDistillEnv(); } catch {}
208
-
209
- let result;
210
- try {
211
- result = await Promise.race([
212
- callHaiku(prompt, distillEnv, 60000),
213
- // outer safety net in case callHaiku's internal timeout doesn't propagate
214
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 90000)),
215
- ]);
216
- } catch (e) {
217
- console.log(`[self-reflect] Haiku call failed: ${e.message}`);
218
- return;
219
- }
220
-
221
- // Parse result
222
- let patterns = [];
223
- try {
224
- const cleaned = result.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
225
- const parsed = JSON.parse(cleaned);
226
- if (Array.isArray(parsed)) {
227
- patterns = parsed.filter(p => typeof p === 'string' && p.length > 5 && p.length <= 80);
228
- }
229
- } catch {
230
- console.log('[self-reflect] Failed to parse Haiku output.');
231
- return;
232
- }
233
-
234
- // === Generate lessons/ from correction signals (independent of patterns result) ===
235
- try {
236
- const lessonsCount = await generateLessons(recentSignals, LESSONS_DIR);
237
- if (lessonsCount > 0) {
238
- console.log(`[self-reflect] Generated ${lessonsCount} lesson(s) in ${LESSONS_DIR}`);
239
- }
240
- } catch (e) {
241
- console.log(`[self-reflect] generateLessons failed (non-fatal): ${e.message}`);
242
- }
243
-
244
- if (patterns.length === 0) {
245
- console.log('[self-reflect] No patterns found this week.');
246
- return;
247
- }
248
-
249
- // Merge into growth.patterns (cap at 3, keep newest)
250
- try {
251
- const yaml = require('js-yaml');
252
- const raw = fs.readFileSync(BRAIN_FILE, 'utf8');
253
- const profile = yaml.load(raw) || {};
254
- if (!profile.growth) profile.growth = {};
255
- const existing = Array.isArray(profile.growth.patterns) ? profile.growth.patterns : [];
256
- // Add new patterns, deduplicate, cap at 3 newest
257
- const merged = [...existing, ...patterns]
258
- .filter((p, i, arr) => arr.indexOf(p) === i)
259
- .slice(-3);
260
- profile.growth.patterns = merged;
261
- profile.growth.last_reflection = new Date().toISOString().slice(0, 10);
262
-
263
- // Preserve locked lines (simple approach: only update growth section)
264
- const dumped = yaml.dump(profile, { lineWidth: -1 });
265
- await writeBrainFileSafe(dumped);
266
- console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.patterns: ${patterns.join(' | ')}`);
267
- } catch (e) {
268
- console.log(`[self-reflect] Failed to write profile: ${e.message}`);
269
- }
270
-
271
- } finally {
272
- try { fs.unlinkSync(LOCK_FILE); } catch {}
273
- }
274
- }
275
-
276
- if (require.main === module) {
277
- run().then(() => {
278
- console.log('✅ self-reflect complete');
279
- }).catch(e => {
280
- console.error(`[self-reflect] Fatal: ${e.message}`);
281
- process.exit(1);
282
- });
283
- }
284
-
285
- module.exports = { run };