@jxsuite/server 0.32.0 → 0.34.0
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 +8 -10
- package/src/ai-api.ts +465 -0
- package/src/project-server.ts +408 -0
- package/src/server.ts +29 -6
- package/src/studio-api.ts +0 -74
- package/src/watch.ts +12 -0
- package/src/claude-session.ts +0 -338
package/src/claude-session.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude-session.js — Claude Agent SDK session manager
|
|
3
|
-
*
|
|
4
|
-
* Manages Claude Code sessions via @anthropic-ai/claude-agent-sdk, providing: - Session creation
|
|
5
|
-
* with streaming output - Follow-up messages via session resume - SSE broadcasting to connected
|
|
6
|
-
* frontend clients - Session interruption and cleanup
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
10
|
-
|
|
11
|
-
import type { Options as AgentOptions, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
-
|
|
13
|
-
interface Session {
|
|
14
|
-
id: string;
|
|
15
|
-
sessionId: string | null;
|
|
16
|
-
projectRoot: string;
|
|
17
|
-
messages: SDKMessage[];
|
|
18
|
-
sseClients: Set<(event: string, data: unknown) => void>;
|
|
19
|
-
status: "active" | "idle" | "stopped";
|
|
20
|
-
abortController: AbortController | null;
|
|
21
|
-
query: AsyncGenerator | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Build a clean env for SDK child processes. Strips session-specific vars from a parent Claude Code
|
|
26
|
-
* instance that would override the SDK's own credential resolution (OAuth from ~/.claude/).
|
|
27
|
-
*/
|
|
28
|
-
function buildSdkEnv() {
|
|
29
|
-
const env = { ...process.env };
|
|
30
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
31
|
-
delete env.ANTHROPIC_BASE_URL;
|
|
32
|
-
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
33
|
-
delete env.CLAUDE_CODE_EXECPATH;
|
|
34
|
-
delete env.CLAUDE_AGENT_SDK_VERSION;
|
|
35
|
-
delete env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
|
|
36
|
-
delete env.CLAUDECODE;
|
|
37
|
-
env.CLAUDE_AGENT_SDK_CLIENT_APP = "jx-studio/0.17.0";
|
|
38
|
-
return env;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const sessions = new Map<string, Session>();
|
|
42
|
-
|
|
43
|
-
let idCounter = 0;
|
|
44
|
-
|
|
45
|
-
function genId() {
|
|
46
|
-
return `ai_${Date.now().toString(36)}_${(idCounter += 1).toString(36)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Broadcast an SSE event to all connected clients for a session.
|
|
51
|
-
*
|
|
52
|
-
* @param {Session} session
|
|
53
|
-
* @param {string} event
|
|
54
|
-
* @param {unknown} data
|
|
55
|
-
*/
|
|
56
|
-
function broadcast(session: Session, event: string, data: unknown) {
|
|
57
|
-
session.messages.push(data as SDKMessage);
|
|
58
|
-
for (const send of session.sseClients) {
|
|
59
|
-
send(event, data);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Process the async message stream from a query.
|
|
65
|
-
*
|
|
66
|
-
* @param {Session} session
|
|
67
|
-
* @param {AsyncGenerator<SDKMessage>} stream
|
|
68
|
-
*/
|
|
69
|
-
async function processStream(session: Session, stream: AsyncGenerator<SDKMessage>) {
|
|
70
|
-
session.status = "active";
|
|
71
|
-
try {
|
|
72
|
-
for await (const message of stream) {
|
|
73
|
-
if ((session.status as string) === "stopped") {
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
if (message.session_id && !session.sessionId) {
|
|
77
|
-
session.sessionId = message.session_id;
|
|
78
|
-
}
|
|
79
|
-
broadcast(session, message.type, message);
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
if (error instanceof Error && error.name !== "AbortError") {
|
|
83
|
-
broadcast(session, "error", { error: String(error), type: "error" });
|
|
84
|
-
}
|
|
85
|
-
} finally {
|
|
86
|
-
session.status = "idle";
|
|
87
|
-
session.query = null;
|
|
88
|
-
session.abortController = null;
|
|
89
|
-
broadcast(session, "done", { type: "done" });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Create a new AI session and begin streaming.
|
|
95
|
-
*
|
|
96
|
-
* @param {string} projectRoot
|
|
97
|
-
* @param {string} message
|
|
98
|
-
* @param {{ systemPrompt?: string }} [opts]
|
|
99
|
-
* @returns {{ id: string }}
|
|
100
|
-
*/
|
|
101
|
-
export function createSession(
|
|
102
|
-
projectRoot: string,
|
|
103
|
-
message: string,
|
|
104
|
-
opts: { systemPrompt?: string } = {},
|
|
105
|
-
) {
|
|
106
|
-
const id = genId();
|
|
107
|
-
const abortController = new AbortController();
|
|
108
|
-
|
|
109
|
-
const session: Session = {
|
|
110
|
-
abortController,
|
|
111
|
-
id,
|
|
112
|
-
messages: [],
|
|
113
|
-
projectRoot,
|
|
114
|
-
query: null,
|
|
115
|
-
sessionId: null,
|
|
116
|
-
sseClients: new Set(),
|
|
117
|
-
status: "active",
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
sessions.set(id, session);
|
|
121
|
-
|
|
122
|
-
const queryOpts: AgentOptions = {
|
|
123
|
-
abortController,
|
|
124
|
-
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
125
|
-
cwd: projectRoot,
|
|
126
|
-
env: buildSdkEnv(),
|
|
127
|
-
includePartialMessages: true,
|
|
128
|
-
maxTurns: 30,
|
|
129
|
-
permissionMode: "acceptEdits",
|
|
130
|
-
persistSession: true,
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
if (opts.systemPrompt) {
|
|
134
|
-
queryOpts.systemPrompt = opts.systemPrompt;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const stream = query({ options: queryOpts, prompt: message });
|
|
138
|
-
session.query = stream;
|
|
139
|
-
void processStream(session, stream);
|
|
140
|
-
|
|
141
|
-
return { id };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Send a follow-up message to an existing session.
|
|
146
|
-
*
|
|
147
|
-
* @param {string} id
|
|
148
|
-
* @param {string} message
|
|
149
|
-
*/
|
|
150
|
-
export function sendMessage(id: string, message: string) {
|
|
151
|
-
const session = sessions.get(id);
|
|
152
|
-
if (!session) {
|
|
153
|
-
throw new Error(`Session not found: ${id}`);
|
|
154
|
-
}
|
|
155
|
-
if (session.status === "active") {
|
|
156
|
-
throw new Error("Session is still processing");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const abortController = new AbortController();
|
|
160
|
-
session.abortController = abortController;
|
|
161
|
-
|
|
162
|
-
const queryOpts: AgentOptions = {
|
|
163
|
-
abortController,
|
|
164
|
-
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
165
|
-
continue: true,
|
|
166
|
-
cwd: session.projectRoot,
|
|
167
|
-
env: buildSdkEnv(),
|
|
168
|
-
includePartialMessages: true,
|
|
169
|
-
maxTurns: 30,
|
|
170
|
-
permissionMode: "acceptEdits",
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
if (session.sessionId) {
|
|
174
|
-
queryOpts.resume = session.sessionId;
|
|
175
|
-
delete queryOpts.continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const stream = query({ options: queryOpts, prompt: message });
|
|
179
|
-
session.query = stream;
|
|
180
|
-
void processStream(session, stream);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Stop a running session (interrupt the current query).
|
|
185
|
-
*
|
|
186
|
-
* @param {string} id
|
|
187
|
-
*/
|
|
188
|
-
export function stopSession(id: string) {
|
|
189
|
-
const session = sessions.get(id);
|
|
190
|
-
if (!session) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
session.status = "stopped";
|
|
194
|
-
if (session.abortController) {
|
|
195
|
-
session.abortController.abort();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Remove a session entirely.
|
|
201
|
-
*
|
|
202
|
-
* @param {string} id
|
|
203
|
-
*/
|
|
204
|
-
export function deleteSession(id: string) {
|
|
205
|
-
stopSession(id);
|
|
206
|
-
sessions.delete(id);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get an SSE Response stream for a session.
|
|
211
|
-
*
|
|
212
|
-
* @param {string} id
|
|
213
|
-
* @returns {Response}
|
|
214
|
-
*/
|
|
215
|
-
export function streamSession(id: string) {
|
|
216
|
-
const session = sessions.get(id);
|
|
217
|
-
if (!session) {
|
|
218
|
-
return new Response("Session not found", { status: 404 });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const encoder = new TextEncoder();
|
|
222
|
-
let sendRef: (event: string, data: unknown) => void = () => {};
|
|
223
|
-
const stream = new ReadableStream({
|
|
224
|
-
cancel() {
|
|
225
|
-
session.sseClients.delete(sendRef);
|
|
226
|
-
},
|
|
227
|
-
start(controller) {
|
|
228
|
-
/** @param {string} event @param {unknown} data */
|
|
229
|
-
sendRef = (event: string, data: unknown) => {
|
|
230
|
-
try {
|
|
231
|
-
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
232
|
-
} catch {
|
|
233
|
-
// Stream closed
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
session.sseClients.add(sendRef);
|
|
238
|
-
|
|
239
|
-
for (const msg of session.messages) {
|
|
240
|
-
sendRef(((msg as Record<string, unknown>).type as string) || "message", msg);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const heartbeat = setInterval(() => {
|
|
244
|
-
try {
|
|
245
|
-
controller.enqueue(encoder.encode(": heartbeat\n\n"));
|
|
246
|
-
} catch {
|
|
247
|
-
clearInterval(heartbeat);
|
|
248
|
-
session.sseClients.delete(sendRef);
|
|
249
|
-
}
|
|
250
|
-
}, 15_000);
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return new Response(stream, {
|
|
255
|
-
headers: {
|
|
256
|
-
"Cache-Control": "no-cache",
|
|
257
|
-
Connection: "keep-alive",
|
|
258
|
-
"Content-Type": "text/event-stream",
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Check if the claude agent SDK can authenticate.
|
|
265
|
-
*
|
|
266
|
-
* @returns {Promise<{ authenticated: boolean; error?: string }>}
|
|
267
|
-
*/
|
|
268
|
-
export async function getAuthStatus() {
|
|
269
|
-
try {
|
|
270
|
-
const ctrl = new AbortController();
|
|
271
|
-
const timeout = setTimeout(() => ctrl.abort(), 15_000);
|
|
272
|
-
const testStream = query({
|
|
273
|
-
options: {
|
|
274
|
-
abortController: ctrl,
|
|
275
|
-
env: buildSdkEnv(),
|
|
276
|
-
includePartialMessages: true,
|
|
277
|
-
maxTurns: 1,
|
|
278
|
-
persistSession: false,
|
|
279
|
-
},
|
|
280
|
-
prompt: "Say OK",
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
for await (const msg of testStream) {
|
|
284
|
-
if (msg.type === "assistant") {
|
|
285
|
-
const content = (msg as Record<string, unknown>).message as
|
|
286
|
-
| { content?: unknown[] }
|
|
287
|
-
| undefined;
|
|
288
|
-
if (Array.isArray(content?.content)) {
|
|
289
|
-
const text = content.content.find(
|
|
290
|
-
(b: unknown) => (b as { type?: string }).type === "text",
|
|
291
|
-
) as { text?: string } | undefined;
|
|
292
|
-
if (text?.text?.toLowerCase().includes("error")) {
|
|
293
|
-
clearTimeout(timeout);
|
|
294
|
-
ctrl.abort();
|
|
295
|
-
return { authenticated: false, error: text.text };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
clearTimeout(timeout);
|
|
299
|
-
ctrl.abort();
|
|
300
|
-
return { authenticated: true };
|
|
301
|
-
}
|
|
302
|
-
if (msg.type === "result") {
|
|
303
|
-
const result = msg as Record<string, unknown>;
|
|
304
|
-
clearTimeout(timeout);
|
|
305
|
-
ctrl.abort();
|
|
306
|
-
if (result.is_error) {
|
|
307
|
-
return {
|
|
308
|
-
authenticated: false,
|
|
309
|
-
error: (result.result as string) || "API error",
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
return { authenticated: true };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
clearTimeout(timeout);
|
|
317
|
-
return { authenticated: true };
|
|
318
|
-
} catch (error) {
|
|
319
|
-
return { authenticated: false, error: String(error) };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get session info.
|
|
325
|
-
*
|
|
326
|
-
* @param {string} id
|
|
327
|
-
*/
|
|
328
|
-
export function getSession(id: string) {
|
|
329
|
-
const session = sessions.get(id);
|
|
330
|
-
if (!session) {
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
return {
|
|
334
|
-
id: session.id,
|
|
335
|
-
messageCount: session.messages.length,
|
|
336
|
-
status: session.status,
|
|
337
|
-
};
|
|
338
|
-
}
|