metame-cli 1.4.18 → 1.4.20
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 +124 -38
- package/index.js +39 -1
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +91 -62
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -32
- package/scripts/daemon-command-router.js +32 -15
- 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-store.js +176 -41
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +43 -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
|
@@ -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,15 +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
|
-
|
|
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:
|
|
703
783
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
704
784
|
2. File sending: User is on MOBILE. When they ask to see/download a file:
|
|
705
785
|
- Just FIND the file path (use Glob/ls if needed)
|
|
706
786
|
- Do NOT read or summarize the file content (wastes tokens)
|
|
707
787
|
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
708
788
|
- Keep response brief: "请查收~! [[FILE:/path/to/file]]"
|
|
709
|
-
- Multiple files: use multiple [[FILE:...]] tags
|
|
789
|
+
- Multiple files: use multiple [[FILE:...]] tags${zdpHint ? '\n Explanation depth (ZPD):\n' + zdpHint : ''}${taskRules}]`;
|
|
790
|
+
}
|
|
710
791
|
|
|
711
792
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
712
793
|
|
|
@@ -719,7 +800,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
719
800
|
2. Read/query actions can execute directly.
|
|
720
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.
|
|
721
802
|
4. Keep output concise: success/failure + key result only.
|
|
722
|
-
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).]`;
|
|
723
805
|
}
|
|
724
806
|
|
|
725
807
|
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
@@ -741,7 +823,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
741
823
|
} catch { /* non-critical */ }
|
|
742
824
|
}
|
|
743
825
|
|
|
744
|
-
|
|
826
|
+
// Always append a compact language guard to prevent accidental Korean/Japanese responses
|
|
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.]';
|
|
828
|
+
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint + langGuard;
|
|
745
829
|
|
|
746
830
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
747
831
|
// Pass the user prompt as label so checkpoint list is human-readable
|
|
@@ -767,7 +851,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
767
851
|
} catch { /* ignore status update failures */ }
|
|
768
852
|
};
|
|
769
853
|
|
|
770
|
-
|
|
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
|
+
}
|
|
771
864
|
clearInterval(typingTimer);
|
|
772
865
|
|
|
773
866
|
// Skill evolution: capture signal + hot path heuristic check
|
|
@@ -816,14 +909,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
816
909
|
const builtinModelsCheck = ['sonnet', 'opus', 'haiku'];
|
|
817
910
|
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
818
911
|
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
819
|
-
log('WARN', `Custom provider/model may have failed (${activeProvCheck}/${model}), output: ${output.slice(0, 200)}`);
|
|
820
912
|
try {
|
|
821
|
-
|
|
822
|
-
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
823
|
-
if (!cfg.daemon) cfg.daemon = {};
|
|
824
|
-
cfg.daemon.model = 'opus';
|
|
825
|
-
writeConfigSafe(cfg);
|
|
826
|
-
config = loadConfig();
|
|
913
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
827
914
|
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
828
915
|
} catch (fbErr) {
|
|
829
916
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
@@ -889,8 +976,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
889
976
|
log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
|
|
890
977
|
|
|
891
978
|
// If session not found (expired/deleted), create new and retry once
|
|
892
|
-
if (errMsg.includes('not found') || errMsg.includes('No session')) {
|
|
893
|
-
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`);
|
|
894
981
|
session = createSession(chatId, session.cwd);
|
|
895
982
|
|
|
896
983
|
const retryArgs = ['-p', '--session-id', session.id];
|
|
@@ -922,14 +1009,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
922
1009
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
923
1010
|
const builtinModels = ['sonnet', 'opus', 'haiku'];
|
|
924
1011
|
if (activeProv !== 'anthropic' || !builtinModels.includes(model)) {
|
|
925
|
-
log('WARN', `Custom provider/model failed (${activeProv}/${model}), falling back to anthropic/opus`);
|
|
926
1012
|
try {
|
|
927
|
-
|
|
928
|
-
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
929
|
-
if (!cfg.daemon) cfg.daemon = {};
|
|
930
|
-
cfg.daemon.model = 'opus';
|
|
931
|
-
writeConfigSafe(cfg);
|
|
932
|
-
config = loadConfig();
|
|
1013
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
933
1014
|
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
934
1015
|
} catch (fallbackErr) {
|
|
935
1016
|
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
@@ -941,8 +1022,6 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
941
1022
|
return { ok: false, error: errMsg };
|
|
942
1023
|
}
|
|
943
1024
|
}
|
|
944
|
-
|
|
945
|
-
return { ok: true };
|
|
946
1025
|
}
|
|
947
1026
|
|
|
948
1027
|
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
|
|
|
@@ -604,19 +614,26 @@ function createCommandRouter(deps) {
|
|
|
604
614
|
}, 5000);
|
|
605
615
|
return;
|
|
606
616
|
}
|
|
617
|
+
// Strict mode: chats with a fixed agent in chat_agent_map must not cross-dispatch
|
|
618
|
+
const _strictChatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
619
|
+
const _isStrictChat = !!(_strictChatAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
620
|
+
|
|
607
621
|
// Nickname-only switch: bypass cooldown + budget (no Claude call)
|
|
608
|
-
|
|
609
|
-
if (
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
622
|
+
// Skipped for strict chats (fixed-agent groups)
|
|
623
|
+
if (!_isStrictChat) {
|
|
624
|
+
const quickAgent = routeAgent(text, config);
|
|
625
|
+
if (quickAgent && !quickAgent.rest) {
|
|
626
|
+
const { key, proj } = quickAgent;
|
|
627
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
628
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key);
|
|
629
|
+
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
630
|
+
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
617
633
|
|
|
618
|
-
|
|
619
|
-
|
|
634
|
+
if (await tryHandleAgentIntent(bot, chatId, text, config)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
620
637
|
}
|
|
621
638
|
|
|
622
639
|
const daemonCfg = (config && config.daemon) || {};
|
|
@@ -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
|
|