metame-cli 1.4.32 → 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 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">
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.33",
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
+ });
@@ -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 };