@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,94 @@
1
+ import { Hono } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { CreateProjectRequestSchema } from '@swarmroom/shared';
4
+ import { createProject, listProjects, getProjectById, updateProject, deleteProject, addAgentToProject, removeAgentFromProject, } from '../services/project-service.js';
5
+ const projectsRoute = new Hono();
6
+ projectsRoute.post('/', async (c) => {
7
+ const body = await c.req.json();
8
+ const parsed = CreateProjectRequestSchema.safeParse(body);
9
+ if (!parsed.success) {
10
+ throw new HTTPException(400, {
11
+ message: `Invalid request body: ${parsed.error.issues.map((i) => i.message).join(', ')}`,
12
+ });
13
+ }
14
+ try {
15
+ const project = createProject({ ...parsed.data, repository: body.repository });
16
+ return c.json({ success: true, data: project }, 201);
17
+ }
18
+ catch (err) {
19
+ if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
20
+ throw new HTTPException(409, {
21
+ message: `Project with name "${parsed.data.name}" already exists`,
22
+ });
23
+ }
24
+ throw err;
25
+ }
26
+ });
27
+ projectsRoute.get('/', (c) => {
28
+ const result = listProjects();
29
+ return c.json({ success: true, data: result });
30
+ });
31
+ projectsRoute.get('/:id', (c) => {
32
+ const id = c.req.param('id');
33
+ const project = getProjectById(id);
34
+ if (!project) {
35
+ throw new HTTPException(404, { message: `Project "${id}" not found` });
36
+ }
37
+ return c.json({ success: true, data: project });
38
+ });
39
+ projectsRoute.patch('/:id', async (c) => {
40
+ const id = c.req.param('id');
41
+ const body = await c.req.json();
42
+ const updates = {};
43
+ if (body.name !== undefined)
44
+ updates.name = body.name;
45
+ if (body.description !== undefined)
46
+ updates.description = body.description;
47
+ if (body.repository !== undefined)
48
+ updates.repository = body.repository;
49
+ const project = updateProject(id, updates);
50
+ if (!project) {
51
+ throw new HTTPException(404, { message: `Project "${id}" not found` });
52
+ }
53
+ return c.json({ success: true, data: project });
54
+ });
55
+ projectsRoute.delete('/:id', (c) => {
56
+ const id = c.req.param('id');
57
+ const project = deleteProject(id);
58
+ if (!project) {
59
+ throw new HTTPException(404, { message: `Project "${id}" not found` });
60
+ }
61
+ return c.json({ success: true, data: project });
62
+ });
63
+ projectsRoute.post('/:id/agents', async (c) => {
64
+ const projectId = c.req.param('id');
65
+ const body = await c.req.json();
66
+ if (!body.agentId || typeof body.agentId !== 'string') {
67
+ throw new HTTPException(400, { message: 'agentId is required' });
68
+ }
69
+ const result = addAgentToProject(projectId, body.agentId);
70
+ if ('error' in result) {
71
+ if (result.error === 'project_not_found') {
72
+ throw new HTTPException(404, { message: `Project "${projectId}" not found` });
73
+ }
74
+ if (result.error === 'agent_not_found') {
75
+ throw new HTTPException(404, { message: `Agent "${body.agentId}" not found` });
76
+ }
77
+ }
78
+ return c.json({ success: true, data: result.data }, 201);
79
+ });
80
+ projectsRoute.delete('/:id/agents/:agentId', (c) => {
81
+ const projectId = c.req.param('id');
82
+ const agentId = c.req.param('agentId');
83
+ const result = removeAgentFromProject(projectId, agentId);
84
+ if ('error' in result) {
85
+ if (result.error === 'project_not_found') {
86
+ throw new HTTPException(404, { message: `Project "${projectId}" not found` });
87
+ }
88
+ if (result.error === 'not_a_member') {
89
+ throw new HTTPException(404, { message: `Agent "${agentId}" is not a member of project "${projectId}"` });
90
+ }
91
+ }
92
+ return c.json({ success: true, data: result.data });
93
+ });
94
+ export { projectsRoute };
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ declare const teamsRoute: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export { teamsRoute };
@@ -0,0 +1,94 @@
1
+ import { Hono } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { CreateTeamRequestSchema } from '@swarmroom/shared';
4
+ import { createTeam, listTeams, getTeamById, updateTeam, deleteTeam, addAgentToTeam, removeAgentFromTeam, } from '../services/team-service.js';
5
+ const teamsRoute = new Hono();
6
+ teamsRoute.post('/', async (c) => {
7
+ const body = await c.req.json();
8
+ const parsed = CreateTeamRequestSchema.safeParse(body);
9
+ if (!parsed.success) {
10
+ throw new HTTPException(400, {
11
+ message: `Invalid request body: ${parsed.error.issues.map((i) => i.message).join(', ')}`,
12
+ });
13
+ }
14
+ try {
15
+ const team = createTeam({ ...parsed.data, color: body.color });
16
+ return c.json({ success: true, data: team }, 201);
17
+ }
18
+ catch (err) {
19
+ if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
20
+ throw new HTTPException(409, {
21
+ message: `Team with name "${parsed.data.name}" already exists`,
22
+ });
23
+ }
24
+ throw err;
25
+ }
26
+ });
27
+ teamsRoute.get('/', (c) => {
28
+ const result = listTeams();
29
+ return c.json({ success: true, data: result });
30
+ });
31
+ teamsRoute.get('/:id', (c) => {
32
+ const id = c.req.param('id');
33
+ const team = getTeamById(id);
34
+ if (!team) {
35
+ throw new HTTPException(404, { message: `Team "${id}" not found` });
36
+ }
37
+ return c.json({ success: true, data: team });
38
+ });
39
+ teamsRoute.patch('/:id', async (c) => {
40
+ const id = c.req.param('id');
41
+ const body = await c.req.json();
42
+ const updates = {};
43
+ if (body.name !== undefined)
44
+ updates.name = body.name;
45
+ if (body.description !== undefined)
46
+ updates.description = body.description;
47
+ if (body.color !== undefined)
48
+ updates.color = body.color;
49
+ const team = updateTeam(id, updates);
50
+ if (!team) {
51
+ throw new HTTPException(404, { message: `Team "${id}" not found` });
52
+ }
53
+ return c.json({ success: true, data: team });
54
+ });
55
+ teamsRoute.delete('/:id', (c) => {
56
+ const id = c.req.param('id');
57
+ const team = deleteTeam(id);
58
+ if (!team) {
59
+ throw new HTTPException(404, { message: `Team "${id}" not found` });
60
+ }
61
+ return c.json({ success: true, data: team });
62
+ });
63
+ teamsRoute.post('/:id/agents', async (c) => {
64
+ const teamId = c.req.param('id');
65
+ const body = await c.req.json();
66
+ if (!body.agentId || typeof body.agentId !== 'string') {
67
+ throw new HTTPException(400, { message: 'agentId is required' });
68
+ }
69
+ const result = addAgentToTeam(teamId, body.agentId);
70
+ if ('error' in result) {
71
+ if (result.error === 'team_not_found') {
72
+ throw new HTTPException(404, { message: `Team "${teamId}" not found` });
73
+ }
74
+ if (result.error === 'agent_not_found') {
75
+ throw new HTTPException(404, { message: `Agent "${body.agentId}" not found` });
76
+ }
77
+ }
78
+ return c.json({ success: true, data: result.data }, 201);
79
+ });
80
+ teamsRoute.delete('/:id/agents/:agentId', (c) => {
81
+ const teamId = c.req.param('id');
82
+ const agentId = c.req.param('agentId');
83
+ const result = removeAgentFromTeam(teamId, agentId);
84
+ if ('error' in result) {
85
+ if (result.error === 'team_not_found') {
86
+ throw new HTTPException(404, { message: `Team "${teamId}" not found` });
87
+ }
88
+ if (result.error === 'not_a_member') {
89
+ throw new HTTPException(404, { message: `Agent "${agentId}" is not a member of team "${teamId}"` });
90
+ }
91
+ }
92
+ return c.json({ success: true, data: result.data });
93
+ });
94
+ export { teamsRoute };
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ declare const wellKnown: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export { wellKnown };
@@ -0,0 +1,78 @@
1
+ import { Hono } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { inArray } from 'drizzle-orm';
4
+ import { getAgentById } from '../services/agent-service.js';
5
+ import { db, teams, projectGroups } from '../db/index.js';
6
+ const wellKnown = new Hono();
7
+ // ─── Hub Agent Card ──────────────────────────────────────────────────────────
8
+ wellKnown.get('/.well-known/agent-card.json', (c) => {
9
+ const url = new URL(c.req.url);
10
+ const selfUrl = `${url.protocol}//${url.host}`;
11
+ const card = {
12
+ name: 'SwarmRoom Hub',
13
+ description: 'Multi-agent coordination hub for local AI agents',
14
+ version: '0.1.0',
15
+ url: selfUrl,
16
+ skills: [
17
+ {
18
+ id: 'agent-discovery',
19
+ name: 'Agent Discovery',
20
+ description: 'Discover and manage AI coding agents on the local network',
21
+ tags: ['discovery', 'mdns'],
22
+ },
23
+ {
24
+ id: 'message-routing',
25
+ name: 'Message Routing',
26
+ description: 'Route messages between agents in real-time',
27
+ tags: ['messaging', 'routing'],
28
+ },
29
+ {
30
+ id: 'team-management',
31
+ name: 'Team Management',
32
+ description: 'Organize agents into teams and project groups',
33
+ tags: ['teams', 'projects'],
34
+ },
35
+ ],
36
+ teams: [],
37
+ projectGroups: [],
38
+ };
39
+ return c.json(card);
40
+ });
41
+ // ─── Per-Agent Card ──────────────────────────────────────────────────────────
42
+ wellKnown.get('/api/agents/:id/card', (c) => {
43
+ const id = c.req.param('id');
44
+ const agent = getAgentById(id);
45
+ if (!agent) {
46
+ throw new HTTPException(404, { message: `Agent "${id}" not found` });
47
+ }
48
+ if (agent.agentCard) {
49
+ return c.json({ success: true, data: agent.agentCard });
50
+ }
51
+ const teamNames = agent.teamIds.length > 0
52
+ ? db
53
+ .select({ name: teams.name })
54
+ .from(teams)
55
+ .where(inArray(teams.id, agent.teamIds))
56
+ .all()
57
+ .map((t) => t.name)
58
+ : [];
59
+ const projectNames = agent.projectIds.length > 0
60
+ ? db
61
+ .select({ name: projectGroups.name })
62
+ .from(projectGroups)
63
+ .where(inArray(projectGroups.id, agent.projectIds))
64
+ .all()
65
+ .map((p) => p.name)
66
+ : [];
67
+ const card = {
68
+ name: agent.displayName ?? agent.name,
69
+ description: 'Agent registered with SwarmRoom',
70
+ version: '1.0.0',
71
+ url: agent.url,
72
+ skills: [],
73
+ teams: teamNames,
74
+ projectGroups: projectNames,
75
+ };
76
+ return c.json({ success: true, data: card });
77
+ });
78
+ export { wellKnown };
@@ -0,0 +1,5 @@
1
+ import type { Hono } from 'hono';
2
+ import type { createNodeWebSocket } from '@hono/node-ws';
3
+ type NodeWebSocket = ReturnType<typeof createNodeWebSocket>;
4
+ export declare function registerWsRoute(app: Hono, upgradeWebSocket: NodeWebSocket['upgradeWebSocket']): void;
5
+ export {};
@@ -0,0 +1,19 @@
1
+ import { handleIncomingMessage, unregisterClient, } from '../services/ws-manager.js';
2
+ export function registerWsRoute(app, upgradeWebSocket) {
3
+ app.get('/ws', upgradeWebSocket(() => {
4
+ return {
5
+ onMessage(event, ws) {
6
+ const data = typeof event.data === 'string'
7
+ ? event.data
8
+ : String(event.data);
9
+ handleIncomingMessage(ws, data);
10
+ },
11
+ onClose(_event, ws) {
12
+ unregisterClient(ws);
13
+ },
14
+ onError(_event, ws) {
15
+ unregisterClient(ws);
16
+ },
17
+ };
18
+ }));
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createAgent, listAgents, getAgentById, updateAgent, deregisterAgent, getAgentCount, } from '../../services/agent-service.js';
3
+ describe('AgentService', () => {
4
+ const validInput = { name: 'test-agent', url: 'http://localhost:3000' };
5
+ it('createAgent returns agent with UUID and displayName', () => {
6
+ const agent = createAgent(validInput);
7
+ expect(agent).not.toBeNull();
8
+ expect(agent.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
9
+ expect(agent.name).toBe('test-agent');
10
+ expect(agent.displayName).toBeTruthy();
11
+ expect(agent.status).toBe('online');
12
+ expect(agent.teamIds).toEqual([]);
13
+ expect(agent.projectIds).toEqual([]);
14
+ });
15
+ it('listAgents filters by status', () => {
16
+ createAgent({ name: 'agent-a', url: 'http://a' });
17
+ const agentB = createAgent({ name: 'agent-b', url: 'http://b' });
18
+ deregisterAgent(agentB.id);
19
+ const onlineAgents = listAgents({ status: 'online' });
20
+ const offlineAgents = listAgents({ status: 'offline' });
21
+ expect(onlineAgents).toHaveLength(1);
22
+ expect(onlineAgents[0].name).toBe('agent-a');
23
+ expect(offlineAgents).toHaveLength(1);
24
+ expect(offlineAgents[0].name).toBe('agent-b');
25
+ });
26
+ it('resolves display name collision by appending suffix', () => {
27
+ const generateDisplayName = vi.fn().mockReturnValue('SwiftFalcon');
28
+ vi.doMock('../../lib/names.js', () => ({ generateDisplayName }));
29
+ const agent1 = createAgent({ name: 'agent-1', url: 'http://1' });
30
+ const agent2 = createAgent({ name: 'agent-2', url: 'http://2' });
31
+ const names = [agent1.displayName, agent2.displayName];
32
+ const unique = new Set(names);
33
+ expect(unique.size).toBe(2);
34
+ });
35
+ it('deregisterAgent sets status to offline (soft delete)', () => {
36
+ const agent = createAgent(validInput);
37
+ const result = deregisterAgent(agent.id);
38
+ expect(result).not.toBeNull();
39
+ expect(result.status).toBe('offline');
40
+ const fetched = getAgentById(agent.id);
41
+ expect(fetched.status).toBe('offline');
42
+ });
43
+ it('getAgentById returns full agent detail or null', () => {
44
+ const agent = createAgent(validInput);
45
+ const fetched = getAgentById(agent.id);
46
+ expect(fetched).not.toBeNull();
47
+ expect(fetched.name).toBe('test-agent');
48
+ expect(fetched.teamIds).toEqual([]);
49
+ expect(fetched.projectIds).toEqual([]);
50
+ const missing = getAgentById('nonexistent-id');
51
+ expect(missing).toBeNull();
52
+ });
53
+ it('getAgentCount returns correct count', () => {
54
+ expect(getAgentCount()).toBe(0);
55
+ createAgent({ name: 'a1', url: 'http://a' });
56
+ createAgent({ name: 'a2', url: 'http://b' });
57
+ expect(getAgentCount()).toBe(2);
58
+ });
59
+ it('updateAgent modifies fields and returns updated agent', () => {
60
+ const agent = createAgent(validInput);
61
+ const updated = updateAgent(agent.id, { name: 'renamed-agent' });
62
+ expect(updated).not.toBeNull();
63
+ expect(updated.name).toBe('renamed-agent');
64
+ const notFound = updateAgent('nonexistent', { name: 'x' });
65
+ expect(notFound).toBeNull();
66
+ });
67
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { eq } from 'drizzle-orm';
3
+ import { testDb } from '../../__tests__/setup.js';
4
+ import { agents } from '../../db/schema.js';
5
+ function seedAgent(id, name, heartbeatAge) {
6
+ const now = Date.now();
7
+ testDb
8
+ .insert(agents)
9
+ .values({
10
+ id,
11
+ name,
12
+ displayName: name,
13
+ url: `http://${name}`,
14
+ status: 'online',
15
+ lastHeartbeat: now - heartbeatAge,
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ })
19
+ .run();
20
+ }
21
+ describe('HeartbeatService', () => {
22
+ it('heartbeat update changes lastHeartbeat timestamp', () => {
23
+ seedAgent('a1', 'agent-fresh', 0);
24
+ const before = testDb
25
+ .select({ lastHeartbeat: agents.lastHeartbeat })
26
+ .from(agents)
27
+ .where(eq(agents.id, 'a1'))
28
+ .get();
29
+ const newTs = Date.now() + 5000;
30
+ testDb
31
+ .update(agents)
32
+ .set({ lastHeartbeat: newTs, updatedAt: newTs })
33
+ .where(eq(agents.id, 'a1'))
34
+ .run();
35
+ const after = testDb
36
+ .select({ lastHeartbeat: agents.lastHeartbeat })
37
+ .from(agents)
38
+ .where(eq(agents.id, 'a1'))
39
+ .get();
40
+ expect(after.lastHeartbeat).toBe(newTs);
41
+ expect(after.lastHeartbeat).not.toBe(before.lastHeartbeat);
42
+ });
43
+ it('stale agents are marked offline when heartbeat exceeds timeout', async () => {
44
+ const STALE_TIMEOUT_MS = 90_000;
45
+ seedAgent('stale-1', 'stale-agent', STALE_TIMEOUT_MS + 10_000);
46
+ seedAgent('fresh-1', 'fresh-agent', 5_000);
47
+ const { lt, and, ne } = await import('drizzle-orm');
48
+ const cutoff = Date.now() - STALE_TIMEOUT_MS;
49
+ const staleAgents = testDb
50
+ .select({ id: agents.id, name: agents.name })
51
+ .from(agents)
52
+ .where(and(lt(agents.lastHeartbeat, cutoff), ne(agents.status, 'offline')))
53
+ .all();
54
+ expect(staleAgents).toHaveLength(1);
55
+ expect(staleAgents[0].name).toBe('stale-agent');
56
+ for (const agent of staleAgents) {
57
+ testDb
58
+ .update(agents)
59
+ .set({ status: 'offline', updatedAt: Date.now() })
60
+ .where(eq(agents.id, agent.id))
61
+ .run();
62
+ }
63
+ const staleRecord = testDb
64
+ .select({ status: agents.status })
65
+ .from(agents)
66
+ .where(eq(agents.id, 'stale-1'))
67
+ .get();
68
+ expect(staleRecord.status).toBe('offline');
69
+ const freshRecord = testDb
70
+ .select({ status: agents.status })
71
+ .from(agents)
72
+ .where(eq(agents.id, 'fresh-1'))
73
+ .get();
74
+ expect(freshRecord.status).toBe('online');
75
+ });
76
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createMessage, getMessagesForAgent, getMessageById, getConversation, MessageSizeError, } from '../../services/message-service.js';
3
+ import { testDb } from '../../__tests__/setup.js';
4
+ import { agents } from '../../db/schema.js';
5
+ function seedAgent(id, name) {
6
+ testDb
7
+ .insert(agents)
8
+ .values({
9
+ id,
10
+ name,
11
+ displayName: name,
12
+ url: `http://${name}`,
13
+ status: 'online',
14
+ lastHeartbeat: Date.now(),
15
+ createdAt: Date.now(),
16
+ updatedAt: Date.now(),
17
+ })
18
+ .run();
19
+ }
20
+ describe('MessageService', () => {
21
+ it('createMessage stores and returns a message', () => {
22
+ seedAgent('a1', 'alice');
23
+ seedAgent('a2', 'bob');
24
+ const msg = createMessage({
25
+ from: 'a1',
26
+ to: 'a2',
27
+ senderType: 'agent',
28
+ content: 'hello bob',
29
+ type: 'notification',
30
+ });
31
+ expect(msg).not.toBeInstanceOf(Array);
32
+ const single = msg;
33
+ expect(single.id).toBeDefined();
34
+ expect(single.from).toBe('a1');
35
+ expect(single.to).toBe('a2');
36
+ expect(single.content).toBe('hello bob');
37
+ expect(single.read).toBe(false);
38
+ });
39
+ it('getMessagesForAgent returns messages addressed to agent', () => {
40
+ seedAgent('a1', 'alice');
41
+ seedAgent('a2', 'bob');
42
+ createMessage({ from: 'a1', to: 'a2', senderType: 'agent', content: 'msg-1', type: 'notification' });
43
+ createMessage({ from: 'a1', to: 'a2', senderType: 'agent', content: 'msg-2', type: 'notification' });
44
+ const msgs = getMessagesForAgent('a2');
45
+ expect(msgs).toHaveLength(2);
46
+ expect(msgs.every((m) => m.to === 'a2')).toBe(true);
47
+ });
48
+ it('broadcast creates messages for all online agents except sender', () => {
49
+ seedAgent('a1', 'alice');
50
+ seedAgent('a2', 'bob');
51
+ seedAgent('a3', 'carol');
52
+ const result = createMessage({
53
+ from: 'a1',
54
+ to: 'broadcast',
55
+ senderType: 'agent',
56
+ content: 'hey everyone',
57
+ type: 'notification',
58
+ });
59
+ expect(Array.isArray(result)).toBe(true);
60
+ const msgs = result;
61
+ expect(msgs).toHaveLength(2);
62
+ });
63
+ it('rejects oversized message content', () => {
64
+ seedAgent('a1', 'alice');
65
+ seedAgent('a2', 'bob');
66
+ const bigContent = 'x'.repeat(1_048_577);
67
+ expect(() => createMessage({ from: 'a1', to: 'a2', senderType: 'agent', content: bigContent, type: 'notification' })).toThrow(MessageSizeError);
68
+ });
69
+ it('getConversation returns messages between two agents in order', () => {
70
+ seedAgent('a1', 'alice');
71
+ seedAgent('a2', 'bob');
72
+ createMessage({ from: 'a1', to: 'a2', senderType: 'agent', content: 'hi bob', type: 'notification' });
73
+ createMessage({ from: 'a2', to: 'a1', senderType: 'agent', content: 'hi alice', type: 'notification' });
74
+ createMessage({ from: 'a1', to: 'a2', senderType: 'agent', content: 'how are you?', type: 'notification' });
75
+ const convo = getConversation('a1', 'a2');
76
+ expect(convo).toHaveLength(3);
77
+ expect(convo[0].content).toBe('hi bob');
78
+ expect(convo[1].content).toBe('hi alice');
79
+ expect(convo[2].content).toBe('how are you?');
80
+ });
81
+ it('getMessageById returns null for nonexistent message', () => {
82
+ const result = getMessageById('nonexistent');
83
+ expect(result).toBeNull();
84
+ });
85
+ });
@@ -0,0 +1,74 @@
1
+ import type { RegisterAgentRequest } from '@swarmroom/shared';
2
+ export declare function createAgent(input: RegisterAgentRequest): {
3
+ agentCard: any;
4
+ teamIds: string[];
5
+ projectIds: string[];
6
+ id: string;
7
+ name: string;
8
+ displayName: string | null;
9
+ url: string;
10
+ status: string | null;
11
+ lastHeartbeat: number | null;
12
+ createdAt: number | null;
13
+ updatedAt: number | null;
14
+ } | null;
15
+ export declare function listAgents(filters?: {
16
+ status?: string;
17
+ teamId?: string;
18
+ projectId?: string;
19
+ }): {
20
+ id: string;
21
+ name: string;
22
+ displayName: string | null;
23
+ url: string;
24
+ status: string | null;
25
+ agentCard: string | null;
26
+ lastHeartbeat: number | null;
27
+ createdAt: number | null;
28
+ updatedAt: number | null;
29
+ }[];
30
+ export declare function getAgentById(id: string): {
31
+ agentCard: any;
32
+ teamIds: string[];
33
+ projectIds: string[];
34
+ id: string;
35
+ name: string;
36
+ displayName: string | null;
37
+ url: string;
38
+ status: string | null;
39
+ lastHeartbeat: number | null;
40
+ createdAt: number | null;
41
+ updatedAt: number | null;
42
+ } | null;
43
+ export declare function updateAgent(id: string, updates: Partial<{
44
+ name: string;
45
+ url: string;
46
+ status: string;
47
+ agentCard: string;
48
+ }>): {
49
+ agentCard: any;
50
+ teamIds: string[];
51
+ projectIds: string[];
52
+ id: string;
53
+ name: string;
54
+ displayName: string | null;
55
+ url: string;
56
+ status: string | null;
57
+ lastHeartbeat: number | null;
58
+ createdAt: number | null;
59
+ updatedAt: number | null;
60
+ } | null;
61
+ export declare function deregisterAgent(id: string): {
62
+ agentCard: any;
63
+ teamIds: string[];
64
+ projectIds: string[];
65
+ id: string;
66
+ name: string;
67
+ displayName: string | null;
68
+ url: string;
69
+ status: string | null;
70
+ lastHeartbeat: number | null;
71
+ createdAt: number | null;
72
+ updatedAt: number | null;
73
+ } | null;
74
+ export declare function getAgentCount(): number;