@ian2018cs/agenthub 0.1.75 → 0.1.77

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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 内置工具:分享项目模板
3
+ *
4
+ * AI 调用 Bash(__share_project_template__ '<json>') 命令,
5
+ * 由 PreToolUse hook 拦截 → 通过 WebSocket 打开前端弹窗 → 等待用户操作 → 返回结果。
6
+ *
7
+ * 飞书端不支持此工具(检测 mutableWriter.current.ws)。
8
+ */
9
+
10
+ import { loadProjectConfig, addProjectManually } from '../../projects.js';
11
+
12
+ const TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟(用户需要时间审阅和修改模板)
13
+
14
+ export default {
15
+ name: '__share_project_template__',
16
+ responseType: 'share-project-template-response',
17
+
18
+ systemPrompt: `
19
+
20
+ ## 分享项目模板工具
21
+
22
+ 当用户想要分享项目、创建项目模板、发布共享项目时,使用以下 Bash 命令打开分享弹窗:
23
+
24
+ \`\`\`bash
25
+ __share_project_template__ '{"path":"/项目路径","displayName":"显示名称","description":"描述","updateNotes":"更新说明","skills":["skill1"],"mcps":["mcp1"],"files":["file1.md"],"gitRepos":[{"name":"子目录名","repo":"git远程地址","branch":"分支名"}]}'
26
+ \`\`\`
27
+
28
+ 参数说明:
29
+ - path (必填): 项目文件夹路径,可以是当前项目、子文件夹或其他项目路径
30
+ - displayName (可选): 模板显示名称,仅首次创建时使用(更新时忽略)
31
+ - description (可选): 模板描述,仅首次创建时使用(更新时忽略)
32
+ - updateNotes (可选): 更新说明,仅更新已有模板时使用
33
+ - skills (可选): 要包含的技能名称数组,空数组=让用户在弹窗中选择
34
+ - mcps (可选): 要包含的 MCP 服务名称数组,空数组=让用户在弹窗中选择
35
+ - files (可选): 要包含的文件相对路径数组,空数组=让用户在弹窗中选择
36
+ - gitRepos (可选): 要包含的 Git 仓库依赖数组,每项包含 name(子目录名)、repo(远程地址)、branch(分支),空数组=让用户在弹窗中选择
37
+
38
+ 该命令会打开一个 UI 弹窗供用户确认和修改,等待用户操作完成后返回结果。
39
+ `,
40
+
41
+ match(hookInput) {
42
+ return hookInput.tool_name === 'Bash' &&
43
+ hookInput.tool_input?.command?.trimStart().startsWith('__share_project_template__');
44
+ },
45
+
46
+ async execute(hookInput, context) {
47
+ const { userUuid, mutableWriter, createRequestId, waitForResponse } = context;
48
+
49
+ // 检测飞书模式:MutableWriter.current 有 .ws 属性(WebSocketWriter),FakeSendWriter 没有
50
+ if (!mutableWriter?.current?.ws) {
51
+ return { decision: 'deny', reason: '分享项目模板功能仅支持网页端使用。' };
52
+ }
53
+
54
+ // 解析参数
55
+ const rawArgs = hookInput.tool_input.command.replace(/^\s*__share_project_template__\s*/, '');
56
+ let jsonStr = rawArgs.trim();
57
+ if ((jsonStr.startsWith("'") && jsonStr.endsWith("'")) ||
58
+ (jsonStr.startsWith('"') && jsonStr.endsWith('"'))) {
59
+ jsonStr = jsonStr.slice(1, -1);
60
+ }
61
+ const params = JSON.parse(jsonStr);
62
+
63
+ if (!params.path) {
64
+ return { decision: 'deny', reason: '缺少必需参数 path(项目路径)。' };
65
+ }
66
+
67
+ // 解析 path → projectKey,并判断是否为已有项目
68
+ const config = await loadProjectConfig(userUuid);
69
+ let projectKey = null;
70
+ let isExistingProject = false;
71
+ for (const [key, entry] of Object.entries(config)) {
72
+ if ((entry.originalPath || key.replace(/-/g, '/')) === params.path) {
73
+ projectKey = key;
74
+ isExistingProject = true;
75
+ break;
76
+ }
77
+ }
78
+ if (!projectKey) {
79
+ // 路径不在项目列表中(可能是子文件夹),自动添加为新项目
80
+ try {
81
+ const project = await addProjectManually(params.path, params.displayName, userUuid);
82
+ projectKey = project.name;
83
+ isExistingProject = false;
84
+ } catch (e) {
85
+ return { decision: 'deny', reason: `无法解析项目路径: ${e.message}` };
86
+ }
87
+ }
88
+
89
+ // 发送 WebSocket 消息打开分享弹窗
90
+ const shareRequestId = createRequestId();
91
+ mutableWriter.send({
92
+ type: 'share-project-template-request',
93
+ requestId: shareRequestId,
94
+ prefillData: {
95
+ projectKey,
96
+ projectPath: params.path,
97
+ isExistingProject,
98
+ displayName: params.displayName || '',
99
+ description: params.description || '',
100
+ updateNotes: params.updateNotes || '',
101
+ skills: params.skills || [],
102
+ mcps: params.mcps || [],
103
+ files: params.files || [],
104
+ gitRepos: params.gitRepos || [],
105
+ }
106
+ });
107
+
108
+ // 等待前端响应(5 分钟超时)
109
+ const response = await waitForResponse(shareRequestId, TIMEOUT_MS);
110
+
111
+ let reason;
112
+ if (!response) {
113
+ reason = '分享项目模板请求超时(5分钟内未收到响应)。';
114
+ } else if (response.cancelled) {
115
+ reason = '用户取消了分享项目模板操作。';
116
+ } else if (response.success) {
117
+ reason = `项目模板已成功提交!提交 ID: ${response.submissionId}。${response.message || ''}`;
118
+ } else {
119
+ reason = `提交失败: ${response.error || '未知错误'}`;
120
+ }
121
+
122
+ return { decision: 'deny', reason };
123
+ }
124
+ };
@@ -7,13 +7,6 @@ export const SYSTEM_AGENT_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer
7
7
  export const SYSTEM_AGENT_REPO_OWNER = 'mcp-server';
