@shaykec/bridge 0.4.18 → 0.4.20
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/canvas-dist/assets/{_basePickBy-BOTBlJNd.js → _basePickBy-CWoeT3J7.js} +1 -1
- package/canvas-dist/assets/{_baseUniq-EF6Y2_Wm.js → _baseUniq-Dtuvtwtn.js} +1 -1
- package/canvas-dist/assets/{arc-C_vIirh2.js → arc-YYWnrNJU.js} +1 -1
- package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-EvM6tQ7I.js → architectureDiagram-VXUJARFQ-CegbV-RR.js} +1 -1
- package/canvas-dist/assets/{blockDiagram-VD42YOAC-B_rbZyqc.js → blockDiagram-VD42YOAC-C2e_j6ry.js} +1 -1
- package/canvas-dist/assets/{c4Diagram-YG6GDRKO-J9PHecY3.js → c4Diagram-YG6GDRKO-rIpnAud9.js} +1 -1
- package/canvas-dist/assets/channel-BzJVlie3.js +1 -0
- package/canvas-dist/assets/{chunk-4BX2VUAB-DjcN96Mk.js → chunk-4BX2VUAB-CpZGetnU.js} +1 -1
- package/canvas-dist/assets/{chunk-55IACEB6-CTdcUQSV.js → chunk-55IACEB6-L0OhcFdd.js} +1 -1
- package/canvas-dist/assets/{chunk-B4BG7PRW-Dcov7eRi.js → chunk-B4BG7PRW-Cv9vsAzg.js} +1 -1
- package/canvas-dist/assets/{chunk-DI55MBZ5-DUJCBZzM.js → chunk-DI55MBZ5-B3p1mU43.js} +1 -1
- package/canvas-dist/assets/{chunk-FMBD7UC4-EfGA9ufe.js → chunk-FMBD7UC4-JCLAHw5x.js} +1 -1
- package/canvas-dist/assets/{chunk-QN33PNHL-Cu6V1xBU.js → chunk-QN33PNHL-C9arKEVq.js} +1 -1
- package/canvas-dist/assets/{chunk-QZHKN3VN-avF3sH_r.js → chunk-QZHKN3VN-Bs1r3d9U.js} +1 -1
- package/canvas-dist/assets/{chunk-TZMSLE5B-CkWW-qpk.js → chunk-TZMSLE5B-_Ye6r84Y.js} +1 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +1 -0
- package/canvas-dist/assets/clone-CXEfuXmc.js +1 -0
- package/canvas-dist/assets/{cose-bilkent-S5V4N54A-DDE4zf7X.js → cose-bilkent-S5V4N54A-2O2oovOj.js} +1 -1
- package/canvas-dist/assets/{dagre-6UL2VRFP-BD6MGb7B.js → dagre-6UL2VRFP-gRmGLrEW.js} +1 -1
- package/canvas-dist/assets/{diagram-PSM6KHXK-yyu-ytzf.js → diagram-PSM6KHXK-B7Li-xxw.js} +1 -1
- package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.js → diagram-QEK2KX5R-B_NNUAm3.js} +1 -1
- package/canvas-dist/assets/{diagram-S2PKOQOG-DuebuBVv.js → diagram-S2PKOQOG-NcK-KHaA.js} +1 -1
- package/canvas-dist/assets/{erDiagram-Q2GNP2WA-AxqPt6IZ.js → erDiagram-Q2GNP2WA-CG7dqzk3.js} +1 -1
- package/canvas-dist/assets/{flowDiagram-NV44I4VS-mDhW3D3Q.js → flowDiagram-NV44I4VS-CBzCj5D6.js} +1 -1
- package/canvas-dist/assets/{ganttDiagram-JELNMOA3-sA8pHJPp.js → ganttDiagram-JELNMOA3-CHw-4qJC.js} +1 -1
- package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-CvLzvhKr.js → gitGraphDiagram-V2S2FVAM-Dqrc4wUs.js} +1 -1
- package/canvas-dist/assets/{graph-BVZqMrwW.js → graph-X9Kzu-pf.js} +1 -1
- package/canvas-dist/assets/{index-CF3qc2Xb.js → index-BQFKo-II.js} +1 -1
- package/canvas-dist/assets/index-DJ49c6u-.js +426 -0
- package/canvas-dist/assets/{infoDiagram-HS3SLOUP-D1Kg3Q9d.js → infoDiagram-HS3SLOUP-CflnZPsm.js} +1 -1
- package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D7ogbx9z.js → journeyDiagram-XKPGCS4Q-D2gkCipQ.js} +1 -1
- package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CDcnICM9.js → kanban-definition-3W4ZIXB7-CtLLz4o8.js} +1 -1
- package/canvas-dist/assets/{layout-CuaK7i3M.js → layout-CjvV_Dms.js} +1 -1
- package/canvas-dist/assets/{linear-CLSTOJ0g.js → linear-D3cIYHoS.js} +1 -1
- package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-TrK7CIKt.js → mindmap-definition-VGOIOE7T-DSgjVg-P.js} +1 -1
- package/canvas-dist/assets/{pieDiagram-ADFJNKIX-BcIKTRbi.js → pieDiagram-ADFJNKIX-B_lYaGFj.js} +1 -1
- package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-EOHXFGoQ.js → quadrantDiagram-AYHSOK5B-DLZLTJe3.js} +1 -1
- package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CJ8lImGs.js → requirementDiagram-UZGBJVZJ-CZE26rhL.js} +1 -1
- package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-4cANY87E.js → sankeyDiagram-TZEHDZUN-DQMRJAPV.js} +1 -1
- package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-D9HrEsci.js → sequenceDiagram-WL72ISMW-BY723FEn.js} +1 -1
- package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-qVbMjauZ.js → stateDiagram-FKZM4ZOC-C_UdOFhy.js} +1 -1
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +1 -0
- package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DDBlkydm.js → timeline-definition-IT6M3QCI-DkrJqww0.js} +1 -1
- package/canvas-dist/assets/{treemap-GDKQZRPO-D4a8udjO.js → treemap-GDKQZRPO-B-6bMZqD.js} +1 -1
- package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DteXAAAu.js → xychartDiagram-PRI3JC2R-DkBhUy_D.js} +1 -1
- package/canvas-dist/index.html +1 -1
- package/package.json +3 -2
- package/src/server.e2e.test.js +10 -36
- package/src/server.js +302 -186
- package/canvas-dist/assets/channel-saCUO1KA.js +0 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
- package/canvas-dist/assets/index-DYNtb52W.js +0 -426
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
- package/src/claude-session.js +0 -381
- package/src/claude-session.test.js +0 -312
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as t,b as r,a,S as s}from"./chunk-DI55MBZ5-DUJCBZzM.js";import{_ as i}from"./index-DYNtb52W.js";import"./chunk-55IACEB6-CTdcUQSV.js";import"./chunk-QN33PNHL-Cu6V1xBU.js";var l={parser:a,get db(){return new s(2)},renderer:r,styles:t,init:i(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")};export{l as diagram};
|
package/src/claude-session.js
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude session manager — wraps the Agent SDK to manage
|
|
3
|
-
* multiple Claude Code session lifecycles for browser chat.
|
|
4
|
-
*
|
|
5
|
-
* Uses the V2 preview interface (unstable_v2_createSession / unstable_v2_resumeSession)
|
|
6
|
-
* for clean send()/stream() multi-turn conversation support.
|
|
7
|
-
*
|
|
8
|
-
* The SDK is an optional dependency — if not installed, chat features
|
|
9
|
-
* are gracefully disabled.
|
|
10
|
-
*
|
|
11
|
-
* Supports multiple concurrent sessions (one per browser tab).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
createEnvelope,
|
|
16
|
-
MSG_CHAT_ASSISTANT,
|
|
17
|
-
MSG_CHAT_STREAM,
|
|
18
|
-
MSG_CHAT_TOOL_USE,
|
|
19
|
-
MSG_CHAT_TOOL_RESULT,
|
|
20
|
-
MSG_CHAT_STATUS,
|
|
21
|
-
} from '@shaykec/shared';
|
|
22
|
-
|
|
23
|
-
import { execSync } from 'child_process';
|
|
24
|
-
import { createRequire } from 'module';
|
|
25
|
-
import { dirname, join } from 'path';
|
|
26
|
-
import { fileURLToPath } from 'url';
|
|
27
|
-
|
|
28
|
-
// Resolve the package tree root (parent of node_modules/@shaykec/bridge)
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
-
const BRIDGE_ROOT = join(dirname(__filename), '..');
|
|
31
|
-
// Walk up: bridge/ -> @shaykec/ -> node_modules/ -> prefix/
|
|
32
|
-
const NPM_PREFIX = join(BRIDGE_ROOT, '..', '..', '..');
|
|
33
|
-
|
|
34
|
-
// Lazy-load the Agent SDK — auto-installs on first use if missing
|
|
35
|
-
let _sdk = null;
|
|
36
|
-
let _sdkLoadError = null;
|
|
37
|
-
|
|
38
|
-
function tryRequireSDK() {
|
|
39
|
-
const require = createRequire(import.meta.url);
|
|
40
|
-
return require('@anthropic-ai/claude-agent-sdk');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function getSDK() {
|
|
44
|
-
if (_sdk) return _sdk;
|
|
45
|
-
if (_sdkLoadError) throw _sdkLoadError;
|
|
46
|
-
try {
|
|
47
|
-
_sdk = tryRequireSDK();
|
|
48
|
-
return _sdk;
|
|
49
|
-
} catch {
|
|
50
|
-
// SDK not installed — install it into the same node_modules tree
|
|
51
|
-
console.log('[chat] Agent SDK not found, installing @anthropic-ai/claude-agent-sdk...');
|
|
52
|
-
try {
|
|
53
|
-
execSync(
|
|
54
|
-
`npm install @anthropic-ai/claude-agent-sdk --registry https://registry.npmjs.org/ --prefix "${NPM_PREFIX}" --no-save`,
|
|
55
|
-
{ stdio: 'inherit', timeout: 60000 }
|
|
56
|
-
);
|
|
57
|
-
_sdk = tryRequireSDK();
|
|
58
|
-
console.log('[chat] Agent SDK installed successfully.');
|
|
59
|
-
return _sdk;
|
|
60
|
-
} catch (installErr) {
|
|
61
|
-
_sdkLoadError = new Error(
|
|
62
|
-
'Failed to install Claude Agent SDK. Chat features require @anthropic-ai/claude-agent-sdk. ' +
|
|
63
|
-
'Install manually: npm install @anthropic-ai/claude-agent-sdk --registry https://registry.npmjs.org/'
|
|
64
|
-
);
|
|
65
|
-
throw _sdkLoadError;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @typedef {object} ChatSessionOptions
|
|
72
|
-
* @property {string} [cwd] - Working directory for Claude Code
|
|
73
|
-
* @property {string} [pluginDir] - Path to the ClaudeTeach plugin
|
|
74
|
-
* @property {function} onMessage - Callback receiving protocol envelopes to broadcast
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {object} SessionEntry
|
|
79
|
-
* @property {import('@anthropic-ai/claude-agent-sdk').SDKSession} session
|
|
80
|
-
* @property {boolean} streaming
|
|
81
|
-
* @property {function} onMessage
|
|
82
|
-
*/
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Manages multiple Claude Code SDK sessions.
|
|
86
|
-
*/
|
|
87
|
-
export class ClaudeSessionManager {
|
|
88
|
-
constructor() {
|
|
89
|
-
/** @type {Map<string, SessionEntry>} */
|
|
90
|
-
this._sessions = new Map();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Check if the Agent SDK is available.
|
|
95
|
-
* @returns {Promise<boolean>}
|
|
96
|
-
*/
|
|
97
|
-
async isAvailable() {
|
|
98
|
-
try {
|
|
99
|
-
await getSDK();
|
|
100
|
-
return true;
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get a session entry by ID.
|
|
108
|
-
* @param {string} sessionId
|
|
109
|
-
* @returns {SessionEntry|undefined}
|
|
110
|
-
*/
|
|
111
|
-
getSession(sessionId) {
|
|
112
|
-
return this._sessions.get(sessionId);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if a session exists and is active.
|
|
117
|
-
* @param {string} sessionId
|
|
118
|
-
* @returns {boolean}
|
|
119
|
-
*/
|
|
120
|
-
isActive(sessionId) {
|
|
121
|
-
return this._sessions.has(sessionId);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if a specific session is currently streaming.
|
|
126
|
-
* @param {string} sessionId
|
|
127
|
-
* @returns {boolean}
|
|
128
|
-
*/
|
|
129
|
-
isStreaming(sessionId) {
|
|
130
|
-
const entry = this._sessions.get(sessionId);
|
|
131
|
-
return entry ? entry.streaming : false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get all active session IDs.
|
|
136
|
-
* @returns {string[]}
|
|
137
|
-
*/
|
|
138
|
-
getActiveSessionIds() {
|
|
139
|
-
return [...this._sessions.keys()];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Create a new chat session.
|
|
144
|
-
* @param {ChatSessionOptions} options
|
|
145
|
-
* @returns {Promise<string>} sessionId
|
|
146
|
-
*/
|
|
147
|
-
async createSession(options = {}) {
|
|
148
|
-
const sdk = await getSDK();
|
|
149
|
-
const onMessage = options.onMessage;
|
|
150
|
-
|
|
151
|
-
const sessionOpts = {
|
|
152
|
-
allowedTools: [
|
|
153
|
-
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
154
|
-
],
|
|
155
|
-
permissionMode: 'bypassPermissions',
|
|
156
|
-
allowDangerouslySkipPermissions: true,
|
|
157
|
-
settingSources: ['user', 'project'],
|
|
158
|
-
includePartialMessages: true,
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
162
|
-
|
|
163
|
-
if (options.pluginDir) {
|
|
164
|
-
sessionOpts.plugins = [{ type: 'local', path: options.pluginDir }];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const session = sdk.unstable_v2_createSession(sessionOpts);
|
|
169
|
-
const sessionId = session.sessionId;
|
|
170
|
-
|
|
171
|
-
this._sessions.set(sessionId, {
|
|
172
|
-
session,
|
|
173
|
-
streaming: false,
|
|
174
|
-
onMessage,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'started', sessionId });
|
|
178
|
-
|
|
179
|
-
return sessionId;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
if (onMessage) {
|
|
182
|
-
const envelope = createEnvelope(MSG_CHAT_STATUS, { status: 'error', message: err.message }, 'bridge');
|
|
183
|
-
onMessage(envelope);
|
|
184
|
-
}
|
|
185
|
-
throw err;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Resume a previous session by ID.
|
|
191
|
-
* @param {string} sessionId
|
|
192
|
-
* @param {ChatSessionOptions} options
|
|
193
|
-
* @returns {Promise<string>} sessionId
|
|
194
|
-
*/
|
|
195
|
-
async resumeSession(sessionId, options = {}) {
|
|
196
|
-
const sdk = await getSDK();
|
|
197
|
-
|
|
198
|
-
// Close existing entry for this ID if present
|
|
199
|
-
await this.closeSession(sessionId);
|
|
200
|
-
|
|
201
|
-
const onMessage = options.onMessage;
|
|
202
|
-
|
|
203
|
-
const sessionOpts = {
|
|
204
|
-
allowedTools: [
|
|
205
|
-
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
206
|
-
],
|
|
207
|
-
permissionMode: 'bypassPermissions',
|
|
208
|
-
allowDangerouslySkipPermissions: true,
|
|
209
|
-
settingSources: ['user', 'project'],
|
|
210
|
-
includePartialMessages: true,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
214
|
-
|
|
215
|
-
if (options.pluginDir) {
|
|
216
|
-
sessionOpts.plugins = [{ type: 'local', path: options.pluginDir }];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const session = sdk.unstable_v2_resumeSession(sessionId, sessionOpts);
|
|
221
|
-
|
|
222
|
-
this._sessions.set(sessionId, {
|
|
223
|
-
session,
|
|
224
|
-
streaming: false,
|
|
225
|
-
onMessage,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'resumed', sessionId });
|
|
229
|
-
|
|
230
|
-
return sessionId;
|
|
231
|
-
} catch (err) {
|
|
232
|
-
if (onMessage) {
|
|
233
|
-
const envelope = createEnvelope(MSG_CHAT_STATUS, { status: 'error', message: err.message }, 'bridge');
|
|
234
|
-
onMessage(envelope);
|
|
235
|
-
}
|
|
236
|
-
throw err;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Send a user message and stream the response for a specific session.
|
|
242
|
-
* Emits protocol envelopes via the session's onMessage callback.
|
|
243
|
-
* @param {string} sessionId
|
|
244
|
-
* @param {string} text - User message text
|
|
245
|
-
*/
|
|
246
|
-
async sendMessage(sessionId, text) {
|
|
247
|
-
const entry = this._sessions.get(sessionId);
|
|
248
|
-
if (!entry) {
|
|
249
|
-
throw new Error(`No active session with ID "${sessionId}". Call createSession() or resumeSession() first.`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (entry.streaming) {
|
|
253
|
-
throw new Error('Already streaming a response. Wait for completion or call stop().');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
entry.streaming = true;
|
|
257
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'thinking' });
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
await entry.session.send(text);
|
|
261
|
-
|
|
262
|
-
let currentText = '';
|
|
263
|
-
|
|
264
|
-
for await (const msg of entry.session.stream()) {
|
|
265
|
-
if (msg.type === 'assistant') {
|
|
266
|
-
const textBlocks = (msg.message?.content || []).filter(b => b.type === 'text');
|
|
267
|
-
for (const block of textBlocks) {
|
|
268
|
-
if (block.text && block.text !== currentText) {
|
|
269
|
-
const delta = block.text.slice(currentText.length);
|
|
270
|
-
if (delta) {
|
|
271
|
-
this._emit(sessionId, MSG_CHAT_STREAM, { delta, fullText: block.text });
|
|
272
|
-
}
|
|
273
|
-
currentText = block.text;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (msg.type === 'tool_use') {
|
|
279
|
-
this._emit(sessionId, MSG_CHAT_TOOL_USE, {
|
|
280
|
-
toolName: msg.tool_name || msg.name,
|
|
281
|
-
toolId: msg.id,
|
|
282
|
-
input: msg.input,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (msg.type === 'tool_result') {
|
|
287
|
-
this._emit(sessionId, MSG_CHAT_TOOL_RESULT, {
|
|
288
|
-
toolId: msg.tool_use_id,
|
|
289
|
-
output: msg.content,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (msg.type === 'result') {
|
|
294
|
-
if (currentText) {
|
|
295
|
-
this._emit(sessionId, MSG_CHAT_ASSISTANT, { text: currentText, sessionId });
|
|
296
|
-
}
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
} catch (err) {
|
|
301
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'error', message: err.message });
|
|
302
|
-
} finally {
|
|
303
|
-
entry.streaming = false;
|
|
304
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'idle' });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Stop the current generation for a specific session.
|
|
310
|
-
* @param {string} sessionId
|
|
311
|
-
*/
|
|
312
|
-
async stop(sessionId) {
|
|
313
|
-
const entry = this._sessions.get(sessionId);
|
|
314
|
-
if (entry && entry.streaming) {
|
|
315
|
-
try {
|
|
316
|
-
entry.session.close();
|
|
317
|
-
} catch { /* best effort */ }
|
|
318
|
-
this._sessions.delete(sessionId);
|
|
319
|
-
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'stopped' });
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* List available sessions for the current project.
|
|
325
|
-
* @param {string} [dir] - Project directory to filter by
|
|
326
|
-
* @returns {Promise<Array>}
|
|
327
|
-
*/
|
|
328
|
-
async listSessions(dir) {
|
|
329
|
-
try {
|
|
330
|
-
const sdk = await getSDK();
|
|
331
|
-
const sessions = await sdk.listSessions({ dir, limit: 20 });
|
|
332
|
-
return sessions.map(s => ({
|
|
333
|
-
sessionId: s.sessionId,
|
|
334
|
-
summary: s.summary,
|
|
335
|
-
lastModified: s.lastModified,
|
|
336
|
-
}));
|
|
337
|
-
} catch {
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Close a specific session.
|
|
344
|
-
* @param {string} sessionId
|
|
345
|
-
*/
|
|
346
|
-
async closeSession(sessionId) {
|
|
347
|
-
const entry = this._sessions.get(sessionId);
|
|
348
|
-
if (entry) {
|
|
349
|
-
try {
|
|
350
|
-
entry.session.close();
|
|
351
|
-
} catch { /* already closed */ }
|
|
352
|
-
this._sessions.delete(sessionId);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Close all sessions and clean up resources.
|
|
358
|
-
*/
|
|
359
|
-
async closeAll() {
|
|
360
|
-
for (const [id, entry] of this._sessions) {
|
|
361
|
-
try {
|
|
362
|
-
entry.session.close();
|
|
363
|
-
} catch { /* already closed */ }
|
|
364
|
-
}
|
|
365
|
-
this._sessions.clear();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Emit a protocol envelope via the session's onMessage callback.
|
|
370
|
-
* @param {string} sessionId
|
|
371
|
-
* @param {string} type
|
|
372
|
-
* @param {object} payload
|
|
373
|
-
*/
|
|
374
|
-
_emit(sessionId, type, payload) {
|
|
375
|
-
const entry = this._sessions.get(sessionId);
|
|
376
|
-
if (entry?.onMessage) {
|
|
377
|
-
const envelope = createEnvelope(type, payload, 'bridge');
|
|
378
|
-
entry.onMessage(envelope);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { ClaudeSessionManager } from './claude-session.js';
|
|
3
|
-
|
|
4
|
-
// Mock the SDK
|
|
5
|
-
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
|
6
|
-
unstable_v2_createSession: vi.fn(),
|
|
7
|
-
unstable_v2_resumeSession: vi.fn(),
|
|
8
|
-
listSessions: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
unstable_v2_createSession,
|
|
13
|
-
unstable_v2_resumeSession,
|
|
14
|
-
listSessions,
|
|
15
|
-
} from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
|
|
17
|
-
describe('ClaudeSessionManager', () => {
|
|
18
|
-
let manager;
|
|
19
|
-
let emittedMessages;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
manager = new ClaudeSessionManager();
|
|
24
|
-
emittedMessages = [];
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const mockOnMessage = (envelope) => {
|
|
28
|
-
emittedMessages.push(envelope);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function makeMockSession(id) {
|
|
32
|
-
return {
|
|
33
|
-
sessionId: id,
|
|
34
|
-
send: vi.fn(),
|
|
35
|
-
stream: vi.fn(),
|
|
36
|
-
close: vi.fn(),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('createSession', () => {
|
|
41
|
-
it('creates a session and emits started status', async () => {
|
|
42
|
-
const mockSession = makeMockSession('test-session-123');
|
|
43
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
44
|
-
|
|
45
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
46
|
-
|
|
47
|
-
expect(sessionId).toBe('test-session-123');
|
|
48
|
-
expect(manager.isActive('test-session-123')).toBe(true);
|
|
49
|
-
expect(emittedMessages).toHaveLength(1);
|
|
50
|
-
expect(emittedMessages[0].type).toBe('chat:status');
|
|
51
|
-
expect(emittedMessages[0].payload.status).toBe('started');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('passes cwd and pluginDir to the SDK', async () => {
|
|
55
|
-
const mockSession = makeMockSession('s1');
|
|
56
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
57
|
-
|
|
58
|
-
await manager.createSession({
|
|
59
|
-
cwd: '/test/dir',
|
|
60
|
-
pluginDir: '/test/plugin',
|
|
61
|
-
onMessage: mockOnMessage,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const opts = unstable_v2_createSession.mock.calls[0][0];
|
|
65
|
-
expect(opts.cwd).toBe('/test/dir');
|
|
66
|
-
expect(opts.plugins).toEqual([{ type: 'local', path: '/test/plugin' }]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('emits error status on SDK failure', async () => {
|
|
70
|
-
unstable_v2_createSession.mockImplementation(() => {
|
|
71
|
-
throw new Error('SDK init failed');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
await expect(
|
|
75
|
-
manager.createSession({ onMessage: mockOnMessage })
|
|
76
|
-
).rejects.toThrow('SDK init failed');
|
|
77
|
-
|
|
78
|
-
expect(emittedMessages[0].type).toBe('chat:status');
|
|
79
|
-
expect(emittedMessages[0].payload.status).toBe('error');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('resumeSession', () => {
|
|
84
|
-
it('resumes a session by ID', async () => {
|
|
85
|
-
const mockSession = makeMockSession('existing-session');
|
|
86
|
-
unstable_v2_resumeSession.mockReturnValue(mockSession);
|
|
87
|
-
|
|
88
|
-
const sessionId = await manager.resumeSession('existing-session', { onMessage: mockOnMessage });
|
|
89
|
-
|
|
90
|
-
expect(sessionId).toBe('existing-session');
|
|
91
|
-
expect(unstable_v2_resumeSession).toHaveBeenCalledWith('existing-session', expect.any(Object));
|
|
92
|
-
expect(emittedMessages[0].payload.status).toBe('resumed');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('sendMessage', () => {
|
|
97
|
-
it('throws if no active session', async () => {
|
|
98
|
-
await expect(manager.sendMessage('nonexistent', 'hello')).rejects.toThrow('No active session');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('sends a message and streams response', async () => {
|
|
102
|
-
async function* fakeStream() {
|
|
103
|
-
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Hello' }] } };
|
|
104
|
-
yield { type: 'result' };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const mockSession = makeMockSession('stream-test');
|
|
108
|
-
mockSession.stream.mockReturnValue(fakeStream());
|
|
109
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
110
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
111
|
-
|
|
112
|
-
emittedMessages = [];
|
|
113
|
-
await manager.sendMessage(sessionId, 'test message');
|
|
114
|
-
|
|
115
|
-
expect(mockSession.send).toHaveBeenCalledWith('test message');
|
|
116
|
-
expect(mockSession.stream).toHaveBeenCalled();
|
|
117
|
-
|
|
118
|
-
const types = emittedMessages.map(m => m.type);
|
|
119
|
-
expect(types).toContain('chat:status');
|
|
120
|
-
expect(types).toContain('chat:stream');
|
|
121
|
-
expect(types).toContain('chat:assistant');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('emits tool-use and tool-result events', async () => {
|
|
125
|
-
async function* fakeStream() {
|
|
126
|
-
yield { type: 'tool_use', name: 'Bash', id: 'tool-1', input: { command: 'ls' } };
|
|
127
|
-
yield { type: 'tool_result', tool_use_id: 'tool-1', content: 'file1\nfile2' };
|
|
128
|
-
yield { type: 'result' };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const mockSession = makeMockSession('tool-test');
|
|
132
|
-
mockSession.stream.mockReturnValue(fakeStream());
|
|
133
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
134
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
135
|
-
|
|
136
|
-
emittedMessages = [];
|
|
137
|
-
await manager.sendMessage(sessionId, 'list files');
|
|
138
|
-
|
|
139
|
-
const toolUse = emittedMessages.find(m => m.type === 'chat:tool-use');
|
|
140
|
-
expect(toolUse).toBeDefined();
|
|
141
|
-
expect(toolUse.payload.toolName).toBe('Bash');
|
|
142
|
-
|
|
143
|
-
const toolResult = emittedMessages.find(m => m.type === 'chat:tool-result');
|
|
144
|
-
expect(toolResult).toBeDefined();
|
|
145
|
-
expect(toolResult.payload.toolId).toBe('tool-1');
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('multiple sessions', () => {
|
|
150
|
-
it('supports multiple concurrent sessions', async () => {
|
|
151
|
-
const session1 = makeMockSession('session-1');
|
|
152
|
-
const session2 = makeMockSession('session-2');
|
|
153
|
-
unstable_v2_createSession
|
|
154
|
-
.mockReturnValueOnce(session1)
|
|
155
|
-
.mockReturnValueOnce(session2);
|
|
156
|
-
|
|
157
|
-
const messages1 = [];
|
|
158
|
-
const messages2 = [];
|
|
159
|
-
|
|
160
|
-
const id1 = await manager.createSession({ onMessage: (e) => messages1.push(e) });
|
|
161
|
-
const id2 = await manager.createSession({ onMessage: (e) => messages2.push(e) });
|
|
162
|
-
|
|
163
|
-
expect(id1).toBe('session-1');
|
|
164
|
-
expect(id2).toBe('session-2');
|
|
165
|
-
expect(manager.isActive('session-1')).toBe(true);
|
|
166
|
-
expect(manager.isActive('session-2')).toBe(true);
|
|
167
|
-
expect(manager.getActiveSessionIds()).toHaveLength(2);
|
|
168
|
-
|
|
169
|
-
// Status messages go to the right callback
|
|
170
|
-
expect(messages1[0].payload.sessionId).toBe('session-1');
|
|
171
|
-
expect(messages2[0].payload.sessionId).toBe('session-2');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('closing one session does not affect others', async () => {
|
|
175
|
-
const session1 = makeMockSession('session-1');
|
|
176
|
-
const session2 = makeMockSession('session-2');
|
|
177
|
-
unstable_v2_createSession
|
|
178
|
-
.mockReturnValueOnce(session1)
|
|
179
|
-
.mockReturnValueOnce(session2);
|
|
180
|
-
|
|
181
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
182
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
183
|
-
|
|
184
|
-
await manager.closeSession('session-1');
|
|
185
|
-
|
|
186
|
-
expect(manager.isActive('session-1')).toBe(false);
|
|
187
|
-
expect(manager.isActive('session-2')).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('closeAll closes all sessions', async () => {
|
|
191
|
-
const session1 = makeMockSession('session-1');
|
|
192
|
-
const session2 = makeMockSession('session-2');
|
|
193
|
-
unstable_v2_createSession
|
|
194
|
-
.mockReturnValueOnce(session1)
|
|
195
|
-
.mockReturnValueOnce(session2);
|
|
196
|
-
|
|
197
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
198
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
199
|
-
|
|
200
|
-
await manager.closeAll();
|
|
201
|
-
|
|
202
|
-
expect(manager.getActiveSessionIds()).toHaveLength(0);
|
|
203
|
-
expect(session1.close).toHaveBeenCalled();
|
|
204
|
-
expect(session2.close).toHaveBeenCalled();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('stop', () => {
|
|
209
|
-
it('is a no-op when not streaming', async () => {
|
|
210
|
-
const mockSession = makeMockSession('s1');
|
|
211
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
212
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
213
|
-
|
|
214
|
-
emittedMessages = [];
|
|
215
|
-
await manager.stop(sessionId);
|
|
216
|
-
|
|
217
|
-
// Session is still active since it wasn't streaming
|
|
218
|
-
expect(manager.isActive(sessionId)).toBe(true);
|
|
219
|
-
expect(emittedMessages).toHaveLength(0);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('listSessions', () => {
|
|
224
|
-
it('returns mapped session list', async () => {
|
|
225
|
-
listSessions.mockResolvedValue([
|
|
226
|
-
{ sessionId: 's1', summary: 'Test', lastModified: 12345 },
|
|
227
|
-
{ sessionId: 's2', summary: 'Other', lastModified: 67890 },
|
|
228
|
-
]);
|
|
229
|
-
|
|
230
|
-
const sessions = await manager.listSessions('/test');
|
|
231
|
-
expect(sessions).toHaveLength(2);
|
|
232
|
-
expect(sessions[0].sessionId).toBe('s1');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('returns empty array on error', async () => {
|
|
236
|
-
listSessions.mockRejectedValue(new Error('fail'));
|
|
237
|
-
const sessions = await manager.listSessions('/test');
|
|
238
|
-
expect(sessions).toEqual([]);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('closeSession', () => {
|
|
243
|
-
it('closes active session and removes from map', async () => {
|
|
244
|
-
const mockSession = makeMockSession('s1');
|
|
245
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
246
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
247
|
-
|
|
248
|
-
expect(manager.isActive(sessionId)).toBe(true);
|
|
249
|
-
await manager.closeSession(sessionId);
|
|
250
|
-
expect(manager.isActive(sessionId)).toBe(false);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe('isAvailable', () => {
|
|
255
|
-
it('returns true when SDK is loadable', async () => {
|
|
256
|
-
const available = await manager.isAvailable();
|
|
257
|
-
expect(available).toBe(true);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('sendMessage edge cases', () => {
|
|
262
|
-
it('throws if already streaming', async () => {
|
|
263
|
-
async function* slowStream() {
|
|
264
|
-
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Hi' }] } };
|
|
265
|
-
// never yields 'result' — hangs forever
|
|
266
|
-
await new Promise(() => {});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const mockSession = makeMockSession('busy');
|
|
270
|
-
mockSession.stream.mockReturnValue(slowStream());
|
|
271
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
272
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
273
|
-
|
|
274
|
-
// Start streaming (don't await — it won't finish)
|
|
275
|
-
const streamPromise = manager.sendMessage(sessionId, 'first');
|
|
276
|
-
|
|
277
|
-
// Wait a tick for streaming flag to be set
|
|
278
|
-
await new Promise(r => setTimeout(r, 10));
|
|
279
|
-
|
|
280
|
-
await expect(manager.sendMessage(sessionId, 'second')).rejects.toThrow('Already streaming');
|
|
281
|
-
|
|
282
|
-
// Clean up
|
|
283
|
-
await manager.closeSession(sessionId);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('emits error status on stream failure', async () => {
|
|
287
|
-
async function* failStream() {
|
|
288
|
-
throw new Error('stream broke');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const mockSession = makeMockSession('fail-stream');
|
|
292
|
-
mockSession.stream.mockReturnValue(failStream());
|
|
293
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
294
|
-
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
295
|
-
|
|
296
|
-
emittedMessages = [];
|
|
297
|
-
await manager.sendMessage(sessionId, 'test');
|
|
298
|
-
|
|
299
|
-
const errorStatus = emittedMessages.find(
|
|
300
|
-
m => m.type === 'chat:status' && m.payload.status === 'error'
|
|
301
|
-
);
|
|
302
|
-
expect(errorStatus).toBeDefined();
|
|
303
|
-
expect(errorStatus.payload.message).toBe('stream broke');
|
|
304
|
-
|
|
305
|
-
// Should return to idle after error
|
|
306
|
-
const idleStatus = emittedMessages.find(
|
|
307
|
-
m => m.type === 'chat:status' && m.payload.status === 'idle'
|
|
308
|
-
);
|
|
309
|
-
expect(idleStatus).toBeDefined();
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
});
|