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 +51 -26
- package/index.js +57 -21
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +92 -0
- package/scripts/daemon-claude-engine.js +6 -29
- package/scripts/daemon-task-scheduler.js +33 -4
- package/scripts/docs/agent-guide.md +50 -0
- package/scripts/docs/file-transfer.md +32 -0
- package/scripts/templates/default-global-claude.md +19 -0
- package/scripts/migrate-v2.js +0 -112
- package/scripts/self-reflect.js +0 -285
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# MetaMe
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="./
|
|
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 ≥
|
|
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.
|
|
41
|
+
> ### 🚀 v1.4.33 — 6-Dimension Soul Schema + Nightly Reflection + Auto-Provisioning
|
|
42
42
|
>
|
|
43
|
-
> - **
|
|
44
|
-
> - **
|
|
45
|
-
> - **
|
|
46
|
-
> - **
|
|
47
|
-
> - **
|
|
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
|
|
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
|
-
|
|
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 ≥
|
|
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.
|
|
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** |
|
|
285
|
-
| **Layered Memory** |
|
|
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
|
-
| **
|
|
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
|
-
│ (
|
|
450
|
+
│ (6-dim soul schema) │
|
|
433
451
|
│ │
|
|
434
452
|
│ ~/.metame/memory.db │
|
|
435
453
|
│ session_tags.json │
|
|
436
|
-
│ (memory
|
|
454
|
+
│ (5-layer memory) │
|
|
455
|
+
│ │
|
|
456
|
+
│ dispatch_to (auto-deployed)│
|
|
437
457
|
└──────────────────────────────┘
|
|
438
458
|
↑
|
|
439
|
-
idle
|
|
440
|
-
|
|
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`):
|
|
444
|
-
- **Daemon
|
|
445
|
-
- **Distillation
|
|
446
|
-
- **Memory Extract
|
|
447
|
-
- **
|
|
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 | ~
|
|
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
|
-
|
|
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
|
@@ -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
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -->
|
package/scripts/migrate-v2.js
DELETED
|
@@ -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();
|
package/scripts/self-reflect.js
DELETED
|
@@ -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 };
|