metame-cli 1.4.19 → 1.4.21
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 +30 -24
- package/index.js +39 -1
- package/package.json +1 -1
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +73 -63
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -39
- package/scripts/daemon-command-router.js +38 -35
- package/scripts/daemon-default.yaml +18 -0
- package/scripts/daemon-exec-commands.js +6 -12
- package/scripts/daemon-file-browser.js +6 -5
- package/scripts/daemon-runtime-lifecycle.js +19 -5
- package/scripts/daemon-session-commands.js +8 -3
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon.js +38 -6
- package/scripts/distill.js +11 -12
- package/scripts/memory-gc.js +239 -0
- package/scripts/memory-index.js +103 -0
- package/scripts/memory-nightly-reflect.js +299 -0
- package/scripts/memory-write.js +192 -0
- package/scripts/memory.js +144 -6
- package/scripts/schema.js +30 -9
- package/scripts/self-reflect.js +121 -5
- package/scripts/session-analytics.js +9 -10
- package/scripts/task-board.js +9 -3
- package/scripts/telegram-adapter.js +77 -9
|
@@ -12,8 +12,29 @@ function createBridgeStarter(deps) {
|
|
|
12
12
|
saveState,
|
|
13
13
|
getSession,
|
|
14
14
|
handleCommand,
|
|
15
|
+
pendingActivations, // optional — used to show smart activation hint
|
|
15
16
|
} = deps;
|
|
16
17
|
|
|
18
|
+
// Returns the best pending activation for a given chatId (excludes self-created)
|
|
19
|
+
function getPendingActivationForChat(chatId) {
|
|
20
|
+
if (!pendingActivations || pendingActivations.size === 0) return null;
|
|
21
|
+
const cid = String(chatId);
|
|
22
|
+
let latest = null;
|
|
23
|
+
for (const rec of pendingActivations.values()) {
|
|
24
|
+
if (rec.createdByChatId === cid) continue;
|
|
25
|
+
if (!latest || rec.createdAt > latest.createdAt) latest = rec;
|
|
26
|
+
}
|
|
27
|
+
return latest;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function unauthorizedMsg(chatId, useSend) {
|
|
31
|
+
const pending = getPendingActivationForChat(chatId);
|
|
32
|
+
if (pending) {
|
|
33
|
+
return `⚠️ 此群未授权\n\n发送以下命令激活 Agent「${pending.agentName}」:\n\`/activate\``;
|
|
34
|
+
}
|
|
35
|
+
return '⚠️ 此群未授权\n\n如已创建 Agent,发送 `/activate` 完成绑定。\n否则请先在主群创建 Agent。';
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
async function startTelegramBridge(config, executeTaskByName) {
|
|
18
39
|
if (!config.telegram || !config.telegram.enabled) return null;
|
|
19
40
|
if (!config.telegram.bot_token) {
|
|
@@ -68,13 +89,13 @@ function createBridgeStarter(deps) {
|
|
|
68
89
|
const trimmedText = msg.text && msg.text.trim();
|
|
69
90
|
const isBindCmd = trimmedText && (
|
|
70
91
|
trimmedText.startsWith('/agent bind')
|
|
71
|
-
|| trimmedText.startsWith('/agent new')
|
|
72
92
|
|| trimmedText.startsWith('/agent-bind-dir')
|
|
73
93
|
|| trimmedText.startsWith('/browse bind')
|
|
94
|
+
|| trimmedText === '/activate'
|
|
74
95
|
);
|
|
75
96
|
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
76
97
|
log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
|
|
77
|
-
bot.sendMessage(chatId,
|
|
98
|
+
bot.sendMessage(chatId, unauthorizedMsg(chatId)).catch(() => {});
|
|
78
99
|
continue;
|
|
79
100
|
}
|
|
80
101
|
|
|
@@ -157,15 +178,14 @@ function createBridgeStarter(deps) {
|
|
|
157
178
|
const trimmedText = text && text.trim();
|
|
158
179
|
const isBindCmd = trimmedText && (
|
|
159
180
|
trimmedText.startsWith('/agent bind')
|
|
160
|
-
|| trimmedText.startsWith('/agent new')
|
|
161
181
|
|| trimmedText.startsWith('/agent-bind-dir')
|
|
162
182
|
|| trimmedText.startsWith('/browse bind')
|
|
183
|
+
|| trimmedText === '/activate'
|
|
163
184
|
);
|
|
164
185
|
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
165
186
|
log('WARN', `Feishu: rejected message from ${chatId}`);
|
|
166
|
-
(
|
|
167
|
-
|
|
168
|
-
: bot.sendMessage(chatId, '⚠️ 此会话未授权\n\n复制发送以下命令注册:\n\n/agent bind personal')).catch(() => {});
|
|
187
|
+
const msg = unauthorizedMsg(chatId);
|
|
188
|
+
(bot.sendMarkdown ? bot.sendMarkdown(chatId, msg) : bot.sendMessage(chatId, msg)).catch(() => {});
|
|
169
189
|
return;
|
|
170
190
|
}
|
|
171
191
|
|
|
@@ -103,7 +103,7 @@ function createClaudeEngine(deps) {
|
|
|
103
103
|
const existsInCwd = recentInCwd.some(s => s.sessionId === safeSessionId);
|
|
104
104
|
return cacheSessionCwdValidation(cacheKey, existsInCwd);
|
|
105
105
|
} catch {
|
|
106
|
-
// Conservative fallback: if validation infra fails, avoid false
|
|
106
|
+
// Conservative fallback: if validation infra fails, avoid false negatives by preserving current session.
|
|
107
107
|
return cacheSessionCwdValidation(cacheKey, true);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -189,6 +189,16 @@ function createClaudeEngine(deps) {
|
|
|
189
189
|
return /(邮件|邮箱|收件箱|mail|email|calendar|日历|日程|会议|提醒|remind|草稿|发送邮件|打开|关闭|启动|切到|前台|音量|静音|睡眠|锁屏|Finder|Safari|微信|WeChat|Terminal|iTerm|System Events)/i.test(text);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Returns true when the message is a task/technical request that warrants full memory hints (rules 3-5).
|
|
193
|
+
// Errs on the side of over-inclusion: false negatives (missing hints) are worse than false positives.
|
|
194
|
+
function isTaskIntent(prompt) {
|
|
195
|
+
const text = String(prompt || '').trim();
|
|
196
|
+
if (!text) return false;
|
|
197
|
+
// Errs on the side of over-inclusion: false negatives (missing hints) are worse than false positives.
|
|
198
|
+
if (/^\/\w+/.test(text)) return true; // slash command / dispatch prefix
|
|
199
|
+
return text.length > 30 || /(node|git|npm|daemon|script|debug|fix|bug|error|api|sql|review|实现|修改|排查|架构|配置|代码|函数|部署|测试|调试|重构|优化|回滚|日志|迁移|升级|接口|监控|错误|修复|异常|警告|单测|崩|死锁|内存)/i.test(text);
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
/**
|
|
193
203
|
* Auto-generate a session name using Haiku (async, non-blocking).
|
|
194
204
|
* Writes to Claude's session file (unified with /rename).
|
|
@@ -227,7 +237,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
227
237
|
* Spawn claude as async child process (non-blocking).
|
|
228
238
|
* Returns { output, error } after process exits.
|
|
229
239
|
*/
|
|
230
|
-
function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000) {
|
|
240
|
+
function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000, metameProject = '') {
|
|
231
241
|
return new Promise((resolve) => {
|
|
232
242
|
const child = spawn(CLAUDE_BIN, args, {
|
|
233
243
|
cwd,
|
|
@@ -237,6 +247,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
237
247
|
...getActiveProviderEnv(),
|
|
238
248
|
CLAUDECODE: undefined,
|
|
239
249
|
METAME_INTERNAL_PROMPT: '1',
|
|
250
|
+
METAME_PROJECT: metameProject || ''
|
|
240
251
|
},
|
|
241
252
|
});
|
|
242
253
|
|
|
@@ -301,7 +312,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
301
312
|
* Calls onStatus callback when tool usage is detected.
|
|
302
313
|
* Returns { output, error } after process exits.
|
|
303
314
|
*/
|
|
304
|
-
function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, chatId = null) {
|
|
315
|
+
function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, chatId = null, metameProject = '') {
|
|
305
316
|
return new Promise((resolve) => {
|
|
306
317
|
// Add stream-json output format (requires --verbose)
|
|
307
318
|
const streamArgs = [...args, '--output-format', 'stream-json', '--verbose'];
|
|
@@ -310,12 +321,17 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
310
321
|
cwd,
|
|
311
322
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
312
323
|
detached: true, // Create new process group so killing -pid kills all sub-agents too
|
|
313
|
-
env: {
|
|
324
|
+
env: {
|
|
325
|
+
...process.env,
|
|
326
|
+
...getActiveProviderEnv(),
|
|
327
|
+
CLAUDECODE: undefined,
|
|
328
|
+
METAME_PROJECT: metameProject || ''
|
|
329
|
+
},
|
|
314
330
|
});
|
|
315
331
|
|
|
316
332
|
// Track active process for /stop
|
|
317
333
|
if (chatId) {
|
|
318
|
-
activeProcesses.set(chatId, { child, aborted: false });
|
|
334
|
+
activeProcesses.set(chatId, { child, aborted: false, startedAt: Date.now() });
|
|
319
335
|
saveActivePids(); // Fix3: persist PID to disk
|
|
320
336
|
}
|
|
321
337
|
|
|
@@ -512,6 +528,23 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
512
528
|
* Shared ask logic — full Claude Code session (stateful, with tools)
|
|
513
529
|
* Now uses spawn (async) instead of execSync to allow parallel requests.
|
|
514
530
|
*/
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Reset active provider back to anthropic/opus and reload config.
|
|
534
|
+
* Returns the freshly loaded config so callers can reassign their local variable.
|
|
535
|
+
*/
|
|
536
|
+
function fallbackToDefaultProvider(reason) {
|
|
537
|
+
log('WARN', `Falling back to anthropic/opus — reason: ${reason}`);
|
|
538
|
+
if (providerMod && providerMod.getActiveName() !== 'anthropic') {
|
|
539
|
+
providerMod.setActive('anthropic');
|
|
540
|
+
}
|
|
541
|
+
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
542
|
+
if (!cfg.daemon) cfg.daemon = {};
|
|
543
|
+
cfg.daemon.model = 'opus';
|
|
544
|
+
writeConfigSafe(cfg);
|
|
545
|
+
return loadConfig();
|
|
546
|
+
}
|
|
547
|
+
|
|
515
548
|
async function askClaude(bot, chatId, prompt, config, readOnly = false) {
|
|
516
549
|
log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
|
|
517
550
|
// Track interaction time for idle/sleep detection
|
|
@@ -670,12 +703,25 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
670
703
|
const _agentMap = { ...(_cfg.telegram ? _cfg.telegram.chat_agent_map : {}), ...(_cfg.feishu ? _cfg.feishu.chat_agent_map : {}) };
|
|
671
704
|
const projectKey = _agentMap[_cid] || projectKeyFromVirtualChatId(_cid);
|
|
672
705
|
|
|
706
|
+
// L1: NOW.md shared whiteboard injection
|
|
707
|
+
if (!session.started) {
|
|
708
|
+
try {
|
|
709
|
+
const nowPath = path.join(HOME, '.metame', 'memory', 'NOW.md');
|
|
710
|
+
if (fs.existsSync(nowPath)) {
|
|
711
|
+
const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
|
|
712
|
+
if (nowContent) {
|
|
713
|
+
memoryHint += `\n\n[Current task context:\n${nowContent}]`;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch { /* non-critical */ }
|
|
717
|
+
}
|
|
718
|
+
|
|
673
719
|
// 1. Inject recent session memories ONLY on first message of a session
|
|
674
720
|
if (!session.started) {
|
|
675
|
-
const recent = memory.recentSessions({ limit:
|
|
721
|
+
const recent = memory.recentSessions({ limit: 1, project: projectKey || undefined });
|
|
676
722
|
if (recent.length > 0) {
|
|
677
723
|
const items = recent.map(r => `- [${r.created_at}] ${r.summary}${r.keywords ? ' (keywords: ' + r.keywords + ')' : ''}`).join('\n');
|
|
678
|
-
memoryHint += `\n\n
|
|
724
|
+
memoryHint += `\n\n[Past session memory:\n${items}]`;
|
|
679
725
|
}
|
|
680
726
|
}
|
|
681
727
|
|
|
@@ -685,10 +731,10 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
685
731
|
if (!session.started) {
|
|
686
732
|
const searchFn = memory.searchFactsAsync || memory.searchFacts;
|
|
687
733
|
const factQuery = buildFactSearchQuery(prompt, projectKey);
|
|
688
|
-
const facts = await Promise.resolve(searchFn(factQuery, { limit:
|
|
734
|
+
const facts = await Promise.resolve(searchFn(factQuery, { limit: 3, project: projectKey || undefined }));
|
|
689
735
|
if (facts.length > 0) {
|
|
690
736
|
const factItems = facts.map(f => `- [${f.relation}] ${f.value}`).join('\n');
|
|
691
|
-
memoryHint += `\n\n
|
|
737
|
+
memoryHint += `\n\n[Relevant facts:\n${factItems}]`;
|
|
692
738
|
log('INFO', `[MEMORY] Injected ${facts.length} facts (query_len=${factQuery.length})`);
|
|
693
739
|
}
|
|
694
740
|
}
|
|
@@ -698,16 +744,50 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
698
744
|
if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
|
|
699
745
|
}
|
|
700
746
|
|
|
747
|
+
// ZPD: build competence hint from brain profile
|
|
748
|
+
let zdpHint = '';
|
|
749
|
+
if (!session.started) {
|
|
750
|
+
try {
|
|
751
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
752
|
+
if (fs.existsSync(brainPath)) {
|
|
753
|
+
const brain = yaml.load(fs.readFileSync(brainPath, 'utf8'));
|
|
754
|
+
const cmap = brain && brain.user_competence_map;
|
|
755
|
+
if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
|
|
756
|
+
const lines = Object.entries(cmap)
|
|
757
|
+
.map(([domain, level]) => ` ${domain}: ${level}`)
|
|
758
|
+
.join('\n');
|
|
759
|
+
zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} catch { /* non-critical */ }
|
|
763
|
+
}
|
|
764
|
+
|
|
701
765
|
// Inject daemon hints only on first message of a session
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
766
|
+
// Task-specific rules (3-5) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
767
|
+
let daemonHint = '';
|
|
768
|
+
if (!session.started) {
|
|
769
|
+
const taskRules = isTaskIntent(prompt) ? `
|
|
770
|
+
3. Knowledge retrieval: When you need context about a specific topic, past decisions, or lessons, call:
|
|
771
|
+
node ~/.metame/memory-search.js "关键词1" "keyword2"
|
|
772
|
+
Also read ~/.metame/memory/INDEX.md to discover available long-form lesson/decision docs, then read specific files as needed.
|
|
773
|
+
Use these before answering complex questions about MetaMe architecture or past decisions.
|
|
774
|
+
4. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
775
|
+
node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
|
|
776
|
+
Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, user_pref, workflow_rule, project_milestone
|
|
777
|
+
Only write verified facts. Do not write speculative or process-description entries.
|
|
778
|
+
When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"
|
|
779
|
+
5. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/NOW.md using:
|
|
780
|
+
\`printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/NOW.md\`
|
|
781
|
+
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/NOW.md\`` : '';
|
|
782
|
+
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
783
|
+
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
784
|
+
2. File sending: User is on MOBILE. When they ask to see/download a file:
|
|
706
785
|
- Just FIND the file path (use Glob/ls if needed)
|
|
707
786
|
- Do NOT read or summarize the file content (wastes tokens)
|
|
708
787
|
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
709
788
|
- Keep response brief: "请查收~! [[FILE:/path/to/file]]"
|
|
710
|
-
- Multiple files: use multiple [[FILE:...]] tags
|
|
789
|
+
- Multiple files: use multiple [[FILE:...]] tags${zdpHint ? '\n Explanation depth (ZPD):\n' + zdpHint : ''}${taskRules}]`;
|
|
790
|
+
}
|
|
711
791
|
|
|
712
792
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
713
793
|
|
|
@@ -720,7 +800,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
720
800
|
2. Read/query actions can execute directly.
|
|
721
801
|
3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
|
|
722
802
|
4. Keep output concise: success/failure + key result only.
|
|
723
|
-
5. If permission is missing, guide user to run /mac perms open then retry.
|
|
803
|
+
5. If permission is missing, guide user to run /mac perms open then retry.
|
|
804
|
+
6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
|
|
724
805
|
}
|
|
725
806
|
|
|
726
807
|
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
@@ -743,7 +824,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
743
824
|
}
|
|
744
825
|
|
|
745
826
|
// Always append a compact language guard to prevent accidental Korean/Japanese responses
|
|
746
|
-
const langGuard = '\n\n[Respond in Simplified Chinese (简体中文) only.]';
|
|
827
|
+
const langGuard = '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
747
828
|
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint + langGuard;
|
|
748
829
|
|
|
749
830
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
@@ -770,7 +851,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
770
851
|
} catch { /* ignore status update failures */ }
|
|
771
852
|
};
|
|
772
853
|
|
|
773
|
-
|
|
854
|
+
let output, error, files, toolUsageLog;
|
|
855
|
+
try {
|
|
856
|
+
({ output, error, files, toolUsageLog } = await spawnClaudeStreaming(args, fullPrompt, session.cwd, onStatus, 600000, chatId, boundProjectKey || ''));
|
|
857
|
+
} catch (spawnErr) {
|
|
858
|
+
clearInterval(typingTimer);
|
|
859
|
+
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
860
|
+
log('ERROR', `spawnClaudeStreaming crashed for ${chatId}: ${spawnErr.message}`);
|
|
861
|
+
await bot.sendMessage(chatId, `❌ 内部错误: ${spawnErr.message}`).catch(() => { });
|
|
862
|
+
return { ok: false, error: spawnErr.message };
|
|
863
|
+
}
|
|
774
864
|
clearInterval(typingTimer);
|
|
775
865
|
|
|
776
866
|
// Skill evolution: capture signal + hot path heuristic check
|
|
@@ -819,14 +909,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
819
909
|
const builtinModelsCheck = ['sonnet', 'opus', 'haiku'];
|
|
820
910
|
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
821
911
|
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
822
|
-
log('WARN', `Custom provider/model may have failed (${activeProvCheck}/${model}), output: ${output.slice(0, 200)}`);
|
|
823
912
|
try {
|
|
824
|
-
|
|
825
|
-
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
826
|
-
if (!cfg.daemon) cfg.daemon = {};
|
|
827
|
-
cfg.daemon.model = 'opus';
|
|
828
|
-
writeConfigSafe(cfg);
|
|
829
|
-
config = loadConfig();
|
|
913
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
830
914
|
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
831
915
|
} catch (fbErr) {
|
|
832
916
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
@@ -892,8 +976,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
892
976
|
log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
|
|
893
977
|
|
|
894
978
|
// If session not found (expired/deleted), create new and retry once
|
|
895
|
-
if (errMsg.includes('not found') || errMsg.includes('No session')) {
|
|
896
|
-
log('WARN', `Session ${session.id} not found, creating new`);
|
|
979
|
+
if (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use')) {
|
|
980
|
+
log('WARN', `Session ${session.id} unusable (${errMsg.includes('already in use') ? 'locked' : 'not found'}), creating new`);
|
|
897
981
|
session = createSession(chatId, session.cwd);
|
|
898
982
|
|
|
899
983
|
const retryArgs = ['-p', '--session-id', session.id];
|
|
@@ -916,23 +1000,13 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
916
1000
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
917
1001
|
return { ok: false, error: retry.error || errMsg };
|
|
918
1002
|
}
|
|
919
|
-
} else if (errMsg === 'Stopped by user' && messageQueue.has(chatId)) {
|
|
920
|
-
// Interrupted by message queue — suppress error, queue timer will handle it
|
|
921
|
-
log('INFO', `Task interrupted by new message for ${chatId}`);
|
|
922
|
-
return { ok: false, error: errMsg, interrupted: true };
|
|
923
1003
|
} else {
|
|
924
1004
|
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus
|
|
925
1005
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
926
1006
|
const builtinModels = ['sonnet', 'opus', 'haiku'];
|
|
927
1007
|
if (activeProv !== 'anthropic' || !builtinModels.includes(model)) {
|
|
928
|
-
log('WARN', `Custom provider/model failed (${activeProv}/${model}), falling back to anthropic/opus`);
|
|
929
1008
|
try {
|
|
930
|
-
|
|
931
|
-
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
932
|
-
if (!cfg.daemon) cfg.daemon = {};
|
|
933
|
-
cfg.daemon.model = 'opus';
|
|
934
|
-
writeConfigSafe(cfg);
|
|
935
|
-
config = loadConfig();
|
|
1009
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
936
1010
|
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
937
1011
|
} catch (fallbackErr) {
|
|
938
1012
|
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
@@ -944,8 +1018,6 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
944
1018
|
return { ok: false, error: errMsg };
|
|
945
1019
|
}
|
|
946
1020
|
}
|
|
947
|
-
|
|
948
|
-
return { ok: true };
|
|
949
1021
|
}
|
|
950
1022
|
|
|
951
1023
|
return {
|
|
@@ -24,6 +24,7 @@ function createCommandRouter(deps) {
|
|
|
24
24
|
log,
|
|
25
25
|
agentTools,
|
|
26
26
|
pendingAgentFlows,
|
|
27
|
+
pendingActivations,
|
|
27
28
|
agentFlowTtlMs,
|
|
28
29
|
} = deps;
|
|
29
30
|
|
|
@@ -426,7 +427,7 @@ function createCommandRouter(deps) {
|
|
|
426
427
|
if (wantsCreate) {
|
|
427
428
|
if (!workspaceDir) {
|
|
428
429
|
await bot.sendMessage(chatId, [
|
|
429
|
-
'
|
|
430
|
+
'我可以帮你创建 Agent,还差一个工作目录。',
|
|
430
431
|
'例如:`给这个群创建一个 Agent,目录是 ~/projects/foo`',
|
|
431
432
|
'也可以直接回我一个路径(`~/`、`/`、`./`、`../` 开头都行)。',
|
|
432
433
|
].join('\n'));
|
|
@@ -434,15 +435,24 @@ function createCommandRouter(deps) {
|
|
|
434
435
|
}
|
|
435
436
|
const agentName = deriveAgentName(input, workspaceDir);
|
|
436
437
|
const roleDelta = deriveCreateRoleDelta(input);
|
|
437
|
-
|
|
438
|
+
// Always skip binding creating chat — new group activates via /activate
|
|
439
|
+
const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, { skipChatBinding: true });
|
|
438
440
|
if (!res.ok) {
|
|
439
441
|
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
|
|
440
442
|
return true;
|
|
441
443
|
}
|
|
442
444
|
const data = res.data || {};
|
|
443
445
|
const projName = projectNameFromResult(data, agentName);
|
|
444
|
-
if (data.
|
|
445
|
-
|
|
446
|
+
if (data.projectKey && pendingActivations) {
|
|
447
|
+
pendingActivations.set(data.projectKey, {
|
|
448
|
+
agentKey: data.projectKey, agentName: projName, cwd: data.cwd,
|
|
449
|
+
createdByChatId: String(chatId), createdAt: Date.now(),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
await bot.sendMessage(chatId,
|
|
453
|
+
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}\n\n` +
|
|
454
|
+
`**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
|
|
455
|
+
);
|
|
446
456
|
return true;
|
|
447
457
|
}
|
|
448
458
|
|
|
@@ -529,6 +539,7 @@ function createCommandRouter(deps) {
|
|
|
529
539
|
'📱 手机端 Claude Code',
|
|
530
540
|
'',
|
|
531
541
|
'⚡ 快速同步电脑工作:',
|
|
542
|
+
'/continue — 接续电脑正在做的工作',
|
|
532
543
|
'/last — 继续电脑上最近的对话',
|
|
533
544
|
'/cd last — 切到电脑最近的项目目录',
|
|
534
545
|
'',
|
|
@@ -565,43 +576,21 @@ function createCommandRouter(deps) {
|
|
|
565
576
|
}
|
|
566
577
|
|
|
567
578
|
// --- Natural language → Claude Code session ---
|
|
568
|
-
// If a task is running:
|
|
579
|
+
// If a task is running: queue message, DON'T kill — will be sent as follow-up after completion
|
|
569
580
|
if (activeProcesses.has(chatId)) {
|
|
570
581
|
const isFirst = !messageQueue.has(chatId);
|
|
571
582
|
if (isFirst) {
|
|
572
|
-
messageQueue.set(chatId, { messages: []
|
|
583
|
+
messageQueue.set(chatId, { messages: [] });
|
|
573
584
|
}
|
|
574
585
|
const q = messageQueue.get(chatId);
|
|
586
|
+
if (q.messages.length >= 10) {
|
|
587
|
+
await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
575
590
|
q.messages.push(text);
|
|
576
|
-
// Only notify once (first message), subsequent ones silently queue
|
|
577
591
|
if (isFirst) {
|
|
578
|
-
await bot.sendMessage(chatId, '📝
|
|
592
|
+
await bot.sendMessage(chatId, '📝 收到,完成后继续处理');
|
|
579
593
|
}
|
|
580
|
-
// Interrupt the running Claude process
|
|
581
|
-
const proc = activeProcesses.get(chatId);
|
|
582
|
-
if (proc && proc.child && !proc.aborted) {
|
|
583
|
-
proc.aborted = true;
|
|
584
|
-
try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
|
|
585
|
-
}
|
|
586
|
-
// Debounce: wait 5s for more messages before processing
|
|
587
|
-
if (q.timer) clearTimeout(q.timer);
|
|
588
|
-
q.timer = setTimeout(async () => {
|
|
589
|
-
// Wait for active process to fully exit (up to 10s)
|
|
590
|
-
for (let i = 0; i < 20 && activeProcesses.has(chatId); i++) {
|
|
591
|
-
await sleep(500);
|
|
592
|
-
}
|
|
593
|
-
const msgs = q.messages.splice(0);
|
|
594
|
-
messageQueue.delete(chatId);
|
|
595
|
-
if (msgs.length === 0) return;
|
|
596
|
-
const combined = msgs.join('\n');
|
|
597
|
-
log('INFO', `Processing ${msgs.length} queued message(s) for ${chatId}`);
|
|
598
|
-
resetCooldown(chatId); // queued msgs already waited, skip cooldown
|
|
599
|
-
try {
|
|
600
|
-
await handleCommand(bot, chatId, combined, config, executeTaskByName);
|
|
601
|
-
} catch (e) {
|
|
602
|
-
log('ERROR', `Queue dispatch failed: ${e.message}`);
|
|
603
|
-
}
|
|
604
|
-
}, 5000);
|
|
605
594
|
return;
|
|
606
595
|
}
|
|
607
596
|
// Strict mode: chats with a fixed agent in chat_agent_map must not cross-dispatch
|
|
@@ -643,8 +632,8 @@ function createCommandRouter(deps) {
|
|
|
643
632
|
}
|
|
644
633
|
const claudeResult = await askClaude(bot, chatId, text, config, readOnly);
|
|
645
634
|
const claudeFailed = !!(claudeResult && claudeResult.ok === false);
|
|
646
|
-
const
|
|
647
|
-
if (claudeFailed && !
|
|
635
|
+
const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
|
|
636
|
+
if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
|
|
648
637
|
const fallbackHandled = await tryHandleMacNaturalLanguageIntent(bot, chatId, text, config, {
|
|
649
638
|
source: 'claude-fallback',
|
|
650
639
|
safeOnly: true,
|
|
@@ -654,6 +643,20 @@ function createCommandRouter(deps) {
|
|
|
654
643
|
log('WARN', `Claude-first mac fallback handled for ${String(chatId).slice(-8)} (mode=${macControlMode})`);
|
|
655
644
|
}
|
|
656
645
|
}
|
|
646
|
+
|
|
647
|
+
// Process queued messages as follow-up in the same session (no kill, no context loss)
|
|
648
|
+
// Use while-loop instead of recursion to avoid unbounded stack growth
|
|
649
|
+
while (messageQueue.has(chatId)) {
|
|
650
|
+
const q = messageQueue.get(chatId);
|
|
651
|
+
const msgs = q.messages.splice(0);
|
|
652
|
+
messageQueue.delete(chatId);
|
|
653
|
+
if (msgs.length === 0) break;
|
|
654
|
+
const combined = msgs.join('\n');
|
|
655
|
+
log('INFO', `Follow-up: processing ${msgs.length} queued message(s) for ${chatId}`);
|
|
656
|
+
resetCooldown(chatId);
|
|
657
|
+
const followUp = await askClaude(bot, chatId, combined, config, readOnly);
|
|
658
|
+
if (followUp && followUp.error === 'Stopped by user') break;
|
|
659
|
+
}
|
|
657
660
|
}
|
|
658
661
|
|
|
659
662
|
return { handleCommand };
|
|
@@ -63,6 +63,24 @@ heartbeat:
|
|
|
63
63
|
notify: false
|
|
64
64
|
enabled: true
|
|
65
65
|
|
|
66
|
+
# 夜间记忆蒸馏:每天 01:00 提炼热区事实为决策与经验文档
|
|
67
|
+
- name: nightly-reflect
|
|
68
|
+
type: script
|
|
69
|
+
command: node ~/.metame/memory-nightly-reflect.js
|
|
70
|
+
at: "01:00"
|
|
71
|
+
require_idle: true
|
|
72
|
+
notify: false
|
|
73
|
+
enabled: true
|
|
74
|
+
|
|
75
|
+
# 记忆索引:每天 01:30 更新 ~/.metame/memory/INDEX.md
|
|
76
|
+
- name: memory-index
|
|
77
|
+
type: script
|
|
78
|
+
command: node ~/.metame/memory-index.js
|
|
79
|
+
at: "01:30"
|
|
80
|
+
require_idle: true
|
|
81
|
+
notify: false
|
|
82
|
+
enabled: true
|
|
83
|
+
|
|
66
84
|
# Legacy flat tasks (no project isolation). New tasks should go under projects: above.
|
|
67
85
|
# Examples — uncomment or add your own:
|
|
68
86
|
#
|
|
@@ -192,8 +192,8 @@ function createExecCommandHandler(deps) {
|
|
|
192
192
|
const st = loadState();
|
|
193
193
|
st.tasks[taskName] = { last_run: new Date().toISOString(), status: 'success', output_preview: (output || '').slice(0, 200) };
|
|
194
194
|
saveState(st);
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
const truncated = truncateOutput(output, 4000);
|
|
196
|
+
const reply = truncated || '(no output)';
|
|
197
197
|
await bot.sendMessage(chatId, `${taskName}\n\n${reply}`);
|
|
198
198
|
}
|
|
199
199
|
return true;
|
|
@@ -348,16 +348,10 @@ function createExecCommandHandler(deps) {
|
|
|
348
348
|
const cwd = session?.cwd || HOME;
|
|
349
349
|
await bot.sendMessage(chatId, `📦 npm publish --otp=${otp} ...`);
|
|
350
350
|
try {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
child.stderr.on('data', d => { stderr += d; });
|
|
356
|
-
const exitCode = await new Promise((resolve) => {
|
|
357
|
-
child.on('close', (code) => resolve(code));
|
|
358
|
-
child.on('error', () => resolve(1));
|
|
359
|
-
});
|
|
360
|
-
const output = (stdout + stderr).trim();
|
|
351
|
+
const result = await runCommand('npm', ['publish', `--otp=${otp}`], { cwd, timeout: 60000 });
|
|
352
|
+
const exitCode = result.code;
|
|
353
|
+
// Merge stdout+stderr: npm may print the success line to either stream depending on version
|
|
354
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
361
355
|
if (exitCode === 0 && output.includes('+ metame-cli@')) {
|
|
362
356
|
const ver = output.match(/metame-cli@([\d.]+)/);
|
|
363
357
|
await bot.sendMessage(chatId, `✅ Published${ver ? ' v' + ver[1] : ''}!`);
|
|
@@ -89,12 +89,13 @@ function createFileBrowser(deps) {
|
|
|
89
89
|
: mode === 'agent-new' ? '/agent-dir'
|
|
90
90
|
: '/cd';
|
|
91
91
|
|
|
92
|
+
const PAGE_SIZE = 10;
|
|
93
|
+
const totalPages = Math.max(1, Math.ceil(entries.length / PAGE_SIZE));
|
|
94
|
+
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
95
|
+
const start = safePage * PAGE_SIZE;
|
|
96
|
+
const pageSubdirs = entries.slice(start, start + PAGE_SIZE);
|
|
97
|
+
|
|
92
98
|
if (bot.sendButtons) {
|
|
93
|
-
const PAGE_SIZE = 10;
|
|
94
|
-
const totalPages = Math.max(1, Math.ceil(entries.length / PAGE_SIZE));
|
|
95
|
-
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
96
|
-
const start = safePage * PAGE_SIZE;
|
|
97
|
-
const pageSubdirs = entries.slice(start, start + PAGE_SIZE);
|
|
98
99
|
const buttons = [];
|
|
99
100
|
buttons.push([{ text: `✓ 选择「${displayPath}」`, callback_data: `${cmd} ${shortenPath(dirPath)}` }]);
|
|
100
101
|
for (const name of pageSubdirs) {
|
|
@@ -93,6 +93,7 @@ function setupRuntimeWatchers(deps) {
|
|
|
93
93
|
const startTime = Date.now();
|
|
94
94
|
let restartDebounce = null;
|
|
95
95
|
let pendingRestart = false;
|
|
96
|
+
let deferredRestartTimer = null; // guard: prevent duplicate deferred restart timers
|
|
96
97
|
|
|
97
98
|
fs.watchFile(daemonScript, { interval: 3000 }, (curr, prev) => {
|
|
98
99
|
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
@@ -103,8 +104,20 @@ function setupRuntimeWatchers(deps) {
|
|
|
103
104
|
log('INFO', `daemon.js changed on disk — deferring restart (${activeProcesses.size} active task(s))`);
|
|
104
105
|
pendingRestart = true;
|
|
105
106
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
// Even with no active processes, wait 5s for any in-flight cleanup
|
|
108
|
+
// (sendCard/sendMarkdown may still be running after activeProcesses.delete)
|
|
109
|
+
log('INFO', 'daemon.js changed on disk — no active tasks, restarting in 5s...');
|
|
110
|
+
if (deferredRestartTimer) clearTimeout(deferredRestartTimer);
|
|
111
|
+
deferredRestartTimer = setTimeout(() => {
|
|
112
|
+
if (activeProcesses.size > 0) {
|
|
113
|
+
log('INFO', `Deferred restart cancelled — ${activeProcesses.size} task(s) started during grace period`);
|
|
114
|
+
deferredRestartTimer = null;
|
|
115
|
+
pendingRestart = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
log('INFO', 'daemon.js changed on disk — exiting for restart...');
|
|
119
|
+
onRestartRequested();
|
|
120
|
+
}, 5000);
|
|
108
121
|
}
|
|
109
122
|
}, 2000);
|
|
110
123
|
});
|
|
@@ -112,9 +125,9 @@ function setupRuntimeWatchers(deps) {
|
|
|
112
125
|
const origDelete = activeProcesses.delete.bind(activeProcesses);
|
|
113
126
|
activeProcesses.delete = function (key) {
|
|
114
127
|
const result = origDelete(key);
|
|
115
|
-
if (pendingRestart && activeProcesses.size === 0) {
|
|
116
|
-
log('INFO', 'All tasks completed — executing deferred restart...');
|
|
117
|
-
setTimeout(onRestartRequested,
|
|
128
|
+
if (pendingRestart && activeProcesses.size === 0 && !deferredRestartTimer) {
|
|
129
|
+
log('INFO', 'All tasks completed — executing deferred restart in 8s...');
|
|
130
|
+
deferredRestartTimer = setTimeout(onRestartRequested, 8000); // 给 sendMessage/deleteMessage 等 cleanup 留出足够时间
|
|
118
131
|
}
|
|
119
132
|
return result;
|
|
120
133
|
};
|
|
@@ -124,6 +137,7 @@ function setupRuntimeWatchers(deps) {
|
|
|
124
137
|
fs.unwatchFile(daemonScript);
|
|
125
138
|
if (reloadDebounce) clearTimeout(reloadDebounce);
|
|
126
139
|
if (restartDebounce) clearTimeout(restartDebounce);
|
|
140
|
+
if (deferredRestartTimer) clearTimeout(deferredRestartTimer);
|
|
127
141
|
activeProcesses.delete = origDelete;
|
|
128
142
|
}
|
|
129
143
|
|
|
@@ -308,8 +308,14 @@ function createSessionCommandHandler(deps) {
|
|
|
308
308
|
return true;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
// /continue — alias for /cd last (sync to computer's latest session)
|
|
312
|
+
if (text === '/continue') {
|
|
313
|
+
// Reuse /cd last logic below
|
|
314
|
+
// fall through with newCwd = 'last'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (text === '/continue' || text === '/cd' || text.startsWith('/cd ')) {
|
|
318
|
+
let newCwd = text === '/continue' ? 'last' : expandPath(text.slice(3).trim());
|
|
313
319
|
if (!newCwd) {
|
|
314
320
|
await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
|
|
315
321
|
return true;
|
|
@@ -333,7 +339,6 @@ function createSessionCommandHandler(deps) {
|
|
|
333
339
|
const name = target.customTitle || target.summary || '';
|
|
334
340
|
const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
|
|
335
341
|
await bot.sendMessage(chatId, `🔄 Synced to: ${label}\n📁 ${path.basename(target.projectPath)}`);
|
|
336
|
-
await sendDirListing(bot, chatId, target.projectPath, null);
|
|
337
342
|
return true;
|
|
338
343
|
}
|
|
339
344
|
await bot.sendMessage(chatId, 'No recent session found.');
|