@ian2018cs/agenthub 0.1.71 → 0.1.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian2018cs/agenthub",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
package/server/index.js CHANGED
@@ -32,6 +32,27 @@ const c = {
32
32
 
33
33
  console.log('PORT from env:', process.env.PORT);
34
34
 
35
+ // 全局兜底:防止 SDK 内部异步异常(如中止 session 时 stream 已关闭导致的 "Operation aborted")crash 进程
36
+ process.on('unhandledRejection', (reason, promise) => {
37
+ const msg = reason?.message || String(reason);
38
+ if (msg.includes('Operation aborted') || msg.includes('operation aborted')) {
39
+ // Claude Agent SDK 在 session 被中止后内部写 stream 引发,属预期行为,仅记录 debug 日志
40
+ console.debug('[unhandledRejection] SDK operation aborted (expected on session stop):', msg);
41
+ return;
42
+ }
43
+ console.error('[unhandledRejection]', reason);
44
+ });
45
+
46
+ process.on('uncaughtException', (err) => {
47
+ const msg = err?.message || String(err);
48
+ if (msg.includes('Operation aborted') || msg.includes('operation aborted')) {
49
+ console.debug('[uncaughtException] SDK operation aborted (expected on session stop):', msg);
50
+ return;
51
+ }
52
+ console.error('[uncaughtException]', err);
53
+ process.exit(1);
54
+ });
55
+
35
56
  import express from 'express';
36
57
  import { WebSocketServer, WebSocket } from 'ws';
37
58
  import os from 'os';
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import { spawn } from 'child_process';
6
6
  import AdmZip from 'adm-zip';
7
7
  import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
8
- import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo } from '../services/system-agent-repo.js';
8
+ import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo, invalidateAgentCache } from '../services/system-agent-repo.js';
9
9
  import { ensureSystemRepo, SYSTEM_REPO_URL } from '../services/system-repo.js';
10
10
  import { ensureSystemMcpRepo, SYSTEM_MCP_REPO_URL } from '../services/system-mcp-repo.js';
11
11
  import { addProjectManually, loadProjectConfig, saveProjectConfig } from '../projects.js';
