@sna-sdk/core 0.2.3 → 0.4.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/README.md +7 -1
- package/dist/core/providers/cc-history-adapter.d.ts +37 -0
- package/dist/core/providers/cc-history-adapter.js +70 -0
- package/dist/core/providers/claude-code.js +57 -7
- package/dist/core/providers/types.d.ts +32 -5
- package/dist/db/schema.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/scripts/sna.js +193 -1
- package/dist/scripts/tu-oneshot.d.ts +2 -0
- package/dist/scripts/tu-oneshot.js +66 -0
- package/dist/server/api-types.d.ts +22 -0
- package/dist/server/history-builder.d.ts +16 -0
- package/dist/server/history-builder.js +25 -0
- package/dist/server/image-store.d.ts +23 -0
- package/dist/server/image-store.js +34 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -0
- package/dist/server/routes/agent.js +91 -10
- package/dist/server/routes/chat.js +22 -0
- package/dist/server/session-manager.d.ts +31 -4
- package/dist/server/session-manager.js +85 -12
- package/dist/server/standalone.js +488 -51
- package/dist/server/ws.js +103 -9
- package/dist/testing/mock-api.d.ts +35 -0
- package/dist/testing/mock-api.js +160 -0
- package/package.json +6 -1
package/dist/server/ws.js
CHANGED
|
@@ -4,6 +4,8 @@ import { getDb } from "../db/schema.js";
|
|
|
4
4
|
import { logger } from "../lib/logger.js";
|
|
5
5
|
import { runOnce } from "./routes/agent.js";
|
|
6
6
|
import { wsReply } from "./api-types.js";
|
|
7
|
+
import { buildHistoryFromDb } from "./history-builder.js";
|
|
8
|
+
import { saveImages } from "./image-store.js";
|
|
7
9
|
function send(ws, data) {
|
|
8
10
|
if (ws.readyState === ws.OPEN) {
|
|
9
11
|
ws.send(JSON.stringify(data));
|
|
@@ -29,10 +31,16 @@ function attachWebSocket(server, sessionManager) {
|
|
|
29
31
|
});
|
|
30
32
|
wss.on("connection", (ws) => {
|
|
31
33
|
logger.log("ws", "client connected");
|
|
32
|
-
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null };
|
|
34
|
+
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
|
|
33
35
|
state.lifecycleUnsub = sessionManager.onSessionLifecycle((event) => {
|
|
34
36
|
send(ws, { type: "session.lifecycle", ...event });
|
|
35
37
|
});
|
|
38
|
+
state.configChangedUnsub = sessionManager.onConfigChanged((event) => {
|
|
39
|
+
send(ws, { type: "session.config-changed", ...event });
|
|
40
|
+
});
|
|
41
|
+
state.stateChangedUnsub = sessionManager.onStateChanged((event) => {
|
|
42
|
+
send(ws, { type: "session.state-changed", ...event });
|
|
43
|
+
});
|
|
36
44
|
ws.on("message", (raw) => {
|
|
37
45
|
let msg;
|
|
38
46
|
try {
|
|
@@ -61,6 +69,10 @@ function attachWebSocket(server, sessionManager) {
|
|
|
61
69
|
state.permissionUnsub = null;
|
|
62
70
|
state.lifecycleUnsub?.();
|
|
63
71
|
state.lifecycleUnsub = null;
|
|
72
|
+
state.configChangedUnsub?.();
|
|
73
|
+
state.configChangedUnsub = null;
|
|
74
|
+
state.stateChangedUnsub?.();
|
|
75
|
+
state.stateChangedUnsub = null;
|
|
64
76
|
});
|
|
65
77
|
});
|
|
66
78
|
return wss;
|
|
@@ -79,10 +91,16 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
79
91
|
return handleAgentStart(ws, msg, sm);
|
|
80
92
|
case "agent.send":
|
|
81
93
|
return handleAgentSend(ws, msg, sm);
|
|
94
|
+
case "agent.resume":
|
|
95
|
+
return handleAgentResume(ws, msg, sm);
|
|
82
96
|
case "agent.restart":
|
|
83
97
|
return handleAgentRestart(ws, msg, sm);
|
|
84
98
|
case "agent.interrupt":
|
|
85
99
|
return handleAgentInterrupt(ws, msg, sm);
|
|
100
|
+
case "agent.set-model":
|
|
101
|
+
return handleAgentSetModel(ws, msg, sm);
|
|
102
|
+
case "agent.set-permission-mode":
|
|
103
|
+
return handleAgentSetPermissionMode(ws, msg, sm);
|
|
86
104
|
case "agent.kill":
|
|
87
105
|
return handleAgentKill(ws, msg, sm);
|
|
88
106
|
case "agent.status":
|
|
@@ -184,6 +202,7 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
184
202
|
model,
|
|
185
203
|
permissionMode,
|
|
186
204
|
env: { SNA_SESSION_ID: sessionId },
|
|
205
|
+
history: msg.history,
|
|
187
206
|
extraArgs
|
|
188
207
|
});
|
|
189
208
|
sm.setProcess(sessionId, proc);
|
|
@@ -199,23 +218,79 @@ function handleAgentSend(ws, msg, sm) {
|
|
|
199
218
|
if (!session?.process?.alive) {
|
|
200
219
|
return replyError(ws, msg, `No active agent session "${sessionId}". Start first.`);
|
|
201
220
|
}
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
const images = msg.images;
|
|
222
|
+
if (!msg.message && !images?.length) {
|
|
223
|
+
return replyError(ws, msg, "message or images required");
|
|
224
|
+
}
|
|
225
|
+
const textContent = msg.message ?? "(image)";
|
|
226
|
+
let meta = msg.meta ? { ...msg.meta } : {};
|
|
227
|
+
if (images?.length) {
|
|
228
|
+
const filenames = saveImages(sessionId, images);
|
|
229
|
+
meta.images = filenames;
|
|
204
230
|
}
|
|
205
231
|
try {
|
|
206
232
|
const db = getDb();
|
|
207
233
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
208
|
-
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId,
|
|
234
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
|
|
209
235
|
} catch {
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
sm.updateSessionState(sessionId, "processing");
|
|
212
238
|
sm.touch(sessionId);
|
|
213
|
-
|
|
239
|
+
if (images?.length) {
|
|
240
|
+
const content = [
|
|
241
|
+
...images.map((img) => ({
|
|
242
|
+
type: "image",
|
|
243
|
+
source: { type: "base64", media_type: img.mimeType, data: img.base64 }
|
|
244
|
+
})),
|
|
245
|
+
...msg.message ? [{ type: "text", text: msg.message }] : []
|
|
246
|
+
];
|
|
247
|
+
session.process.send(content);
|
|
248
|
+
} else {
|
|
249
|
+
session.process.send(msg.message);
|
|
250
|
+
}
|
|
214
251
|
wsReply(ws, msg, { status: "sent" });
|
|
215
252
|
}
|
|
253
|
+
function handleAgentResume(ws, msg, sm) {
|
|
254
|
+
const sessionId = msg.session ?? "default";
|
|
255
|
+
const session = sm.getOrCreateSession(sessionId);
|
|
256
|
+
if (session.process?.alive) {
|
|
257
|
+
return replyError(ws, msg, "Session already running. Use agent.send instead.");
|
|
258
|
+
}
|
|
259
|
+
const history = buildHistoryFromDb(sessionId);
|
|
260
|
+
if (history.length === 0 && !msg.prompt) {
|
|
261
|
+
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
262
|
+
}
|
|
263
|
+
const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
|
|
264
|
+
const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
|
|
265
|
+
const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
|
|
266
|
+
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
267
|
+
const provider = getProvider(providerName);
|
|
268
|
+
try {
|
|
269
|
+
const proc = provider.spawn({
|
|
270
|
+
cwd: session.cwd,
|
|
271
|
+
prompt: msg.prompt,
|
|
272
|
+
model,
|
|
273
|
+
permissionMode,
|
|
274
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
275
|
+
history: history.length > 0 ? history : void 0,
|
|
276
|
+
extraArgs
|
|
277
|
+
});
|
|
278
|
+
sm.setProcess(sessionId, proc, "resumed");
|
|
279
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
|
|
280
|
+
wsReply(ws, msg, {
|
|
281
|
+
status: "resumed",
|
|
282
|
+
provider: providerName,
|
|
283
|
+
sessionId: session.id,
|
|
284
|
+
historyCount: history.length
|
|
285
|
+
});
|
|
286
|
+
} catch (e) {
|
|
287
|
+
replyError(ws, msg, e.message);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
216
290
|
function handleAgentRestart(ws, msg, sm) {
|
|
217
291
|
const sessionId = msg.session ?? "default";
|
|
218
292
|
try {
|
|
293
|
+
const ccSessionId = sm.getSession(sessionId)?.ccSessionId;
|
|
219
294
|
const { config } = sm.restartSession(
|
|
220
295
|
sessionId,
|
|
221
296
|
{
|
|
@@ -226,12 +301,13 @@ function handleAgentRestart(ws, msg, sm) {
|
|
|
226
301
|
},
|
|
227
302
|
(cfg) => {
|
|
228
303
|
const prov = getProvider(cfg.provider);
|
|
304
|
+
const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
|
|
229
305
|
return prov.spawn({
|
|
230
306
|
cwd: sm.getSession(sessionId).cwd,
|
|
231
307
|
model: cfg.model,
|
|
232
308
|
permissionMode: cfg.permissionMode,
|
|
233
309
|
env: { SNA_SESSION_ID: sessionId },
|
|
234
|
-
extraArgs: [...cfg.extraArgs ?? [],
|
|
310
|
+
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
235
311
|
});
|
|
236
312
|
}
|
|
237
313
|
);
|
|
@@ -245,6 +321,20 @@ function handleAgentInterrupt(ws, msg, sm) {
|
|
|
245
321
|
const interrupted = sm.interruptSession(sessionId);
|
|
246
322
|
wsReply(ws, msg, { status: interrupted ? "interrupted" : "no_session" });
|
|
247
323
|
}
|
|
324
|
+
function handleAgentSetModel(ws, msg, sm) {
|
|
325
|
+
const sessionId = msg.session ?? "default";
|
|
326
|
+
const model = msg.model;
|
|
327
|
+
if (!model) return replyError(ws, msg, "model is required");
|
|
328
|
+
const updated = sm.setSessionModel(sessionId, model);
|
|
329
|
+
wsReply(ws, msg, { status: updated ? "updated" : "no_session", model });
|
|
330
|
+
}
|
|
331
|
+
function handleAgentSetPermissionMode(ws, msg, sm) {
|
|
332
|
+
const sessionId = msg.session ?? "default";
|
|
333
|
+
const permissionMode = msg.permissionMode;
|
|
334
|
+
if (!permissionMode) return replyError(ws, msg, "permissionMode is required");
|
|
335
|
+
const updated = sm.setSessionPermissionMode(sessionId, permissionMode);
|
|
336
|
+
wsReply(ws, msg, { status: updated ? "updated" : "no_session", permissionMode });
|
|
337
|
+
}
|
|
248
338
|
function handleAgentKill(ws, msg, sm) {
|
|
249
339
|
const sessionId = msg.session ?? "default";
|
|
250
340
|
const killed = sm.killSession(sessionId);
|
|
@@ -253,10 +343,14 @@ function handleAgentKill(ws, msg, sm) {
|
|
|
253
343
|
function handleAgentStatus(ws, msg, sm) {
|
|
254
344
|
const sessionId = msg.session ?? "default";
|
|
255
345
|
const session = sm.getSession(sessionId);
|
|
346
|
+
const alive = session?.process?.alive ?? false;
|
|
256
347
|
wsReply(ws, msg, {
|
|
257
|
-
alive
|
|
348
|
+
alive,
|
|
349
|
+
agentStatus: !alive ? "disconnected" : session?.state === "processing" ? "busy" : "idle",
|
|
258
350
|
sessionId: session?.process?.sessionId ?? null,
|
|
259
|
-
|
|
351
|
+
ccSessionId: session?.ccSessionId ?? null,
|
|
352
|
+
eventCount: session?.eventCounter ?? 0,
|
|
353
|
+
config: session?.lastStartConfig ?? null
|
|
260
354
|
});
|
|
261
355
|
}
|
|
262
356
|
async function handleAgentRunOnce(ws, msg, sm) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock Anthropic Messages API server for testing.
|
|
5
|
+
*
|
|
6
|
+
* Implements POST /v1/messages with streaming SSE responses.
|
|
7
|
+
* Set ANTHROPIC_BASE_URL=http://localhost:<port> and
|
|
8
|
+
* ANTHROPIC_API_KEY=any-string to redirect Claude Code here.
|
|
9
|
+
*
|
|
10
|
+
* All requests and responses are logged to stdout (captured by sna tu api:up → .sna/mock-api.log).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { startMockAnthropicServer } from "@sna-sdk/core/testing";
|
|
14
|
+
* const mock = await startMockAnthropicServer();
|
|
15
|
+
* process.env.ANTHROPIC_BASE_URL = `http://localhost:${mock.port}`;
|
|
16
|
+
* process.env.ANTHROPIC_API_KEY = "test-key";
|
|
17
|
+
* // ... spawn claude code, run tests ...
|
|
18
|
+
* mock.close();
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface MockServer {
|
|
22
|
+
port: number;
|
|
23
|
+
server: http.Server;
|
|
24
|
+
close: () => void;
|
|
25
|
+
/** Messages received by the mock server */
|
|
26
|
+
requests: Array<{
|
|
27
|
+
model: string;
|
|
28
|
+
messages: any[];
|
|
29
|
+
stream: boolean;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
declare function startMockAnthropicServer(): Promise<MockServer>;
|
|
34
|
+
|
|
35
|
+
export { type MockServer, startMockAnthropicServer };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function ts() {
|
|
5
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
6
|
+
}
|
|
7
|
+
async function startMockAnthropicServer() {
|
|
8
|
+
const requests = [];
|
|
9
|
+
const server = http.createServer(async (req, res) => {
|
|
10
|
+
console.log(`[${ts()}] ${req.method} ${req.url} ${req.headers["content-type"] ?? ""}`);
|
|
11
|
+
if (req.method === "OPTIONS") {
|
|
12
|
+
res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*" });
|
|
13
|
+
res.end();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
19
|
+
const rawBody = Buffer.concat(chunks).toString();
|
|
20
|
+
let body;
|
|
21
|
+
try {
|
|
22
|
+
body = JSON.parse(rawBody);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.log(`[${ts()}] ERROR: invalid JSON body: ${rawBody.slice(0, 200)}`);
|
|
25
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
26
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const entry = { model: body.model, messages: body.messages, stream: body.stream, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
30
|
+
requests.push(entry);
|
|
31
|
+
const lastUser = body.messages?.filter((m) => m.role === "user").pop();
|
|
32
|
+
let userText = "(no text)";
|
|
33
|
+
if (typeof lastUser?.content === "string") {
|
|
34
|
+
userText = lastUser.content;
|
|
35
|
+
} else if (Array.isArray(lastUser?.content)) {
|
|
36
|
+
const textBlocks = lastUser.content.filter((b) => b.type === "text").map((b) => b.text);
|
|
37
|
+
const realText = textBlocks.find((t) => !t.startsWith("<system-reminder>"));
|
|
38
|
+
userText = realText ?? textBlocks[textBlocks.length - 1] ?? "(no text)";
|
|
39
|
+
}
|
|
40
|
+
console.log(`[${ts()}] BODY KEYS: ${Object.keys(body).join(", ")}`);
|
|
41
|
+
try {
|
|
42
|
+
const dumpPath = path.join(process.cwd(), ".sna/mock-api-last-request.json");
|
|
43
|
+
fs.writeFileSync(dumpPath, JSON.stringify(body, null, 2));
|
|
44
|
+
console.log(`[${ts()}] FULL BODY dumped to .sna/mock-api-last-request.json`);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
if (body.system) {
|
|
48
|
+
const sysText = typeof body.system === "string" ? body.system : JSON.stringify(body.system);
|
|
49
|
+
console.log(`[${ts()}] SYSTEM PROMPT (${sysText.length} chars): ${sysText.slice(0, 300)}...`);
|
|
50
|
+
if (sysText.includes("\uC720\uB2C8") || sysText.includes("\uCEE4\uD53C") || sysText.includes("\uAE30\uC5B5")) {
|
|
51
|
+
console.log(`[${ts()}] *** HISTORY FOUND IN SYSTEM PROMPT ***`);
|
|
52
|
+
for (const keyword of ["\uC720\uB2C8", "\uCEE4\uD53C", "\uAE30\uC5B5"]) {
|
|
53
|
+
const idx = sysText.indexOf(keyword);
|
|
54
|
+
if (idx >= 0) console.log(`[${ts()}] "${keyword}" at pos ${idx}: ...${sysText.slice(Math.max(0, idx - 50), idx + 80)}...`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log(`[${ts()}] REQ model=${body.model} stream=${body.stream} messages=${body.messages?.length} user="${userText.slice(0, 120)}"`);
|
|
59
|
+
for (let mi = 0; mi < body.messages.length; mi++) {
|
|
60
|
+
const m = body.messages[mi];
|
|
61
|
+
const role = m.role;
|
|
62
|
+
let preview = "";
|
|
63
|
+
if (typeof m.content === "string") {
|
|
64
|
+
preview = m.content.slice(0, 150);
|
|
65
|
+
} else if (Array.isArray(m.content)) {
|
|
66
|
+
preview = m.content.map((b) => {
|
|
67
|
+
if (b.type === "text") return `text:"${b.text.slice(0, 100)}"`;
|
|
68
|
+
if (b.type === "image") return `image:${b.source?.media_type}`;
|
|
69
|
+
return b.type;
|
|
70
|
+
}).join(" | ");
|
|
71
|
+
}
|
|
72
|
+
console.log(`[${ts()}] [${mi}] ${role}: ${preview}`);
|
|
73
|
+
}
|
|
74
|
+
const replyText = [...userText].reverse().join("");
|
|
75
|
+
const messageId = `msg_mock_${Date.now()}`;
|
|
76
|
+
if (body.stream) {
|
|
77
|
+
res.writeHead(200, {
|
|
78
|
+
"Content-Type": "text/event-stream",
|
|
79
|
+
"Cache-Control": "no-cache",
|
|
80
|
+
"Connection": "keep-alive"
|
|
81
|
+
});
|
|
82
|
+
const send = (event, data) => {
|
|
83
|
+
res.write(`event: ${event}
|
|
84
|
+
data: ${JSON.stringify(data)}
|
|
85
|
+
|
|
86
|
+
`);
|
|
87
|
+
};
|
|
88
|
+
send("message_start", {
|
|
89
|
+
type: "message_start",
|
|
90
|
+
message: {
|
|
91
|
+
id: messageId,
|
|
92
|
+
type: "message",
|
|
93
|
+
role: "assistant",
|
|
94
|
+
model: body.model,
|
|
95
|
+
content: [],
|
|
96
|
+
stop_reason: null,
|
|
97
|
+
usage: { input_tokens: 100, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
send("content_block_start", {
|
|
101
|
+
type: "content_block_start",
|
|
102
|
+
index: 0,
|
|
103
|
+
content_block: { type: "text", text: "" }
|
|
104
|
+
});
|
|
105
|
+
const words = replyText.split(" ");
|
|
106
|
+
for (const word of words) {
|
|
107
|
+
send("content_block_delta", {
|
|
108
|
+
type: "content_block_delta",
|
|
109
|
+
index: 0,
|
|
110
|
+
delta: { type: "text_delta", text: word + " " }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
send("content_block_stop", { type: "content_block_stop", index: 0 });
|
|
114
|
+
send("message_delta", {
|
|
115
|
+
type: "message_delta",
|
|
116
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
117
|
+
usage: { output_tokens: words.length * 2 }
|
|
118
|
+
});
|
|
119
|
+
send("message_stop", { type: "message_stop" });
|
|
120
|
+
res.end();
|
|
121
|
+
console.log(`[${ts()}] RES stream complete reply="${replyText.slice(0, 80)}"`);
|
|
122
|
+
} else {
|
|
123
|
+
const response = {
|
|
124
|
+
id: messageId,
|
|
125
|
+
type: "message",
|
|
126
|
+
role: "assistant",
|
|
127
|
+
model: body.model,
|
|
128
|
+
content: [{ type: "text", text: replyText }],
|
|
129
|
+
stop_reason: "end_turn",
|
|
130
|
+
usage: { input_tokens: 100, output_tokens: 20, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
|
|
131
|
+
};
|
|
132
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
133
|
+
res.end(JSON.stringify(response));
|
|
134
|
+
console.log(`[${ts()}] RES json reply="${replyText.slice(0, 80)}"`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log(`[${ts()}] 404 ${req.method} ${req.url}`);
|
|
139
|
+
res.writeHead(404);
|
|
140
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
141
|
+
});
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
server.listen(0, () => {
|
|
144
|
+
const port = server.address().port;
|
|
145
|
+
console.log(`[${ts()}] Mock Anthropic API server listening on :${port}`);
|
|
146
|
+
resolve({
|
|
147
|
+
port,
|
|
148
|
+
server,
|
|
149
|
+
close: () => {
|
|
150
|
+
console.log(`[${ts()}] Mock API server shutting down`);
|
|
151
|
+
server.close();
|
|
152
|
+
},
|
|
153
|
+
requests
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
startMockAnthropicServer
|
|
160
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sna-sdk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"source": "./src/lib/sna-run.ts",
|
|
64
64
|
"types": "./dist/lib/sna-run.d.ts",
|
|
65
65
|
"default": "./dist/lib/sna-run.js"
|
|
66
|
+
},
|
|
67
|
+
"./testing": {
|
|
68
|
+
"source": "./src/testing/mock-api.ts",
|
|
69
|
+
"types": "./dist/testing/mock-api.d.ts",
|
|
70
|
+
"default": "./dist/testing/mock-api.js"
|
|
66
71
|
}
|
|
67
72
|
},
|
|
68
73
|
"engines": {
|