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
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ npm install -g metame-cli && metame
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
-
> ### 🚀 v1.4.
|
|
35
|
+
> ### 🚀 v1.4.19 — Multi-User ACL + Session Context Preview
|
|
36
36
|
>
|
|
37
37
|
> - **Multi-user permission system**: role-based ACL (admin / member / stranger) — share your bots with teammates without giving them full access. Manage users with `/user` commands.
|
|
38
38
|
> - **Session context preview**: `/resume` and `/sessions` now show the last message snippet so you know exactly what to pick up.
|
|
@@ -243,11 +243,11 @@ systemctl --user start metame
|
|
|
243
243
|
|
|
244
244
|
**Create your first Agent:**
|
|
245
245
|
|
|
246
|
-
1.
|
|
247
|
-
2.
|
|
248
|
-
3.
|
|
246
|
+
1. In any existing group, say: `Create an agent, directory ~/xxx, responsible for xxx`
|
|
247
|
+
2. Bot replies: ✅ Agent created — **send `/activate` in your new group to bind it**
|
|
248
|
+
3. Create a new group, add the bot, send `/activate` → binding complete
|
|
249
249
|
|
|
250
|
-
> Want more Agents? Repeat:
|
|
250
|
+
> Want more Agents? Repeat: create in any group → new target group → `/activate`. Each group = independent AI workspace.
|
|
251
251
|
|
|
252
252
|
---
|
|
253
253
|
|
|
@@ -274,23 +274,26 @@ MetaMe's design philosophy: **one folder = one agent.**
|
|
|
274
274
|
|
|
275
275
|
Give an agent a directory, drop a `CLAUDE.md` inside describing its role, and you're done. The folder is the agent — it can be a code project, a blog repo, any workspace you already have.
|
|
276
276
|
|
|
277
|
-
### Option 1: Just say it (
|
|
277
|
+
### Option 1: Just say it (recommended)
|
|
278
278
|
|
|
279
|
-
No commands needed. Tell the bot what you want in plain language —
|
|
279
|
+
No commands needed. Tell the bot what you want in plain language. **The agent is created without binding to the current group** — send `/activate` in your new target group to complete the binding:
|
|
280
280
|
|
|
281
281
|
```
|
|
282
|
-
You: Create an agent
|
|
283
|
-
Bot: ✅ Agent
|
|
284
|
-
Name: assistant
|
|
282
|
+
You: Create an agent, directory ~/projects/assistant, responsible for writing and content
|
|
283
|
+
Bot: ✅ Agent「assistant」created
|
|
285
284
|
Dir: ~/projects/assistant
|
|
285
|
+
📝 CLAUDE.md written
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
Next: send /activate in your new group to bind
|
|
288
|
+
|
|
289
|
+
── In the new group ──
|
|
289
290
|
|
|
290
|
-
You:
|
|
291
|
-
Bot:
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
You: /activate
|
|
292
|
+
Bot: 🤖 assistant bound
|
|
293
|
+
Dir: ~/projects/assistant
|
|
294
|
+
|
|
295
|
+
You: Change this agent's role to: focused on Python backend development
|
|
296
|
+
Bot: ✅ Role definition updated in CLAUDE.md
|
|
294
297
|
|
|
295
298
|
You: List all agents
|
|
296
299
|
Bot: 📋 Agent list
|
|
@@ -299,21 +302,22 @@ Bot: 📋 Agent list
|
|
|
299
302
|
...
|
|
300
303
|
```
|
|
301
304
|
|
|
302
|
-
Supported intents: create, bind, unbind, edit role, list — just say it naturally.
|
|
305
|
+
Supported intents: create, bind (`/agent bind`), unbind, edit role, list — just say it naturally.
|
|
303
306
|
|
|
304
|
-
### Option 2:
|
|
307
|
+
### Option 2: Commands
|
|
305
308
|
|
|
306
309
|
Use `/agent` commands in any Telegram/Feishu group:
|
|
307
310
|
|
|
308
311
|
| Command | What it does |
|
|
309
312
|
|---------|-------------|
|
|
310
|
-
| `/
|
|
311
|
-
| `/agent bind <name> [dir]` |
|
|
313
|
+
| `/activate` | In a new group, sends this to auto-bind the most recently created pending agent. |
|
|
314
|
+
| `/agent bind <name> [dir]` | Manual bind: register this group as a named agent. Works anytime — no need to recreate if agent already exists. |
|
|
312
315
|
| `/agent list` | Show all configured agents. |
|
|
313
316
|
| `/agent edit` | Update the current agent's role description (rewrites its `CLAUDE.md` section). |
|
|
317
|
+
| `/agent unbind` | Remove this group's agent binding. |
|
|
314
318
|
| `/agent reset` | Remove the current agent's role section. |
|
|
315
319
|
|
|
316
|
-
|
|
320
|
+
> **Binding protection**: Each group can only be bound to one agent. Existing bindings cannot be overwritten without explicit `force:true`.
|
|
317
321
|
|
|
318
322
|
### From config file (for power users)
|
|
319
323
|
|
|
@@ -359,6 +363,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
359
363
|
|
|
360
364
|
| Command | Action |
|
|
361
365
|
|---------|--------|
|
|
366
|
+
| `/continue` | Sync to computer's current work (session + directory) |
|
|
362
367
|
| `/last` | Resume most recent session |
|
|
363
368
|
| `/new` | Start new session (project picker) |
|
|
364
369
|
| `/resume` | Pick from session list |
|
|
@@ -367,7 +372,8 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
367
372
|
| `/undo <hash>` | Roll back to a specific git checkpoint |
|
|
368
373
|
| `/list` | Browse & download project files |
|
|
369
374
|
| `/model` | Switch model (sonnet/opus/haiku) |
|
|
370
|
-
| `/
|
|
375
|
+
| `/activate` | Activate and bind the most recently created pending agent in a new group |
|
|
376
|
+
| `/agent bind <name> [dir]` | Manually register group as dedicated agent |
|
|
371
377
|
| `/mac` | macOS control helper: permissions check/open + AppleScript/JXA execution |
|
|
372
378
|
| `/sh <cmd>` | Raw shell — bypasses Claude |
|
|
373
379
|
| `/memory` | Memory stats: fact count, session tags, DB size |
|
|
@@ -416,7 +422,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
416
422
|
## Security
|
|
417
423
|
|
|
418
424
|
- All data stays on your machine. No cloud, no telemetry.
|
|
419
|
-
- `allowed_chat_ids` whitelist —
|
|
425
|
+
- `allowed_chat_ids` whitelist — new groups get a smart prompt: if a pending agent activation exists, they're guided to send `/activate`; otherwise they receive setup instructions.
|
|
420
426
|
- `operator_ids` for shared groups — non-operators get read-only mode.
|
|
421
427
|
- `~/.metame/` directory is mode 700.
|
|
422
428
|
- Bot tokens stored locally, never transmitted.
|
|
@@ -427,7 +433,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
|
|
|
427
433
|
|--------|-------|
|
|
428
434
|
| Daemon memory (idle) | ~100 MB RSS — standard Node.js process baseline |
|
|
429
435
|
| Daemon CPU (idle, between heartbeats) | ~0% — event-loop sleeping |
|
|
430
|
-
| Cognitive profile injection | ~
|
|
436
|
+
| Cognitive profile injection | ~600 tokens/session (0.3% of 200k context) |
|
|
431
437
|
| Dispatch latency (Unix socket) | <100ms |
|
|
432
438
|
| Memory consolidation (per session) | ~1,500–2,000 tokens input + ~50–300 tokens output (Haiku) |
|
|
433
439
|
| Session summary (per session) | ~400–900 tokens input + ≤250 tokens output (Haiku) |
|
package/index.js
CHANGED
|
@@ -30,7 +30,7 @@ if (!fs.existsSync(METAME_DIR)) {
|
|
|
30
30
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
31
31
|
// IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
|
|
32
32
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
33
|
-
const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js'];
|
|
33
|
+
const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
|
|
34
34
|
const DAEMON_MODULE_SCRIPTS = (() => {
|
|
35
35
|
try {
|
|
36
36
|
return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
|
|
@@ -78,6 +78,44 @@ if (scriptsUpdated) {
|
|
|
78
78
|
console.log('📦 Scripts synced to ~/.metame/ — daemon will auto-restart when idle.');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// ---------------------------------------------------------
|
|
82
|
+
// Deploy bundled skills to ~/.claude/skills/
|
|
83
|
+
// Only installs if not already present — never overwrites user customizations.
|
|
84
|
+
// ---------------------------------------------------------
|
|
85
|
+
const CLAUDE_SKILLS_DIR = path.join(HOME_DIR, '.claude', 'skills');
|
|
86
|
+
const bundledSkillsDir = path.join(__dirname, 'skills');
|
|
87
|
+
if (fs.existsSync(bundledSkillsDir)) {
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(CLAUDE_SKILLS_DIR)) {
|
|
90
|
+
fs.mkdirSync(CLAUDE_SKILLS_DIR, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
const skillsInstalled = [];
|
|
93
|
+
for (const skillName of fs.readdirSync(bundledSkillsDir)) {
|
|
94
|
+
const srcSkill = path.join(bundledSkillsDir, skillName);
|
|
95
|
+
const destSkill = path.join(CLAUDE_SKILLS_DIR, skillName);
|
|
96
|
+
if (!fs.statSync(srcSkill).isDirectory()) continue;
|
|
97
|
+
if (fs.existsSync(destSkill)) continue; // already installed, respect user's version
|
|
98
|
+
// Copy skill directory recursively
|
|
99
|
+
const copyDir = (src, dest) => {
|
|
100
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
101
|
+
for (const entry of fs.readdirSync(src)) {
|
|
102
|
+
const s = path.join(src, entry);
|
|
103
|
+
const d = path.join(dest, entry);
|
|
104
|
+
if (fs.statSync(s).isDirectory()) copyDir(s, d);
|
|
105
|
+
else fs.copyFileSync(s, d);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
copyDir(srcSkill, destSkill);
|
|
109
|
+
skillsInstalled.push(skillName);
|
|
110
|
+
}
|
|
111
|
+
if (skillsInstalled.length > 0) {
|
|
112
|
+
console.log(`🧠 Skills installed: ${skillsInstalled.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Non-fatal
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
81
119
|
// Load daemon config for local launch flags
|
|
82
120
|
let daemonCfg = {};
|
|
83
121
|
try {
|
package/package.json
CHANGED
|
@@ -26,6 +26,10 @@ function createAdminCommandHandler(deps) {
|
|
|
26
26
|
skillEvolution,
|
|
27
27
|
taskBoard,
|
|
28
28
|
taskEnvelope,
|
|
29
|
+
getActiveProcesses,
|
|
30
|
+
getMessageQueue,
|
|
31
|
+
loadState,
|
|
32
|
+
saveState,
|
|
29
33
|
} = deps;
|
|
30
34
|
|
|
31
35
|
function resolveProjectKey(targetName, projects) {
|
|
@@ -102,7 +106,13 @@ function createAdminCommandHandler(deps) {
|
|
|
102
106
|
if (fs.existsSync(BRAIN_FILE)) {
|
|
103
107
|
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
104
108
|
if (doc.identity) msg += `\nProfile: ${doc.identity.nickname || 'unknown'}`;
|
|
105
|
-
|
|
109
|
+
const nowPath = require('path').join(require('os').homedir(), '.metame', 'memory', 'NOW.md');
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(nowPath)) {
|
|
112
|
+
const nowContent = fs.readFileSync(nowPath, 'utf8').trim().split('\n')[0];
|
|
113
|
+
if (nowContent) msg += `\nNOW: ${nowContent.slice(0, 80)}`;
|
|
114
|
+
}
|
|
115
|
+
} catch { /* ignore */ }
|
|
106
116
|
}
|
|
107
117
|
} catch { /* ignore */ }
|
|
108
118
|
await bot.sendMessage(chatId, msg);
|
|
@@ -639,6 +649,57 @@ function createAdminCommandHandler(deps) {
|
|
|
639
649
|
return { handled: true, config };
|
|
640
650
|
}
|
|
641
651
|
|
|
652
|
+
// /recover — kill all stuck tasks and reset message queues
|
|
653
|
+
if (text === '/recover') {
|
|
654
|
+
const activeProcesses = getActiveProcesses ? getActiveProcesses() : null;
|
|
655
|
+
const messageQueue = getMessageQueue ? getMessageQueue() : null;
|
|
656
|
+
if (!activeProcesses) {
|
|
657
|
+
await bot.sendMessage(chatId, '❌ 无法访问任务状态');
|
|
658
|
+
return { handled: true, config };
|
|
659
|
+
}
|
|
660
|
+
const stuckChatIds = [...activeProcesses.keys()];
|
|
661
|
+
let killed = 0;
|
|
662
|
+
for (const cid of stuckChatIds) {
|
|
663
|
+
const proc = activeProcesses.get(cid);
|
|
664
|
+
if (proc && proc.child) {
|
|
665
|
+
try { process.kill(-proc.child.pid, 'SIGTERM'); } catch { try { proc.child.kill('SIGTERM'); } catch { } }
|
|
666
|
+
killed++;
|
|
667
|
+
}
|
|
668
|
+
activeProcesses.delete(cid);
|
|
669
|
+
if (messageQueue && messageQueue.has(cid)) {
|
|
670
|
+
const q = messageQueue.get(cid);
|
|
671
|
+
if (q && q.timer) clearTimeout(q.timer);
|
|
672
|
+
messageQueue.delete(cid);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Clear stale sessions (started: false = never completed first message, likely locked)
|
|
676
|
+
try {
|
|
677
|
+
const state = loadState();
|
|
678
|
+
let cleared = 0;
|
|
679
|
+
for (const [cid, sess] of Object.entries(state.sessions || {})) {
|
|
680
|
+
if (sess && !sess.started) {
|
|
681
|
+
delete state.sessions[cid];
|
|
682
|
+
cleared++;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (cleared > 0) saveState(state);
|
|
686
|
+
} catch { /* non-critical */ }
|
|
687
|
+
// SIGKILL stragglers after 3s grace period
|
|
688
|
+
if (killed > 0) {
|
|
689
|
+
setTimeout(() => {
|
|
690
|
+
for (const cid of stuckChatIds) {
|
|
691
|
+
// proc references are stale but child.pid is still valid for cleanup
|
|
692
|
+
try { const proc = activeProcesses.get(cid); if (proc && proc.child) process.kill(-proc.child.pid, 'SIGKILL'); } catch { }
|
|
693
|
+
}
|
|
694
|
+
}, 3000);
|
|
695
|
+
}
|
|
696
|
+
const summary = killed > 0
|
|
697
|
+
? `✅ 已重置 ${killed} 个卡住的任务,可重新发送消息。`
|
|
698
|
+
: '✅ 当前没有卡住的任务。';
|
|
699
|
+
await bot.sendMessage(chatId, summary);
|
|
700
|
+
return { handled: true, config };
|
|
701
|
+
}
|
|
702
|
+
|
|
642
703
|
// /doctor — diagnostics; /fix — restore backup; /reset — reset model to sonnet
|
|
643
704
|
if (text === '/fix') {
|
|
644
705
|
if (restoreConfig()) {
|
|
@@ -696,15 +757,36 @@ function createAdminCommandHandler(deps) {
|
|
|
696
757
|
const hasBak = fs.existsSync(bakFile);
|
|
697
758
|
checks.push(hasBak ? '✅ 有备份' : '⚠️ 无备份');
|
|
698
759
|
|
|
760
|
+
// Check for stuck tasks (only flag tasks running > 10 minutes as suspicious)
|
|
761
|
+
const activeProcesses = getActiveProcesses ? getActiveProcesses() : null;
|
|
762
|
+
let hasStuck = false;
|
|
763
|
+
if (activeProcesses && activeProcesses.size > 0) {
|
|
764
|
+
const now = Date.now();
|
|
765
|
+
const stuckThreshold = 10 * 60 * 1000; // 10 minutes
|
|
766
|
+
const entries = [...activeProcesses.entries()];
|
|
767
|
+
const stuckEntries = entries.filter(([, proc]) => proc && proc.startedAt && (now - proc.startedAt) > stuckThreshold);
|
|
768
|
+
if (stuckEntries.length > 0) {
|
|
769
|
+
const stuckList = stuckEntries.map(([cid, proc]) => `${cid.slice(-8)}(${Math.round((now - proc.startedAt) / 60000)}min)`).join(', ');
|
|
770
|
+
checks.push(`⚠️ ${stuckEntries.length} 个任务疑似卡住 (${stuckList})`);
|
|
771
|
+
hasStuck = true;
|
|
772
|
+
issues++;
|
|
773
|
+
} else {
|
|
774
|
+
checks.push(`✅ ${entries.length} 个任务正常运行中`);
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
checks.push('✅ 无运行中任务');
|
|
778
|
+
}
|
|
779
|
+
|
|
699
780
|
let msg = `🏥 诊断\n${checks.join('\n')}`;
|
|
700
781
|
if (issues > 0) {
|
|
701
782
|
if (bot.sendButtons) {
|
|
702
783
|
const buttons = [];
|
|
703
|
-
if (
|
|
704
|
-
buttons.push([{ text: '
|
|
784
|
+
if (hasStuck) buttons.push([{ text: '🔧 一键重置卡住任务', callback_data: '/recover' }]);
|
|
785
|
+
if (hasBak) buttons.push([{ text: '📦 恢复配置备份', callback_data: '/fix' }]);
|
|
786
|
+
buttons.push([{ text: '🔄 重置模型 opus', callback_data: '/reset' }]);
|
|
705
787
|
await bot.sendButtons(chatId, msg, buttons);
|
|
706
788
|
} else {
|
|
707
|
-
msg += '\n/fix 恢复备份 /reset 重置opus';
|
|
789
|
+
msg += '\n/recover 重置卡住任务 /fix 恢复备份 /reset 重置opus';
|
|
708
790
|
await bot.sendMessage(chatId, msg);
|
|
709
791
|
}
|
|
710
792
|
} else {
|
|
@@ -21,6 +21,7 @@ function createAgentCommandHandler(deps) {
|
|
|
21
21
|
getSessionRecentContext,
|
|
22
22
|
pendingBinds,
|
|
23
23
|
pendingAgentFlows,
|
|
24
|
+
pendingActivations,
|
|
24
25
|
doBindAgent,
|
|
25
26
|
mergeAgentRole,
|
|
26
27
|
agentTools,
|
|
@@ -29,6 +30,30 @@ function createAgentCommandHandler(deps) {
|
|
|
29
30
|
agentBindTtlMs,
|
|
30
31
|
} = deps;
|
|
31
32
|
|
|
33
|
+
// Pending activations have no TTL — they persist until consumed.
|
|
34
|
+
// The creating chatId is stored to prevent self-activation.
|
|
35
|
+
|
|
36
|
+
function storePendingActivation(agentKey, agentName, cwd, createdByChatId) {
|
|
37
|
+
if (!pendingActivations) return;
|
|
38
|
+
pendingActivations.set(agentKey, {
|
|
39
|
+
agentKey, agentName, cwd,
|
|
40
|
+
createdByChatId: String(createdByChatId),
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns the latest pending activation, excluding the creating chat
|
|
46
|
+
function getLatestActivationForChat(chatId) {
|
|
47
|
+
if (!pendingActivations || pendingActivations.size === 0) return null;
|
|
48
|
+
const cid = String(chatId);
|
|
49
|
+
let latest = null;
|
|
50
|
+
for (const rec of pendingActivations.values()) {
|
|
51
|
+
if (rec.createdByChatId === cid) continue; // creating chat cannot self-activate
|
|
52
|
+
if (!latest || rec.createdAt > latest.createdAt) latest = rec;
|
|
53
|
+
}
|
|
54
|
+
return latest;
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
function resolveTtl(valueOrGetter, fallbackMs) {
|
|
33
58
|
const raw = typeof valueOrGetter === 'function' ? valueOrGetter() : valueOrGetter;
|
|
34
59
|
const num = Number(raw);
|
|
@@ -136,9 +161,15 @@ function createAgentCommandHandler(deps) {
|
|
|
136
161
|
return { ok: true, data: legacy };
|
|
137
162
|
}
|
|
138
163
|
|
|
139
|
-
async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc) {
|
|
164
|
+
async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
|
|
165
|
+
// Default: skip binding the creating chat — let the target group activate via /activate
|
|
166
|
+
const { skipChatBinding = true } = opts;
|
|
140
167
|
if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
|
|
141
|
-
|
|
168
|
+
const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding });
|
|
169
|
+
if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
|
|
170
|
+
storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
|
|
171
|
+
}
|
|
172
|
+
return res;
|
|
142
173
|
}
|
|
143
174
|
const bound = await doBindAgent({ sendMessage: async () => {} }, chatId, name, dir);
|
|
144
175
|
if (!bound || bound.ok === false) {
|
|
@@ -272,40 +303,7 @@ function createAgentCommandHandler(deps) {
|
|
|
272
303
|
return true;
|
|
273
304
|
}
|
|
274
305
|
|
|
275
|
-
//
|
|
276
|
-
{
|
|
277
|
-
const flow = getFreshFlow(String(chatId));
|
|
278
|
-
if (flow && flow.step === 'name' && text && !text.startsWith('/')) {
|
|
279
|
-
flow.name = text.trim();
|
|
280
|
-
flow.step = 'desc';
|
|
281
|
-
setFlow(String(chatId), flow);
|
|
282
|
-
await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n请描述这个 Agent 的角色和职责(用自然语言):`);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
if (flow && flow.step === 'desc' && text && !text.startsWith('/')) {
|
|
286
|
-
pendingAgentFlows.delete(String(chatId));
|
|
287
|
-
const { dir, name } = flow;
|
|
288
|
-
const description = text.trim();
|
|
289
|
-
await bot.sendMessage(chatId, `⏳ 正在配置 Agent「${name}」,稍等...`);
|
|
290
|
-
const created = await createAgentViaUnifiedApi(chatId, name, dir, description);
|
|
291
|
-
if (!created.ok) {
|
|
292
|
-
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${created.error}`);
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
if (created.data && created.data.cwd && typeof attachOrCreateSession === 'function') {
|
|
296
|
-
attachOrCreateSession(chatId, normalizeCwd(created.data.cwd), name || '');
|
|
297
|
-
}
|
|
298
|
-
const roleInfo = created.data.role || {};
|
|
299
|
-
if (roleInfo.skipped) {
|
|
300
|
-
await bot.sendMessage(chatId, '✅ Agent 创建成功');
|
|
301
|
-
} else if (roleInfo.created) {
|
|
302
|
-
await bot.sendMessage(chatId, '📝 已创建 CLAUDE.md 并写入角色定义');
|
|
303
|
-
} else {
|
|
304
|
-
await bot.sendMessage(chatId, '📝 已将角色定义合并进现有 CLAUDE.md');
|
|
305
|
-
}
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
306
|
+
// wizard state machine removed — use natural language to create agents
|
|
309
307
|
|
|
310
308
|
// /agent edit wait-input flow (kept for command compatibility)
|
|
311
309
|
{
|
|
@@ -355,7 +353,7 @@ function createAgentCommandHandler(deps) {
|
|
|
355
353
|
}
|
|
356
354
|
const agents = res.data.agents || [];
|
|
357
355
|
if (agents.length === 0) {
|
|
358
|
-
await bot.sendMessage(chatId, '暂无已配置的 Agent。\n
|
|
356
|
+
await bot.sendMessage(chatId, '暂无已配置的 Agent。\n用自然语言说"创建一个agent,目录是~/xxx",或 /agent bind <名称> <目录>。');
|
|
359
357
|
return true;
|
|
360
358
|
}
|
|
361
359
|
const lines = ['📋 已配置的 Agent:', ''];
|
|
@@ -373,19 +371,12 @@ function createAgentCommandHandler(deps) {
|
|
|
373
371
|
return true;
|
|
374
372
|
}
|
|
375
373
|
|
|
376
|
-
// /agent new (wizard)
|
|
377
|
-
if (agentSub === 'new') {
|
|
378
|
-
setFlow(String(chatId), { step: 'dir' });
|
|
379
|
-
await sendBrowse(bot, chatId, 'agent-new', HOME, '步骤1/3:选择这个 Agent 的工作目录');
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
374
|
// /agent edit [描述]
|
|
384
375
|
if (agentSub === 'edit') {
|
|
385
376
|
const cfg = loadConfig();
|
|
386
377
|
const { boundProj } = getBoundProject(chatId, cfg);
|
|
387
378
|
if (!boundProj || !boundProj.cwd) {
|
|
388
|
-
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent
|
|
379
|
+
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先用自然语言创建 Agent 或 /agent bind <名称> <目录>');
|
|
389
380
|
return true;
|
|
390
381
|
}
|
|
391
382
|
const cwd = normalizeCwd(boundProj.cwd);
|
|
@@ -432,7 +423,7 @@ function createAgentCommandHandler(deps) {
|
|
|
432
423
|
const cfg = loadConfig();
|
|
433
424
|
const { boundProj } = getBoundProject(chatId, cfg);
|
|
434
425
|
if (!boundProj || !boundProj.cwd) {
|
|
435
|
-
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent
|
|
426
|
+
await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先用自然语言创建 Agent 或 /agent bind <名称> <目录>');
|
|
436
427
|
return true;
|
|
437
428
|
}
|
|
438
429
|
const cwd = normalizeCwd(boundProj.cwd);
|
|
@@ -457,7 +448,7 @@ function createAgentCommandHandler(deps) {
|
|
|
457
448
|
const projects = config.projects || {};
|
|
458
449
|
const entries = Object.entries(projects).filter(([, p]) => p.cwd);
|
|
459
450
|
if (entries.length === 0) {
|
|
460
|
-
await bot.sendMessage(chatId, '暂无已配置的 Agent。\n
|
|
451
|
+
await bot.sendMessage(chatId, '暂无已配置的 Agent。\n用自然语言说"创建一个agent,目录是~/xxx",或 /agent bind <名称> <目录>。');
|
|
461
452
|
return true;
|
|
462
453
|
}
|
|
463
454
|
const currentSession = getSession(chatId);
|
|
@@ -472,6 +463,41 @@ function createAgentCommandHandler(deps) {
|
|
|
472
463
|
}
|
|
473
464
|
}
|
|
474
465
|
|
|
466
|
+
// /activate — bind this unbound chat to the most recently created pending agent
|
|
467
|
+
if (text === '/activate' || text.startsWith('/activate ')) {
|
|
468
|
+
const cfg = loadConfig();
|
|
469
|
+
const { boundKey } = getBoundProject(chatId, cfg);
|
|
470
|
+
if (boundKey) {
|
|
471
|
+
await bot.sendMessage(chatId, `此群已绑定到「${boundKey}」,无需激活。如需更换请先 /agent unbind`);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
const activation = getLatestActivationForChat(chatId);
|
|
475
|
+
if (!activation) {
|
|
476
|
+
// Check if this chat was the creator (self-activate attempt)
|
|
477
|
+
if (pendingActivations) {
|
|
478
|
+
for (const rec of pendingActivations.values()) {
|
|
479
|
+
if (rec.createdByChatId === String(chatId)) {
|
|
480
|
+
await bot.sendMessage(chatId,
|
|
481
|
+
`❌ 不能在创建来源群激活。\n请在你新建的目标群里发送 \`/activate\`\n\n` +
|
|
482
|
+
`或在任意群用: \`/agent bind ${rec.agentName} ${rec.cwd}\``
|
|
483
|
+
);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// No pending activation at all — guide to manual bind
|
|
489
|
+
await bot.sendMessage(chatId,
|
|
490
|
+
'没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
|
|
491
|
+
);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
const bindRes = await bindViaUnifiedApi(bot, chatId, activation.agentName, activation.cwd);
|
|
495
|
+
if (bindRes.ok) {
|
|
496
|
+
pendingActivations && pendingActivations.delete(activation.agentKey);
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
475
501
|
// /agent-bind-dir <path>: internal callback for bind picker
|
|
476
502
|
if (text.startsWith('/agent-bind-dir ')) {
|
|
477
503
|
const dirPath = expandPath(text.slice(16).trim());
|
|
@@ -485,22 +511,6 @@ function createAgentCommandHandler(deps) {
|
|
|
485
511
|
return true;
|
|
486
512
|
}
|
|
487
513
|
|
|
488
|
-
// /agent-dir <path>: internal callback for /agent new wizard
|
|
489
|
-
if (text.startsWith('/agent-dir ')) {
|
|
490
|
-
const dirPath = expandPath(text.slice(11).trim());
|
|
491
|
-
const flow = getFreshFlow(String(chatId));
|
|
492
|
-
if (!flow || flow.step !== 'dir') {
|
|
493
|
-
await bot.sendMessage(chatId, '❌ 没有待完成的 /agent new,请重新发送 /agent new');
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
flow.dir = dirPath;
|
|
497
|
-
flow.step = 'name';
|
|
498
|
-
setFlow(String(chatId), flow);
|
|
499
|
-
const displayPath = dirPath.replace(HOME, '~');
|
|
500
|
-
await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}\n\n步骤2/3:给这个 Agent 起个名字?`);
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
514
|
return false;
|
|
505
515
|
}
|
|
506
516
|
|
|
@@ -37,7 +37,7 @@ function createAgentTools(deps) {
|
|
|
37
37
|
if (!cfg[adapterKey].chat_agent_map) cfg[adapterKey].chat_agent_map = {};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function bindAgentToChat(chatId, agentName, workspaceDir) {
|
|
40
|
+
async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false } = {}) {
|
|
41
41
|
try {
|
|
42
42
|
const safeName = sanitizeText(agentName, 120);
|
|
43
43
|
if (!safeName) return { ok: false, error: 'agentName is required' };
|
|
@@ -64,6 +64,16 @@ function createAgentTools(deps) {
|
|
|
64
64
|
return { ok: false, error: `workspaceDir is not a directory: ${resolvedDir}` };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Overwrite protection: reject if chat is already bound to a different agent
|
|
68
|
+
const existingKey = cfg[adapterKey].chat_agent_map[String(chatId)];
|
|
69
|
+
if (existingKey && existingKey !== projectKey && !force) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: `此群已绑定到 "${existingKey}",如需覆盖请使用 force:true`,
|
|
73
|
+
data: { existingKey },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
const idVal = typeof chatId === 'number' ? chatId : String(chatId);
|
|
68
78
|
if (!cfg[adapterKey].allowed_chat_ids.includes(idVal)) cfg[adapterKey].allowed_chat_ids.push(idVal);
|
|
69
79
|
|
|
@@ -165,30 +175,57 @@ ${safeDelta}
|
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId) {
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false } = {}) {
|
|
179
|
+
let bindData;
|
|
180
|
+
|
|
181
|
+
if (skipChatBinding) {
|
|
182
|
+
// Create the project entry without touching chat_agent_map
|
|
183
|
+
const safeName = sanitizeText(agentName, 120);
|
|
184
|
+
if (!safeName) return { ok: false, error: 'agentName is required' };
|
|
185
|
+
const resolvedDir = resolveWorkspaceDir(workspaceDir);
|
|
186
|
+
if (!resolvedDir) return { ok: false, error: 'workspaceDir is required' };
|
|
187
|
+
if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
|
|
188
|
+
return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
|
|
189
|
+
}
|
|
190
|
+
const cfg = loadConfig();
|
|
191
|
+
if (!cfg.projects) cfg.projects = {};
|
|
192
|
+
const projectKey = toProjectKey(safeName, chatId);
|
|
193
|
+
const existed = !!cfg.projects[projectKey];
|
|
194
|
+
if (!existed) {
|
|
195
|
+
cfg.projects[projectKey] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
|
|
196
|
+
writeConfigSafe(cfg);
|
|
197
|
+
backupConfig();
|
|
198
|
+
}
|
|
199
|
+
bindData = {
|
|
200
|
+
projectKey,
|
|
201
|
+
cwd: resolvedDir,
|
|
202
|
+
isNewProject: !existed,
|
|
203
|
+
chatId: null, // not bound to any chat
|
|
204
|
+
project: cfg.projects[projectKey],
|
|
205
|
+
};
|
|
206
|
+
} else {
|
|
207
|
+
const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
|
|
208
|
+
if (!bindResult.ok) return bindResult;
|
|
209
|
+
bindData = bindResult.data;
|
|
210
|
+
}
|
|
171
211
|
|
|
172
212
|
const roleText = sanitizeText(roleDescription, 1200);
|
|
173
213
|
if (!roleText) {
|
|
174
|
-
return { ok: true, data: { ...
|
|
214
|
+
return { ok: true, data: { ...bindData, role: { skipped: true } } };
|
|
175
215
|
}
|
|
176
216
|
|
|
177
|
-
const roleResult = await editAgentRoleDefinition(
|
|
217
|
+
const roleResult = await editAgentRoleDefinition(bindData.cwd, roleText);
|
|
178
218
|
if (!roleResult.ok) {
|
|
179
219
|
return {
|
|
180
220
|
ok: false,
|
|
181
|
-
error: `agent
|
|
182
|
-
data: { ...
|
|
221
|
+
error: `agent created but role update failed: ${roleResult.error}`,
|
|
222
|
+
data: { ...bindData, roleError: roleResult.error },
|
|
183
223
|
};
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
return {
|
|
187
227
|
ok: true,
|
|
188
|
-
data: {
|
|
189
|
-
...bindResult.data,
|
|
190
|
-
role: roleResult.data,
|
|
191
|
-
},
|
|
228
|
+
data: { ...bindData, role: roleResult.data },
|
|
192
229
|
};
|
|
193
230
|
}
|
|
194
231
|
|