8
8
  export const SYSTEM_AGENT_REPO_NAME = 'hupoer-agents';
9
9
 
10
- // ─── In-memory cache ──────────────────────────────────────────────────────────
11
- const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
12
- let agentCache = { agents: null, cachedAt: 0 };
13
-
14
- export function invalidateAgentCache() {
15
- agentCache = { agents: null, cachedAt: 0 };
16
- }
17
10
 
18
11
  function runGit(args, cwd = null) {
19
12
  return new Promise((resolve, reject) => {
@@ -42,7 +35,7 @@ function getRepoPath() {
42
35
  }
43
36
 
44
37
  /**
45
- * Ensure the agent repo is cloned locally. If already present, tries to pull.
38
+ * Ensure the agent repo is cloned locally. Does NOT pull; call pullAgentRepo() explicitly.
46
39
  * Returns the path to the local clone.
47
40
  */
48
41
  export async function ensureAgentRepo() {
@@ -50,12 +43,6 @@ export async function ensureAgentRepo() {
50
43
 
51
44
  try {
52
45
  await fs.access(repoPath);
53
- // Already cloned — try to pull latest
54
- try {
55
- await runGit(['pull', '--ff-only'], repoPath);
56
- } catch (err) {
57
- console.log('[AgentRepo] Failed to pull, using existing clone:', err.message);
58
- }
59
46
  } catch {
60
47
  // Not yet cloned
61
48
  await fs.mkdir(path.dirname(repoPath), { recursive: true });
@@ -71,6 +58,19 @@ export async function ensureAgentRepo() {
71
58
  return repoPath;
72
59
  }
73
60
 
61
+ /**
62
+ * Pull latest from remote. Called explicitly on user-triggered refresh.
63
+ */
64
+ export async function pullAgentRepo() {
65
+ const repoPath = getRepoPath();
66
+ try {
67
+ await runGit(['pull', '--ff-only'], repoPath);
68
+ } catch (err) {
69
+ console.log('[AgentRepo] Failed to pull:', err.message);
70
+ throw err;
71
+ }
72
+ }
73
+
74
74
  /**
75
75
  * Parse agent.yaml from an agent directory.
76
76
  * Returns the parsed metadata object or null if invalid.
@@ -89,7 +89,8 @@ async function parseAgentYaml(agentDir) {
89
89
  author: '',
90
90
  skills: [],
91
91
  mcps: [],
92
- files: []
92
+ files: [],
93
+ git_repos: []
93
94
  };
94
95
 
95
96
  // Parse scalar fields
@@ -153,6 +154,21 @@ async function parseAgentYaml(agentDir) {
153
154
  }
154
155
  }
155
156
 
157
+ // Parse git_repos list
158
+ const gitReposSection = content.match(/^git_repos:\s*\n((?:[ \t]+.+\n?)*)/m);
159
+ if (gitReposSection) {
160
+ const lines = gitReposSection[1].split('\n');
161
+ let current = null;
162
+ for (const line of lines) {
163
+ const nameLine = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
164
+ const repoLine = line.match(/^\s+repo:\s*["']?(.+?)["']?\s*$/);
165
+ const branchLine = line.match(/^\s+branch:\s*["']?(.+?)["']?\s*$/);
166
+ if (nameLine) { current = { name: nameLine[1].trim() }; result.git_repos.push(current); }
167
+ else if (repoLine && current) { current.repo = repoLine[1].trim(); }
168
+ else if (branchLine && current) { current.branch = branchLine[1].trim(); }
169
+ }
170
+ }
171
+
156
172
  return result;
157
173
  } catch {
158
174
  return null;
@@ -161,23 +177,25 @@ async function parseAgentYaml(agentDir) {
161
177
 
162
178
  /**
163
179
  * Scan the agent repo for available agents.
164
- * @param {boolean} force - When true, always git pull and re-scan (bypasses cache).
165
- * When false (default), returns cached result if < 5 min old.
180
+ * @param {boolean} pull - When true, git pull before scanning (e.g. user-triggered refresh).
181
+ * When false (default), read directly from local disk fast path.
166
182
  * Returns array of agent metadata objects.
167
183
  */
168
- export async function scanAgents(force = false) {
169
- // Return cached result if fresh and not forced
170
- if (!force && agentCache.agents !== null && Date.now() - agentCache.cachedAt < CACHE_TTL_MS) {
171
- return agentCache.agents;
172
- }
173
-
184
+ export async function scanAgents(pull = false) {
174
185
  let repoPath;
175
186
  try {
176
187
  repoPath = await ensureAgentRepo();
177
188
  } catch (err) {
178
189
  console.error('[AgentRepo] Could not access agent repo:', err.message);
179
- // Return stale cache on network error rather than empty list
180
- return agentCache.agents ?? [];
190
+ return [];
191
+ }
192
+
193
+ if (pull) {
194
+ try {
195
+ await pullAgentRepo();
196
+ } catch (err) {
197
+ console.warn('[AgentRepo] Pull failed, scanning existing local data:', err.message);
198
+ }
181
199
  }
182
200
 
183
201
  const agents = [];
@@ -211,7 +229,6 @@ export async function scanAgents(force = false) {
211
229
  });
212
230
  }
213
231
 
214
- agentCache = { agents, cachedAt: Date.now() };
215
232
  return agents;
216
233
  }
217
234
 
@@ -43,6 +43,7 @@ export function getPublicPaths() {
43
43
  mcpRepoDir: path.join(DATA_DIR, 'mcp-repo'),
44
44
  agentRepoDir: path.join(DATA_DIR, 'agent-repo'),
45
45
  agentSubmissionsDir: path.join(DATA_DIR, 'agent-submissions'),
46
+ gitRepoDir: path.join(DATA_DIR, 'git-repo'),
46
47
  };
47
48
  }
48
49
 
package/shared/brand.js CHANGED
@@ -27,6 +27,10 @@ export const CHAT_EXAMPLE_PROMPTS = [
27
27
  { icon: '💡', text: '给我一些解决问题的思路' },
28
28
  ];
29
29
 
30
+ // 新对话的默认权限模式。
31
+ // 可选值: 'default'(默认模式)| 'acceptEdits'(接受编辑)| 'bypassPermissions'(跳过权限)| 'plan'(计划模式)
32
+ export const DEFAULT_PERMISSION_MODE = 'default';
33
+
30
34
  // 飞书绑定引导文案(出现 3 次,统一成函数避免漂移)
31
35
  export const feishuBindingGuide = () =>
32
36
  `👋 你好!请先完成账号绑定:\n\n` +