@swarmroom/server 0.1.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.
Files changed (61) hide show
  1. package/dist/__tests__/setup.d.ts +6 -0
  2. package/dist/__tests__/setup.js +88 -0
  3. package/dist/app.d.ts +3 -0
  4. package/dist/app.js +41 -0
  5. package/dist/db/index.d.ts +6 -0
  6. package/dist/db/index.js +62 -0
  7. package/dist/db/schema.d.ts +655 -0
  8. package/dist/db/schema.js +61 -0
  9. package/dist/db/seed.d.ts +1 -0
  10. package/dist/db/seed.js +15 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +30 -0
  13. package/dist/lib/names.d.ts +1 -0
  14. package/dist/lib/names.js +32 -0
  15. package/dist/mcp/server.d.ts +2 -0
  16. package/dist/mcp/server.js +154 -0
  17. package/dist/mcp/transport.d.ts +2 -0
  18. package/dist/mcp/transport.js +27 -0
  19. package/dist/middleware/cors.d.ts +1 -0
  20. package/dist/middleware/cors.js +8 -0
  21. package/dist/middleware/error-handler.d.ts +2 -0
  22. package/dist/middleware/error-handler.js +10 -0
  23. package/dist/routes/__tests__/agents.test.d.ts +1 -0
  24. package/dist/routes/__tests__/agents.test.js +80 -0
  25. package/dist/routes/agents.d.ts +3 -0
  26. package/dist/routes/agents.js +100 -0
  27. package/dist/routes/health.d.ts +3 -0
  28. package/dist/routes/health.js +14 -0
  29. package/dist/routes/messages.d.ts +3 -0
  30. package/dist/routes/messages.js +65 -0
  31. package/dist/routes/projects.d.ts +3 -0
  32. package/dist/routes/projects.js +94 -0
  33. package/dist/routes/teams.d.ts +3 -0
  34. package/dist/routes/teams.js +94 -0
  35. package/dist/routes/well-known.d.ts +3 -0
  36. package/dist/routes/well-known.js +78 -0
  37. package/dist/routes/ws.d.ts +5 -0
  38. package/dist/routes/ws.js +19 -0
  39. package/dist/services/__tests__/agent-service.test.d.ts +1 -0
  40. package/dist/services/__tests__/agent-service.test.js +67 -0
  41. package/dist/services/__tests__/heartbeat-service.test.d.ts +1 -0
  42. package/dist/services/__tests__/heartbeat-service.test.js +76 -0
  43. package/dist/services/__tests__/message-service.test.d.ts +1 -0
  44. package/dist/services/__tests__/message-service.test.js +85 -0
  45. package/dist/services/agent-service.d.ts +74 -0
  46. package/dist/services/agent-service.js +131 -0
  47. package/dist/services/heartbeat-service.d.ts +2 -0
  48. package/dist/services/heartbeat-service.js +35 -0
  49. package/dist/services/mdns-browser.d.ts +2 -0
  50. package/dist/services/mdns-browser.js +53 -0
  51. package/dist/services/mdns-service.d.ts +2 -0
  52. package/dist/services/mdns-service.js +66 -0
  53. package/dist/services/message-service.d.ts +82 -0
  54. package/dist/services/message-service.js +172 -0
  55. package/dist/services/project-service.d.ts +130 -0
  56. package/dist/services/project-service.js +104 -0
  57. package/dist/services/team-service.d.ts +130 -0
  58. package/dist/services/team-service.js +104 -0
  59. package/dist/services/ws-manager.d.ts +19 -0
  60. package/dist/services/ws-manager.js +165 -0
  61. package/package.json +47 -0
