@ian2018cs/agenthub 0.1.69 → 0.1.71
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-CyYbCDk1.js +192 -0
- package/dist/assets/index-DQaPJRqa.css +32 -0
- package/dist/assets/{vendor-icons-DxBNDMja.js → vendor-icons-CFgKYN6c.js} +77 -72
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/server/claude-sdk.js +231 -15
- package/server/index.js +33 -1
- package/server/routes/agents.js +336 -5
- package/server/routes/mcp.js +122 -147
- package/server/routes/skills.js +341 -1
- package/server/services/system-mcp-repo.js +1 -1
- package/server/services/system-repo.js +1 -1
- package/dist/assets/index-HOTjBpXH.css +0 -32
- package/dist/assets/index-u5cEXvaS.js +0 -184
package/server/claude-sdk.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - WebSocket message streaming
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { query, renameSession } from '@anthropic-ai/claude-agent-sdk';
|
|
15
|
+
import { query, renameSession, forkSession } from '@anthropic-ai/claude-agent-sdk';
|
|
16
16
|
// Used to mint unique approval request IDs when randomUUID is not available.
|
|
17
17
|
// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
|
|
18
18
|
import crypto from 'crypto';
|
|
@@ -24,9 +24,50 @@ import { getUserPaths } from './services/user-directories.js';
|
|
|
24
24
|
import { usageDb } from './database/db.js';
|
|
25
25
|
import { calculateCost, normalizeModelName } from './services/pricing.js';
|
|
26
26
|
import { evaluate as evaluateToolGuard } from './services/tool-guard/index.js';
|
|
27
|
+
import { loadProjectConfig, addProjectManually } from './projects.js';
|
|
27
28
|
|
|
28
29
|
// Session tracking: Map of session IDs to active query instances
|
|
29
30
|
const activeSessions = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MutableWriter wraps a WebSocketWriter and buffers messages while the client
|
|
34
|
+
* is disconnected. When the client reconnects, switchTo() replays the buffer
|
|
35
|
+
* to the new writer so no streaming chunks are lost.
|
|
36
|
+
*/
|
|
37
|
+
class MutableWriter {
|
|
38
|
+
constructor(ws) {
|
|
39
|
+
this.current = ws;
|
|
40
|
+
this.buffer = [];
|
|
41
|
+
this.MAX_BUFFER = 500;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
send(data) {
|
|
45
|
+
if (this.current?.ws?.readyState === 1) {
|
|
46
|
+
this.current.send(data);
|
|
47
|
+
} else if (this.buffer.length < this.MAX_BUFFER) {
|
|
48
|
+
this.buffer.push(data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setSessionId(sessionId) {
|
|
53
|
+
this.current?.setSessionId(sessionId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getSessionId() {
|
|
57
|
+
return this.current?.getSessionId();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Attach a new WebSocketWriter and replay any buffered messages to it. */
|
|
61
|
+
switchTo(newWs) {
|
|
62
|
+
this.current = newWs;
|
|
63
|
+
const buf = this.buffer;
|
|
64
|
+
this.buffer = [];
|
|
65
|
+
for (const msg of buf) {
|
|
66
|
+
newWs.send(msg);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
// In-memory registry of pending tool approvals keyed by requestId.
|
|
31
72
|
// This does not persist approvals or share across processes; it exists so the
|
|
32
73
|
// SDK can pause tool execution while the UI decides what to do.
|
|
@@ -37,6 +78,30 @@ const pendingToolApprovals = new Map();
|
|
|
37
78
|
// introduced to avoid hanging the run when no decision arrives.
|
|
38
79
|
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
|
39
80
|
|
|
81
|
+
// ─── 分享项目模板:pending 请求管理 ───
|
|
82
|
+
const pendingShareTemplateRequests = new Map();
|
|
83
|
+
const SHARE_TEMPLATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟(用户需要时间审阅和修改模板)
|
|
84
|
+
|
|
85
|
+
function waitForShareTemplateResponse(requestId) {
|
|
86
|
+
return new Promise(resolve => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
const finalize = (result) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
pendingShareTemplateRequests.delete(requestId);
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
resolve(result);
|
|
94
|
+
};
|
|
95
|
+
const timeout = setTimeout(() => finalize(null), SHARE_TEMPLATE_TIMEOUT_MS);
|
|
96
|
+
pendingShareTemplateRequests.set(requestId, finalize);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveShareTemplateRequest(requestId, result) {
|
|
101
|
+
const resolver = pendingShareTemplateRequests.get(requestId);
|
|
102
|
+
if (resolver) resolver(result);
|
|
103
|
+
}
|
|
104
|
+
|
|
40
105
|
// Generate a stable request ID for UI approval flows.
|
|
41
106
|
// This does not encode tool details or get shown to users; it exists so the UI
|
|
42
107
|
// can respond to the correct pending request without collisions.
|
|
@@ -206,7 +271,28 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
206
271
|
console.log(`Using model: ${sdkOptions.model}`);
|
|
207
272
|
|
|
208
273
|
// Map system prompt configuration
|
|
209
|
-
const
|
|
274
|
+
const SHARE_PROJECT_TEMPLATE_TOOL_DESC = `
|
|
275
|
+
|
|
276
|
+
## 分享项目模板工具
|
|
277
|
+
|
|
278
|
+
当用户想要分享项目、创建项目模板、发布共享项目时,使用以下 Bash 命令打开分享弹窗:
|
|
279
|
+
|
|
280
|
+
\`\`\`bash
|
|
281
|
+
__share_project_template__ '{"path":"/项目路径","displayName":"显示名称","description":"描述","updateNotes":"更新说明","skills":["skill1"],"mcps":["mcp1"],"files":["file1.md"]}'
|
|
282
|
+
\`\`\`
|
|
283
|
+
|
|
284
|
+
参数说明:
|
|
285
|
+
- path (必填): 项目文件夹路径,可以是当前项目、子文件夹或其他项目路径
|
|
286
|
+
- displayName (可选): 模板显示名称,仅首次创建时使用(更新时忽略)
|
|
287
|
+
- description (可选): 模板描述,仅首次创建时使用(更新时忽略)
|
|
288
|
+
- updateNotes (可选): 更新说明,仅更新已有模板时使用
|
|
289
|
+
- skills (可选): 要包含的技能名称数组,空数组=让用户在弹窗中选择
|
|
290
|
+
- mcps (可选): 要包含的 MCP 服务名称数组,空数组=让用户在弹窗中选择
|
|
291
|
+
- files (可选): 要包含的文件相对路径数组,空数组=让用户在弹窗中选择
|
|
292
|
+
|
|
293
|
+
该命令会打开一个 UI 弹窗供用户确认和修改,等待用户操作完成后返回结果。
|
|
294
|
+
`;
|
|
295
|
+
const baseAppend = PRODUCT_SYSTEM_DESC + SHARE_PROJECT_TEMPLATE_TOOL_DESC;
|
|
210
296
|
const extraAppend = options.appendSystemPrompt ? `\n\n${options.appendSystemPrompt}` : '';
|
|
211
297
|
sdkOptions.systemPrompt = {
|
|
212
298
|
type: 'preset',
|
|
@@ -233,14 +319,15 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
233
319
|
* @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
|
|
234
320
|
* @param {string} tempDir - Temp directory for cleanup
|
|
235
321
|
*/
|
|
236
|
-
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, abortController = null) {
|
|
322
|
+
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, abortController = null, mutableWriter = null) {
|
|
237
323
|
activeSessions.set(sessionId, {
|
|
238
324
|
instance: queryInstance,
|
|
239
325
|
startTime: Date.now(),
|
|
240
326
|
status: 'active',
|
|
241
327
|
tempImagePaths,
|
|
242
328
|
tempDir,
|
|
243
|
-
abortController
|
|
329
|
+
abortController,
|
|
330
|
+
mutableWriter
|
|
244
331
|
});
|
|
245
332
|
}
|
|
246
333
|
|
|
@@ -261,6 +348,22 @@ function getSession(sessionId) {
|
|
|
261
348
|
return activeSessions.get(sessionId);
|
|
262
349
|
}
|
|
263
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Attaches a new WebSocketWriter to an active session, replaying any messages
|
|
353
|
+
* that were buffered while the previous connection was closed.
|
|
354
|
+
* @param {string} sessionId - Session identifier
|
|
355
|
+
* @param {Object} newWs - New WebSocketWriter instance
|
|
356
|
+
* @returns {boolean} True if the session was found and updated
|
|
357
|
+
*/
|
|
358
|
+
function updateSessionWriter(sessionId, newWs) {
|
|
359
|
+
const session = activeSessions.get(sessionId);
|
|
360
|
+
if (session?.mutableWriter) {
|
|
361
|
+
session.mutableWriter.switchTo(newWs);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
264
367
|
/**
|
|
265
368
|
* Gets all active session IDs
|
|
266
369
|
* @returns {Array<string>} Array of active session IDs
|
|
@@ -497,6 +600,10 @@ async function loadMcpConfig(cwd, userUuid) {
|
|
|
497
600
|
*/
|
|
498
601
|
async function queryClaudeSDK(command, options = {}, ws) {
|
|
499
602
|
const { sessionId, userUuid } = options;
|
|
603
|
+
|
|
604
|
+
// Wrap the WebSocketWriter in a MutableWriter so messages are buffered
|
|
605
|
+
// if the client disconnects mid-stream and replayed on reconnect.
|
|
606
|
+
const mutableWriter = new MutableWriter(ws);
|
|
500
607
|
let capturedSessionId = sessionId;
|
|
501
608
|
let sessionCreatedSent = false;
|
|
502
609
|
let tempImagePaths = [];
|
|
@@ -540,6 +647,98 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
540
647
|
...((sdkOptions.hooks || {}).PreToolUse || []),
|
|
541
648
|
{
|
|
542
649
|
hooks: [async (hookInput) => {
|
|
650
|
+
// ===== 分享项目模板拦截(在 ToolGuard 之前,匹配后直接 return) =====
|
|
651
|
+
if (hookInput.tool_name === 'Bash' &&
|
|
652
|
+
hookInput.tool_input?.command?.trimStart().startsWith('__share_project_template__')) {
|
|
653
|
+
try {
|
|
654
|
+
// 检测飞书模式:MutableWriter.current 有 .ws 属性(WebSocketWriter),FakeSendWriter 没有
|
|
655
|
+
if (!mutableWriter?.current?.ws) {
|
|
656
|
+
return {
|
|
657
|
+
hookSpecificOutput: {
|
|
658
|
+
hookEventName: 'PreToolUse',
|
|
659
|
+
permissionDecision: 'deny',
|
|
660
|
+
permissionDecisionReason: '分享项目模板功能仅支持网页端使用。',
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// 解析参数
|
|
666
|
+
const rawArgs = hookInput.tool_input.command.replace(/^\s*__share_project_template__\s*/, '');
|
|
667
|
+
let jsonStr = rawArgs.trim();
|
|
668
|
+
if ((jsonStr.startsWith("'") && jsonStr.endsWith("'")) ||
|
|
669
|
+
(jsonStr.startsWith('"') && jsonStr.endsWith('"'))) {
|
|
670
|
+
jsonStr = jsonStr.slice(1, -1);
|
|
671
|
+
}
|
|
672
|
+
const params = JSON.parse(jsonStr);
|
|
673
|
+
|
|
674
|
+
if (!params.path) {
|
|
675
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
676
|
+
permissionDecisionReason: '缺少必需参数 path(项目路径)。' } };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 解析 path → projectKey,并判断是否为已有项目
|
|
680
|
+
const config = await loadProjectConfig(userUuid);
|
|
681
|
+
let projectKey = null;
|
|
682
|
+
let isExistingProject = false;
|
|
683
|
+
for (const [key, entry] of Object.entries(config)) {
|
|
684
|
+
if ((entry.originalPath || key.replace(/-/g, '/')) === params.path) {
|
|
685
|
+
projectKey = key;
|
|
686
|
+
isExistingProject = true;
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (!projectKey) {
|
|
691
|
+
// 路径不在项目列表中(可能是子文件夹),自动添加为新项目
|
|
692
|
+
try {
|
|
693
|
+
const project = await addProjectManually(params.path, params.displayName, userUuid);
|
|
694
|
+
projectKey = project.name;
|
|
695
|
+
isExistingProject = false;
|
|
696
|
+
} catch (e) {
|
|
697
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
698
|
+
permissionDecisionReason: `无法解析项目路径: ${e.message}` } };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 发送 WebSocket 消息打开分享弹窗
|
|
703
|
+
const shareRequestId = createRequestId();
|
|
704
|
+
mutableWriter.send({
|
|
705
|
+
type: 'share-project-template-request',
|
|
706
|
+
requestId: shareRequestId,
|
|
707
|
+
prefillData: {
|
|
708
|
+
projectKey,
|
|
709
|
+
projectPath: params.path,
|
|
710
|
+
isExistingProject,
|
|
711
|
+
displayName: params.displayName || '',
|
|
712
|
+
description: params.description || '',
|
|
713
|
+
updateNotes: params.updateNotes || '',
|
|
714
|
+
skills: params.skills || [],
|
|
715
|
+
mcps: params.mcps || [],
|
|
716
|
+
files: params.files || [],
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// 等待前端响应(5 分钟超时)
|
|
721
|
+
const response = await waitForShareTemplateResponse(shareRequestId);
|
|
722
|
+
|
|
723
|
+
let reason;
|
|
724
|
+
if (!response) {
|
|
725
|
+
reason = '分享项目模板请求超时(5分钟内未收到响应)。';
|
|
726
|
+
} else if (response.cancelled) {
|
|
727
|
+
reason = '用户取消了分享项目模板操作。';
|
|
728
|
+
} else if (response.success) {
|
|
729
|
+
reason = `项目模板已成功提交!提交 ID: ${response.submissionId}。${response.message || ''}`;
|
|
730
|
+
} else {
|
|
731
|
+
reason = `提交失败: ${response.error || '未知错误'}`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } };
|
|
735
|
+
} catch (err) {
|
|
736
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
737
|
+
permissionDecisionReason: `分享项目模板失败: ${err.message}` } };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ===== 安全守卫(Tool Guard)=====
|
|
543
742
|
try {
|
|
544
743
|
const guardResult = await evaluateToolGuard(hookInput.tool_name, hookInput.tool_input, {
|
|
545
744
|
userUuid,
|
|
@@ -603,7 +802,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
603
802
|
}
|
|
604
803
|
|
|
605
804
|
const requestId = createRequestId();
|
|
606
|
-
|
|
805
|
+
mutableWriter.send({
|
|
607
806
|
type: 'claude-permission-request',
|
|
608
807
|
requestId,
|
|
609
808
|
toolName,
|
|
@@ -619,7 +818,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
619
818
|
timeoutMs: approvalTimeoutMs,
|
|
620
819
|
signal: context?.signal,
|
|
621
820
|
onCancel: (reason) => {
|
|
622
|
-
|
|
821
|
+
mutableWriter.send({
|
|
623
822
|
type: 'claude-permission-cancelled',
|
|
624
823
|
requestId,
|
|
625
824
|
reason,
|
|
@@ -660,7 +859,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
660
859
|
|
|
661
860
|
// Track the query instance for abort capability
|
|
662
861
|
if (capturedSessionId) {
|
|
663
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
862
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController, mutableWriter);
|
|
664
863
|
}
|
|
665
864
|
|
|
666
865
|
// Process streaming messages
|
|
@@ -670,17 +869,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
670
869
|
if (message.session_id && !capturedSessionId) {
|
|
671
870
|
|
|
672
871
|
capturedSessionId = message.session_id;
|
|
673
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
872
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController, mutableWriter);
|
|
674
873
|
|
|
675
874
|
// Set session ID on writer
|
|
676
875
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
677
|
-
|
|
876
|
+
mutableWriter.setSessionId(capturedSessionId);
|
|
678
877
|
}
|
|
679
878
|
|
|
680
879
|
// Send session-created event only once for new sessions
|
|
681
880
|
if (!sessionId && !sessionCreatedSent) {
|
|
682
881
|
sessionCreatedSent = true;
|
|
683
|
-
|
|
882
|
+
mutableWriter.send({
|
|
684
883
|
type: 'session-created',
|
|
685
884
|
sessionId: capturedSessionId
|
|
686
885
|
});
|
|
@@ -693,7 +892,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
693
892
|
|
|
694
893
|
// Transform and send message to WebSocket
|
|
695
894
|
const transformedMessage = transformMessage(message);
|
|
696
|
-
|
|
895
|
+
mutableWriter.send({
|
|
697
896
|
type: 'claude-response',
|
|
698
897
|
data: transformedMessage
|
|
699
898
|
});
|
|
@@ -703,7 +902,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
703
902
|
const tokenBudget = extractTokenBudget(message);
|
|
704
903
|
if (tokenBudget) {
|
|
705
904
|
console.log('Token budget from modelUsage:', tokenBudget);
|
|
706
|
-
|
|
905
|
+
mutableWriter.send({
|
|
707
906
|
type: 'token-budget',
|
|
708
907
|
data: tokenBudget
|
|
709
908
|
});
|
|
@@ -789,7 +988,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
789
988
|
// to the frontend, so we must not send a duplicate 'claude-complete'.
|
|
790
989
|
if (!abortController.signal.aborted) {
|
|
791
990
|
console.log('Streaming complete, sending claude-complete event');
|
|
792
|
-
|
|
991
|
+
mutableWriter.send({
|
|
793
992
|
type: 'claude-complete',
|
|
794
993
|
sessionId: capturedSessionId,
|
|
795
994
|
exitCode: 0,
|
|
@@ -822,7 +1021,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
822
1021
|
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
823
1022
|
|
|
824
1023
|
// Send error to WebSocket
|
|
825
|
-
|
|
1024
|
+
mutableWriter.send({
|
|
826
1025
|
type: 'claude-error',
|
|
827
1026
|
error: error.message
|
|
828
1027
|
});
|
|
@@ -908,6 +1107,20 @@ async function renameSessionForUser(sessionId, title, userUuid) {
|
|
|
908
1107
|
await renameSession(sessionId, title);
|
|
909
1108
|
}
|
|
910
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* Fork a session at a given message, creating a new branch conversation.
|
|
1112
|
+
* @param {string} sessionId - Session to fork
|
|
1113
|
+
* @param {string} userUuid - User UUID for directory isolation
|
|
1114
|
+
* @param {object} opts - Fork options (upToMessageId, title)
|
|
1115
|
+
* @returns {Promise<string>} New session UUID
|
|
1116
|
+
*/
|
|
1117
|
+
async function forkSessionForUser(sessionId, userUuid, opts = {}) {
|
|
1118
|
+
const userPaths = getUserPaths(userUuid);
|
|
1119
|
+
process.env.CLAUDE_CONFIG_DIR = userPaths.claudeDir;
|
|
1120
|
+
const result = await forkSession(sessionId, opts);
|
|
1121
|
+
return result.sessionId;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
911
1124
|
// Export public API
|
|
912
1125
|
export {
|
|
913
1126
|
queryClaudeSDK,
|
|
@@ -915,5 +1128,8 @@ export {
|
|
|
915
1128
|
isClaudeSDKSessionActive,
|
|
916
1129
|
getActiveClaudeSDKSessions,
|
|
917
1130
|
resolveToolApproval,
|
|
918
|
-
|
|
1131
|
+
resolveShareTemplateRequest,
|
|
1132
|
+
renameSessionForUser,
|
|
1133
|
+
forkSessionForUser,
|
|
1134
|
+
updateSessionWriter
|
|
919
1135
|
};
|
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, resolveShareTemplateRequest, renameSessionForUser, forkSessionForUser, 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';
|
|
@@ -455,6 +455,22 @@ app.put('/api/projects/:projectName/sessions/:sessionId/rename', authenticateTok
|
|
|
455
455
|
}
|
|
456
456
|
});
|
|
457
457
|
|
|
458
|
+
// Fork session endpoint — creates a new branch conversation from a given point
|
|
459
|
+
app.post('/api/projects/:projectName/sessions/:sessionId/fork', authenticateToken, async (req, res) => {
|
|
460
|
+
try {
|
|
461
|
+
const { sessionId } = req.params;
|
|
462
|
+
const opts = {};
|
|
463
|
+
if (req.body?.upToMessageId) {
|
|
464
|
+
opts.upToMessageId = req.body.upToMessageId;
|
|
465
|
+
}
|
|
466
|
+
const newSessionId = await forkSessionForUser(sessionId, req.user.uuid, opts);
|
|
467
|
+
res.json({ newSessionId });
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error(`[API] Error forking session ${req.params.sessionId}:`, error);
|
|
470
|
+
res.status(500).json({ error: error.message });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
458
474
|
// Delete project endpoint
|
|
459
475
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
460
476
|
try {
|
|
@@ -803,11 +819,27 @@ function handleChatConnection(ws, userData) {
|
|
|
803
819
|
rememberEntry: data.rememberEntry
|
|
804
820
|
});
|
|
805
821
|
}
|
|
822
|
+
} else if (data.type === 'share-project-template-response') {
|
|
823
|
+
// Relay the user's share-project-template decision back to the PreToolUse hook.
|
|
824
|
+
if (data.requestId) {
|
|
825
|
+
resolveShareTemplateRequest(data.requestId, {
|
|
826
|
+
success: Boolean(data.success),
|
|
827
|
+
cancelled: Boolean(data.cancelled),
|
|
828
|
+
submissionId: data.submissionId || null,
|
|
829
|
+
message: data.message || '',
|
|
830
|
+
error: data.error || '',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
806
833
|
} else if (data.type === 'check-session-status') {
|
|
807
834
|
// Check if a specific session is currently processing
|
|
808
835
|
const sessionId = data.sessionId;
|
|
809
836
|
const isActive = isClaudeSDKSessionActive(sessionId);
|
|
810
837
|
|
|
838
|
+
// If still running, attach new writer to receive buffered + future messages
|
|
839
|
+
if (isActive) {
|
|
840
|
+
updateSessionWriter(sessionId, writer);
|
|
841
|
+
}
|
|
842
|
+
|
|
811
843
|
writer.send({
|
|
812
844
|
type: 'session-status',
|
|
813
845
|
sessionId,
|