metame-cli 1.6.2 → 1.6.3

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/index.js CHANGED
@@ -1532,7 +1532,18 @@ if (AUTO_UPDATE.enabled) {
1532
1532
  timeout: 60000,
1533
1533
  ...(process.platform === 'win32' ? { shell: process.env.COMSPEC || true } : {}),
1534
1534
  });
1535
- console.log(`${icon("ok")} Updated to ${latest}. Restart metame to use the new version.`);
1535
+ // Auto-restart the running daemon so the new code takes effect immediately.
1536
+ // Safe by design: requestDaemonRestart short-circuits with status 'not_running'
1537
+ // when no daemon.pid exists, and the current CLI process is never itself the
1538
+ // daemon (metame daemon spawns a separate node process via DAEMON_SCRIPT).
1539
+ const restart = requestDaemonRestart({ reason: 'auto-update' });
1540
+ if (restart.ok) {
1541
+ console.log(`${icon("ok")} Updated to ${latest}. Daemon restart requested (${restart.status}).`);
1542
+ } else if (restart.status === 'not_running') {
1543
+ console.log(`${icon("ok")} Updated to ${latest}.`);
1544
+ } else {
1545
+ console.log(`${icon("ok")} Updated to ${latest}. Daemon restart failed (${restart.status}); run \`metame restart\`.`);
1546
+ }
1536
1547
  } catch (e) {
1537
1548
  const msg = e.stderr ? e.stderr.toString().trim().split('\n').pop() : '';
1538
1549
  console.log(`${icon("warn")} Auto-update failed${msg ? ': ' + msg : ''}. Run manually: npm install -g metame-cli`);
@@ -2080,17 +2091,44 @@ if (isDaemon) {
2080
2091
  console.log(" Skipped.\n");
2081
2092
  }
2082
2093
 
2094
+ // Tiny cross-platform "open URL in default browser" helper. Fire-and-
2095
+ // forget — if the GUI isn't available (SSH/Docker/CI), we silently fall
2096
+ // back and the user copy-pastes the URL from the terminal output.
2097
+ const tryOpenInBrowser = (url) => {
2098
+ try {
2099
+ const { spawn: _spawn } = require('child_process');
2100
+ const platform = process.platform;
2101
+ const cmd = platform === 'darwin'
2102
+ ? 'open'
2103
+ : platform === 'win32'
2104
+ ? 'cmd'
2105
+ : 'xdg-open';
2106
+ const args = platform === 'win32' ? ['/c', 'start', '""', url] : [url];
2107
+ const child = _spawn(cmd, args, {
2108
+ stdio: 'ignore',
2109
+ detached: true,
2110
+ ...(platform === 'win32' ? { shell: false, windowsHide: true } : {}),
2111
+ });
2112
+ child.on('error', () => { /* GUI unavailable, ignore */ });
2113
+ child.unref();
2114
+ return true;
2115
+ } catch { return false; }
2116
+ };
2117
+
2083
2118
  // --- Feishu Setup ---
2084
2119
  console.log(`━━━ ${icon("feishu")} Feishu (Lark) Setup ━━━`);
2085
2120
  console.log("");
2121
+ const FEISHU_CONSOLE_URL = 'https://open.feishu.cn/app';
2086
2122
  console.log("Step 1: Create an App");
2087
- console.log("Go to: https://open.feishu.cn/app");
2123
+ console.log(`Opening browser to ${FEISHU_CONSOLE_URL} ...`);
2124
+ const opened = tryOpenInBrowser(FEISHU_CONSOLE_URL);
2125
+ if (!opened) console.log(` • Open this URL manually: ${FEISHU_CONSOLE_URL}`);
2088
2126
  console.log(" • Click '创建企业自建应用' (Create Enterprise App)");
2089
2127
  console.log(" • Fill in app name and description");
2090
2128
  console.log("");
2091
2129
  console.log("Step 2: Get Credentials");
2092
2130
  console.log(" • In left sidebar → '凭证与基础信息' (Credentials)");
2093
- console.log(" • Copy App ID and App Secret");
2131
+ console.log(" • Copy App ID and App Secret — you'll paste them below.");
2094
2132
  console.log("");
2095
2133
  console.log("Step 3: Enable Bot");
2096
2134
  console.log(" • In left sidebar → '应用能力' → '机器人' (Bot)");
@@ -2103,12 +2141,14 @@ if (isDaemon) {
2103
2141
  console.log("");
2104
2142
  console.log("Step 5: Add Permissions");
2105
2143
  console.log(" • In left sidebar → '权限管理' (Permissions)");
2106
- console.log(" • Search and enable these 5 permissions:");
2144
+ console.log(" • Search and enable these 7 permissions:");
2107
2145
  console.log(" → im:message (获取与发送单聊、群组消息)");
2108
2146
  console.log(" → im:message.p2p_msg:readonly (读取用户发给机器人的单聊消息)");
2109
2147
  console.log(" → im:message.group_at_msg:readonly (接收群聊中@机器人消息事件)");
2110
2148
  console.log(" → im:message:send_as_bot (以应用的身份发消息)");
2111
- console.log(" → im:resource (文件上传下载 - for file transfer)");
2149
+ console.log(" → im:resource (文件上传下载 - send-to-user skill)");
2150
+ console.log(" → im:chat (创建群聊 - 自动建 agent 群)");
2151
+ console.log(" → im:chat.member (邀请用户进群 - 自动建 agent 群)");
2112
2152
  console.log("");
2113
2153
  console.log("Step 6: Publish");
2114
2154
  console.log(" • In left sidebar → '版本管理与发布' (Version Management)");
@@ -2125,7 +2165,47 @@ if (isDaemon) {
2125
2165
  cfg.feishu.app_id = feishuAppId;
2126
2166
  cfg.feishu.app_secret = feishuSecret;
2127
2167
  if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
2128
- console.log(` ${icon("ok")} Feishu configured!`);
2168
+
2169
+ // Immediate credential validation — catches "wrong app_secret",
2170
+ // "app not published", "permission missing" etc. up front instead
2171
+ // of after the user has finished setup and tried to send a message.
2172
+ //
2173
+ // CAUTION: feishu-adapter.js triggers a synchronous SDK auto-install
2174
+ // (and process.exit(1) on failure) at module-load time when the
2175
+ // @larksuiteoapi/node-sdk is missing. We MUST avoid that path here:
2176
+ // (a) it would block the wizard for tens of seconds, and (b) an
2177
+ // install failure would kill `metame daemon init` outright,
2178
+ // bypassing our try/catch. Pre-check via require.resolve and skip
2179
+ // validation if the SDK isn't already available — the daemon will
2180
+ // install it lazily on first start and validate then.
2181
+ console.log(`\n ${icon("info")} Validating credentials with Feishu...`);
2182
+ let sdkResolved = null;
2183
+ try {
2184
+ sdkResolved = require.resolve('@larksuiteoapi/node-sdk');
2185
+ } catch { /* not yet installed */ }
2186
+ if (!sdkResolved) {
2187
+ console.log(` ${icon("info")} Skipped: Feishu SDK not installed yet (will validate on first daemon start).`);
2188
+ } else {
2189
+ try {
2190
+ const feishuAdapter = require(path.join(__dirname, 'scripts', 'feishu-adapter.js'));
2191
+ const bot = feishuAdapter.createBot({
2192
+ app_id: feishuAppId,
2193
+ app_secret: feishuSecret,
2194
+ });
2195
+ const validation = await bot.validateCredentials();
2196
+ if (validation && validation.ok) {
2197
+ console.log(` ${icon("ok")} Credentials valid. Feishu configured!`);
2198
+ } else {
2199
+ console.log(` ${icon("warn")} Credentials accepted but Feishu rejected the test call:`);
2200
+ console.log(` ${validation && validation.error ? validation.error : 'unknown error'}`);
2201
+ console.log(" Common causes: app not published yet, wrong app_secret,");
2202
+ console.log(" missing im:message permission. Daemon will still start; fix");
2203
+ console.log(" in the console and re-run `metame restart`.");
2204
+ }
2205
+ } catch (e) {
2206
+ console.log(` ${icon("warn")} Validation skipped (${e.message}). Config saved anyway.`);
2207
+ }
2208
+ }
2129
2209
  console.log(" Note: allowed_chat_ids is empty = deny all users.");
2130
2210
  console.log(" Add chat IDs to daemon.yaml or use /agent bind from target chat.\n");
2131
2211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -68,13 +68,20 @@ function classifyAgentIntent(input) {
68
68
  if (hasThirdPartyName && hasAgentWord && !isAboutOurAgents) return null;
69
69
 
70
70
  const hasAgentContext = /(agent|智能体|工作区|人设|绑定|当前群|这个群|chat|workspace)/i.test(text);
71
+ // Question-form prefixes ("如何/怎么/能不能/可以吗") indicate the user is
72
+ // asking ABOUT creating an agent, not requesting one — exclude from create intent.
73
+ const isQuestion = /^(如何|怎么|怎样|能不能|可不可以|可以吗|是否)/i.test(text) || /(吗\?|吗?|\?$|?$)/.test(text);
71
74
  const wantsList = /(列出|查看|显示|有哪些|list|show)/i.test(text) && /(agent|智能体|工作区|绑定)/i.test(text);
72
75
  const wantsUnbind = /(解绑|取消绑定|断开绑定|unbind|unassign)/i.test(text) && hasAgentContext;
73
76
  const wantsEditRole =
74
77
  ((/(角色|职责|人设)/i.test(text) && /(改|修改|调整|更新|变成|改成|改为)/i.test(text)) ||
75
78
  /(把这个agent|把当前agent|当前群.*角色|当前群.*职责)/i.test(text));
79
+ // Relaxed: a bare "新建 agent" with no path is enough — daemon will derive
80
+ // a default workspace at ~/AGI/<name>/ if no path is given.
76
81
  const wantsCreate =
77
- (/(创建|新建|新增|搞一个|加一个|create)/i.test(text) && /(agent|智能体|人设|工作区)/i.test(text) && (directAction || hasWorkspacePath));
82
+ /(创建|新建|新增|搞一个|加一个|create)/i.test(text)
83
+ && /(agent|智能体|人设|工作区)/i.test(text)
84
+ && !isQuestion;
78
85
  const wantsBind =
79
86
  !wantsCreate &&
80
87
  (/(绑定|bind)/i.test(text) && hasAgentContext && (directAction || hasWorkspacePath));
@@ -89,10 +96,12 @@ function classifyAgentIntent(input) {
89
96
  /(?:怎么|如何|手册|文档|说明).{0,12}(配置|管理|使用).{0,12}(agent|智能体|机器人|bot)/i.test(text) ||
90
97
  /(?:agent|智能体|机器人|bot).{0,12}(怎么|如何).{0,12}(配置|管理|使用)/i.test(text);
91
98
 
99
+ // wantsCreate is checked BEFORE wantsList so that "新建 agent 用于查看 X"
100
+ // (which contains both 新建 and 查看) is correctly routed to create.
101
+ if (wantsCreate) return { action: 'create', workspaceDir };
92
102
  if (wantsList) return { action: 'list', workspaceDir };
93
103
  if (wantsUnbind) return { action: 'unbind', workspaceDir };
94
104
  if (wantsEditRole) return { action: 'edit_role', workspaceDir };
95
- if (wantsCreate) return { action: 'create', workspaceDir };
96
105
  if (wantsBind) return { action: 'bind', workspaceDir };
97
106
  if (wantsAgentDoc) return { action: 'agent_doc', workspaceDir };
98
107
  if (wantsActivate) return { action: 'activate', workspaceDir };
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const VALID_ENGINES = new Set(['claude', 'codex', 'unknown']);
6
+ const VALID_STATUSES = new Set(['indexed', 'summarized', 'extracted', 'error', 'archived']);
7
+
8
+ function normalizeEngine(engine) {
9
+ const value = String(engine || 'unknown').trim().toLowerCase();
10
+ return VALID_ENGINES.has(value) ? value : 'unknown';
11
+ }
12
+
13
+ function normalizeStatus(status) {
14
+ const value = String(status || 'indexed').trim().toLowerCase();
15
+ return VALID_STATUSES.has(value) ? value : 'indexed';
16
+ }
17
+
18
+ function stableId({ engine, sessionId, sourceHash }) {
19
+ const seed = `${normalizeEngine(engine)}:${String(sessionId || '').trim()}:${String(sourceHash || '').trim()}`;
20
+ return `ss_${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 24)}`;
21
+ }
22
+
23
+ function requireSessionSource(source) {
24
+ if (!source || typeof source !== 'object') throw new Error('session source is required');
25
+ const sessionId = String(source.sessionId || source.session_id || '').trim();
26
+ if (!sessionId) throw new Error('session source requires sessionId');
27
+ const sourceHash = String(source.sourceHash || source.source_hash || '').trim();
28
+ if (!sourceHash) throw new Error('session source requires sourceHash');
29
+ return { sessionId, sourceHash };
30
+ }
31
+
32
+ function upsertSessionSource(db, source) {
33
+ const { sessionId, sourceHash } = requireSessionSource(source);
34
+ const engine = normalizeEngine(source.engine);
35
+ const id = stableId({ engine, sessionId, sourceHash });
36
+ const status = normalizeStatus(source.status);
37
+
38
+ db.prepare(`
39
+ INSERT INTO session_sources (
40
+ id, engine, session_id, project, scope, agent_key, cwd,
41
+ source_path, source_hash, source_size, first_ts, last_ts,
42
+ message_count, tool_call_count, tool_error_count,
43
+ status, error_message, created_at, updated_at
44
+ )
45
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
46
+ ON CONFLICT(engine, session_id, source_hash) DO UPDATE SET
47
+ project=excluded.project,
48
+ scope=excluded.scope,
49
+ agent_key=excluded.agent_key,
50
+ cwd=excluded.cwd,
51
+ source_path=excluded.source_path,
52
+ source_size=excluded.source_size,
53
+ first_ts=excluded.first_ts,
54
+ last_ts=excluded.last_ts,
55
+ message_count=excluded.message_count,
56
+ tool_call_count=excluded.tool_call_count,
57
+ tool_error_count=excluded.tool_error_count,
58
+ status=excluded.status,
59
+ error_message=excluded.error_message,
60
+ updated_at=datetime('now')
61
+ `).run(
62
+ id,
63
+ engine,
64
+ sessionId,
65
+ source.project || '*',
66
+ source.scope || null,
67
+ source.agentKey || source.agent_key || null,
68
+ source.cwd || null,
69
+ source.sourcePath || source.source_path || null,
70
+ sourceHash,
71
+ Number(source.sourceSize || source.source_size || 0) || 0,
72
+ source.firstTs || source.first_ts || null,
73
+ source.lastTs || source.last_ts || null,
74
+ Number(source.messageCount || source.message_count || 0) || 0,
75
+ Number(source.toolCallCount || source.tool_call_count || 0) || 0,
76
+ Number(source.toolErrorCount || source.tool_error_count || 0) || 0,
77
+ status,
78
+ source.errorMessage || source.error_message || null,
79
+ );
80
+
81
+ return { ok: true, id };
82
+ }
83
+
84
+ function getSessionSource(db, { engine = 'unknown', sessionId, sourceHash }) {
85
+ if (!sessionId || !sourceHash) return null;
86
+ return db.prepare(`
87
+ SELECT * FROM session_sources
88
+ WHERE engine = ? AND session_id = ? AND source_hash = ?
89
+ LIMIT 1
90
+ `).get(normalizeEngine(engine), String(sessionId), String(sourceHash)) || null;
91
+ }
92
+
93
+ function findSessionSources(db, { project = null, scope = null, engine = null, limit = 20 } = {}) {
94
+ const clauses = [];
95
+ const params = [];
96
+ if (project) { clauses.push('(project = ? OR project = ?)'); params.push(project, '*'); }
97
+ if (scope) { clauses.push('(scope = ? OR scope IS NULL)'); params.push(scope); }
98
+ if (engine) { clauses.push('engine = ?'); params.push(normalizeEngine(engine)); }
99
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
100
+ return db.prepare(`
101
+ SELECT * FROM session_sources
102
+ ${where}
103
+ ORDER BY COALESCE(last_ts, updated_at, created_at) DESC
104
+ LIMIT ?
105
+ `).all(...params, Number(limit) || 20);
106
+ }
107
+
108
+ function markSessionSourceStatus(db, id, status, errorMessage = null) {
109
+ if (!id) return { ok: false, changed: 0 };
110
+ const normalized = normalizeStatus(status);
111
+ const result = db.prepare(`
112
+ UPDATE session_sources
113
+ SET status = ?, error_message = ?, updated_at = datetime('now')
114
+ WHERE id = ?
115
+ `).run(normalized, errorMessage || null, id);
116
+ return { ok: true, changed: result.changes || 0 };
117
+ }
118
+
119
+ module.exports = {
120
+ upsertSessionSource,
121
+ getSessionSource,
122
+ findSessionSources,
123
+ markSessionSourceStatus,
124
+ _internal: { normalizeEngine, normalizeStatus, stableId },
125
+ };
@@ -88,7 +88,7 @@ function createAgentIntentHandler(deps) {
88
88
  backupConfig,
89
89
  } = deps;
90
90
 
91
- return async function handleAgentIntent(bot, chatId, text, config) {
91
+ return async function handleAgentIntent(bot, chatId, text, config, senderId = null) {
92
92
  if (!agentTools || !text || text.startsWith('/')) return false;
93
93
  const key = String(chatId);
94
94
  if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
@@ -188,15 +188,6 @@ function createAgentIntentHandler(deps) {
188
188
  }
189
189
 
190
190
  if (intent.action === 'create') {
191
- if (!intent.workspaceDir) {
192
- await bot.sendMessage(chatId, [
193
- '我可以帮你创建 Agent,还差一个工作目录。',
194
- '例如:`给这个群创建一个 Agent,目录是 ~/projects/foo`',
195
- 'Windows 也可以直接发:`C:\\\\work\\\\foo`',
196
- '也可以直接回我一个路径(`~/`、`/`、`./`、`../`、`C:\\\\` 开头都行)。',
197
- ].join('\n'));
198
- return true;
199
- }
200
191
  const agentName = deriveAgentName(input, intent.workspaceDir);
201
192
  const roleDelta = deriveCreateRoleDelta(input);
202
193
  const inferredEngine = inferAgentEngineFromText(input);
@@ -204,7 +195,7 @@ function createAgentIntentHandler(deps) {
204
195
  agentTools,
205
196
  chatId,
206
197
  agentName,
207
- workspaceDir: intent.workspaceDir,
198
+ workspaceDir: intent.workspaceDir, // empty string OK — agent-tools derives ~/AGI/<name>/
208
199
  roleDescription: roleDelta,
209
200
  pendingActivations,
210
201
  skipChatBinding: true,
@@ -212,6 +203,8 @@ function createAgentIntentHandler(deps) {
212
203
  attachOrCreateSession,
213
204
  normalizeCwd,
214
205
  getDefaultEngine,
206
+ bot, // for optional auto-create-chat
207
+ senderOpenId: senderId,
215
208
  });
216
209
  if (!res.ok) {
217
210
  await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
@@ -220,10 +213,53 @@ function createAgentIntentHandler(deps) {
220
213
  const data = res.data || {};
221
214
  const projName = projectNameFromResult(data, agentName);
222
215
  const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
223
- await bot.sendMessage(chatId,
224
- `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
225
- `**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
226
- );
216
+ // If createWorkspaceAgent auto-created a Feishu chat and bound it, the
217
+ // bot has already messaged the new chat — just confirm in the source chat.
218
+ if (data.autoChat && data.autoChat.chatId && !data.autoChat.error) {
219
+ await bot.sendMessage(chatId,
220
+ `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
221
+ `已自动建好飞书群「${data.autoChat.name}」并把你拉进群里——直接打开新群和它对话即可。`
222
+ );
223
+ } else if (data.autoChat && data.autoChat.chatId && data.autoChat.error) {
224
+ // Chat was created but binding failed mid-way — give the user a clear
225
+ // recovery path so they don't end up with an orphan group.
226
+ await bot.sendMessage(chatId,
227
+ `⚠️ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
228
+ `飞书群「${data.autoChat.name}」也已建好,但自动绑定失败:${data.autoChat.error}\n\n` +
229
+ `**手动恢复**: 在新群里发送 \`/activate\` 即可补绑(30分钟内有效);\n` +
230
+ `或在任意群发 \`/agent bind ${projName} ${data.cwd || ''}\` 直接指定绑定。`
231
+ );
232
+ } else {
233
+ // Existing-user upgrade path: the most common failure here is missing
234
+ // im:chat / im:chat.member permissions on the user's Feishu app
235
+ // (older installs predate Fix 1's expanded permission list). Detect
236
+ // that case and tell the user exactly what to fix, instead of
237
+ // burying it in a generic "自动建群失败" line.
238
+ const autoErr = (data.autoChat && data.autoChat.error) || '';
239
+ const autoCode = data.autoChat && data.autoChat.code;
240
+ const isPermissionError = autoCode === 99991663
241
+ || /im:chat|权限|permission|forbidden|scope/i.test(autoErr);
242
+ if (isPermissionError) {
243
+ await bot.sendMessage(chatId,
244
+ `⚠️ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
245
+ `自动建群失败:飞书应用缺少 \`im:chat\` 或 \`im:chat.member\` 权限。\n\n` +
246
+ `**修复方法**(2 分钟):\n` +
247
+ `1. 打开 https://open.feishu.cn/app 找到这个应用\n` +
248
+ `2. 左侧「权限管理」搜索并开通这两个权限:\n` +
249
+ ` • \`im:chat\`(创建群聊)\n` +
250
+ ` • \`im:chat.member\`(邀请用户进群)\n` +
251
+ `3. 左侧「版本管理与发布」→ 创建新版本 → 申请发布\n` +
252
+ `4. 回这里再说一次「新建 agent ${projName}」即可\n\n` +
253
+ `也可以**手动**:在飞书新建群,把 bot 拉进去,发送 \`/activate\`(30 分钟内有效)。`
254
+ );
255
+ } else {
256
+ const fallbackHint = autoErr ? `\n(自动建群失败:${autoErr})` : '';
257
+ await bot.sendMessage(chatId,
258
+ `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}${fallbackHint}\n\n` +
259
+ `**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
260
+ );
261
+ }
262
+ }
227
263
  return true;
228
264
  }
229
265
 
@@ -33,6 +33,24 @@ function createAgentTools(deps) {
33
33
  return normalizeCwd ? normalizeCwd(expanded) : path.resolve(expanded);
34
34
  }
35
35
 
36
+ // Derive a default workspace path "~/AGI/<safeName>" when the user names the
37
+ // agent but skips the path. The directory is created on demand so the rest
38
+ // of the pipeline (CLAUDE.md write, daemon.yaml entry) can proceed without
39
+ // forcing the user to mkdir manually.
40
+ function deriveDefaultWorkspaceDir(safeName) {
41
+ const cleanName = String(safeName || '').replace(/[\\/:*?"<>|]/g, '').trim();
42
+ if (!cleanName) return null;
43
+ return path.join(HOME, 'AGI', cleanName);
44
+ }
45
+
46
+ function ensureWorkspaceDir(dir) {
47
+ if (!dir) return false;
48
+ try {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
51
+ } catch { return false; }
52
+ }
53
+
36
54
  function getAdapterKey(chatId) {
37
55
  return typeof chatId === 'number' ? 'telegram' : 'feishu';
38
56
  }
@@ -87,11 +105,27 @@ function createAgentTools(deps) {
87
105
  const existing = cfg.projects[projectKey];
88
106
  if (existing && existing.cwd) resolvedDir = resolveWorkspaceDir(existing.cwd);
89
107
  }
108
+ // No path supplied and no prior project → derive ~/AGI/<safeName>/.
109
+ if (!resolvedDir) {
110
+ const defaultDir = deriveDefaultWorkspaceDir(safeName);
111
+ if (defaultDir && ensureWorkspaceDir(defaultDir)) {
112
+ resolvedDir = resolveWorkspaceDir(defaultDir);
113
+ }
114
+ }
90
115
  if (!resolvedDir) {
91
116
  return { ok: false, error: 'workspaceDir is required for a new agent' };
92
117
  }
118
+ // If the resolved path doesn't exist yet but is the derived default,
119
+ // mkdir on the spot. Stay strict for user-supplied paths so typos don't
120
+ // silently spawn directories in the wrong place.
93
121
  if (!fs.existsSync(resolvedDir)) {
94
- return { ok: false, error: `workspaceDir not found: ${resolvedDir}` };
122
+ const defaultDir = deriveDefaultWorkspaceDir(safeName);
123
+ const isDefault = defaultDir && resolveWorkspaceDir(defaultDir) === resolvedDir;
124
+ if (isDefault && ensureWorkspaceDir(resolvedDir)) {
125
+ // ok — created it
126
+ } else {
127
+ return { ok: false, error: `workspaceDir not found: ${resolvedDir}` };
128
+ }
95
129
  }
96
130
  if (!fs.statSync(resolvedDir).isDirectory()) {
97
131
  return { ok: false, error: `workspaceDir is not a directory: ${resolvedDir}` };
@@ -236,9 +270,24 @@ ${safeDelta}
236
270
  // Create the project entry without touching chat_agent_map
237
271
  const safeName = sanitizeText(agentName, 120);
238
272
  if (!safeName) return { ok: false, error: 'agentName is required' };
239
- const resolvedDir = resolveWorkspaceDir(workspaceDir);
273
+ let resolvedDir = resolveWorkspaceDir(workspaceDir);
274
+ // Same default-derivation rule as bindAgentToChat: when the user names
275
+ // the agent but doesn't pin a path, anchor it under ~/AGI/<safeName>/.
276
+ if (!resolvedDir) {
277
+ const defaultDir = deriveDefaultWorkspaceDir(safeName);
278
+ if (defaultDir && ensureWorkspaceDir(defaultDir)) {
279
+ resolvedDir = resolveWorkspaceDir(defaultDir);
280
+ }
281
+ }
240
282
  if (!resolvedDir) return { ok: false, error: 'workspaceDir is required' };
241
- if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
283
+ if (!fs.existsSync(resolvedDir)) {
284
+ const defaultDir = deriveDefaultWorkspaceDir(safeName);
285
+ const isDefault = defaultDir && resolveWorkspaceDir(defaultDir) === resolvedDir;
286
+ if (!(isDefault && ensureWorkspaceDir(resolvedDir))) {
287
+ return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
288
+ }
289
+ }
290
+ if (!fs.statSync(resolvedDir).isDirectory()) {
242
291
  return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
243
292
  }
244
293
  const cfg = loadConfig();
@@ -181,6 +181,11 @@ async function createWorkspaceAgent({
181
181
  normalizeCwd,
182
182
  getDefaultEngine,
183
183
  legacyCreate,
184
+ // Optional: enable auto-create-chat when set. Requires the bot to expose
185
+ // createChat (currently only feishu-adapter) and a senderOpenId so the
186
+ // human creator can be invited into the new chat.
187
+ bot = null,
188
+ senderOpenId = null,
184
189
  }) {
185
190
  let res;
186
191
  if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
@@ -198,6 +203,99 @@ async function createWorkspaceAgent({
198
203
 
199
204
  const data = res.data || {};
200
205
  if (skipChatBinding) {
206
+ // Try the one-shot path: create a Feishu chat for this agent right now,
207
+ // invite the human creator, and bind chat→agent so /activate is unneeded.
208
+ // Pre-conditions: caller provided a bot with createChat, a senderOpenId
209
+ // that looks like Feishu open_id, and bindAgentToChat is wired up.
210
+ const canAutoCreate = bot
211
+ && typeof bot.createChat === 'function'
212
+ && typeof senderOpenId === 'string' && senderOpenId.startsWith('ou_')
213
+ && agentTools && typeof agentTools.bindAgentToChat === 'function';
214
+
215
+ if (canAutoCreate) {
216
+ const projName = (data.project && data.project.name) || agentName || data.projectKey;
217
+ const chatName = `MetaMe · ${projName}`;
218
+ const createRes = await bot.createChat({
219
+ name: chatName,
220
+ description: `Agent ${projName} — auto-created`,
221
+ ownerOpenId: senderOpenId,
222
+ inviteOpenIds: [senderOpenId],
223
+ });
224
+ if (createRes.ok && createRes.chatId) {
225
+ const newChatId = createRes.chatId;
226
+ const bindRes = await agentTools.bindAgentToChat(newChatId, projName, data.cwd, { engine });
227
+ if (bindRes.ok) {
228
+ // Skip /activate — directly attach the session in the new chat.
229
+ attachBoundSession({
230
+ attachOrCreateSession,
231
+ projectKey: data.projectKey,
232
+ chatId: newChatId,
233
+ cwd: data.cwd,
234
+ name: projName,
235
+ engine: data.project && data.project.engine,
236
+ normalizeCwd,
237
+ getDefaultEngine,
238
+ });
239
+ // Greeting in the freshly created chat so the user sees the bot is alive.
240
+ try {
241
+ await bot.sendMessage(newChatId, `🤖 ${projName} 已上线。直接说话即可,本群已绑定到 \`${data.projectKey}\`。`);
242
+ } catch { /* non-fatal */ }
243
+ return {
244
+ ok: true,
245
+ data: { ...data, autoChat: { chatId: newChatId, name: chatName } },
246
+ };
247
+ }
248
+ // Bind failed AFTER chat was created — keep enough info for recovery.
249
+ // Surface chatId/name so the user knows which chat exists, and write
250
+ // pendingActivations so /activate from inside that new chat works.
251
+ if (data.projectKey && pendingActivations) {
252
+ pendingActivations.set(data.projectKey, {
253
+ agentKey: data.projectKey,
254
+ agentName: projName,
255
+ cwd: data.cwd,
256
+ createdByChatId: String(chatId),
257
+ createdAt: Date.now(),
258
+ // Hint for downstream: the chat already exists, /activate from it.
259
+ preCreatedChatId: newChatId,
260
+ preCreatedChatName: chatName,
261
+ });
262
+ }
263
+ return {
264
+ ok: true,
265
+ data: {
266
+ ...data,
267
+ autoChat: {
268
+ chatId: newChatId,
269
+ name: chatName,
270
+ error: `bind failed: ${bindRes.error}`,
271
+ recoverable: true,
272
+ },
273
+ },
274
+ };
275
+ }
276
+ // createChat failed — fall through to /activate path with diagnostic.
277
+ const errSummary = createRes.error || 'unknown';
278
+ // Forward the underlying error code (e.g. 99991663 = permission denied)
279
+ // so the caller can give a targeted upgrade hint instead of a generic
280
+ // "auto-create failed" message.
281
+ const errCode = createRes.code || null;
282
+ // Still register pendingActivations so user can /activate manually.
283
+ if (data.projectKey && pendingActivations) {
284
+ pendingActivations.set(data.projectKey, {
285
+ agentKey: data.projectKey,
286
+ agentName: projName,
287
+ cwd: data.cwd,
288
+ createdByChatId: String(chatId),
289
+ createdAt: Date.now(),
290
+ });
291
+ }
292
+ return {
293
+ ok: true,
294
+ data: { ...data, autoChat: { error: errSummary, code: errCode } },
295
+ };
296
+ }
297
+
298
+ // No auto-create — original /activate flow.
201
299
  if (data.projectKey && pendingActivations) {
202
300
  pendingActivations.set(data.projectKey, {
203
301
  agentKey: data.projectKey,
@@ -49,7 +49,7 @@ function createBridgeStarter(deps) {
49
49
  return text || null;
50
50
  }
51
51
 
52
- async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
52
+ async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl, fromAllowedChat }) {
53
53
  const trimmed = String(text || '').trim();
54
54
  const normalizedSenderId = normalizeSenderId(senderId);
55
55
  if (!trimmed || bypassAcl || !userAcl) {
@@ -58,10 +58,15 @@ function createBridgeStarter(deps) {
58
58
 
59
59
  let userCtx;
60
60
  try {
61
- userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
61
+ userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {}, { fromAllowedChat: !!fromAllowedChat });
62
62
  } catch {
63
63
  return { blocked: false, readOnly: false, senderId: normalizedSenderId };
64
64
  }
65
+ // Audit trail for implicit-admin upgrades — these users are NOT in users.yaml
66
+ // and gain admin via group-whitelist trust, so make their action visible.
67
+ if (userCtx && userCtx.implicitAdmin) {
68
+ try { log('INFO', `[ACL] implicit admin via allowed_chat_ids: chat=${chatId} sender=${normalizedSenderId}`); } catch { /* non-fatal */ }
69
+ }
65
70
 
66
71
  const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
67
72
  if (userCmd && userCmd.handled) {
@@ -803,6 +808,7 @@ function createBridgeStarter(deps) {
803
808
  config: liveCfg,
804
809
  senderId,
805
810
  bypassAcl: !isAllowedChat && !!isBindCmd,
811
+ fromAllowedChat: isAllowedChat,
806
812
  });
807
813
  if (acl.blocked) return;
808
814
  log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
@@ -850,6 +856,7 @@ function createBridgeStarter(deps) {
850
856
  config: liveCfg,
851
857
  senderId,
852
858
  bypassAcl: !isAllowedChat && !!isBindCmd,
859
+ fromAllowedChat: isAllowedChat,
853
860
  });
854
861
  if (acl.blocked) return;
855
862
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
@@ -593,7 +593,7 @@ function createCommandRouter(deps) {
593
593
  return;
594
594
  }
595
595
 
596
- if (await tryHandleAgentIntent(bot, chatId, text, config)) {
596
+ if (await tryHandleAgentIntent(bot, chatId, text, config, senderId)) {
597
597
  return;
598
598
  }
599
599
  }