@@ -0,0 +1,104 @@
1
+ import { eq, and, count } from 'drizzle-orm';
2
+ import { db, projectGroups, agentProjects, agents } from '../db/index.js';
3
+ export function createProject(input) {
4
+ const id = crypto.randomUUID();
5
+ const now = Date.now();
6
+ db.insert(projectGroups)
7
+ .values({
8
+ id,
9
+ name: input.name,
10
+ description: input.description ?? null,
11
+ repository: input.repository ?? null,
12
+ createdAt: now,
13
+ })
14
+ .run();
15
+ if (input.agentIds && input.agentIds.length > 0) {
16
+ for (const agentId of input.agentIds) {
17
+ db.insert(agentProjects).values({ agentId, projectId: id }).run();
18
+ }
19
+ }
20
+ return getProjectById(id);
21
+ }
22
+ export function listProjects() {
23
+ const allProjects = db.select().from(projectGroups).all();
24
+ return allProjects.map((project) => {
25
+ const result = db
26
+ .select({ value: count() })
27
+ .from(agentProjects)
28
+ .where(eq(agentProjects.projectId, project.id))
29
+ .get();
30
+ return {
31
+ ...project,
32
+ agentCount: result?.value ?? 0,
33
+ };
34
+ });
35
+ }
36
+ export function getProjectById(id) {
37
+ const project = db.select().from(projectGroups).where(eq(projectGroups.id, id)).get();
38
+ if (!project)
39
+ return null;
40
+ const memberRows = db
41
+ .select()
42
+ .from(agents)
43
+ .innerJoin(agentProjects, eq(agents.id, agentProjects.agentId))
44
+ .where(eq(agentProjects.projectId, id))
45
+ .all();
46
+ return {
47
+ ...project,
48
+ agents: memberRows.map((r) => ({
49
+ ...r.agents,
50
+ agentCard: r.agents.agentCard ? JSON.parse(r.agents.agentCard) : null,
51
+ })),
52
+ };
53
+ }
54
+ export function updateProject(id, updates) {
55
+ const existing = db.select().from(projectGroups).where(eq(projectGroups.id, id)).get();
56
+ if (!existing)
57
+ return null;
58
+ const values = {};
59
+ if (updates.name !== undefined)
60
+ values.name = updates.name;
61
+ if (updates.description !== undefined)
62
+ values.description = updates.description;
63
+ if (updates.repository !== undefined)
64
+ values.repository = updates.repository;
65
+ if (Object.keys(values).length > 0) {
66
+ db.update(projectGroups).set(values).where(eq(projectGroups.id, id)).run();
67
+ }
68
+ return getProjectById(id);
69
+ }
70
+ export function deleteProject(id) {
71
+ const existing = db.select().from(projectGroups).where(eq(projectGroups.id, id)).get();
72
+ if (!existing)
73
+ return null;
74
+ db.delete(agentProjects).where(eq(agentProjects.projectId, id)).run();
75
+ db.delete(projectGroups).where(eq(projectGroups.id, id)).run();
76
+ return existing;
77
+ }
78
+ export function addAgentToProject(projectId, agentId) {
79
+ const project = db.select().from(projectGroups).where(eq(projectGroups.id, projectId)).get();
80
+ if (!project)
81
+ return { error: 'project_not_found' };
82
+ const agent = db.select().from(agents).where(eq(agents.id, agentId)).get();
83
+ if (!agent)
84
+ return { error: 'agent_not_found' };
85
+ db.insert(agentProjects).values({ agentId, projectId }).run();
86
+ return { data: getProjectById(projectId) };
87
+ }
88
+ export function removeAgentFromProject(projectId, agentId) {
89
+ const project = db.select().from(projectGroups).where(eq(projectGroups.id, projectId)).get();
90
+ if (!project)
91
+ return { error: 'project_not_found' };
92
+ const membership = db
93
+ .select()
94
+ .from(agentProjects)
95
+ .where(eq(agentProjects.projectId, projectId))
96
+ .all()
97
+ .find((r) => r.agentId === agentId);
98
+ if (!membership)
99
+ return { error: 'not_a_member' };
100
+ db.delete(agentProjects)
101
+ .where(and(eq(agentProjects.projectId, projectId), eq(agentProjects.agentId, agentId)))
102
+ .run();
103
+ return { data: getProjectById(projectId) };
104
+ }
@@ -0,0 +1,130 @@
1
+ import type { CreateTeamRequest } from '@swarmroom/shared';
2
+ export declare function createTeam(input: CreateTeamRequest & {
3
+ color?: string;
4
+ }): {
5
+ agents: {
6
+ agentCard: any;
7
+ id: string;
8
+ name: string;
9
+ displayName: string | null;
10
+ url: string;
11
+ status: string | null;
12
+ lastHeartbeat: number | null;
13
+ createdAt: number | null;
14
+ updatedAt: number | null;
15
+ }[];
16
+ id: string;
17
+ name: string;
18
+ description: string | null;
19
+ color: string | null;
20
+ createdAt: number | null;
21
+ } | null;
22
+ export declare function listTeams(): {
23
+ agentCount: number;
24
+ id: string;
25
+ name: string;
26
+ description: string | null;
27
+ color: string | null;
28
+ createdAt: number | null;
29
+ }[];
30
+ export declare function getTeamById(id: string): {
31
+ agents: {
32
+ agentCard: any;
33
+ id: string;
34
+ name: string;
35
+ displayName: string | null;
36
+ url: string;
37
+ status: string | null;
38
+ lastHeartbeat: number | null;
39
+ createdAt: number | null;
40
+ updatedAt: number | null;
41
+ }[];
42
+ id: string;
43
+ name: string;
44
+ description: string | null;
45
+ color: string | null;
46
+ createdAt: number | null;
47
+ } | null;
48
+ export declare function updateTeam(id: string, updates: Partial<{
49
+ name: string;
50
+ description: string;
51
+ color: string;
52
+ }>): {
53
+ agents: {
54
+ agentCard: any;
55
+ id: string;
56
+ name: string;
57
+ displayName: string | null;
58
+ url: string;
59
+ status: string | null;
60
+ lastHeartbeat: number | null;
61
+ createdAt: number | null;
62
+ updatedAt: number | null;
63
+ }[];
64
+ id: string;
65
+ name: string;
66
+ description: string | null;
67
+ color: string | null;
68
+ createdAt: number | null;
69
+ } | null;
70
+ export declare function deleteTeam(id: string): {
71
+ id: string;
72
+ name: string;
73
+ description: string | null;
74
+ color: string | null;
75
+ createdAt: number | null;
76
+ } | null;
77
+ export declare function addAgentToTeam(teamId: string, agentId: string): {
78
+ error: "team_not_found";
79
+ data?: undefined;
80
+ } | {
81
+ error: "agent_not_found";
82
+ data?: undefined;
83
+ } | {
84
+ data: {
85
+ agents: {
86
+ agentCard: any;
87
+ id: string;
88
+ name: string;
89
+ displayName: string | null;
90
+ url: string;
91
+ status: string | null;
92
+ lastHeartbeat: number | null;
93
+ createdAt: number | null;
94
+ updatedAt: number | null;
95
+ }[];
96
+ id: string;
97
+ name: string;
98
+ description: string | null;
99
+ color: string | null;
100
+ createdAt: number | null;
101
+ } | null;
102
+ error?: undefined;
103
+ };
104
+ export declare function removeAgentFromTeam(teamId: string, agentId: string): {
105
+ error: "team_not_found";
106
+ data?: undefined;
107
+ } | {
108
+ error: "not_a_member";
109
+ data?: undefined;
110
+ } | {
111
+ data: {
112
+ agents: {
113
+ agentCard: any;
114
+ id: string;
115
+ name: string;
116
+ displayName: string | null;
117
+ url: string;
118
+ status: string | null;
119
+ lastHeartbeat: number | null;
120
+ createdAt: number | null;
121
+ updatedAt: number | null;
122
+ }[];
123
+ id: string;
124
+ name: string;
125
+ description: string | null;
126
+ color: string | null;
127
+ createdAt: number | null;
128
+ } | null;
129
+ error?: undefined;
130
+ };
@@ -0,0 +1,104 @@
1
+ import { eq, and, count } from 'drizzle-orm';
2
+ import { db, teams, agentTeams, agents } from '../db/index.js';
3
+ export function createTeam(input) {
4
+ const id = crypto.randomUUID();
5
+ const now = Date.now();
6
+ db.insert(teams)
7
+ .values({
8
+ id,
9
+ name: input.name,
10
+ description: input.description ?? null,
11
+ color: input.color ?? '#6366f1',
12
+ createdAt: now,
13
+ })
14
+ .run();
15
+ if (input.agentIds && input.agentIds.length > 0) {
16
+ for (const agentId of input.agentIds) {
17
+ db.insert(agentTeams).values({ agentId, teamId: id }).run();
18
+ }
19
+ }
20
+ return getTeamById(id);
21
+ }
22
+ export function listTeams() {
23
+ const allTeams = db.select().from(teams).all();
24
+ return allTeams.map((team) => {
25
+ const result = db
26
+ .select({ value: count() })
27
+ .from(agentTeams)
28
+ .where(eq(agentTeams.teamId, team.id))
29
+ .get();
30
+ return {
31
+ ...team,
32
+ agentCount: result?.value ?? 0,
33
+ };
34
+ });
35
+ }
36
+ export function getTeamById(id) {
37
+ const team = db.select().from(teams).where(eq(teams.id, id)).get();
38
+ if (!team)
39
+ return null;
40
+ const memberRows = db
41
+ .select()
42
+ .from(agents)
43
+ .innerJoin(agentTeams, eq(agents.id, agentTeams.agentId))
44
+ .where(eq(agentTeams.teamId, id))
45
+ .all();
46
+ return {
47
+ ...team,
48
+ agents: memberRows.map((r) => ({
49
+ ...r.agents,
50
+ agentCard: r.agents.agentCard ? JSON.parse(r.agents.agentCard) : null,
51
+ })),
52
+ };
53
+ }
54
+ export function updateTeam(id, updates) {
55
+ const existing = db.select().from(teams).where(eq(teams.id, id)).get();
56
+ if (!existing)
57
+ return null;
58
+ const values = {};
59
+ if (updates.name !== undefined)
60
+ values.name = updates.name;
61
+ if (updates.description !== undefined)
62
+ values.description = updates.description;
63
+ if (updates.color !== undefined)
64
+ values.color = updates.color;
65
+ if (Object.keys(values).length > 0) {
66
+ db.update(teams).set(values).where(eq(teams.id, id)).run();
67
+ }
68
+ return getTeamById(id);
69
+ }
70
+ export function deleteTeam(id) {
71
+ const existing = db.select().from(teams).where(eq(teams.id, id)).get();
72
+ if (!existing)
73
+ return null;
74
+ db.delete(agentTeams).where(eq(agentTeams.teamId, id)).run();
75
+ db.delete(teams).where(eq(teams.id, id)).run();
76
+ return existing;
77
+ }
78
+ export function addAgentToTeam(teamId, agentId) {
79
+ const team = db.select().from(teams).where(eq(teams.id, teamId)).get();
80
+ if (!team)
81
+ return { error: 'team_not_found' };
82
+ const agent = db.select().from(agents).where(eq(agents.id, agentId)).get();
83
+ if (!agent)
84
+ return { error: 'agent_not_found' };
85
+ db.insert(agentTeams).values({ agentId, teamId }).run();
86
+ return { data: getTeamById(teamId) };
87
+ }
88
+ export function removeAgentFromTeam(teamId, agentId) {
89
+ const team = db.select().from(teams).where(eq(teams.id, teamId)).get();
90
+ if (!team)
91
+ return { error: 'team_not_found' };
92
+ const membership = db
93
+ .select()
94
+ .from(agentTeams)
95
+ .where(eq(agentTeams.teamId, teamId))
96
+ .all()
97
+ .find((r) => r.agentId === agentId);
98
+ if (!membership)
99
+ return { error: 'not_a_member' };
100
+ db.delete(agentTeams)
101
+ .where(and(eq(agentTeams.teamId, teamId), eq(agentTeams.agentId, agentId)))
102
+ .run();
103
+ return { data: getTeamById(teamId) };
104
+ }
@@ -0,0 +1,19 @@
1
+ import type { WSContext } from 'hono/ws';
2
+ import type { WSMessageType } from '@swarmroom/shared';
3
+ export declare function registerClient(clientId: string, ws: WSContext): void;
4
+ export declare function unregisterClient(ws: WSContext): void;
5
+ export declare function sendToClient(clientId: string, type: WSMessageType, payload: unknown): void;
6
+ export declare function broadcast(type: WSMessageType, payload: unknown): void;
7
+ export declare function sendToDashboards(type: WSMessageType, payload: unknown): void;
8
+ export declare function sendToDaemons(type: WSMessageType, payload: unknown): void;
9
+ export declare function hasActiveConnections(clientId: string): boolean;
10
+ export declare function handleIncomingMessage(ws: WSContext, raw: string): void;
11
+ export declare function broadcastAgentOnline(agent: {
12
+ id: string;
13
+ name: string;
14
+ }): void;
15
+ export declare function broadcastAgentOffline(agent: {
16
+ id: string;
17
+ name: string;
18
+ }): void;
19
+ export declare function pushMessageToRecipient(recipientId: string, message: unknown): void;
@@ -0,0 +1,165 @@
1
+ import { DAEMON_KEY } from '@swarmroom/shared';
2
+ const DASHBOARD_KEY = '__dashboard__';
3
+ const PING_INTERVAL_MS = 30_000;
4
+ const PONG_TIMEOUT_MS = 10_000;
5
+ const clients = new Map();
6
+ const pingTimers = new Map();
7
+ const pongTimers = new Map();
8
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
9
+ function makeMessage(type, payload) {
10
+ const msg = {
11
+ type,
12
+ payload,
13
+ timestamp: new Date().toISOString(),
14
+ };
15
+ return JSON.stringify(msg);
16
+ }
17
+ function safeSend(ws, data) {
18
+ try {
19
+ if (ws.readyState === 1) {
20
+ ws.send(data);
21
+ }
22
+ }
23
+ catch {
24
+ }
25
+ }
26
+ // ─── Ping/Pong ───────────────────────────────────────────────────────────────
27
+ function startPing(ws) {
28
+ const interval = setInterval(() => {
29
+ if (ws.readyState !== 1) {
30
+ clearPing(ws);
31
+ return;
32
+ }
33
+ safeSend(ws, makeMessage('heartbeat', { ping: true }));
34
+ const timeout = setTimeout(() => {
35
+ console.log('[ws] Client failed pong check, closing connection');
36
+ ws.close(1000, 'pong timeout');
37
+ }, PONG_TIMEOUT_MS);
38
+ pongTimers.set(ws, timeout);
39
+ }, PING_INTERVAL_MS);
40
+ pingTimers.set(ws, interval);
41
+ }
42
+ function clearPing(ws) {
43
+ const interval = pingTimers.get(ws);
44
+ if (interval) {
45
+ clearInterval(interval);
46
+ pingTimers.delete(ws);
47
+ }
48
+ const timeout = pongTimers.get(ws);
49
+ if (timeout) {
50
+ clearTimeout(timeout);
51
+ pongTimers.delete(ws);
52
+ }
53
+ }
54
+ function handlePong(ws) {
55
+ const timeout = pongTimers.get(ws);
56
+ if (timeout) {
57
+ clearTimeout(timeout);
58
+ pongTimers.delete(ws);
59
+ }
60
+ }
61
+ // ─── Registration ────────────────────────────────────────────────────────────
62
+ export function registerClient(clientId, ws) {
63
+ const existing = clients.get(clientId) ?? [];
64
+ existing.push(ws);
65
+ clients.set(clientId, existing);
66
+ startPing(ws);
67
+ console.log(`[ws] Client registered: ${clientId} (${existing.length} connection(s))`);
68
+ }
69
+ export function unregisterClient(ws) {
70
+ for (const [clientId, connections] of clients.entries()) {
71
+ const idx = connections.indexOf(ws);
72
+ if (idx !== -1) {
73
+ connections.splice(idx, 1);
74
+ console.log(`[ws] Client disconnected: ${clientId} (${connections.length} remaining)`);
75
+ if (connections.length === 0) {
76
+ clients.delete(clientId);
77
+ }
78
+ clearPing(ws);
79
+ return;
80
+ }
81
+ }
82
+ clearPing(ws);
83
+ }
84
+ // ─── Sending ─────────────────────────────────────────────────────────────────
85
+ export function sendToClient(clientId, type, payload) {
86
+ const connections = clients.get(clientId);
87
+ if (!connections || connections.length === 0)
88
+ return;
89
+ const data = makeMessage(type, payload);
90
+ for (const ws of connections) {
91
+ safeSend(ws, data);
92
+ }
93
+ }
94
+ export function broadcast(type, payload) {
95
+ const data = makeMessage(type, payload);
96
+ for (const connections of clients.values()) {
97
+ for (const ws of connections) {
98
+ safeSend(ws, data);
99
+ }
100
+ }
101
+ }
102
+ export function sendToDashboards(type, payload) {
103
+ sendToClient(DASHBOARD_KEY, type, payload);
104
+ }
105
+ export function sendToDaemons(type, payload) {
106
+ sendToClient(DAEMON_KEY, type, payload);
107
+ }
108
+ export function hasActiveConnections(clientId) {
109
+ const connections = clients.get(clientId);
110
+ return !!connections && connections.length > 0;
111
+ }
112
+ // ─── Message Handling ────────────────────────────────────────────────────────
113
+ export function handleIncomingMessage(ws, raw) {
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(raw);
117
+ }
118
+ catch {
119
+ safeSend(ws, makeMessage('error', { message: 'Invalid JSON' }));
120
+ return;
121
+ }
122
+ if (!parsed.type) {
123
+ safeSend(ws, makeMessage('error', { message: 'Missing "type" field' }));
124
+ return;
125
+ }
126
+ switch (parsed.type) {
127
+ case 'register': {
128
+ const payload = parsed.payload;
129
+ if (payload?.clientType === 'dashboard') {
130
+ registerClient(DASHBOARD_KEY, ws);
131
+ safeSend(ws, makeMessage('register', { status: 'ok', clientType: 'dashboard' }));
132
+ }
133
+ else if (payload?.clientType === 'daemon') {
134
+ registerClient(DAEMON_KEY, ws);
135
+ safeSend(ws, makeMessage('register', { status: 'ok', clientType: 'daemon' }));
136
+ }
137
+ else if (payload?.agentId) {
138
+ registerClient(payload.agentId, ws);
139
+ safeSend(ws, makeMessage('register', { status: 'ok', agentId: payload.agentId }));
140
+ }
141
+ else {
142
+ safeSend(ws, makeMessage('error', { message: 'Register requires agentId or clientType' }));
143
+ }
144
+ break;
145
+ }
146
+ case 'heartbeat': {
147
+ handlePong(ws);
148
+ break;
149
+ }
150
+ default: {
151
+ safeSend(ws, makeMessage('error', { message: `Unknown message type: ${parsed.type}` }));
152
+ }
153
+ }
154
+ }
155
+ // ─── Public Integration ──────────────────────────────────────────────────────
156
+ export function broadcastAgentOnline(agent) {
157
+ broadcast('agent_online', { agentId: agent.id, name: agent.name });
158
+ }
159
+ export function broadcastAgentOffline(agent) {
160
+ broadcast('agent_offline', { agentId: agent.id, name: agent.name });
161
+ }
162
+ export function pushMessageToRecipient(recipientId, message) {
163
+ sendToClient(recipientId, 'message', message);
164
+ sendToDashboards('message', message);
165
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@swarmroom/server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "SwarmRoom hub server - HTTP, WebSocket, MCP, and mDNS services",
6
+ "keywords": ["swarmroom", "server", "hub", "mcp", "websocket"],
7
+ "license": "MIT",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/visual-z/swarm.git",
14
+ "directory": "packages/server"
15
+ },
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": "./dist/index.js",
20
+ "./dist/*": "./dist/*"
21
+ },
22
+ "files": ["dist", "README.md", "LICENSE"],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsx watch src/index.ts",
26
+ "start": "node dist/index.js",
27
+ "test": "vitest run",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@homebridge/ciao": "^1.3.5",
32
+ "@hono/node-server": "^1.19.9",
33
+ "@hono/node-ws": "^1.3.0",
34
+ "@modelcontextprotocol/sdk": "^1.26.0",
35
+ "@swarmroom/shared": "^0.1.0",
36
+ "better-sqlite3": "^12.6.2",
37
+ "drizzle-orm": "^0.45.1",
38
+ "hono": "^4.11.9",
39
+ "tsx": "^4.21.0",
40
+ "zod": "^4.3.6"
41
+ },
42
+ "devDependencies": {
43
+ "@types/better-sqlite3": "^7.6.13",
44
+ "drizzle-kit": "^0.31.9",
45
+ "vitest": "^4.0.18"
46
+ }
47
+ }