metame-cli 1.4.0 → 1.4.5
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 +15 -6
- package/index.js +15 -1
- package/package.json +1 -1
- package/scripts/daemon.js +317 -124
- package/scripts/distill.js +45 -9
- package/scripts/feishu-adapter.js +2 -2
- package/scripts/memory-extract.js +2 -1
- package/scripts/memory-search.js +95 -66
- package/scripts/schema.js +2 -5
- package/scripts/signal-capture.js +19 -9
package/README.md
CHANGED
|
@@ -205,6 +205,8 @@ metame daemon install-launchd # Auto-start on boot + crash recovery
|
|
|
205
205
|
|
|
206
206
|
Done. Open Telegram, message your bot.
|
|
207
207
|
|
|
208
|
+
> **First message?** New chats aren't whitelisted yet. The bot will reply with a one-step setup command — just send `/bind personal ~/` and you're in.
|
|
209
|
+
|
|
208
210
|
---
|
|
209
211
|
|
|
210
212
|
## Core Capabilities
|
|
@@ -216,7 +218,7 @@ Done. Open Telegram, message your bot.
|
|
|
216
218
|
| **Mobile Bridge** | Full Claude Code via Telegram/Feishu. Stateful sessions, file transfer both ways, real-time streaming status. |
|
|
217
219
|
| **Skill Evolution** | Self-healing skill system. Auto-discovers missing skills, learns from browser recordings, evolves after every task. Skills get smarter over time. |
|
|
218
220
|
| **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. |
|
|
219
|
-
| **Multi-Agent** | Multiple projects with dedicated chat groups. `/bind` for one-tap setup. True parallel execution. |
|
|
221
|
+
| **Multi-Agent** | Multiple projects with dedicated chat groups. `/agent bind` for one-tap setup. True parallel execution. |
|
|
220
222
|
| **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
|
|
221
223
|
| **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini — zero config file mutation. |
|
|
222
224
|
| **Metacognition** | Detects behavioral patterns (decision style, comfort zones, goal drift) and injects mirror observations. Zero extra API cost. |
|
|
@@ -298,10 +300,11 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
298
300
|
| `/new` | Start new session (project picker) |
|
|
299
301
|
| `/resume` | Pick from session list |
|
|
300
302
|
| `/stop` | Interrupt current task (ESC) |
|
|
301
|
-
| `/undo` |
|
|
303
|
+
| `/undo` | Show recent messages as buttons — tap to roll back context + code to before that message |
|
|
304
|
+
| `/undo <hash>` | Roll back to a specific git checkpoint |
|
|
302
305
|
| `/list` | Browse & download project files |
|
|
303
306
|
| `/model` | Switch model (sonnet/opus/haiku) |
|
|
304
|
-
| `/bind <name
|
|
307
|
+
| `/agent bind <name> [dir]` | Register group as dedicated agent |
|
|
305
308
|
| `/sh <cmd>` | Raw shell — bypasses Claude |
|
|
306
309
|
| `/memory` | Memory stats: fact count, session tags, DB size |
|
|
307
310
|
| `/memory <keyword>` | Search long-term facts by keyword |
|
|
@@ -340,7 +343,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
340
343
|
## Security
|
|
341
344
|
|
|
342
345
|
- All data stays on your machine. No cloud, no telemetry.
|
|
343
|
-
- `allowed_chat_ids` whitelist — unauthorized users
|
|
346
|
+
- `allowed_chat_ids` whitelist — unauthorized users get a one-step `/bind` guide instead of silent rejection.
|
|
344
347
|
- `operator_ids` for shared groups — non-operators get read-only mode.
|
|
345
348
|
- `~/.metame/` directory is mode 700.
|
|
346
349
|
- Bot tokens stored locally, never transmitted.
|
|
@@ -359,14 +362,20 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
359
362
|
|
|
360
363
|
> Both memory consolidation and session summarization run in the background via Haiku (`--model haiku`). Input is capped by code: skeleton text ≤ 3,000 chars, summary output ≤ 500 chars. Neither runs per-message — memory consolidation triggers on sleep mode (30-min idle), summaries trigger once per idle session.
|
|
361
364
|
|
|
362
|
-
## Plugin
|
|
365
|
+
## Plugin
|
|
363
366
|
|
|
364
|
-
|
|
367
|
+
Install directly into Claude Code without npm:
|
|
365
368
|
|
|
366
369
|
```bash
|
|
367
370
|
claude plugin install github:Yaron9/MetaMe/plugin
|
|
368
371
|
```
|
|
369
372
|
|
|
373
|
+
Includes: cognitive profile injection, daemon (Telegram/Feishu), heartbeat tasks, layered memory, all mobile commands, slash commands (`/metame:evolve`, `/metame:daemon`, `/metame:refresh`, etc.).
|
|
374
|
+
|
|
375
|
+
**One key difference from the npm CLI:** the plugin daemon starts when you open Claude Code and stops when you close it. It does not run 24/7 in the background. For always-on mobile access (receiving messages while Claude Code is closed), use the npm CLI with `metame daemon install-launchd`.
|
|
376
|
+
|
|
377
|
+
Use the plugin if you prefer not to install a global npm package and only need mobile access while Claude Code is open. Use the npm CLI (`metame-cli`) for 24/7 daemon, the `metame` command, and first-run interview.
|
|
378
|
+
|
|
370
379
|
## License
|
|
371
380
|
|
|
372
381
|
MIT
|
package/index.js
CHANGED
|
@@ -140,7 +140,21 @@ function ensureHookInstalled() {
|
|
|
140
140
|
ensureHookInstalled();
|
|
141
141
|
|
|
142
142
|
// ---------------------------------------------------------
|
|
143
|
-
// 1.6b
|
|
143
|
+
// 1.6b LOCAL ACTIVITY HEARTBEAT
|
|
144
|
+
// ---------------------------------------------------------
|
|
145
|
+
// Touch ~/.metame/local_active so the daemon knows the user is active on desktop.
|
|
146
|
+
// This prevents dream tasks (require_idle: true) from firing during live Claude sessions.
|
|
147
|
+
try {
|
|
148
|
+
const localActiveFile = path.join(METAME_DIR, 'local_active');
|
|
149
|
+
// Ensure file exists (open with 'a' is a no-op if it already exists)
|
|
150
|
+
fs.closeSync(fs.openSync(localActiveFile, 'a'));
|
|
151
|
+
// Update mtime so daemon idle detection sees fresh activity
|
|
152
|
+
const now = new Date();
|
|
153
|
+
fs.utimesSync(localActiveFile, now, now);
|
|
154
|
+
} catch { /* non-fatal */ }
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------
|
|
157
|
+
// 1.6c ENSURE PROJECT-LEVEL MCP CONFIG
|
|
144
158
|
// ---------------------------------------------------------
|
|
145
159
|
// MCP servers are registered per-project via .mcp.json (not user-scope ~/.claude.json)
|
|
146
160
|
// so they only load when working in projects that need them.
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -924,19 +924,41 @@ function physiologicalHeartbeat(config) {
|
|
|
924
924
|
}
|
|
925
925
|
|
|
926
926
|
// ---------------------------------------------------------
|
|
927
|
-
// HEARTBEAT
|
|
927
|
+
// HEARTBEAT TASK HELPERS (single source of truth)
|
|
928
928
|
// ---------------------------------------------------------
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Collect all heartbeat tasks from config (general + per-project).
|
|
932
|
+
* Each project task gets _project metadata attached.
|
|
933
|
+
* Returns { general: [...], project: [...], all: [...] }
|
|
934
|
+
*/
|
|
935
|
+
function getAllTasks(cfg) {
|
|
936
|
+
const general = (cfg.heartbeat && cfg.heartbeat.tasks) || [];
|
|
937
|
+
const project = [];
|
|
938
|
+
const generalNames = new Set(general.map(t => t.name));
|
|
939
|
+
for (const [key, proj] of Object.entries(cfg.projects || {})) {
|
|
934
940
|
for (const t of (proj.heartbeat_tasks || [])) {
|
|
935
|
-
if (
|
|
936
|
-
|
|
941
|
+
if (generalNames.has(t.name)) log('WARN', `Duplicate task name "${t.name}" in project "${key}" and general heartbeat`);
|
|
942
|
+
project.push({ ...t, _project: { key, name: proj.name || key, color: proj.color || 'blue', icon: proj.icon || '🤖' } });
|
|
937
943
|
}
|
|
938
944
|
}
|
|
939
|
-
|
|
945
|
+
return { general, project, all: [...general, ...project] };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Find a task by name across all groups.
|
|
950
|
+
*/
|
|
951
|
+
function findTask(cfg, name) {
|
|
952
|
+
const { general, project } = getAllTasks(cfg);
|
|
953
|
+
const found = general.find(t => t.name === name) || project.find(t => t.name === name);
|
|
954
|
+
return found || null;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ---------------------------------------------------------
|
|
958
|
+
// HEARTBEAT SCHEDULER
|
|
959
|
+
// ---------------------------------------------------------
|
|
960
|
+
function startHeartbeat(config, notifyFn) {
|
|
961
|
+
const { all: tasks } = getAllTasks(config);
|
|
940
962
|
|
|
941
963
|
const enabledTasks = tasks.filter(t => t.enabled !== false);
|
|
942
964
|
const checkIntervalSec = (config.daemon && config.daemon.heartbeat_check_interval) || 60;
|
|
@@ -978,6 +1000,9 @@ function startHeartbeat(config, notifyFn) {
|
|
|
978
1000
|
log('INFO', '[DAEMON] Entering Sleep Mode');
|
|
979
1001
|
// Generate summaries for sessions idle 2-24h
|
|
980
1002
|
spawnSessionSummaries();
|
|
1003
|
+
} else if (!idle && _inSleepMode) {
|
|
1004
|
+
_inSleepMode = false;
|
|
1005
|
+
log('INFO', '[DAEMON] Exiting Sleep Mode — local activity detected');
|
|
981
1006
|
}
|
|
982
1007
|
|
|
983
1008
|
// ② Task heartbeat (burns tokens on schedule)
|
|
@@ -1101,9 +1126,10 @@ async function startTelegramBridge(config, executeTaskByName) {
|
|
|
1101
1126
|
// Exception: /bind and /agent bind/new are allowed from any chat so users can self-register new groups
|
|
1102
1127
|
const allowedIds = (loadConfig().telegram && loadConfig().telegram.allowed_chat_ids) || [];
|
|
1103
1128
|
const trimmedText = msg.text && msg.text.trim();
|
|
1104
|
-
const isBindCmd = trimmedText && (trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
|
|
1129
|
+
const isBindCmd = trimmedText && (trimmedText.startsWith('/bind') || trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
|
|
1105
1130
|
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
1106
1131
|
log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
|
|
1132
|
+
bot.sendMessage(chatId, `⚠️ This chat (ID: ${chatId}) is not authorized.\n\nTo get started, send:\n/bind personal ~/\n\nThis will register this chat and bind it to your home directory.`).catch(() => {});
|
|
1107
1133
|
continue;
|
|
1108
1134
|
}
|
|
1109
1135
|
|
|
@@ -1527,6 +1553,7 @@ async function doBindAgent(bot, chatId, agentName, agentCwd) {
|
|
|
1527
1553
|
}
|
|
1528
1554
|
|
|
1529
1555
|
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false) {
|
|
1556
|
+
if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
|
|
1530
1557
|
const state = loadState();
|
|
1531
1558
|
|
|
1532
1559
|
// --- /chatid: reply with current chatId ---
|
|
@@ -1714,7 +1741,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
1714
1741
|
const factCount = s.facts ?? '?';
|
|
1715
1742
|
const tagFile = path.join(HOME, '.metame', 'session_tags.json');
|
|
1716
1743
|
let tagCount = 0;
|
|
1717
|
-
try { tagCount = Object.keys(JSON.parse(fs.readFileSync(tagFile, 'utf8'))).length; } catch {}
|
|
1744
|
+
try { tagCount = Object.keys(JSON.parse(fs.readFileSync(tagFile, 'utf8'))).length; } catch { }
|
|
1718
1745
|
const lines = [
|
|
1719
1746
|
`🧠 *Memory Stats*`,
|
|
1720
1747
|
`━━━━━━━━━━━━━━━━`,
|
|
@@ -1971,6 +1998,11 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
1971
1998
|
}
|
|
1972
1999
|
}
|
|
1973
2000
|
|
|
2001
|
+
// /bind <name> [cwd] → alias for /agent bind <name> [cwd]
|
|
2002
|
+
if (text === '/bind' || text.startsWith('/bind ')) {
|
|
2003
|
+
text = '/agent bind' + text.slice(5);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1974
2006
|
if (text === '/agent' || text.startsWith('/agent ')) {
|
|
1975
2007
|
const agentArg = text === '/agent' ? '' : text.slice(7).trim();
|
|
1976
2008
|
const agentParts = agentArg.split(/\s+/);
|
|
@@ -2269,22 +2301,25 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
2269
2301
|
}
|
|
2270
2302
|
|
|
2271
2303
|
if (text === '/tasks') {
|
|
2304
|
+
const { general, project } = getAllTasks(config);
|
|
2272
2305
|
let msg = '';
|
|
2273
|
-
|
|
2274
|
-
const legacyTasks = (config.heartbeat && config.heartbeat.tasks) || [];
|
|
2275
|
-
if (legacyTasks.length > 0) {
|
|
2306
|
+
if (general.length > 0) {
|
|
2276
2307
|
msg += '📋 General:\n';
|
|
2277
|
-
for (const t of
|
|
2308
|
+
for (const t of general) {
|
|
2278
2309
|
const ts = state.tasks[t.name] || {};
|
|
2279
2310
|
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
|
|
2280
2311
|
}
|
|
2281
2312
|
}
|
|
2282
|
-
// Project tasks grouped
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2313
|
+
// Project tasks grouped by _project
|
|
2314
|
+
const byProject = new Map();
|
|
2315
|
+
for (const t of project) {
|
|
2316
|
+
const pk = t._project.key;
|
|
2317
|
+
if (!byProject.has(pk)) byProject.set(pk, { proj: t._project, tasks: [] });
|
|
2318
|
+
byProject.get(pk).tasks.push(t);
|
|
2319
|
+
}
|
|
2320
|
+
for (const [, { proj, tasks }] of byProject) {
|
|
2321
|
+
msg += `\n${proj.icon} ${proj.name}:\n`;
|
|
2322
|
+
for (const t of tasks) {
|
|
2288
2323
|
const ts = state.tasks[t.name] || {};
|
|
2289
2324
|
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
|
|
2290
2325
|
}
|
|
@@ -2403,13 +2438,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
2403
2438
|
return;
|
|
2404
2439
|
}
|
|
2405
2440
|
const taskName = text.slice(5).trim();
|
|
2406
|
-
const
|
|
2407
|
-
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
2408
|
-
for (const t of (proj.heartbeat_tasks || [])) {
|
|
2409
|
-
allRunTasks.push({ ...t, _project: { key, name: proj.name || key, color: proj.color || 'blue', icon: proj.icon || '🤖' } });
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
const task = allRunTasks.find(t => t.name === taskName);
|
|
2441
|
+
const task = findTask(config, taskName);
|
|
2413
2442
|
if (!task) { await bot.sendMessage(chatId, `❌ Task "${taskName}" not found`); return; }
|
|
2414
2443
|
|
|
2415
2444
|
// Script tasks: quick, run inline
|
|
@@ -2676,7 +2705,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
2676
2705
|
}
|
|
2677
2706
|
|
|
2678
2707
|
const session = getSession(chatId);
|
|
2679
|
-
if (!session || !session.id
|
|
2708
|
+
if (!session || !session.id) {
|
|
2680
2709
|
await bot.sendMessage(chatId, 'No active session to undo.');
|
|
2681
2710
|
return;
|
|
2682
2711
|
}
|
|
@@ -2684,107 +2713,190 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
2684
2713
|
const cwd = session.cwd;
|
|
2685
2714
|
const arg = text.slice(5).trim();
|
|
2686
2715
|
|
|
2687
|
-
//
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2716
|
+
// /undo <hash> — git reset to specific checkpoint (advanced usage)
|
|
2717
|
+
if (arg) {
|
|
2718
|
+
if (!cwd) {
|
|
2719
|
+
await bot.sendMessage(chatId, '❌ 当前 session 无工作目录,无法执行 git undo');
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
let isGitRepo = false;
|
|
2723
|
+
try { execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore', timeout: 3000 }); isGitRepo = true; } catch { }
|
|
2724
|
+
const checkpoints = isGitRepo ? listCheckpoints(cwd) : [];
|
|
2725
|
+
const match = checkpoints.find(cp => cp.hash.startsWith(arg));
|
|
2726
|
+
if (!match) {
|
|
2727
|
+
await bot.sendMessage(chatId, `❌ 未找到 checkpoint: ${arg}`);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
try {
|
|
2731
|
+
let diffFiles = '';
|
|
2732
|
+
try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000 }).trim(); } catch { }
|
|
2733
|
+
execSync(`git reset --hard ${match.hash}`, { cwd, stdio: 'ignore', timeout: 10000 });
|
|
2734
|
+
// Truncate context to checkpoint time (covers multi-turn rollback)
|
|
2735
|
+
truncateSessionToCheckpoint(session.id, match.message);
|
|
2736
|
+
const fileList = diffFiles ? diffFiles.split('\n').map(f => path.basename(f)).join(', ') : '';
|
|
2737
|
+
const fileCount = diffFiles ? diffFiles.split('\n').length : 0;
|
|
2738
|
+
let msg = `⏪ 已回退到 ${cpDisplayLabel(match.message)}`;
|
|
2739
|
+
if (fileCount > 0) msg += `\n📁 ${fileCount} 个文件恢复: ${fileList}`;
|
|
2740
|
+
log('INFO', `/undo <hash> executed for ${chatId}: reset to ${match.hash.slice(0, 8)}, files=${fileCount}`);
|
|
2741
|
+
await bot.sendMessage(chatId, msg);
|
|
2742
|
+
cleanupCheckpoints(cwd);
|
|
2743
|
+
} catch (e) {
|
|
2744
|
+
await bot.sendMessage(chatId, `❌ Undo failed: ${e.message}`);
|
|
2745
|
+
}
|
|
2698
2746
|
return;
|
|
2699
2747
|
}
|
|
2700
2748
|
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
const
|
|
2749
|
+
// /undo (no arg) — show recent user messages as buttons to pick rollback point
|
|
2750
|
+
try {
|
|
2751
|
+
const sessionFile = findSessionFile(session.id);
|
|
2752
|
+
if (!sessionFile) {
|
|
2753
|
+
await bot.sendMessage(chatId, '⚠️ 找不到 session 文件,无法列出历史消息');
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(l => l.trim());
|
|
2757
|
+
|
|
2758
|
+
// Helper: extract real user text (skip tool_result entries and system annotations)
|
|
2759
|
+
const extractUserText = (obj) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const content = obj.message?.content;
|
|
2762
|
+
if (typeof content === 'string') return content.trim();
|
|
2763
|
+
if (Array.isArray(content)) {
|
|
2764
|
+
// Skip entries that are purely tool results
|
|
2765
|
+
if (content.every(c => c.type === 'tool_result')) return '';
|
|
2766
|
+
// Find first text item that isn't a system annotation (exact patterns only)
|
|
2767
|
+
const SYSTEM_ANNOTATION = /^\[(Image source|Pasted|Attachment|File):/;
|
|
2768
|
+
const item = content.find(c => c.type === 'text' && c.text && !SYSTEM_ANNOTATION.test(c.text));
|
|
2769
|
+
return item?.text?.trim() || '';
|
|
2770
|
+
}
|
|
2771
|
+
} catch { }
|
|
2772
|
+
return '';
|
|
2773
|
+
};
|
|
2774
|
+
|
|
2775
|
+
// Collect only real human-written user messages (skip tool results / annotations)
|
|
2776
|
+
const userMsgs = [];
|
|
2777
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2778
|
+
try {
|
|
2779
|
+
const obj = JSON.parse(lines[i]);
|
|
2780
|
+
if (obj.type === 'user' && obj.message?.role === 'user') {
|
|
2781
|
+
const text = extractUserText(obj);
|
|
2782
|
+
if (text) userMsgs.push({ idx: i, obj, text });
|
|
2783
|
+
}
|
|
2784
|
+
} catch { }
|
|
2785
|
+
}
|
|
2786
|
+
if (userMsgs.length === 0) {
|
|
2787
|
+
await bot.sendMessage(chatId, '⚠️ 没有可回退的历史消息');
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// Show last 10 (most recent first)
|
|
2792
|
+
const recent = userMsgs.slice(-10).reverse();
|
|
2704
2793
|
if (bot.sendButtons) {
|
|
2705
|
-
const buttons = recent.map((
|
|
2706
|
-
const
|
|
2707
|
-
|
|
2794
|
+
const buttons = recent.map(({ idx, text, obj }) => {
|
|
2795
|
+
const msgText = text.replace(/\n/g, ' ').slice(0, 28);
|
|
2796
|
+
let timeLabel = '';
|
|
2797
|
+
if (obj.timestamp) {
|
|
2798
|
+
const d = new Date(obj.timestamp);
|
|
2799
|
+
if (!isNaN(d)) timeLabel = ` (${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')})`;
|
|
2800
|
+
}
|
|
2801
|
+
return [{ text: `⏪ ${msgText}${timeLabel}`, callback_data: `/undo_to ${idx}` }];
|
|
2708
2802
|
});
|
|
2709
|
-
await bot.sendButtons(chatId,
|
|
2803
|
+
await bot.sendButtons(chatId, `↩️ 回退到哪条消息之前?(共 ${userMsgs.length} 轮)`, buttons);
|
|
2710
2804
|
} else {
|
|
2711
|
-
let msg = '
|
|
2712
|
-
recent.forEach(
|
|
2713
|
-
msg +=
|
|
2805
|
+
let msg = '回退到哪条消息之前?回复 /undo_to <序号>\n\n';
|
|
2806
|
+
recent.forEach(({ idx, text }) => {
|
|
2807
|
+
msg += `[${idx}] ${text.slice(0, 40)}\n`;
|
|
2714
2808
|
});
|
|
2715
2809
|
await bot.sendMessage(chatId, msg);
|
|
2716
2810
|
}
|
|
2811
|
+
} catch (e) {
|
|
2812
|
+
await bot.sendMessage(chatId, `❌ Undo failed: ${e.message}`);
|
|
2813
|
+
}
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// /undo_to <lineIdx> — restore session to before the message at given JSONL line index
|
|
2818
|
+
if (text.startsWith('/undo_to ')) {
|
|
2819
|
+
const idx = parseInt(text.slice(9).trim(), 10);
|
|
2820
|
+
if (isNaN(idx) || idx < 0) {
|
|
2821
|
+
await bot.sendMessage(chatId, '❌ 无效的回退序号');
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// Kill any running task
|
|
2826
|
+
if (messageQueue.has(chatId)) {
|
|
2827
|
+
const q = messageQueue.get(chatId);
|
|
2828
|
+
if (q.timer) clearTimeout(q.timer);
|
|
2829
|
+
messageQueue.delete(chatId);
|
|
2830
|
+
}
|
|
2831
|
+
const proc2 = activeProcesses.get(chatId);
|
|
2832
|
+
if (proc2 && proc2.child) {
|
|
2833
|
+
proc2.aborted = true;
|
|
2834
|
+
try { process.kill(-proc2.child.pid, 'SIGINT'); } catch { proc2.child.kill('SIGINT'); }
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
const session2 = getSession(chatId);
|
|
2838
|
+
if (!session2 || !session2.id) {
|
|
2839
|
+
await bot.sendMessage(chatId, 'No active session.');
|
|
2717
2840
|
return;
|
|
2718
2841
|
}
|
|
2719
2842
|
|
|
2720
|
-
// /undo <hash> — execute git reset
|
|
2721
2843
|
try {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2844
|
+
const sessionFile2 = findSessionFile(session2.id);
|
|
2845
|
+
if (!sessionFile2) { await bot.sendMessage(chatId, '❌ 找不到 session 文件'); return; }
|
|
2846
|
+
|
|
2847
|
+
const lines2 = fs.readFileSync(sessionFile2, 'utf8').split('\n').filter(l => l.trim());
|
|
2848
|
+
if (idx >= lines2.length) {
|
|
2849
|
+
await bot.sendMessage(chatId, '❌ 序号超出范围,session 已变化,请重新 /undo');
|
|
2726
2850
|
return;
|
|
2727
2851
|
}
|
|
2728
2852
|
|
|
2729
|
-
// Get
|
|
2730
|
-
let
|
|
2853
|
+
// Get target message text + timestamp for display and git matching
|
|
2854
|
+
let targetMsg = '', targetTs = 0;
|
|
2731
2855
|
try {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2856
|
+
const obj = JSON.parse(lines2[idx]);
|
|
2857
|
+
const content = obj.message?.content;
|
|
2858
|
+
if (typeof content === 'string') targetMsg = content;
|
|
2859
|
+
else if (Array.isArray(content)) targetMsg = content.find(c => c.type === 'text')?.text || '';
|
|
2860
|
+
if (obj.timestamp) targetTs = new Date(obj.timestamp).getTime() || 0;
|
|
2861
|
+
} catch { }
|
|
2737
2862
|
|
|
2738
|
-
//
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
//
|
|
2746
|
-
const
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
break; // Found a message before checkpoint, stop
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
} catch { }
|
|
2763
|
-
}
|
|
2764
|
-
if (cutIdx > 0) {
|
|
2765
|
-
const kept = lines.slice(0, cutIdx);
|
|
2766
|
-
fs.writeFileSync(sessionFile, kept.join('\n') + '\n', 'utf8');
|
|
2767
|
-
log('INFO', `Truncated session at line ${cutIdx} (${lines.length - cutIdx} lines removed)`);
|
|
2863
|
+
// Git reset first (before JSONL truncation) so failure leaves state consistent
|
|
2864
|
+
let gitMsg2 = '';
|
|
2865
|
+
const cwd2 = session2.cwd;
|
|
2866
|
+
if (cwd2) {
|
|
2867
|
+
let isGitRepo2 = false;
|
|
2868
|
+
try { execSync('git rev-parse --is-inside-work-tree', { cwd: cwd2, stdio: 'ignore', timeout: 3000 }); isGitRepo2 = true; } catch { }
|
|
2869
|
+
if (isGitRepo2) {
|
|
2870
|
+
// Exclude safety checkpoints from matching to avoid confusion
|
|
2871
|
+
const checkpoints2 = listCheckpoints(cwd2).filter(cp => !cp.message.includes('[metame-safety]'));
|
|
2872
|
+
const cpMatch = targetTs
|
|
2873
|
+
? checkpoints2.find(cp => { const t = new Date(cpExtractTimestamp(cp.message) || 0).getTime(); return t > 0 && t <= targetTs; })
|
|
2874
|
+
: checkpoints2[0];
|
|
2875
|
+
if (cpMatch) {
|
|
2876
|
+
let diffFiles2 = '';
|
|
2877
|
+
try { diffFiles2 = execSync(`git diff --name-only HEAD ${cpMatch.hash}`, { cwd: cwd2, encoding: 'utf8', timeout: 5000 }).trim(); } catch { }
|
|
2878
|
+
if (diffFiles2) {
|
|
2879
|
+
// Save current state with distinct prefix (excluded from normal /undo list)
|
|
2880
|
+
gitCheckpoint(cwd2, `[metame-safety] before rollback to: ${targetMsg.slice(0, 40)}`);
|
|
2881
|
+
execSync(`git reset --hard ${cpMatch.hash}`, { cwd: cwd2, stdio: 'ignore', timeout: 10000 });
|
|
2882
|
+
gitMsg2 = `\n📁 ${diffFiles2.split('\n').length} 个文件已恢复`;
|
|
2883
|
+
cleanupCheckpoints(cwd2);
|
|
2768
2884
|
}
|
|
2769
2885
|
}
|
|
2770
2886
|
}
|
|
2771
|
-
} catch (truncErr) {
|
|
2772
|
-
log('WARN', `Session truncation failed (non-fatal): ${truncErr.message}`);
|
|
2773
2887
|
}
|
|
2774
2888
|
|
|
2775
|
-
|
|
2776
|
-
const
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
msg += `\n📁 ${fileCount} 个文件恢复: ${fileList}`;
|
|
2781
|
-
}
|
|
2782
|
-
await bot.sendMessage(chatId, msg);
|
|
2889
|
+
// Truncate JSONL after git reset succeeds
|
|
2890
|
+
const kept2 = lines2.slice(0, idx);
|
|
2891
|
+
fs.writeFileSync(sessionFile2, kept2.length ? kept2.join('\n') + '\n' : '', 'utf8');
|
|
2892
|
+
_sessionFileCache.delete(session2.id);
|
|
2893
|
+
const removed2 = lines2.length - kept2.length;
|
|
2783
2894
|
|
|
2784
|
-
|
|
2785
|
-
|
|
2895
|
+
const preview = targetMsg.replace(/\n/g, ' ').slice(0, 30) || `行 ${idx}`;
|
|
2896
|
+
log('INFO', `/undo_to ${idx} for ${chatId}: removed=${removed2} lines${gitMsg2 ? ', ' + gitMsg2.trim() : ''}`);
|
|
2897
|
+
await bot.sendMessage(chatId, `⏪ 已回退到「${preview}」之前\n🧠 上下文回滚 ${removed2} 行${gitMsg2}`);
|
|
2786
2898
|
} catch (e) {
|
|
2787
|
-
await bot.sendMessage(chatId, `❌
|
|
2899
|
+
await bot.sendMessage(chatId, `❌ 回退失败: ${e.message}`);
|
|
2788
2900
|
}
|
|
2789
2901
|
return;
|
|
2790
2902
|
}
|
|
@@ -3016,7 +3128,8 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
|
|
|
3016
3128
|
'/cd <path> — 切换工作目录',
|
|
3017
3129
|
'/session — 查看当前会话',
|
|
3018
3130
|
'/stop — 中断当前任务 (ESC)',
|
|
3019
|
-
'/undo —
|
|
3131
|
+
'/undo — 选择历史消息,点击回退到该条之前',
|
|
3132
|
+
'/undo <hash> — 回退到指定 git checkpoint',
|
|
3020
3133
|
'/quit — 结束会话,重新加载 MCP/配置',
|
|
3021
3134
|
'',
|
|
3022
3135
|
`⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
|
|
@@ -3117,6 +3230,80 @@ function findSessionFile(sessionId) {
|
|
|
3117
3230
|
return null;
|
|
3118
3231
|
}
|
|
3119
3232
|
|
|
3233
|
+
/**
|
|
3234
|
+
* Truncate the last conversation turn (user message + assistant response) from a session JSONL.
|
|
3235
|
+
* Finds the last {type:"user"} entry and removes it plus everything after.
|
|
3236
|
+
* Returns the number of lines removed, or 0 if nothing was truncated.
|
|
3237
|
+
*/
|
|
3238
|
+
function truncateSessionLastTurn(sessionId) {
|
|
3239
|
+
try {
|
|
3240
|
+
const sessionFile = findSessionFile(sessionId);
|
|
3241
|
+
if (!sessionFile) return 0;
|
|
3242
|
+
const fileContent = fs.readFileSync(sessionFile, 'utf8');
|
|
3243
|
+
const lines = fileContent.split('\n').filter(l => l.trim());
|
|
3244
|
+
// Find the last user-type entry (walk backwards)
|
|
3245
|
+
let cutIdx = -1;
|
|
3246
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3247
|
+
try {
|
|
3248
|
+
const obj = JSON.parse(lines[i]);
|
|
3249
|
+
if (obj.type === 'user') { cutIdx = i; break; }
|
|
3250
|
+
} catch { /* skip malformed lines */ }
|
|
3251
|
+
}
|
|
3252
|
+
if (cutIdx <= 0) return 0; // nothing to cut (keep at least line 0)
|
|
3253
|
+
const kept = lines.slice(0, cutIdx);
|
|
3254
|
+
fs.writeFileSync(sessionFile, kept.join('\n') + '\n', 'utf8');
|
|
3255
|
+
// Invalidate cache so next findSessionFile call re-reads fresh
|
|
3256
|
+
_sessionFileCache.delete(sessionId);
|
|
3257
|
+
const removed = lines.length - kept.length;
|
|
3258
|
+
log('INFO', `truncateSessionLastTurn: removed ${removed} lines from ${path.basename(sessionFile)}`);
|
|
3259
|
+
return removed;
|
|
3260
|
+
} catch (e) {
|
|
3261
|
+
log('WARN', `truncateSessionLastTurn failed: ${e.message}`);
|
|
3262
|
+
return 0;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
/**
|
|
3267
|
+
* Truncate session JSONL to the point before a given checkpoint (timestamp-based).
|
|
3268
|
+
* Used for /undo <hash> to handle multi-turn rollback correctly.
|
|
3269
|
+
* Falls back to truncateSessionLastTurn if timestamp parsing fails.
|
|
3270
|
+
*/
|
|
3271
|
+
function truncateSessionToCheckpoint(sessionId, checkpointMessage) {
|
|
3272
|
+
try {
|
|
3273
|
+
const cpTs = cpExtractTimestamp(checkpointMessage);
|
|
3274
|
+
const cpTime = cpTs ? new Date(cpTs).getTime() : 0;
|
|
3275
|
+
if (!cpTime) return truncateSessionLastTurn(sessionId);
|
|
3276
|
+
|
|
3277
|
+
const sessionFile = findSessionFile(sessionId);
|
|
3278
|
+
if (!sessionFile) return 0;
|
|
3279
|
+
const fileContent = fs.readFileSync(sessionFile, 'utf8');
|
|
3280
|
+
const lines = fileContent.split('\n').filter(l => l.trim());
|
|
3281
|
+
|
|
3282
|
+
// Find the first user message at or after checkpoint time → cut there
|
|
3283
|
+
let cutIdx = -1;
|
|
3284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3285
|
+
try {
|
|
3286
|
+
const obj = JSON.parse(lines[i]);
|
|
3287
|
+
if (obj.type === 'user' && obj.timestamp) {
|
|
3288
|
+
const msgTime = new Date(obj.timestamp).getTime();
|
|
3289
|
+
if (msgTime && msgTime >= cpTime) { cutIdx = i; break; }
|
|
3290
|
+
}
|
|
3291
|
+
} catch { /* skip malformed lines */ }
|
|
3292
|
+
}
|
|
3293
|
+
if (cutIdx <= 0) return truncateSessionLastTurn(sessionId); // fallback
|
|
3294
|
+
|
|
3295
|
+
const kept = lines.slice(0, cutIdx);
|
|
3296
|
+
fs.writeFileSync(sessionFile, kept.join('\n') + '\n', 'utf8');
|
|
3297
|
+
_sessionFileCache.delete(sessionId);
|
|
3298
|
+
const removed = lines.length - kept.length;
|
|
3299
|
+
log('INFO', `truncateSessionToCheckpoint: removed ${removed} lines from ${path.basename(sessionFile)}`);
|
|
3300
|
+
return removed;
|
|
3301
|
+
} catch (e) {
|
|
3302
|
+
log('WARN', `truncateSessionToCheckpoint failed: ${e.message}`);
|
|
3303
|
+
return truncateSessionLastTurn(sessionId); // fallback
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3120
3307
|
/**
|
|
3121
3308
|
* Scan all project session indexes, return most recent N sessions.
|
|
3122
3309
|
* Results cached for 10 seconds to avoid repeated directory scans.
|
|
@@ -3603,13 +3790,26 @@ let lastInteractionTime = Date.now(); // updated on every incoming message
|
|
|
3603
3790
|
let _inSleepMode = false; // tracks current sleep state for log transitions
|
|
3604
3791
|
|
|
3605
3792
|
const IDLE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
3793
|
+
const LOCAL_ACTIVE_FILE = path.join(METAME_DIR, 'local_active');
|
|
3606
3794
|
|
|
3607
3795
|
/**
|
|
3608
3796
|
* Returns true when user has been inactive for >30min AND no sessions are running.
|
|
3797
|
+
* Checks BOTH mobile adapter activity (Telegram/Feishu) AND the local_active heartbeat
|
|
3798
|
+
* file (updated by Claude Code / index.js on each session start).
|
|
3609
3799
|
* Dream tasks (require_idle: true) only execute in this state.
|
|
3610
3800
|
*/
|
|
3611
3801
|
function isUserIdle() {
|
|
3612
|
-
|
|
3802
|
+
// Check mobile adapter activity (Telegram/Feishu)
|
|
3803
|
+
if (Date.now() - lastInteractionTime <= IDLE_THRESHOLD_MS) return false;
|
|
3804
|
+
// Check local desktop activity via ~/.metame/local_active mtime
|
|
3805
|
+
try {
|
|
3806
|
+
if (fs.existsSync(LOCAL_ACTIVE_FILE)) {
|
|
3807
|
+
const mtime = fs.statSync(LOCAL_ACTIVE_FILE).mtimeMs;
|
|
3808
|
+
if (Date.now() - mtime < IDLE_THRESHOLD_MS) return false;
|
|
3809
|
+
}
|
|
3810
|
+
} catch { /* ignore — treat as idle if file unreadable */ }
|
|
3811
|
+
// Only idle if no active Claude sub-processes either
|
|
3812
|
+
return activeProcesses.size === 0;
|
|
3613
3813
|
}
|
|
3614
3814
|
|
|
3615
3815
|
// Fix3: persist child PIDs so next daemon startup can kill orphans
|
|
@@ -4410,9 +4610,10 @@ async function startFeishuBridge(config, executeTaskByName) {
|
|
|
4410
4610
|
const liveCfg = loadConfig();
|
|
4411
4611
|
const allowedIds = (liveCfg.feishu && liveCfg.feishu.allowed_chat_ids) || [];
|
|
4412
4612
|
const trimmedText = text && text.trim();
|
|
4413
|
-
const isBindCmd = trimmedText && (trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
|
|
4613
|
+
const isBindCmd = trimmedText && (trimmedText.startsWith('/bind') || trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
|
|
4414
4614
|
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
4415
4615
|
log('WARN', `Feishu: rejected message from ${chatId}`);
|
|
4616
|
+
bot.sendMessage(chatId, `⚠️ 此会话 (ID: ${chatId}) 未授权。\n\n发送以下命令注册:\n/bind personal ~/\n\n这会将此会话绑定到你的主目录。`).catch(() => {});
|
|
4416
4617
|
return;
|
|
4417
4618
|
}
|
|
4418
4619
|
|
|
@@ -4590,7 +4791,7 @@ async function main() {
|
|
|
4590
4791
|
qmd.startDaemon().then(running => {
|
|
4591
4792
|
if (running) log('INFO', '[QMD] Semantic search daemon started (localhost:8181)');
|
|
4592
4793
|
else log('INFO', '[QMD] Available but daemon not started — will use CLI fallback');
|
|
4593
|
-
}).catch(() => {});
|
|
4794
|
+
}).catch(() => { });
|
|
4594
4795
|
}
|
|
4595
4796
|
} catch { /* qmd-client not available, skip */ }
|
|
4596
4797
|
// Hourly heartbeat so daemon.log stays fresh even when idle (visible aliveness check)
|
|
@@ -4600,14 +4801,7 @@ async function main() {
|
|
|
4600
4801
|
|
|
4601
4802
|
// Task executor lookup (always reads fresh config)
|
|
4602
4803
|
function executeTaskByName(name) {
|
|
4603
|
-
const
|
|
4604
|
-
let task = legacy.find(t => t.name === name);
|
|
4605
|
-
if (!task) {
|
|
4606
|
-
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
4607
|
-
const found = (proj.heartbeat_tasks || []).find(t => t.name === name);
|
|
4608
|
-
if (found) { task = { ...found, _project: { key, name: proj.name || key, color: proj.color || 'blue', icon: proj.icon || '🤖' } }; break; }
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4804
|
+
const task = findTask(config, name);
|
|
4611
4805
|
if (!task) return { success: false, error: `Task "${name}" not found` };
|
|
4612
4806
|
return executeTask(task, config);
|
|
4613
4807
|
}
|
|
@@ -4686,10 +4880,9 @@ async function main() {
|
|
|
4686
4880
|
refreshLogMaxSize(config);
|
|
4687
4881
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
4688
4882
|
heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
log('INFO', `Config reloaded: ${totalCount} tasks (${projectCount} in projects)`);
|
|
4883
|
+
const { general, project } = getAllTasks(config);
|
|
4884
|
+
const totalCount = general.length + project.length;
|
|
4885
|
+
log('INFO', `Config reloaded: ${totalCount} tasks (${project.length} in projects)`);
|
|
4693
4886
|
return { success: true, tasks: totalCount };
|
|
4694
4887
|
}
|
|
4695
4888
|
// Expose reloadConfig to handleCommand via closure
|
|
@@ -4797,11 +4990,11 @@ if (process.argv.includes('--run')) {
|
|
|
4797
4990
|
process.exit(1);
|
|
4798
4991
|
}
|
|
4799
4992
|
const config = loadConfig();
|
|
4800
|
-
const
|
|
4801
|
-
const task = tasks.find(t => t.name === taskName);
|
|
4993
|
+
const task = findTask(config, taskName);
|
|
4802
4994
|
if (!task) {
|
|
4995
|
+
const { all } = getAllTasks(config);
|
|
4803
4996
|
console.error(`Task "${taskName}" not found in daemon.yaml`);
|
|
4804
|
-
console.error(`Available: ${
|
|
4997
|
+
console.error(`Available: ${all.map(t => t.name).join(', ') || '(none)'}`);
|
|
4805
4998
|
process.exit(1);
|
|
4806
4999
|
}
|
|
4807
5000
|
const result = executeTask(task, config);
|
package/scripts/distill.js
CHANGED
|
@@ -82,14 +82,14 @@ async function distill() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
|
-
// 3. Parse signals (preserve confidence from signal-capture)
|
|
85
|
+
// 3. Parse signals (preserve confidence + type from signal-capture)
|
|
86
86
|
const signals = [];
|
|
87
87
|
let highConfidenceCount = 0;
|
|
88
88
|
for (const line of lines) {
|
|
89
89
|
try {
|
|
90
90
|
const entry = JSON.parse(line);
|
|
91
91
|
if (entry.prompt) {
|
|
92
|
-
signals.push(entry.prompt);
|
|
92
|
+
signals.push({ text: entry.prompt, type: entry.type || 'implicit' });
|
|
93
93
|
if (entry.confidence === 'high') highConfidenceCount++;
|
|
94
94
|
}
|
|
95
95
|
} catch {
|
|
@@ -127,18 +127,24 @@ async function distill() {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// 5. Build distillation prompt (compact + session-aware)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
// Input budget: keep total prompt under INPUT_TOKEN_BUDGET to control cost/latency.
|
|
131
|
+
// Priority: system prompt + profile + writable keys (must keep) > user messages > session context
|
|
132
|
+
const INPUT_TOKEN_BUDGET = 4000; // ~12K chars mixed zh/en
|
|
133
133
|
|
|
134
134
|
const writableKeys = getWritableKeysForPrompt();
|
|
135
135
|
|
|
136
|
-
//
|
|
136
|
+
// Reserve budget for fixed parts (system prompt template ~600 tokens, profile, writable keys)
|
|
137
|
+
const fixedOverhead = 600; // system prompt template + rules
|
|
138
|
+
const profileTokens = estimateTokens(currentProfile);
|
|
139
|
+
const keysTokens = estimateTokens(writableKeys);
|
|
140
|
+
const reservedTokens = fixedOverhead + profileTokens + keysTokens;
|
|
141
|
+
const availableForContent = Math.max(INPUT_TOKEN_BUDGET - reservedTokens, 200);
|
|
142
|
+
|
|
143
|
+
// Build session context (lower priority — truncate first)
|
|
137
144
|
let sessionSection = sessionContext
|
|
138
145
|
? `\nSESSION CONTEXT (what actually happened in the latest coding session):\n${sessionContext}\n`
|
|
139
146
|
: '';
|
|
140
147
|
|
|
141
|
-
// Add summary for long sessions (~30 tokens when present)
|
|
142
148
|
if (sessionSummary) {
|
|
143
149
|
const pivotText = sessionSummary.pivots.length > 0
|
|
144
150
|
? `\nPivots: ${sessionSummary.pivots.join('; ')}`
|
|
@@ -146,12 +152,41 @@ async function distill() {
|
|
|
146
152
|
sessionSection += `Summary: ${sessionSummary.intent} → ${sessionSummary.outcome}${pivotText}\n`;
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
// Goal context section (~11 tokens when present)
|
|
150
155
|
let goalContext = '';
|
|
151
156
|
if (sessionAnalytics) {
|
|
152
157
|
try { goalContext = sessionAnalytics.formatGoalContext(BRAIN_FILE); } catch { }
|
|
153
158
|
}
|
|
154
|
-
|
|
159
|
+
let goalSection = goalContext ? `\n${goalContext}\n` : '';
|
|
160
|
+
|
|
161
|
+
// Allocate remaining budget: user messages get priority over session context
|
|
162
|
+
const sessionTokens = estimateTokens(sessionSection + goalSection);
|
|
163
|
+
let budgetForMessages = availableForContent - sessionTokens;
|
|
164
|
+
|
|
165
|
+
// If not enough room, drop session context first, then trim messages
|
|
166
|
+
if (budgetForMessages < 100) {
|
|
167
|
+
sessionSection = '';
|
|
168
|
+
goalSection = '';
|
|
169
|
+
budgetForMessages = availableForContent;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Format signals: tag metacognitive signals so Haiku treats them differently
|
|
173
|
+
const formatSignal = (s, i) => {
|
|
174
|
+
const tag = s.type === 'metacognitive' ? ' [META]' : '';
|
|
175
|
+
return `${i + 1}. "${s.text}"${tag}`;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Truncate user messages to fit budget (keep most recent, they're more relevant)
|
|
179
|
+
let truncatedSignals = signals;
|
|
180
|
+
let userMessages = signals.map(formatSignal).join('\n');
|
|
181
|
+
if (estimateTokens(userMessages) > budgetForMessages) {
|
|
182
|
+
// Drop oldest messages until we fit
|
|
183
|
+
while (truncatedSignals.length > 1 && estimateTokens(
|
|
184
|
+
truncatedSignals.map(formatSignal).join('\n')
|
|
185
|
+
) > budgetForMessages) {
|
|
186
|
+
truncatedSignals = truncatedSignals.slice(1);
|
|
187
|
+
}
|
|
188
|
+
userMessages = truncatedSignals.map(formatSignal).join('\n');
|
|
189
|
+
}
|
|
155
190
|
|
|
156
191
|
const distillPrompt = `You are a MetaMe cognitive profile distiller. Extract COGNITIVE TRAITS and PREFERENCES — how the user thinks, decides, and communicates. NOT a memory system. Do NOT store facts.
|
|
157
192
|
|
|
@@ -172,6 +207,7 @@ RULES:
|
|
|
172
207
|
3. Only output fields from WRITABLE FIELDS. Any other key will be rejected.
|
|
173
208
|
4. For enum fields, use one of the listed values.
|
|
174
209
|
5. Strong directives (以后一律/always/never/from now on) → _confidence: high. Otherwise: normal.
|
|
210
|
+
6. Messages tagged [META] are metacognitive signals (self-reflection, strategy shifts, error awareness). These are HIGH VALUE for cognition fields — extract decision_style, error_response, receptive_to_challenge, and behavioral patterns from them.
|
|
175
211
|
7. Add _confidence and _source blocks mapping field keys to confidence level and triggering quote.
|
|
176
212
|
8. NEVER extract agent identity or role definitions. Messages like "你是贾维斯/你的角色是.../you are Jarvis" define the AGENT, not the USER. The profile is about the USER's cognition only.
|
|
177
213
|
|
|
@@ -118,7 +118,7 @@ function createBot(config) {
|
|
|
118
118
|
* Send markdown as Feishu interactive card (lark_md renders bold, lists, code, links)
|
|
119
119
|
*/
|
|
120
120
|
async sendMarkdown(chatId, markdown) {
|
|
121
|
-
const elements = toMdChunks(markdown).map(c => ({ tag: 'markdown', content: c }));
|
|
121
|
+
const elements = toMdChunks(markdown).map(c => ({ tag: 'markdown', content: c, text_size: 'x-large' }));
|
|
122
122
|
return _sendInteractive(chatId, { schema: '2.0', body: { elements } });
|
|
123
123
|
},
|
|
124
124
|
|
|
@@ -132,7 +132,7 @@ function createBot(config) {
|
|
|
132
132
|
*/
|
|
133
133
|
async sendCard(chatId, { title, body, color = 'blue' }) {
|
|
134
134
|
const header = { title: { tag: 'plain_text', content: title }, template: color };
|
|
135
|
-
const elements = body ? toMdChunks(body).map(c => ({ tag: 'markdown', content: c })) : [];
|
|
135
|
+
const elements = body ? toMdChunks(body).map(c => ({ tag: 'markdown', content: c, text_size: 'x-large' })) : [];
|
|
136
136
|
return _sendInteractive(chatId, { schema: '2.0', header, body: { elements } });
|
|
137
137
|
},
|
|
138
138
|
|
|
@@ -28,8 +28,9 @@ const FACT_EXTRACTION_PROMPT = `你是精准的知识提取引擎。从以下会
|
|
|
28
28
|
- bug_lesson(Bug根因:什么设计/假设导致了问题)
|
|
29
29
|
- arch_convention(架构约定:系统组件的行为边界)
|
|
30
30
|
- config_fact(配置事实:某个值的真实含义,尤其反直觉的)
|
|
31
|
+
- config_change(配置变更:用户选择/确认了某个具体配置值,如”字体选了x-large”、”间隔改为2h”)
|
|
31
32
|
- user_pref(用户明确表达的偏好/红线)
|
|
32
|
-
- workflow_rule
|
|
33
|
+
- workflow_rule(工作流戒律:如”不要在某情况下做某事”的反常识流)
|
|
33
34
|
- project_milestone(项目里程碑:主要架构重构、版本发布等跨会话级成果)
|
|
34
35
|
|
|
35
36
|
绝对不提取:
|
package/scripts/memory-search.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* memory-search.js — Cross-session memory recall CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* node memory-search.js "<query>"
|
|
7
|
-
* node memory-search.js
|
|
8
|
-
* node memory-search.js --
|
|
9
|
-
* node memory-search.js --
|
|
6
|
+
* node memory-search.js "<query>" # hybrid search (QMD + FTS5)
|
|
7
|
+
* node memory-search.js "<q1>" "<q2>" "<q3>" # multi-keyword parallel search
|
|
8
|
+
* node memory-search.js --facts "<query>" # search facts only
|
|
9
|
+
* node memory-search.js --sessions "<query>" # search sessions only
|
|
10
|
+
* node memory-search.js --recent # show recent sessions
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* Multi-keyword: results are deduplicated by fact ID, best rank wins.
|
|
13
|
+
* Async: uses QMD hybrid search (BM25 + vector) when available, falls back to FTS5.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
'use strict';
|
|
@@ -16,7 +18,6 @@
|
|
|
16
18
|
const path = require('path');
|
|
17
19
|
const os = require('os');
|
|
18
20
|
|
|
19
|
-
// Support both local dev and installed (~/.metame/) paths
|
|
20
21
|
const memoryPath = [
|
|
21
22
|
path.join(os.homedir(), '.metame', 'memory.js'),
|
|
22
23
|
path.join(__dirname, 'memory.js'),
|
|
@@ -31,69 +32,97 @@ const memory = require(memoryPath);
|
|
|
31
32
|
|
|
32
33
|
const args = process.argv.slice(2);
|
|
33
34
|
const mode = args[0] && args[0].startsWith('--') ? args[0] : null;
|
|
34
|
-
const
|
|
35
|
+
const queries = mode ? args.slice(1) : args;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
async function main() {
|
|
38
|
+
try {
|
|
39
|
+
if (mode === '--recent') {
|
|
40
|
+
const rows = memory.recentSessions({ limit: 5 });
|
|
41
|
+
console.log(JSON.stringify(rows.map(r => ({
|
|
42
|
+
type: 'session',
|
|
43
|
+
project: r.project,
|
|
44
|
+
date: r.created_at,
|
|
45
|
+
summary: r.summary,
|
|
46
|
+
})), null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!queries.length || !queries[0]) {
|
|
51
|
+
console.log('[]');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const useAsync = typeof memory.searchFactsAsync === 'function';
|
|
56
|
+
|
|
57
|
+
if (mode === '--facts') {
|
|
58
|
+
const results = await searchMulti(queries, { searchFn: q => useAsync ? memory.searchFactsAsync(q, { limit: 5 }) : memory.searchFacts(q, { limit: 5 }), type: 'fact' });
|
|
59
|
+
console.log(JSON.stringify(results, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (mode === '--sessions') {
|
|
64
|
+
const results = await searchMulti(queries, { searchFn: q => Promise.resolve(memory.searchSessions(q, { limit: 5 })), type: 'session' });
|
|
65
|
+
console.log(JSON.stringify(results, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Default: search both facts and sessions, all queries in parallel
|
|
70
|
+
const factResults = await searchMulti(queries, {
|
|
71
|
+
searchFn: q => useAsync ? memory.searchFactsAsync(q, { limit: 5 }) : Promise.resolve(memory.searchFacts(q, { limit: 5 })),
|
|
50
72
|
type: 'fact',
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})), null, 2));
|
|
57
|
-
|
|
58
|
-
} else if (mode === '--sessions') {
|
|
59
|
-
if (!query) { console.log('[]'); process.exit(0); }
|
|
60
|
-
const sessions = memory.searchSessions(query, { limit: 5 });
|
|
61
|
-
console.log(JSON.stringify(sessions.map(s => ({
|
|
73
|
+
limit: 5,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const sessionResults = await searchMulti(queries, {
|
|
77
|
+
searchFn: q => Promise.resolve(memory.searchSessions(q, { limit: 3 })),
|
|
62
78
|
type: 'session',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
69
|
-
// Default: search both facts and sessions
|
|
70
|
-
if (!query) { console.log('[]'); process.exit(0); }
|
|
71
|
-
const facts = (typeof memory.searchFacts === 'function')
|
|
72
|
-
? memory.searchFacts(query, { limit: 3 })
|
|
73
|
-
: [];
|
|
74
|
-
const sessions = memory.searchSessions(query, { limit: 3 });
|
|
75
|
-
|
|
76
|
-
const results = [
|
|
77
|
-
...facts.map(f => ({
|
|
78
|
-
type: 'fact',
|
|
79
|
-
entity: f.entity,
|
|
80
|
-
relation: f.relation,
|
|
81
|
-
value: f.value,
|
|
82
|
-
confidence: f.confidence,
|
|
83
|
-
date: f.created_at,
|
|
84
|
-
})),
|
|
85
|
-
...sessions.map(s => ({
|
|
86
|
-
type: 'session',
|
|
87
|
-
project: s.project,
|
|
88
|
-
date: s.created_at,
|
|
89
|
-
summary: s.summary,
|
|
90
|
-
})),
|
|
91
|
-
];
|
|
79
|
+
limit: 3,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(JSON.stringify([...factResults, ...sessionResults], null, 2));
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.log('[]');
|
|
86
|
+
} finally {
|
|
87
|
+
try { memory.close(); } catch {}
|
|
94
88
|
}
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.log('[]');
|
|
97
|
-
} finally {
|
|
98
|
-
try { memory.close(); } catch {}
|
|
99
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run multiple queries in parallel, deduplicate and format results.
|
|
93
|
+
*/
|
|
94
|
+
async function searchMulti(queries, { searchFn, type, limit = 5 }) {
|
|
95
|
+
const allResults = await Promise.all(queries.map(q => searchFn(q).catch(() => [])));
|
|
96
|
+
|
|
97
|
+
// Deduplicate by id (facts) or created_at+project (sessions)
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const merged = [];
|
|
100
|
+
|
|
101
|
+
for (const batch of allResults) {
|
|
102
|
+
for (const item of (batch || [])) {
|
|
103
|
+
const key = type === 'fact'
|
|
104
|
+
? `f:${item.id || item.entity + item.value}`
|
|
105
|
+
: `s:${item.created_at}:${item.project}`;
|
|
106
|
+
if (!seen.has(key)) {
|
|
107
|
+
seen.add(key);
|
|
108
|
+
merged.push(type === 'fact' ? {
|
|
109
|
+
type: 'fact',
|
|
110
|
+
entity: item.entity,
|
|
111
|
+
relation: item.relation,
|
|
112
|
+
value: item.value,
|
|
113
|
+
confidence: item.confidence,
|
|
114
|
+
date: item.created_at,
|
|
115
|
+
} : {
|
|
116
|
+
type: 'session',
|
|
117
|
+
project: item.project,
|
|
118
|
+
date: item.created_at,
|
|
119
|
+
summary: item.summary,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return merged.slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main();
|
package/scripts/schema.js
CHANGED
|
@@ -44,13 +44,10 @@ const SCHEMA = {
|
|
|
44
44
|
'cognition.info_processing.entry_point': { tier: 'T3', type: 'enum', values: ['big_picture', 'details', 'examples'] },
|
|
45
45
|
'cognition.info_processing.preferred_format': { tier: 'T3', type: 'enum', values: ['structured', 'narrative', 'visual_metaphor'] },
|
|
46
46
|
'cognition.abstraction.default_level': { tier: 'T3', type: 'enum', values: ['strategic', 'architectural', 'implementation', 'operational'] },
|
|
47
|
-
'cognition.abstraction.range': { tier: 'T3', type: 'enum', values: ['narrow', 'wide'] },
|
|
48
47
|
'cognition.cognitive_load.chunk_size': { tier: 'T3', type: 'enum', values: ['small', 'medium', 'large'] },
|
|
49
|
-
'cognition.
|
|
50
|
-
'cognition.motivation.primary_driver': { tier: 'T3', type: 'enum', values: ['autonomy', 'competence', 'meaning', 'social_proof'] },
|
|
51
|
-
'cognition.motivation.energy_source': { tier: 'T3', type: 'enum', values: ['creation', 'optimization', 'problem_solving', 'teaching'] },
|
|
52
|
-
'cognition.metacognition.self_awareness': { tier: 'T3', type: 'enum', values: ['high', 'medium', 'low'] },
|
|
48
|
+
'cognition.motivation.driver': { tier: 'T3', type: 'enum', values: ['autonomy', 'competence', 'meaning', 'creation', 'optimization'] },
|
|
53
49
|
'cognition.metacognition.receptive_to_challenge': { tier: 'T3', type: 'enum', values: ['yes', 'sometimes', 'no'] },
|
|
50
|
+
'cognition.metacognition.error_response': { tier: 'T3', type: 'enum', values: ['quick_pivot', 'root_cause_first', 'seek_help', 'retry_same'] },
|
|
54
51
|
|
|
55
52
|
// === T4: Context ===
|
|
56
53
|
'context.focus': { tier: 'T4', type: 'string', maxChars: 80 },
|
|
@@ -29,6 +29,10 @@ const IMPLICIT_EN = /I (prefer|like|hate|usually|tend to|always)/i;
|
|
|
29
29
|
const CORRECTION_ZH = /不是.*我(要|想|说)的|我说的不是|你理解错了|不对.*应该/;
|
|
30
30
|
const CORRECTION_EN = /(no,? I meant|that's not what I|you misunderstood|wrong.+should be)/i;
|
|
31
31
|
|
|
32
|
+
// Metacognitive signals → normal confidence (self-reflection, strategy shifts)
|
|
33
|
+
const META_ZH = /我(发现|意识到|觉得|反思|总结|复盘)|想错了|换个(思路|方向|方案)|回头(想想|看看)|之前的(方案|思路|方向).*(不行|不对|有问题)|我的(问题|毛病|习惯)是|下次(应该|要|得)/;
|
|
34
|
+
const META_EN = /(I realize|looking back|on reflection|my (mistake|problem|habit) is|let me rethink|wrong approach|next time I should)/i;
|
|
35
|
+
|
|
32
36
|
// Read JSON from stdin
|
|
33
37
|
let input = '';
|
|
34
38
|
process.stdin.setEncoding('utf8');
|
|
@@ -38,7 +42,11 @@ process.stdin.on('end', () => {
|
|
|
38
42
|
const data = JSON.parse(input);
|
|
39
43
|
const prompt = (data.prompt || '').trim();
|
|
40
44
|
|
|
45
|
+
// === LAYER 0: Metacognitive bypass — always capture self-reflection ===
|
|
46
|
+
const isMeta = META_ZH.test(prompt) || META_EN.test(prompt);
|
|
47
|
+
|
|
41
48
|
// === LAYER 1: Hard filters (definitely not preferences) ===
|
|
49
|
+
// Metacognitive signals bypass all hard filters — they reveal how user thinks
|
|
42
50
|
|
|
43
51
|
// Skip empty or very short messages
|
|
44
52
|
// Chinese chars carry more info per char, so use weighted length
|
|
@@ -48,43 +56,43 @@ process.stdin.on('end', () => {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
// Skip messages that are purely code or file paths
|
|
51
|
-
if (/^(```|\/[\w/]+\.\w+$)/.test(prompt)) {
|
|
59
|
+
if (!isMeta && /^(```|\/[\w/]+\.\w+$)/.test(prompt)) {
|
|
52
60
|
process.exit(0);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// Skip common non-preference commands
|
|
56
|
-
if (/^(\/\w+|!metame|git |npm |pnpm |yarn |brew |sudo |cd |ls |cat |mkdir )/.test(prompt)) {
|
|
64
|
+
if (!isMeta && /^(\/\w+|!metame|git |npm |pnpm |yarn |brew |sudo |cd |ls |cat |mkdir )/.test(prompt)) {
|
|
57
65
|
process.exit(0);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
// Skip pure task instructions (fix/add/delete/refactor/debug/deploy/test/run/build)
|
|
61
|
-
if (/^(帮我|请你|麻烦)?\s*(fix|add|delete|remove|refactor|debug|deploy|test|run|build|create|update|implement|write|generate|make)/i.test(prompt)) {
|
|
69
|
+
if (!isMeta && /^(帮我|请你|麻烦)?\s*(fix|add|delete|remove|refactor|debug|deploy|test|run|build|create|update|implement|write|generate|make)/i.test(prompt)) {
|
|
62
70
|
process.exit(0);
|
|
63
71
|
}
|
|
64
|
-
if (/^(帮我|请你|麻烦)?\s*(修|加|删|重构|调试|部署|测试|运行|构建|创建|更新|实现|写|生成|做)/.test(prompt)) {
|
|
72
|
+
if (!isMeta && /^(帮我|请你|麻烦)?\s*(修|加|删|重构|调试|部署|测试|运行|构建|创建|更新|实现|写|生成|做)/.test(prompt)) {
|
|
65
73
|
process.exit(0);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
// Skip agent identity definitions (these belong in project CLAUDE.md, not user profile)
|
|
69
|
-
if (/^(你是|你叫|你的(角色|身份|职责|任务)|你负责|你现在是|from now on you are|you are now|your role is)/i.test(prompt)) {
|
|
77
|
+
if (!isMeta && /^(你是|你叫|你的(角色|身份|职责|任务)|你负责|你现在是|from now on you are|you are now|your role is)/i.test(prompt)) {
|
|
70
78
|
process.exit(0);
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
// Skip pasted error logs / stack traces
|
|
74
|
-
if (/^(Error|TypeError|SyntaxError|ReferenceError|at\s+\w+|Traceback|FATAL|WARN|ERR!)/i.test(prompt)) {
|
|
82
|
+
if (!isMeta && /^(Error|TypeError|SyntaxError|ReferenceError|at\s+\w+|Traceback|FATAL|WARN|ERR!)/i.test(prompt)) {
|
|
75
83
|
process.exit(0);
|
|
76
84
|
}
|
|
77
|
-
if (prompt.split('\n').length > 10) {
|
|
85
|
+
if (!isMeta && prompt.split('\n').length > 10) {
|
|
78
86
|
// Multi-line pastes are usually code or logs, not preferences
|
|
79
87
|
process.exit(0);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
// Skip pure questions with no preference signal
|
|
83
|
-
if (/^(what|how|why|where|when|which|can you|could you|is there|are there|does|do you)\s/i.test(prompt) &&
|
|
91
|
+
if (!isMeta && /^(what|how|why|where|when|which|can you|could you|is there|are there|does|do you)\s/i.test(prompt) &&
|
|
84
92
|
!/prefer|like|hate|always|never|style|习惯|偏好|喜欢|讨厌/.test(prompt)) {
|
|
85
93
|
process.exit(0);
|
|
86
94
|
}
|
|
87
|
-
if (/^(什么|怎么|为什么|哪|能不能|可以|是不是)\s/.test(prompt) &&
|
|
95
|
+
if (!isMeta && /^(什么|怎么|为什么|哪|能不能|可以|是不是)\s/.test(prompt) &&
|
|
88
96
|
!/偏好|喜欢|讨厌|习惯|以后|一律|总是|永远|记住/.test(prompt)) {
|
|
89
97
|
process.exit(0);
|
|
90
98
|
}
|
|
@@ -93,12 +101,14 @@ process.stdin.on('end', () => {
|
|
|
93
101
|
const isStrong = STRONG_SIGNAL_ZH.test(prompt) || STRONG_SIGNAL_EN.test(prompt);
|
|
94
102
|
const isCorrection = CORRECTION_ZH.test(prompt) || CORRECTION_EN.test(prompt);
|
|
95
103
|
const confidence = (isStrong || isCorrection) ? 'high' : 'normal';
|
|
104
|
+
const signalType = isMeta ? 'metacognitive' : isCorrection ? 'correction' : isStrong ? 'directive' : 'implicit';
|
|
96
105
|
|
|
97
106
|
// Append to buffer
|
|
98
107
|
const entry = {
|
|
99
108
|
ts: new Date().toISOString(),
|
|
100
109
|
prompt: prompt,
|
|
101
110
|
confidence: confidence,
|
|
111
|
+
type: signalType,
|
|
102
112
|
session: data.session_id || null,
|
|
103
113
|
cwd: data.cwd || null
|
|
104
114
|
};
|