@ian2018cs/agenthub 0.1.68 → 0.1.70
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/dist/assets/index-DURCpZD_.css +32 -0
- package/dist/assets/index-DxBc5bLY.js +186 -0
- package/dist/assets/{vendor-icons-DxBNDMja.js → vendor-icons-BWqhkbta.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/claude-sdk.js +75 -13
- package/server/index.js +6 -1
- package/server/routes/agents.js +337 -6
- package/server/routes/mcp.js +122 -147
- package/server/routes/skills.js +83 -0
- package/server/services/system-mcp-repo.js +1 -1
- package/server/services/system-repo.js +1 -1
- package/dist/assets/index-C9BhBQzI.js +0 -184
- package/dist/assets/index-HOTjBpXH.css +0 -32
|
@@ -378,4 +378,4 @@ import{a as y}from"./vendor-react-Bv0Nkan8.js";/**
|
|
|
378
378
|
*
|
|
379
379
|
* This source code is licensed under the ISC license.
|
|
380
380
|
* See the LICENSE file in the root directory of this source tree.
|
|
381
|
-
*/const Z1=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],t0=e("zap",Z1);export{u2 as $,B1 as A,E1 as B,W1 as C,c2 as D,o2 as E,i2 as F,r2 as G,
|
|
381
|
+
*/const Z1=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],t0=e("zap",Z1);export{u2 as $,B1 as A,E1 as B,W1 as C,c2 as D,o2 as E,i2 as F,r2 as G,h2 as H,x2 as I,K1 as J,Q1 as K,m2 as L,f2 as M,X1 as N,q2 as O,b2 as P,T2 as Q,j2 as R,L2 as S,O2 as T,K2 as U,F1 as V,M2 as W,a0 as X,_2 as Y,t0 as Z,a2 as _,E2 as a,V2 as a0,z2 as a1,J2 as a2,T1 as a3,t2 as a4,D1 as a5,X2 as a6,F2 as a7,n2 as a8,Q2 as a9,W2 as aa,Y2 as ab,Z2 as ac,g2 as ad,e0 as ae,I2 as af,B2 as ag,v2 as ah,s2 as b,S2 as c,I1 as d,O1 as e,l2 as f,k2 as g,G1 as h,R2 as i,G2 as j,e2 as k,C2 as l,H2 as m,U2 as n,P2 as o,Y1 as p,J1 as q,R1 as r,A2 as s,w2 as t,$2 as u,y2 as v,d2 as w,N2 as x,D2 as y,p2 as z};
|
package/dist/index.html
CHANGED
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-DxBc5bLY.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-Bv0Nkan8.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-sVRjxPVQ.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
|
|
32
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-icons-
|
|
32
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-icons-BWqhkbta.js">
|
|
33
33
|
<link rel="modulepreload" crossorigin href="/assets/vendor-katex-DK8hFnhL.js">
|
|
34
34
|
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-CjscLcYM.js">
|
|
35
35
|
<link rel="modulepreload" crossorigin href="/assets/vendor-syntax-BKENXTeY.js">
|
|
36
36
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CvdiG4-n.js">
|
|
37
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
37
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DURCpZD_.css">
|
|
38
38
|
</head>
|
|
39
39
|
<body>
|
|
40
40
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/claude-sdk.js
CHANGED
|
@@ -27,6 +27,46 @@ import { evaluate as evaluateToolGuard } from './services/tool-guard/index.js';
|
|
|
27
27
|
|
|
28
28
|
// Session tracking: Map of session IDs to active query instances
|
|
29
29
|
const activeSessions = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* MutableWriter wraps a WebSocketWriter and buffers messages while the client
|
|
33
|
+
* is disconnected. When the client reconnects, switchTo() replays the buffer
|
|
34
|
+
* to the new writer so no streaming chunks are lost.
|
|
35
|
+
*/
|
|
36
|
+
class MutableWriter {
|
|
37
|
+
constructor(ws) {
|
|
38
|
+
this.current = ws;
|
|
39
|
+
this.buffer = [];
|
|
40
|
+
this.MAX_BUFFER = 500;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
send(data) {
|
|
44
|
+
if (this.current?.ws?.readyState === 1) {
|
|
45
|
+
this.current.send(data);
|
|
46
|
+
} else if (this.buffer.length < this.MAX_BUFFER) {
|
|
47
|
+
this.buffer.push(data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setSessionId(sessionId) {
|
|
52
|
+
this.current?.setSessionId(sessionId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSessionId() {
|
|
56
|
+
return this.current?.getSessionId();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Attach a new WebSocketWriter and replay any buffered messages to it. */
|
|
60
|
+
switchTo(newWs) {
|
|
61
|
+
this.current = newWs;
|
|
62
|
+
const buf = this.buffer;
|
|
63
|
+
this.buffer = [];
|
|
64
|
+
for (const msg of buf) {
|
|
65
|
+
newWs.send(msg);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
// In-memory registry of pending tool approvals keyed by requestId.
|
|
31
71
|
// This does not persist approvals or share across processes; it exists so the
|
|
32
72
|
// SDK can pause tool execution while the UI decides what to do.
|
|
@@ -233,14 +273,15 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
233
273
|
* @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
|
|
234
274
|
* @param {string} tempDir - Temp directory for cleanup
|
|
235
275
|
*/
|
|
236
|
-
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, abortController = null) {
|
|
276
|
+
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, abortController = null, mutableWriter = null) {
|
|
237
277
|
activeSessions.set(sessionId, {
|
|
238
278
|
instance: queryInstance,
|
|
239
279
|
startTime: Date.now(),
|
|
240
280
|
status: 'active',
|
|
241
281
|
tempImagePaths,
|
|
242
282
|
tempDir,
|
|
243
|
-
abortController
|
|
283
|
+
abortController,
|
|
284
|
+
mutableWriter
|
|
244
285
|
});
|
|
245
286
|
}
|
|
246
287
|
|
|
@@ -261,6 +302,22 @@ function getSession(sessionId) {
|
|
|
261
302
|
return activeSessions.get(sessionId);
|
|
262
303
|
}
|
|
263
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Attaches a new WebSocketWriter to an active session, replaying any messages
|
|
307
|
+
* that were buffered while the previous connection was closed.
|
|
308
|
+
* @param {string} sessionId - Session identifier
|
|
309
|
+
* @param {Object} newWs - New WebSocketWriter instance
|
|
310
|
+
* @returns {boolean} True if the session was found and updated
|
|
311
|
+
*/
|
|
312
|
+
function updateSessionWriter(sessionId, newWs) {
|
|
313
|
+
const session = activeSessions.get(sessionId);
|
|
314
|
+
if (session?.mutableWriter) {
|
|
315
|
+
session.mutableWriter.switchTo(newWs);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
264
321
|
/**
|
|
265
322
|
* Gets all active session IDs
|
|
266
323
|
* @returns {Array<string>} Array of active session IDs
|
|
@@ -497,6 +554,10 @@ async function loadMcpConfig(cwd, userUuid) {
|
|
|
497
554
|
*/
|
|
498
555
|
async function queryClaudeSDK(command, options = {}, ws) {
|
|
499
556
|
const { sessionId, userUuid } = options;
|
|
557
|
+
|
|
558
|
+
// Wrap the WebSocketWriter in a MutableWriter so messages are buffered
|
|
559
|
+
// if the client disconnects mid-stream and replayed on reconnect.
|
|
560
|
+
const mutableWriter = new MutableWriter(ws);
|
|
500
561
|
let capturedSessionId = sessionId;
|
|
501
562
|
let sessionCreatedSent = false;
|
|
502
563
|
let tempImagePaths = [];
|
|
@@ -603,7 +664,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
603
664
|
}
|
|
604
665
|
|
|
605
666
|
const requestId = createRequestId();
|
|
606
|
-
|
|
667
|
+
mutableWriter.send({
|
|
607
668
|
type: 'claude-permission-request',
|
|
608
669
|
requestId,
|
|
609
670
|
toolName,
|
|
@@ -619,7 +680,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
619
680
|
timeoutMs: approvalTimeoutMs,
|
|
620
681
|
signal: context?.signal,
|
|
621
682
|
onCancel: (reason) => {
|
|
622
|
-
|
|
683
|
+
mutableWriter.send({
|
|
623
684
|
type: 'claude-permission-cancelled',
|
|
624
685
|
requestId,
|
|
625
686
|
reason,
|
|
@@ -660,7 +721,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
660
721
|
|
|
661
722
|
// Track the query instance for abort capability
|
|
662
723
|
if (capturedSessionId) {
|
|
663
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
724
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController, mutableWriter);
|
|
664
725
|
}
|
|
665
726
|
|
|
666
727
|
// Process streaming messages
|
|
@@ -670,17 +731,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
670
731
|
if (message.session_id && !capturedSessionId) {
|
|
671
732
|
|
|
672
733
|
capturedSessionId = message.session_id;
|
|
673
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
734
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController, mutableWriter);
|
|
674
735
|
|
|
675
736
|
// Set session ID on writer
|
|
676
737
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
677
|
-
|
|
738
|
+
mutableWriter.setSessionId(capturedSessionId);
|
|
678
739
|
}
|
|
679
740
|
|
|
680
741
|
// Send session-created event only once for new sessions
|
|
681
742
|
if (!sessionId && !sessionCreatedSent) {
|
|
682
743
|
sessionCreatedSent = true;
|
|
683
|
-
|
|
744
|
+
mutableWriter.send({
|
|
684
745
|
type: 'session-created',
|
|
685
746
|
sessionId: capturedSessionId
|
|
686
747
|
});
|
|
@@ -693,7 +754,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
693
754
|
|
|
694
755
|
// Transform and send message to WebSocket
|
|
695
756
|
const transformedMessage = transformMessage(message);
|
|
696
|
-
|
|
757
|
+
mutableWriter.send({
|
|
697
758
|
type: 'claude-response',
|
|
698
759
|
data: transformedMessage
|
|
699
760
|
});
|
|
@@ -703,7 +764,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
703
764
|
const tokenBudget = extractTokenBudget(message);
|
|
704
765
|
if (tokenBudget) {
|
|
705
766
|
console.log('Token budget from modelUsage:', tokenBudget);
|
|
706
|
-
|
|
767
|
+
mutableWriter.send({
|
|
707
768
|
type: 'token-budget',
|
|
708
769
|
data: tokenBudget
|
|
709
770
|
});
|
|
@@ -789,7 +850,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
789
850
|
// to the frontend, so we must not send a duplicate 'claude-complete'.
|
|
790
851
|
if (!abortController.signal.aborted) {
|
|
791
852
|
console.log('Streaming complete, sending claude-complete event');
|
|
792
|
-
|
|
853
|
+
mutableWriter.send({
|
|
793
854
|
type: 'claude-complete',
|
|
794
855
|
sessionId: capturedSessionId,
|
|
795
856
|
exitCode: 0,
|
|
@@ -822,7 +883,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
822
883
|
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
823
884
|
|
|
824
885
|
// Send error to WebSocket
|
|
825
|
-
|
|
886
|
+
mutableWriter.send({
|
|
826
887
|
type: 'claude-error',
|
|
827
888
|
error: error.message
|
|
828
889
|
});
|
|
@@ -915,5 +976,6 @@ export {
|
|
|
915
976
|
isClaudeSDKSessionActive,
|
|
916
977
|
getActiveClaudeSDKSessions,
|
|
917
978
|
resolveToolApproval,
|
|
918
|
-
renameSessionForUser
|
|
979
|
+
renameSessionForUser,
|
|
980
|
+
updateSessionWriter
|
|
919
981
|
};
|
package/server/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import fetch from 'node-fetch';
|
|
|
43
43
|
import mime from 'mime-types';
|
|
44
44
|
|
|
45
45
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, updateProjectLastActivity } from './projects.js';
|
|
46
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, renameSessionForUser } from './claude-sdk.js';
|
|
46
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, renameSessionForUser, updateSessionWriter } from './claude-sdk.js';
|
|
47
47
|
import authRoutes from './routes/auth.js';
|
|
48
48
|
import mcpRoutes from './routes/mcp.js';
|
|
49
49
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
@@ -808,6 +808,11 @@ function handleChatConnection(ws, userData) {
|
|
|
808
808
|
const sessionId = data.sessionId;
|
|
809
809
|
const isActive = isClaudeSDKSessionActive(sessionId);
|
|
810
810
|
|
|
811
|
+
// If still running, attach new writer to receive buffered + future messages
|
|
812
|
+
if (isActive) {
|
|
813
|
+
updateSessionWriter(sessionId, writer);
|
|
814
|
+
}
|
|
815
|
+
|
|
811
816
|
writer.send({
|
|
812
817
|
type: 'session-status',
|
|
813
818
|
sessionId,
|
package/server/routes/agents.js
CHANGED
|
@@ -6,6 +6,8 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
|
|
8
8
|
import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo } from '../services/system-agent-repo.js';
|
|
9
|
+
import { ensureSystemRepo, SYSTEM_REPO_URL } from '../services/system-repo.js';
|
|
10
|
+
import { ensureSystemMcpRepo, SYSTEM_MCP_REPO_URL } from '../services/system-mcp-repo.js';
|
|
9
11
|
import { addProjectManually, loadProjectConfig, saveProjectConfig } from '../projects.js';
|
|
10
12
|
import { agentSubmissionDb, userDb } from '../database/db.js';
|
|
11
13
|
import { chatCompletion } from '../services/llm.js';
|
|
@@ -77,6 +79,86 @@ function runGit(args, cwd = null) {
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Recursively copy all files and directories from src to dst.
|
|
84
|
+
*/
|
|
85
|
+
async function copyDirRecursive(src, dst) {
|
|
86
|
+
await fs.mkdir(dst, { recursive: true });
|
|
87
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const srcPath = path.join(src, entry.name);
|
|
90
|
+
const dstPath = path.join(dst, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
await copyDirRecursive(srcPath, dstPath);
|
|
93
|
+
} else {
|
|
94
|
+
await fs.copyFile(srcPath, dstPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Recursively add all files under dirPath into a ZIP object under the given zipPrefix.
|
|
101
|
+
*/
|
|
102
|
+
async function addDirToZip(zip, dirPath, zipPrefix) {
|
|
103
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
106
|
+
const zipPath = zipPrefix ? `${zipPrefix}/${entry.name}` : entry.name;
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
await addDirToZip(zip, fullPath, zipPath);
|
|
109
|
+
} else {
|
|
110
|
+
const content = await fs.readFile(fullPath);
|
|
111
|
+
zip.addFile(zipPath, content);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse skills list from agent.yaml content string.
|
|
118
|
+
* Returns array of { name, repo }.
|
|
119
|
+
*/
|
|
120
|
+
function parseYamlSkills(yamlContent) {
|
|
121
|
+
const skills = [];
|
|
122
|
+
const section = yamlContent.match(/^skills:\s*\n((?:[ \t]+.+\n?)*)/m);
|
|
123
|
+
if (!section) return skills;
|
|
124
|
+
const lines = section[1].split('\n');
|
|
125
|
+
let current = null;
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
|
|
128
|
+
if (nameMatch) {
|
|
129
|
+
if (current) skills.push(current);
|
|
130
|
+
current = { name: nameMatch[1].trim(), repo: '' };
|
|
131
|
+
}
|
|
132
|
+
const repoMatch = line.match(/^\s+repo:\s*["']?(.*?)["']?\s*$/);
|
|
133
|
+
if (repoMatch && current) current.repo = repoMatch[1].trim();
|
|
134
|
+
}
|
|
135
|
+
if (current) skills.push(current);
|
|
136
|
+
return skills;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse MCPs list from agent.yaml content string.
|
|
141
|
+
* Returns array of { name, repo }.
|
|
142
|
+
*/
|
|
143
|
+
function parseYamlMcps(yamlContent) {
|
|
144
|
+
const mcps = [];
|
|
145
|
+
const section = yamlContent.match(/^mcps:\s*\n((?:[ \t]+.+\n?)*)/m);
|
|
146
|
+
if (!section) return mcps;
|
|
147
|
+
const lines = section[1].split('\n');
|
|
148
|
+
let current = null;
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
|
|
151
|
+
if (nameMatch) {
|
|
152
|
+
if (current) mcps.push(current);
|
|
153
|
+
current = { name: nameMatch[1].trim(), repo: '' };
|
|
154
|
+
}
|
|
155
|
+
const repoMatch = line.match(/^\s+repo:\s*["']?(.*?)["']?\s*$/);
|
|
156
|
+
if (repoMatch && current) current.repo = repoMatch[1].trim();
|
|
157
|
+
}
|
|
158
|
+
if (current) mcps.push(current);
|
|
159
|
+
return mcps;
|
|
160
|
+
}
|
|
161
|
+
|
|
80
162
|
/**
|
|
81
163
|
* Ensure a skill repo is available for the user. Clone if needed.
|
|
82
164
|
* Returns the local path to the skill directory inside the repo.
|
|
@@ -549,7 +631,15 @@ router.get('/preview', async (req, res) => {
|
|
|
549
631
|
if (name.startsWith('.')) continue;
|
|
550
632
|
const linkPath = path.join(userPaths.skillsDir, name);
|
|
551
633
|
const repo = await getSkillRepoUrl(linkPath, publicPaths.skillsRepoDir);
|
|
552
|
-
|
|
634
|
+
let localPath = '';
|
|
635
|
+
if (!repo) {
|
|
636
|
+
// Skill not from a git repo — check if it's a user-imported skill
|
|
637
|
+
try {
|
|
638
|
+
const realPath = await fs.realpath(linkPath);
|
|
639
|
+
if (realPath.includes('/skills-import/')) localPath = realPath;
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
skills.push({ name, repo, localPath });
|
|
553
643
|
}
|
|
554
644
|
} catch {}
|
|
555
645
|
|
|
@@ -558,9 +648,22 @@ router.get('/preview', async (req, res) => {
|
|
|
558
648
|
try {
|
|
559
649
|
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
560
650
|
const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
651
|
+
|
|
652
|
+
// User-scoped MCPs (top-level mcpServers)
|
|
561
653
|
for (const name of Object.keys(claudeConfig.mcpServers || {})) {
|
|
562
654
|
const repo = await getMcpRepoUrl(name, publicPaths.mcpRepoDir);
|
|
563
|
-
|
|
655
|
+
const config = !repo ? (claudeConfig.mcpServers[name] || null) : null;
|
|
656
|
+
mcps.push({ name, repo, config, scope: 'user', mcpProjectPath: null });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Project-scoped MCPs (projects[projectPath].mcpServers)
|
|
660
|
+
const projectMcpServers = claudeConfig.projects?.[projectPath]?.mcpServers || {};
|
|
661
|
+
for (const name of Object.keys(projectMcpServers)) {
|
|
662
|
+
// Skip if already listed as user-scoped
|
|
663
|
+
if (mcps.some(m => m.name === name)) continue;
|
|
664
|
+
const repo = await getMcpRepoUrl(name, publicPaths.mcpRepoDir);
|
|
665
|
+
const config = !repo ? (projectMcpServers[name] || null) : null;
|
|
666
|
+
mcps.push({ name, repo, config, scope: 'local', mcpProjectPath: projectPath });
|
|
564
667
|
}
|
|
565
668
|
} catch {}
|
|
566
669
|
|
|
@@ -708,15 +811,15 @@ router.post('/generate-description', async (req, res) => {
|
|
|
708
811
|
const skillList = skills.length > 0 ? skills.map(s => `- ${s}`).join('\n') : '(无)';
|
|
709
812
|
const mcpList = mcps.length > 0 ? mcps.map(m => `- ${m}`).join('\n') : '(无)';
|
|
710
813
|
|
|
711
|
-
const systemPrompt = `你是一个技术文档专家,专门为
|
|
814
|
+
const systemPrompt = `你是一个技术文档专家,专门为 共享项目 编写简洁、清晰的功能描述。
|
|
712
815
|
描述应该:
|
|
713
816
|
- 简明扼要,2-4 句话
|
|
714
|
-
-
|
|
817
|
+
- 说明项目的主要用途和能力
|
|
715
818
|
- 自然流畅,不使用模板化套话
|
|
716
819
|
- 使用中文
|
|
717
820
|
只输出描述内容,不要有前缀或标题。`;
|
|
718
821
|
|
|
719
|
-
const userPrompt =
|
|
822
|
+
const userPrompt = `请根据以下信息,为这个项目生成一段功能描述:
|
|
720
823
|
|
|
721
824
|
${claudeMdContent ? `## CLAUDE.md 内容\n${claudeMdContent.slice(0, 3000)}\n` : '## CLAUDE.md\n(未找到)\n'}
|
|
722
825
|
## 已集成的 Skills
|
|
@@ -792,9 +895,44 @@ router.post('/submit', async (req, res) => {
|
|
|
792
895
|
}
|
|
793
896
|
}
|
|
794
897
|
|
|
898
|
+
// Include local skill files for imported skills (repo is absolute path)
|
|
899
|
+
const parsedSkills = parseYamlSkills(agentYaml);
|
|
900
|
+
for (const skill of parsedSkills) {
|
|
901
|
+
if (!skill.repo.startsWith('/')) continue; // Only handle absolute local paths
|
|
902
|
+
try {
|
|
903
|
+
await addDirToZip(zip, skill.repo, `_skill_files/${skill.name}`);
|
|
904
|
+
console.log(`[AgentSubmit] Included local skill files for "${skill.name}" from ${skill.repo}`);
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.warn(`[AgentSubmit] Could not include skill files for "${skill.name}": ${e.message}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Capture user-configured MCP configs (repo is empty)
|
|
911
|
+
const parsedMcps = parseYamlMcps(agentYaml);
|
|
912
|
+
if (parsedMcps.some(m => !m.repo)) {
|
|
913
|
+
try {
|
|
914
|
+
const userPaths = getUserPaths(userUuid);
|
|
915
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
916
|
+
const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
917
|
+
const projectMcpServers = claudeConfig.projects?.[projectPath]?.mcpServers || {};
|
|
918
|
+
for (const mcp of parsedMcps) {
|
|
919
|
+
if (mcp.repo) continue; // Already has a repo URL
|
|
920
|
+
// Check user-scoped first, then project-scoped
|
|
921
|
+
const serverConfig = claudeConfig.mcpServers?.[mcp.name] ?? projectMcpServers[mcp.name];
|
|
922
|
+
if (serverConfig) {
|
|
923
|
+
const mcpConfig = { [mcp.name]: serverConfig };
|
|
924
|
+
zip.addFile(`_mcp_configs/${mcp.name}/mcp.json`, Buffer.from(JSON.stringify(mcpConfig, null, 2)));
|
|
925
|
+
console.log(`[AgentSubmit] Captured user-configured MCP config for "${mcp.name}"`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
} catch (e) {
|
|
929
|
+
console.warn(`[AgentSubmit] Could not capture MCP configs: ${e.message}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
795
933
|
// Save ZIP to disk
|
|
796
934
|
const publicPaths = getPublicPaths();
|
|
797
|
-
const userSubmitDir = path.join(publicPaths.agentSubmissionsDir,
|
|
935
|
+
const userSubmitDir = path.join(publicPaths.agentSubmissionsDir, userUuid);
|
|
798
936
|
await fs.mkdir(userSubmitDir, { recursive: true });
|
|
799
937
|
|
|
800
938
|
const timestamp = Date.now();
|
|
@@ -946,6 +1084,199 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
946
1084
|
// Get submitter name for commit message
|
|
947
1085
|
const submitter = submission.username || submission.email || `user-${submission.user_id}`;
|
|
948
1086
|
|
|
1087
|
+
// Get submitter's UUID for skill/MCP operations
|
|
1088
|
+
const allUsers = userDb.getAllUsers();
|
|
1089
|
+
const submitterUser = allUsers.find(u => u.id === submission.user_id);
|
|
1090
|
+
const submitterUuid = submitterUser?.uuid;
|
|
1091
|
+
|
|
1092
|
+
// Migrate local skills and user-configured MCPs before publishing
|
|
1093
|
+
const agentYamlPath = path.join(extractDir, 'agent.yaml');
|
|
1094
|
+
let agentYamlContent;
|
|
1095
|
+
try {
|
|
1096
|
+
agentYamlContent = await fs.readFile(agentYamlPath, 'utf-8');
|
|
1097
|
+
} catch {
|
|
1098
|
+
agentYamlContent = null;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (agentYamlContent && submitterUuid) {
|
|
1102
|
+
// === A. Migrate local skills (repo is absolute path starting with "/") ===
|
|
1103
|
+
const yamlSkills = parseYamlSkills(agentYamlContent);
|
|
1104
|
+
const localSkills = yamlSkills.filter(s => s.repo.startsWith('/'));
|
|
1105
|
+
|
|
1106
|
+
if (localSkills.length > 0) {
|
|
1107
|
+
let skillRepoPath;
|
|
1108
|
+
try {
|
|
1109
|
+
skillRepoPath = await ensureSystemRepo(); // git pull or clone
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
console.error('[AgentApprove] Failed to ensure system skill repo:', e.message);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (skillRepoPath) {
|
|
1115
|
+
for (const skill of localSkills) {
|
|
1116
|
+
try {
|
|
1117
|
+
// Copy skill folder from extracted ZIP to system skill repo
|
|
1118
|
+
const srcSkillDir = path.join(extractDir, '_skill_files', skill.name);
|
|
1119
|
+
const destSkillDir = path.join(skillRepoPath, skill.name);
|
|
1120
|
+
|
|
1121
|
+
// Verify source exists in ZIP
|
|
1122
|
+
try {
|
|
1123
|
+
await fs.access(srcSkillDir);
|
|
1124
|
+
} catch {
|
|
1125
|
+
console.warn(`[AgentApprove] Skill files for "${skill.name}" not found in submission ZIP, skipping`);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Remove old version in repo if it exists
|
|
1130
|
+
await fs.rm(destSkillDir, { recursive: true, force: true });
|
|
1131
|
+
await copyDirRecursive(srcSkillDir, destSkillDir);
|
|
1132
|
+
console.log(`[AgentApprove] Copied skill "${skill.name}" to system skill repo`);
|
|
1133
|
+
|
|
1134
|
+
// Git: add, commit, push
|
|
1135
|
+
await runGit(['add', skill.name], skillRepoPath);
|
|
1136
|
+
await runGit(
|
|
1137
|
+
['commit', '-m', `feat: add skill ${skill.name} from approved agent submission #${submission.id}`],
|
|
1138
|
+
skillRepoPath
|
|
1139
|
+
);
|
|
1140
|
+
await runGit(['push'], skillRepoPath);
|
|
1141
|
+
console.log(`[AgentApprove] Pushed skill "${skill.name}" to remote`);
|
|
1142
|
+
|
|
1143
|
+
// Clean up submitter's skills-import directory for this skill
|
|
1144
|
+
const userPaths = getUserPaths(submitterUuid);
|
|
1145
|
+
try {
|
|
1146
|
+
await fs.rm(path.join(userPaths.skillsImportDir, skill.name), { recursive: true, force: true });
|
|
1147
|
+
} catch {}
|
|
1148
|
+
|
|
1149
|
+
// Re-install skill for submitter from the system repo
|
|
1150
|
+
await installSkill(skill.name, SYSTEM_REPO_URL, submitterUuid);
|
|
1151
|
+
console.log(`[AgentApprove] Re-installed skill "${skill.name}" for submitter from system repo`);
|
|
1152
|
+
|
|
1153
|
+
// Update agent.yaml: replace absolute path with system repo git URL
|
|
1154
|
+
const escapedName = skill.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1155
|
+
agentYamlContent = agentYamlContent.replace(
|
|
1156
|
+
new RegExp(`(- name: "${escapedName}"\\s*\\n\\s+repo: )"[^"]*"`, 'gm'),
|
|
1157
|
+
`$1"${SYSTEM_REPO_URL}"`
|
|
1158
|
+
);
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
console.error(`[AgentApprove] Failed to migrate skill "${skill.name}":`, e.message);
|
|
1161
|
+
// Continue with other skills even if one fails
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// === B. Migrate user-configured MCPs (repo is empty, config in _mcp_configs/) ===
|
|
1168
|
+
const yamlMcps = parseYamlMcps(agentYamlContent);
|
|
1169
|
+
const localMcps = yamlMcps.filter(m => !m.repo);
|
|
1170
|
+
|
|
1171
|
+
if (localMcps.length > 0) {
|
|
1172
|
+
let mcpRepoPath;
|
|
1173
|
+
try {
|
|
1174
|
+
mcpRepoPath = await ensureSystemMcpRepo(); // git pull or clone
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
console.error('[AgentApprove] Failed to ensure system MCP repo:', e.message);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (mcpRepoPath) {
|
|
1180
|
+
for (const mcp of localMcps) {
|
|
1181
|
+
try {
|
|
1182
|
+
// Read captured MCP config from ZIP extraction
|
|
1183
|
+
const mcpConfigPath = path.join(extractDir, '_mcp_configs', mcp.name, 'mcp.json');
|
|
1184
|
+
let mcpConfig;
|
|
1185
|
+
try {
|
|
1186
|
+
mcpConfig = JSON.parse(await fs.readFile(mcpConfigPath, 'utf-8'));
|
|
1187
|
+
} catch {
|
|
1188
|
+
console.warn(`[AgentApprove] No captured config for MCP "${mcp.name}", skipping`);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Create MCP folder in system MCP repo
|
|
1193
|
+
const mcpFolderPath = path.join(mcpRepoPath, mcp.name);
|
|
1194
|
+
await fs.mkdir(mcpFolderPath, { recursive: true });
|
|
1195
|
+
|
|
1196
|
+
// Write mcp.json
|
|
1197
|
+
await fs.writeFile(path.join(mcpFolderPath, 'mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
1198
|
+
|
|
1199
|
+
// Write mcp.yaml with basic metadata
|
|
1200
|
+
const mcpYaml = `name: "${mcp.name}"\ndescription: "MCP service migrated from shared project '${submission.display_name}' (submission #${submission.id})"\n`;
|
|
1201
|
+
await fs.writeFile(path.join(mcpFolderPath, 'mcp.yaml'), mcpYaml);
|
|
1202
|
+
console.log(`[AgentApprove] Created MCP "${mcp.name}" in system MCP repo`);
|
|
1203
|
+
|
|
1204
|
+
// Git: add, commit, push
|
|
1205
|
+
await runGit(['add', mcp.name], mcpRepoPath);
|
|
1206
|
+
await runGit(
|
|
1207
|
+
['commit', '-m', `feat: add mcp ${mcp.name} from approved agent submission #${submission.id}`],
|
|
1208
|
+
mcpRepoPath
|
|
1209
|
+
);
|
|
1210
|
+
await runGit(['push'], mcpRepoPath);
|
|
1211
|
+
console.log(`[AgentApprove] Pushed MCP "${mcp.name}" to remote`);
|
|
1212
|
+
|
|
1213
|
+
// Remove MCP from submitter's .claude.json (user-scoped or project-scoped)
|
|
1214
|
+
const userPaths = getUserPaths(submitterUuid);
|
|
1215
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
1216
|
+
try {
|
|
1217
|
+
const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
1218
|
+
let changed = false;
|
|
1219
|
+
// Remove from user-scoped
|
|
1220
|
+
if (claudeConfig.mcpServers) {
|
|
1221
|
+
for (const serverKey of Object.keys(mcpConfig)) {
|
|
1222
|
+
if (serverKey in claudeConfig.mcpServers) {
|
|
1223
|
+
delete claudeConfig.mcpServers[serverKey];
|
|
1224
|
+
changed = true;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// Remove from all project-scoped entries
|
|
1229
|
+
if (claudeConfig.projects) {
|
|
1230
|
+
for (const projectConfig of Object.values(claudeConfig.projects)) {
|
|
1231
|
+
if (projectConfig?.mcpServers) {
|
|
1232
|
+
for (const serverKey of Object.keys(mcpConfig)) {
|
|
1233
|
+
if (serverKey in projectConfig.mcpServers) {
|
|
1234
|
+
delete projectConfig.mcpServers[serverKey];
|
|
1235
|
+
changed = true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (changed) {
|
|
1242
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
1243
|
+
}
|
|
1244
|
+
} catch (e) {
|
|
1245
|
+
console.warn(`[AgentApprove] Could not remove MCP "${mcp.name}" from submitter .claude.json:`, e.message);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Re-install MCP for submitter from system repo
|
|
1249
|
+
await installMcp(mcp.name, SYSTEM_MCP_REPO_URL, submitterUuid);
|
|
1250
|
+
console.log(`[AgentApprove] Re-installed MCP "${mcp.name}" for submitter from system repo`);
|
|
1251
|
+
|
|
1252
|
+
// Update agent.yaml: replace empty repo with system MCP repo git URL
|
|
1253
|
+
const escapedName = mcp.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1254
|
+
agentYamlContent = agentYamlContent.replace(
|
|
1255
|
+
new RegExp(`(- name: "${escapedName}"\\s*\\n\\s+repo: )""`, 'gm'),
|
|
1256
|
+
`$1"${SYSTEM_MCP_REPO_URL}"`
|
|
1257
|
+
);
|
|
1258
|
+
} catch (e) {
|
|
1259
|
+
console.error(`[AgentApprove] Failed to migrate MCP "${mcp.name}":`, e.message);
|
|
1260
|
+
// Continue with other MCPs even if one fails
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Write updated agent.yaml back to extractDir so publishAgentToRepo uses correct repo URLs
|
|
1267
|
+
if (localSkills.length > 0 || localMcps.length > 0) {
|
|
1268
|
+
try {
|
|
1269
|
+
await fs.writeFile(agentYamlPath, agentYamlContent, 'utf-8');
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
console.error('[AgentApprove] Failed to write updated agent.yaml:', e.message);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Remove temporary migration directories from extractDir before publishing to agent repo
|
|
1276
|
+
await fs.rm(path.join(extractDir, '_skill_files'), { recursive: true, force: true });
|
|
1277
|
+
await fs.rm(path.join(extractDir, '_mcp_configs'), { recursive: true, force: true });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
949
1280
|
// Publish to git repo
|
|
950
1281
|
try {
|
|
951
1282
|
await publishAgentToRepo(submission.agent_name, extractDir, newVersion, submitter);
|