@sna-sdk/core 0.1.1 → 0.3.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.
@@ -0,0 +1,140 @@
1
+ import http from "http";
2
+ function ts() {
3
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
4
+ }
5
+ async function startMockAnthropicServer() {
6
+ const requests = [];
7
+ const server = http.createServer(async (req, res) => {
8
+ console.log(`[${ts()}] ${req.method} ${req.url} ${req.headers["content-type"] ?? ""}`);
9
+ if (req.method === "OPTIONS") {
10
+ res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*" });
11
+ res.end();
12
+ return;
13
+ }
14
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
15
+ const chunks = [];
16
+ for await (const chunk of req) chunks.push(chunk);
17
+ const rawBody = Buffer.concat(chunks).toString();
18
+ let body;
19
+ try {
20
+ body = JSON.parse(rawBody);
21
+ } catch (e) {
22
+ console.log(`[${ts()}] ERROR: invalid JSON body: ${rawBody.slice(0, 200)}`);
23
+ res.writeHead(400, { "Content-Type": "application/json" });
24
+ res.end(JSON.stringify({ error: "invalid JSON" }));
25
+ return;
26
+ }
27
+ const entry = { model: body.model, messages: body.messages, stream: body.stream, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
28
+ requests.push(entry);
29
+ const lastUser = body.messages?.filter((m) => m.role === "user").pop();
30
+ let userText = "(no text)";
31
+ if (typeof lastUser?.content === "string") {
32
+ userText = lastUser.content;
33
+ } else if (Array.isArray(lastUser?.content)) {
34
+ const textBlocks = lastUser.content.filter((b) => b.type === "text").map((b) => b.text);
35
+ const realText = textBlocks.find((t) => !t.startsWith("<system-reminder>"));
36
+ userText = realText ?? textBlocks[textBlocks.length - 1] ?? "(no text)";
37
+ }
38
+ console.log(`[${ts()}] REQ model=${body.model} stream=${body.stream} messages=${body.messages?.length} user="${userText.slice(0, 120)}"`);
39
+ for (let mi = 0; mi < body.messages.length; mi++) {
40
+ const m = body.messages[mi];
41
+ const role = m.role;
42
+ let preview = "";
43
+ if (typeof m.content === "string") {
44
+ preview = m.content.slice(0, 150);
45
+ } else if (Array.isArray(m.content)) {
46
+ preview = m.content.map((b) => {
47
+ if (b.type === "text") return `text:"${b.text.slice(0, 100)}"`;
48
+ if (b.type === "image") return `image:${b.source?.media_type}`;
49
+ return b.type;
50
+ }).join(" | ");
51
+ }
52
+ console.log(`[${ts()}] [${mi}] ${role}: ${preview}`);
53
+ }
54
+ const replyText = [...userText].reverse().join("");
55
+ const messageId = `msg_mock_${Date.now()}`;
56
+ if (body.stream) {
57
+ res.writeHead(200, {
58
+ "Content-Type": "text/event-stream",
59
+ "Cache-Control": "no-cache",
60
+ "Connection": "keep-alive"
61
+ });
62
+ const send = (event, data) => {
63
+ res.write(`event: ${event}
64
+ data: ${JSON.stringify(data)}
65
+
66
+ `);
67
+ };
68
+ send("message_start", {
69
+ type: "message_start",
70
+ message: {
71
+ id: messageId,
72
+ type: "message",
73
+ role: "assistant",
74
+ model: body.model,
75
+ content: [],
76
+ stop_reason: null,
77
+ usage: { input_tokens: 100, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
78
+ }
79
+ });
80
+ send("content_block_start", {
81
+ type: "content_block_start",
82
+ index: 0,
83
+ content_block: { type: "text", text: "" }
84
+ });
85
+ const words = replyText.split(" ");
86
+ for (const word of words) {
87
+ send("content_block_delta", {
88
+ type: "content_block_delta",
89
+ index: 0,
90
+ delta: { type: "text_delta", text: word + " " }
91
+ });
92
+ }
93
+ send("content_block_stop", { type: "content_block_stop", index: 0 });
94
+ send("message_delta", {
95
+ type: "message_delta",
96
+ delta: { stop_reason: "end_turn", stop_sequence: null },
97
+ usage: { output_tokens: words.length * 2 }
98
+ });
99
+ send("message_stop", { type: "message_stop" });
100
+ res.end();
101
+ console.log(`[${ts()}] RES stream complete reply="${replyText.slice(0, 80)}"`);
102
+ } else {
103
+ const response = {
104
+ id: messageId,
105
+ type: "message",
106
+ role: "assistant",
107
+ model: body.model,
108
+ content: [{ type: "text", text: replyText }],
109
+ stop_reason: "end_turn",
110
+ usage: { input_tokens: 100, output_tokens: 20, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
111
+ };
112
+ res.writeHead(200, { "Content-Type": "application/json" });
113
+ res.end(JSON.stringify(response));
114
+ console.log(`[${ts()}] RES json reply="${replyText.slice(0, 80)}"`);
115
+ }
116
+ return;
117
+ }
118
+ console.log(`[${ts()}] 404 ${req.method} ${req.url}`);
119
+ res.writeHead(404);
120
+ res.end(JSON.stringify({ error: "Not found" }));
121
+ });
122
+ return new Promise((resolve) => {
123
+ server.listen(0, () => {
124
+ const port = server.address().port;
125
+ console.log(`[${ts()}] Mock Anthropic API server listening on :${port}`);
126
+ resolve({
127
+ port,
128
+ server,
129
+ close: () => {
130
+ console.log(`[${ts()}] Mock API server shutting down`);
131
+ server.close();
132
+ },
133
+ requests
134
+ });
135
+ });
136
+ });
137
+ }
138
+ export {
139
+ startMockAnthropicServer
140
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.1.1",
3
+ "version": "0.3.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": {
@@ -86,12 +91,14 @@
86
91
  "better-sqlite3": "^12.6.2",
87
92
  "chalk": "^5.0.0",
88
93
  "hono": "^4.12.7",
89
- "js-yaml": "^4.1.0"
94
+ "js-yaml": "^4.1.0",
95
+ "ws": "^8.20.0"
90
96
  },
91
97
  "devDependencies": {
92
98
  "@types/better-sqlite3": "^7.6.13",
93
99
  "@types/js-yaml": "^4.0.9",
94
100
  "@types/node": "^22.0.0",
101
+ "@types/ws": "^8.18.1",
95
102
  "tsup": "^8.0.0",
96
103
  "tsx": "^4.0.0",
97
104
  "typescript": "^5.0.0"