@@ -444,8 +444,7 @@ router.get('/', async (req, res) => {
444
444
  */
445
445
  router.post('/refresh', async (_req, res) => {
446
446
  try {
447
- await ensureAgentRepo();
448
- const agents = await scanAgents();
447
+ const agents = await scanAgents(true);
449
448
  res.json({ success: true, agentCount: agents.length });
450
449
  } catch (error) {
451
450
  console.error('Error refreshing agent repo:', error);
@@ -1295,7 +1294,7 @@ router.post('/submissions/:id/approve', async (req, res) => {
1295
1294
  if (updateAllUsers) {
1296
1295
  // Force-update all users who have this agent installed
1297
1296
  try {
1298
- const freshAgents = await scanAgents();
1297
+ const freshAgents = await scanAgents(true);
1299
1298
  const publishedAgent = freshAgents.find(a =>
1300
1299
  a.dirName === submission.agent_name || a.name === submission.agent_name
1301
1300
  );
@@ -7,6 +7,14 @@ 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
  function runGit(args, cwd = null) {
11
19
  return new Promise((resolve, reject) => {
12
20
  const opts = {
@@ -153,15 +161,23 @@ async function parseAgentYaml(agentDir) {
153
161
 
154
162
  /**
155
163
  * 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.
156
166
  * Returns array of agent metadata objects.
157
167
  */
158
- export async function scanAgents() {
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
+
159
174
  let repoPath;
160
175
  try {
161
176
  repoPath = await ensureAgentRepo();
162
177
  } catch (err) {
163
178
  console.error('[AgentRepo] Could not access agent repo:', err.message);
164
- return [];
179
+ // Return stale cache on network error rather than empty list
180
+ return agentCache.agents ?? [];
165
181
  }
166
182
 
167
183
  const agents = [];
@@ -195,6 +211,7 @@ export async function scanAgents() {
195
211
  });
196
212
  }
197
213
 
214
+ agentCache = { agents, cachedAt: Date.now() };
198
215
  return agents;
199
216
  }
200
217
 
@@ -7,39 +7,35 @@ const SYSTEM_MCP_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-mcps.git
7
7
  const SYSTEM_MCP_REPO_OWNER = 'mcp-server';
8
8
  const SYSTEM_MCP_REPO_NAME = 'hupoer-mcps';
9
9
 
10
- function cloneRepository(url, destinationPath) {
10
+ function runGit(args, cwd) {
11
11
  return new Promise((resolve, reject) => {
12
- const gitProcess = spawn('git', ['clone', '--depth', '1', url, destinationPath], {
12
+ const gitProcess = spawn('git', args, {
13
+ cwd,
13
14
  stdio: ['ignore', 'pipe', 'pipe'],
14
15
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
15
16
  });
16
-
17
17
  let stderr = '';
18
18
  gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
19
19
  gitProcess.on('close', (code) => {
20
20
  if (code === 0) resolve();
21
- else reject(new Error(stderr || `Git clone failed with code ${code}`));
21
+ else reject(new Error(stderr || `Git ${args[0]} failed with code ${code}`));
22
22
  });
23
23
  gitProcess.on('error', (err) => { reject(err); });
24
24
  });
25
25
  }
26
26
 
27
- function updateRepository(repoPath) {
28
- return new Promise((resolve, reject) => {
29
- const gitProcess = spawn('git', ['pull', '--ff-only'], {
30
- cwd: repoPath,
31
- stdio: ['ignore', 'pipe', 'pipe'],
32
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
33
- });
34
-
35
- let stderr = '';
36
- gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
37
- gitProcess.on('close', (code) => {
38
- if (code === 0) resolve();
39
- else reject(new Error(stderr || `Git pull failed with code ${code}`));
40
- });
41
- gitProcess.on('error', (err) => { reject(err); });
42
- });
27
+ async function updateRepository(repoPath) {
28
+ try {
29
+ await runGit(['pull', '--ff-only'], repoPath);
30
+ } catch (err) {
31
+ if (err.message.includes('overwritten by merge') || err.message.includes('overwritten by checkout') || err.message.includes('local changes')) {
32
+ console.log('[SystemMcpRepo] git pull failed due to local changes, discarding and retrying:', err.message);
33
+ await runGit(['checkout', '--', '.'], repoPath);
34
+ await runGit(['pull', '--ff-only'], repoPath);
35
+ } else {
36
+ throw err;
37
+ }
38
+ }
43
39
  }
44
40
 
45
41
  export async function ensureSystemMcpRepo() {
@@ -58,7 +54,7 @@ export async function ensureSystemMcpRepo() {
58
54
  // Not yet cloned
59
55
  await fs.mkdir(path.dirname(publicRepoPath), { recursive: true });
60
56
  try {
61
- await cloneRepository(SYSTEM_MCP_REPO_URL, publicRepoPath);
57
+ await runGit(['clone', '--depth', '1', SYSTEM_MCP_REPO_URL, publicRepoPath], undefined);
62
58
  console.log('[SystemMcpRepo] Cloned system MCP repo to', publicRepoPath);
63
59
  } catch (err) {
64
60
  console.error('[SystemMcpRepo] Failed to clone system MCP repo:', err.message);
@@ -7,43 +7,35 @@ const SYSTEM_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-skills.git';
7
7
  const SYSTEM_REPO_OWNER = 'mcp-server';
8
8
  const SYSTEM_REPO_NAME = 'hupoer-skills';
9
9
 
10
- function cloneRepository(url, destinationPath) {
10
+ function runGit(args, cwd) {
11
11
  return new Promise((resolve, reject) => {
12
- const gitProcess = spawn('git', ['clone', '--depth', '1', url, destinationPath], {
12
+ const gitProcess = spawn('git', args, {
13
+ cwd,
13
14
  stdio: ['ignore', 'pipe', 'pipe'],
14
15
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
15
16
  });
16
-
17
17
  let stderr = '';
18
18
  gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
19
-
20
19
  gitProcess.on('close', (code) => {
21
20
  if (code === 0) resolve();
22
- else reject(new Error(stderr || `Git clone failed with code ${code}`));
21
+ else reject(new Error(stderr || `Git ${args[0]} failed with code ${code}`));
23
22
  });
24
-
25
23
  gitProcess.on('error', (err) => { reject(err); });
26
24
  });
27
25
  }
28
26
 
29
- function updateRepository(repoPath) {
30
- return new Promise((resolve, reject) => {
31
- const gitProcess = spawn('git', ['pull', '--ff-only'], {
32
- cwd: repoPath,
33
- stdio: ['ignore', 'pipe', 'pipe'],
34
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
35
- });
36
-
37
- let stderr = '';
38
- gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
39
-
40
- gitProcess.on('close', (code) => {
41
- if (code === 0) resolve();
42
- else reject(new Error(stderr || `Git pull failed with code ${code}`));
43
- });
44
-
45
- gitProcess.on('error', (err) => { reject(err); });
46
- });
27
+ async function updateRepository(repoPath) {
28
+ try {
29
+ await runGit(['pull', '--ff-only'], repoPath);
30
+ } catch (err) {
31
+ if (err.message.includes('overwritten by merge') || err.message.includes('overwritten by checkout') || err.message.includes('local changes')) {
32
+ console.log('[SystemRepo] git pull failed due to local changes, discarding and retrying:', err.message);
33
+ await runGit(['checkout', '--', '.'], repoPath);
34
+ await runGit(['pull', '--ff-only'], repoPath);
35
+ } else {
36
+ throw err;
37
+ }
38
+ }
47
39
  }
48
40
 
49
41
  /**
@@ -67,7 +59,7 @@ export async function ensureSystemRepo() {
67
59
  // Not yet cloned
68
60
  await fs.mkdir(path.dirname(publicRepoPath), { recursive: true });
69
61
  try {
70
- await cloneRepository(SYSTEM_REPO_URL, publicRepoPath);
62
+ await runGit(['clone', '--depth', '1', SYSTEM_REPO_URL, publicRepoPath], undefined);
71
63
  console.log('[SystemRepo] Cloned system repo to', publicRepoPath);
72
64
  } catch (err) {
73
65
  console.error('[SystemRepo] Failed to clone system repo:', err.message);