kitty-hive 0.1.1 → 0.2.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 (60) hide show
  1. package/README.md +106 -129
  2. package/README.zh.md +136 -105
  3. package/channel.ts +194 -431
  4. package/dist/auth.d.ts +10 -6
  5. package/dist/auth.js +32 -17
  6. package/dist/auth.js.map +1 -1
  7. package/dist/db.d.ts +32 -16
  8. package/dist/db.js +210 -113
  9. package/dist/db.js.map +1 -1
  10. package/dist/federation-http.d.ts +2 -0
  11. package/dist/federation-http.js +233 -0
  12. package/dist/federation-http.js.map +1 -0
  13. package/dist/index.js +79 -76
  14. package/dist/index.js.map +1 -1
  15. package/dist/log.d.ts +3 -0
  16. package/dist/log.js +10 -0
  17. package/dist/log.js.map +1 -0
  18. package/dist/mcp/agent-tools.d.ts +2 -0
  19. package/dist/mcp/agent-tools.js +66 -0
  20. package/dist/mcp/agent-tools.js.map +1 -0
  21. package/dist/mcp/dm-tools.d.ts +2 -0
  22. package/dist/mcp/dm-tools.js +54 -0
  23. package/dist/mcp/dm-tools.js.map +1 -0
  24. package/dist/mcp/federation-tools.d.ts +2 -0
  25. package/dist/mcp/federation-tools.js +30 -0
  26. package/dist/mcp/federation-tools.js.map +1 -0
  27. package/dist/mcp/server.d.ts +2 -0
  28. package/dist/mcp/server.js +62 -0
  29. package/dist/mcp/server.js.map +1 -0
  30. package/dist/mcp/task-tools.d.ts +2 -0
  31. package/dist/mcp/task-tools.js +128 -0
  32. package/dist/mcp/task-tools.js.map +1 -0
  33. package/dist/mcp/team-tools.d.ts +2 -0
  34. package/dist/mcp/team-tools.js +95 -0
  35. package/dist/mcp/team-tools.js.map +1 -0
  36. package/dist/models.d.ts +22 -10
  37. package/dist/models.js +1 -3
  38. package/dist/models.js.map +1 -1
  39. package/dist/server.d.ts +2 -3
  40. package/dist/server.js +38 -761
  41. package/dist/server.js.map +1 -1
  42. package/dist/sessions.d.ts +15 -0
  43. package/dist/sessions.js +107 -0
  44. package/dist/sessions.js.map +1 -0
  45. package/dist/tools/dm.d.ts +3 -2
  46. package/dist/tools/dm.js +14 -29
  47. package/dist/tools/dm.js.map +1 -1
  48. package/dist/tools/start.d.ts +3 -3
  49. package/dist/tools/start.js +18 -15
  50. package/dist/tools/start.js.map +1 -1
  51. package/dist/tools/task.d.ts +1 -1
  52. package/dist/tools/task.js +6 -5
  53. package/dist/tools/task.js.map +1 -1
  54. package/dist/tools/team.d.ts +62 -13
  55. package/dist/tools/team.js +102 -30
  56. package/dist/tools/team.js.map +1 -1
  57. package/package.json +2 -2
  58. package/dist/tools/room.d.ts +0 -39
  59. package/dist/tools/room.js +0 -35
  60. package/dist/tools/room.js.map +0 -1
package/dist/auth.d.ts CHANGED
@@ -1,7 +1,11 @@
1
+ import { z } from 'zod';
1
2
  import type { Agent } from './models.js';
2
- /**
3
- * Extract agent from Bearer token.
4
- * Returns the agent if valid, null otherwise.
5
- * Also updates last_seen timestamp.
6
- */
7
- export declare function authenticateToken(authHeader: string | undefined): Agent | null;
3
+ export declare const asParam: z.ZodOptional<z.ZodString>;
4
+ export declare function resolveAgent(extra: any, asValue?: string): Agent | null;
5
+ export declare function authError(): {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError: boolean;
11
+ };
package/dist/auth.js CHANGED
@@ -1,19 +1,34 @@
1
- import { getAgentByToken, touchAgent } from './db.js';
2
- /**
3
- * Extract agent from Bearer token.
4
- * Returns the agent if valid, null otherwise.
5
- * Also updates last_seen timestamp.
6
- */
7
- export function authenticateToken(authHeader) {
8
- if (!authHeader)
9
- return null;
10
- const match = authHeader.match(/^Bearer\s+(\S+)$/i);
11
- if (!match)
12
- return null;
13
- const agent = getAgentByToken(match[1]);
14
- if (!agent)
15
- return null;
16
- touchAgent(agent.id);
17
- return agent;
1
+ import { z } from 'zod';
2
+ import { getAgentById, touchAgent } from './db.js';
3
+ import { sessionAgents } from './sessions.js';
4
+ export const asParam = z.string().optional().describe('Your agent id (from hive.start). Optional — session binding takes precedence.');
5
+ export function resolveAgent(extra, asValue) {
6
+ // 1) Session binding (preferred)
7
+ const sessionId = extra?.sessionId;
8
+ if (sessionId) {
9
+ const agentId = sessionAgents.get(sessionId);
10
+ if (agentId) {
11
+ const agent = getAgentById(agentId);
12
+ if (agent) {
13
+ touchAgent(agent.id);
14
+ return agent;
15
+ }
16
+ }
17
+ }
18
+ // 2) `as` param fallback (id only — names not unique)
19
+ if (asValue) {
20
+ const agent = getAgentById(asValue);
21
+ if (agent) {
22
+ touchAgent(agent.id);
23
+ return agent;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ export function authError() {
29
+ return {
30
+ content: [{ type: 'text', text: 'Error: Not authenticated. Call hive.start first.' }],
31
+ isError: true,
32
+ };
18
33
  }
19
34
  //# sourceMappingURL=auth.js.map
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAGtD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B;IAC9D,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+EAA+E,CAAC,CAAC;AAEvI,MAAM,UAAU,YAAY,CAAC,KAAU,EAAE,OAAgB;IACvD,iCAAiC;IACjC,MAAM,SAAS,GAAG,KAAK,EAAE,SAAS,CAAC;IACnC,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,KAAK,EAAE,CAAC;gBAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBAAC,OAAO,KAAK,CAAC;YAAC,CAAC;QACpD,CAAC;IACH,CAAC;IACD,sDAAsD;IACtD,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,KAAK,EAAE,CAAC;YAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAAC,OAAO,KAAK,CAAC;QAAC,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,kDAAkD,EAAE,CAAC;QAC9F,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC"}
package/dist/db.d.ts CHANGED
@@ -1,26 +1,42 @@
1
1
  import Database from 'better-sqlite3';
2
- import type { Agent, Room, RoomEvent, RoomEventType, Task, TaskEvent, TaskEventType } from './models.js';
2
+ import type { Agent, Team, TeamMember, TeamEvent, TeamEventType, DMMessage, Task, TaskEvent, TaskEventType } from './models.js';
3
3
  export declare function initDB(dbPath?: string): Database.Database;
4
4
  export declare function getDB(): Database.Database;
5
5
  export declare function createAgent(displayName: string, tool: string, roles: string, expertise: string): Agent;
6
6
  export declare function getAgentByToken(token: string): Agent | undefined;
7
7
  export declare function getAgentById(id: string): Agent | undefined;
8
- export declare function getAgentByName(name: string): Agent | undefined;
8
+ export declare function getAgentsByName(name: string): Agent[];
9
+ export declare function listAllAgents(activeOnly?: boolean): Agent[];
9
10
  export declare function findAgentByRole(role: string): Agent | undefined;
10
11
  export declare function touchAgent(id: string): void;
11
- export declare function createRoom(kind: string, hostAgentId: string | null, name?: string): Room;
12
- export declare function getRoomById(id: string): Room | undefined;
13
- export declare function getLobby(): Room | undefined;
14
- export declare function findDMRoom(agentA: string, agentB: string): Room | undefined;
15
- export declare function listTeams(): Room[];
16
- export declare function appendRoomEvent(roomId: string, type: RoomEventType, actorAgentId: string | null, payload?: object): RoomEvent;
17
- export declare function getRoomEvents(roomId: string, since?: number, limit?: number): RoomEvent[];
18
- export declare function getLastRoomEventTs(roomId: string): string | null;
19
- export declare function getLatestRoomEvents(roomId: string, limit?: number): RoomEvent[];
20
- export declare function getRoomMembers(roomId: string): string[];
21
- export declare function isMember(roomId: string, agentId: string): boolean;
22
- export declare function getAgentRooms(agentId: string, kind?: string, activeOnly?: boolean): Room[];
23
- export declare function createTask(title: string, creatorId: string, assigneeId?: string, sourceRoomId?: string, input?: object): Task;
12
+ export declare function renameAgent(id: string, newName: string): void;
13
+ export interface ResolvedAgent {
14
+ agent?: Agent;
15
+ source: 'id' | 'team-nickname' | 'display-name';
16
+ }
17
+ export declare function resolveAddressee(callerAgentId: string, target: string): ResolvedAgent | {
18
+ error: string;
19
+ };
20
+ export declare function createTeam(name: string, hostAgentId: string | null): Team;
21
+ export declare function getTeamById(id: string): Team | undefined;
22
+ export declare function getTeamByName(name: string): Team | undefined;
23
+ export declare function listTeams(activeOnly?: boolean): Team[];
24
+ export declare function getAgentTeams(agentId: string, activeOnly?: boolean): Team[];
25
+ export declare function addTeamMember(teamId: string, agentId: string, nickname?: string | null): TeamMember;
26
+ export declare function removeTeamMember(teamId: string, agentId: string): void;
27
+ export declare function isTeamMember(teamId: string, agentId: string): boolean;
28
+ export declare function getTeamMembers(teamId: string): TeamMember[];
29
+ export declare function getTeamMemberAgentIds(teamId: string): string[];
30
+ export declare function setTeamNickname(teamId: string, agentId: string, nickname: string | null): void;
31
+ export declare function getTeamMember(teamId: string, agentId: string): TeamMember | undefined;
32
+ export declare function getTeamDisplayName(teamId: string, agentId: string): string;
33
+ export declare function appendTeamEvent(teamId: string, type: TeamEventType, actorAgentId: string | null, payload?: object): TeamEvent;
34
+ export declare function getTeamEvents(teamId: string, since?: number, limit?: number): TeamEvent[];
35
+ export declare function getLatestTeamEvents(teamId: string, limit?: number): TeamEvent[];
36
+ export declare function appendDM(fromAgentId: string, toAgentId: string, content: string): DMMessage;
37
+ export declare function getMaxIncomingDMId(toAgentId: string, fromAgentId: string): number;
38
+ export declare function getDMConversation(agentA: string, agentB: string, since?: number, limit?: number): DMMessage[];
39
+ export declare function createTask(title: string, creatorId: string, assigneeId?: string, sourceTeamId?: string, input?: object): Task;
24
40
  export declare function getTaskById(id: string): Task | undefined;
25
41
  export declare function updateTaskStatus(id: string, status: string, extras?: Record<string, any>): void;
26
42
  export declare function getAgentTasks(agentId: string, status?: string): Task[];
@@ -29,7 +45,7 @@ export declare function getTaskEvents(taskId: string, since?: number, limit?: nu
29
45
  export declare function getReadCursor(agentId: string, targetType: string, targetId: string): number;
30
46
  export declare function setReadCursor(agentId: string, targetType: string, targetId: string, seq: number): void;
31
47
  export interface UnreadSummary {
32
- type: 'room' | 'task';
48
+ type: 'team' | 'task' | 'dm';
33
49
  id: string;
34
50
  name: string | null;
35
51
  kind: string;
package/dist/db.js CHANGED
@@ -28,26 +28,45 @@ export function initDB(dbPath) {
28
28
  CREATE INDEX IF NOT EXISTS idx_agents_token ON agents(token);
29
29
  CREATE INDEX IF NOT EXISTS idx_agents_roles ON agents(roles);
30
30
 
31
- CREATE TABLE IF NOT EXISTS rooms (
31
+ CREATE TABLE IF NOT EXISTS teams (
32
32
  id TEXT PRIMARY KEY,
33
- name TEXT,
34
- kind TEXT NOT NULL CHECK(kind IN ('lobby','dm','team')),
33
+ name TEXT NOT NULL UNIQUE,
35
34
  host_agent_id TEXT REFERENCES agents(id),
36
35
  created_at TEXT NOT NULL,
37
36
  closed_at TEXT
38
37
  );
39
- CREATE INDEX IF NOT EXISTS idx_rooms_kind ON rooms(kind);
40
38
 
41
- CREATE TABLE IF NOT EXISTS room_events (
39
+ CREATE TABLE IF NOT EXISTS team_members (
40
+ team_id TEXT NOT NULL REFERENCES teams(id),
41
+ agent_id TEXT NOT NULL REFERENCES agents(id),
42
+ nickname TEXT,
43
+ joined_at TEXT NOT NULL,
44
+ PRIMARY KEY (team_id, agent_id),
45
+ UNIQUE (team_id, nickname)
46
+ );
47
+ CREATE INDEX IF NOT EXISTS idx_team_members_agent ON team_members(agent_id);
48
+
49
+ CREATE TABLE IF NOT EXISTS team_events (
42
50
  id INTEGER PRIMARY KEY AUTOINCREMENT,
43
- room_id TEXT NOT NULL REFERENCES rooms(id),
51
+ team_id TEXT NOT NULL REFERENCES teams(id),
44
52
  seq INTEGER NOT NULL,
45
- type TEXT NOT NULL CHECK(type IN ('join','leave','message')),
53
+ type TEXT NOT NULL CHECK(type IN ('join','leave','message','rename')),
46
54
  actor_agent_id TEXT REFERENCES agents(id),
47
55
  payload_json TEXT DEFAULT '{}',
48
56
  ts TEXT NOT NULL
49
57
  );
50
- CREATE INDEX IF NOT EXISTS idx_room_events_room_seq ON room_events(room_id, seq);
58
+ CREATE INDEX IF NOT EXISTS idx_team_events_team_seq ON team_events(team_id, seq);
59
+
60
+ CREATE TABLE IF NOT EXISTS dm_messages (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ seq INTEGER NOT NULL,
63
+ from_agent_id TEXT NOT NULL REFERENCES agents(id),
64
+ to_agent_id TEXT NOT NULL REFERENCES agents(id),
65
+ content TEXT NOT NULL,
66
+ ts TEXT NOT NULL
67
+ );
68
+ CREATE INDEX IF NOT EXISTS idx_dm_to_seq ON dm_messages(to_agent_id, seq);
69
+ CREATE INDEX IF NOT EXISTS idx_dm_from_seq ON dm_messages(from_agent_id, seq);
51
70
 
52
71
  CREATE TABLE IF NOT EXISTS tasks (
53
72
  id TEXT PRIMARY KEY,
@@ -58,7 +77,7 @@ export function initDB(dbPath) {
58
77
  CHECK(status IN ('created','proposing','approved','in_progress','completed','failed','canceled')),
59
78
  workflow_json TEXT,
60
79
  current_step INTEGER DEFAULT 0,
61
- source_room_id TEXT REFERENCES rooms(id),
80
+ source_team_id TEXT REFERENCES teams(id),
62
81
  input_json TEXT DEFAULT '{}',
63
82
  created_at TEXT NOT NULL,
64
83
  completed_at TEXT
@@ -80,7 +99,7 @@ export function initDB(dbPath) {
80
99
 
81
100
  CREATE TABLE IF NOT EXISTS read_cursors (
82
101
  agent_id TEXT NOT NULL REFERENCES agents(id),
83
- target_type TEXT NOT NULL CHECK(target_type IN ('room','task')),
102
+ target_type TEXT NOT NULL CHECK(target_type IN ('team','task','dm')),
84
103
  target_id TEXT NOT NULL,
85
104
  last_seq INTEGER NOT NULL DEFAULT 0,
86
105
  PRIMARY KEY (agent_id, target_type, target_id)
@@ -123,8 +142,14 @@ export function getAgentByToken(token) {
123
142
  export function getAgentById(id) {
124
143
  return getDB().prepare('SELECT * FROM agents WHERE id = ?').get(id);
125
144
  }
126
- export function getAgentByName(name) {
127
- return getDB().prepare('SELECT * FROM agents WHERE display_name = ?').get(name);
145
+ export function getAgentsByName(name) {
146
+ return getDB().prepare('SELECT * FROM agents WHERE display_name = ?').all(name);
147
+ }
148
+ export function listAllAgents(activeOnly = false) {
149
+ const sql = activeOnly
150
+ ? "SELECT * FROM agents WHERE status = 'active' ORDER BY last_seen DESC"
151
+ : 'SELECT * FROM agents ORDER BY last_seen DESC';
152
+ return getDB().prepare(sql).all();
128
153
  }
129
154
  export function findAgentByRole(role) {
130
155
  return getDB().prepare("SELECT * FROM agents WHERE roles LIKE ? AND status = 'active' ORDER BY last_seen DESC LIMIT 1").get(`%${role}%`);
@@ -132,106 +157,159 @@ export function findAgentByRole(role) {
132
157
  export function touchAgent(id) {
133
158
  getDB().prepare('UPDATE agents SET last_seen = ? WHERE id = ?').run(nowISO(), id);
134
159
  }
135
- // --- Room queries ---
136
- export function createRoom(kind, hostAgentId, name) {
137
- const room = {
138
- id: ulid(), name: name ?? null, kind: kind,
139
- host_agent_id: hostAgentId, created_at: nowISO(), closed_at: null,
160
+ export function renameAgent(id, newName) {
161
+ getDB().prepare('UPDATE agents SET display_name = ? WHERE id = ?').run(newName, id);
162
+ }
163
+ export function resolveAddressee(callerAgentId, target) {
164
+ // 1) direct id
165
+ const byId = getAgentById(target);
166
+ if (byId)
167
+ return { agent: byId, source: 'id' };
168
+ // 2) nickname in caller's teams
169
+ const nickMatches = getDB().prepare(`
170
+ SELECT DISTINCT a.* FROM agents a
171
+ JOIN team_members tm ON tm.agent_id = a.id
172
+ WHERE tm.nickname = ?
173
+ AND tm.team_id IN (SELECT team_id FROM team_members WHERE agent_id = ?)
174
+ `).all(target, callerAgentId);
175
+ if (nickMatches.length === 1)
176
+ return { agent: nickMatches[0], source: 'team-nickname' };
177
+ if (nickMatches.length > 1)
178
+ return { error: `Nickname "${target}" matches multiple agents in your teams. Use agent id instead.` };
179
+ // 3) global display_name
180
+ const nameMatches = getAgentsByName(target);
181
+ if (nameMatches.length === 1)
182
+ return { agent: nameMatches[0], source: 'display-name' };
183
+ if (nameMatches.length > 1)
184
+ return { error: `Display name "${target}" matches multiple agents. Use agent id instead.` };
185
+ return { error: `No agent found matching "${target}".` };
186
+ }
187
+ // --- Team queries ---
188
+ export function createTeam(name, hostAgentId) {
189
+ const team = {
190
+ id: ulid(), name,
191
+ host_agent_id: hostAgentId,
192
+ created_at: nowISO(), closed_at: null,
140
193
  };
141
194
  getDB().prepare(`
142
- INSERT INTO rooms (id, name, kind, host_agent_id, created_at, closed_at)
143
- VALUES (@id, @name, @kind, @host_agent_id, @created_at, @closed_at)
144
- `).run(room);
145
- return room;
195
+ INSERT INTO teams (id, name, host_agent_id, created_at, closed_at)
196
+ VALUES (@id, @name, @host_agent_id, @created_at, @closed_at)
197
+ `).run(team);
198
+ return team;
199
+ }
200
+ export function getTeamById(id) {
201
+ return getDB().prepare('SELECT * FROM teams WHERE id = ?').get(id);
202
+ }
203
+ export function getTeamByName(name) {
204
+ return getDB().prepare('SELECT * FROM teams WHERE name = ?').get(name);
205
+ }
206
+ export function listTeams(activeOnly = true) {
207
+ const sql = activeOnly
208
+ ? 'SELECT * FROM teams WHERE closed_at IS NULL ORDER BY created_at DESC'
209
+ : 'SELECT * FROM teams ORDER BY created_at DESC';
210
+ return getDB().prepare(sql).all();
211
+ }
212
+ export function getAgentTeams(agentId, activeOnly = true) {
213
+ const sql = activeOnly
214
+ ? `SELECT t.* FROM teams t JOIN team_members tm ON tm.team_id = t.id WHERE tm.agent_id = ? AND t.closed_at IS NULL ORDER BY tm.joined_at DESC`
215
+ : `SELECT t.* FROM teams t JOIN team_members tm ON tm.team_id = t.id WHERE tm.agent_id = ? ORDER BY tm.joined_at DESC`;
216
+ return getDB().prepare(sql).all(agentId);
217
+ }
218
+ // --- Team membership ---
219
+ export function addTeamMember(teamId, agentId, nickname = null) {
220
+ const member = { team_id: teamId, agent_id: agentId, nickname, joined_at: nowISO() };
221
+ getDB().prepare(`
222
+ INSERT INTO team_members (team_id, agent_id, nickname, joined_at)
223
+ VALUES (@team_id, @agent_id, @nickname, @joined_at)
224
+ ON CONFLICT(team_id, agent_id) DO NOTHING
225
+ `).run(member);
226
+ return member;
146
227
  }
147
- export function getRoomById(id) {
148
- return getDB().prepare('SELECT * FROM rooms WHERE id = ?').get(id);
228
+ export function removeTeamMember(teamId, agentId) {
229
+ getDB().prepare('DELETE FROM team_members WHERE team_id = ? AND agent_id = ?').run(teamId, agentId);
149
230
  }
150
- export function getLobby() {
151
- return getDB().prepare("SELECT * FROM rooms WHERE kind = 'lobby' LIMIT 1").get();
231
+ export function isTeamMember(teamId, agentId) {
232
+ const row = getDB().prepare('SELECT 1 FROM team_members WHERE team_id = ? AND agent_id = ?').get(teamId, agentId);
233
+ return !!row;
152
234
  }
153
- export function findDMRoom(agentA, agentB) {
154
- return getDB().prepare(`
155
- SELECT r.* FROM rooms r
156
- WHERE r.kind = 'dm'
157
- AND EXISTS (SELECT 1 FROM room_events e WHERE e.room_id = r.id AND e.type = 'join' AND e.actor_agent_id = ?)
158
- AND EXISTS (SELECT 1 FROM room_events e WHERE e.room_id = r.id AND e.type = 'join' AND e.actor_agent_id = ?)
159
- LIMIT 1
160
- `).get(agentA, agentB);
161
- }
162
- export function listTeams() {
163
- return getDB().prepare("SELECT * FROM rooms WHERE kind = 'team' AND closed_at IS NULL ORDER BY created_at DESC").all();
164
- }
165
- // --- Room event queries ---
166
- export function appendRoomEvent(roomId, type, actorAgentId, payload = {}) {
235
+ export function getTeamMembers(teamId) {
236
+ return getDB().prepare('SELECT * FROM team_members WHERE team_id = ?').all(teamId);
237
+ }
238
+ export function getTeamMemberAgentIds(teamId) {
239
+ return getDB().prepare('SELECT agent_id FROM team_members WHERE team_id = ?').all(teamId).map(r => r.agent_id);
240
+ }
241
+ export function setTeamNickname(teamId, agentId, nickname) {
242
+ getDB().prepare('UPDATE team_members SET nickname = ? WHERE team_id = ? AND agent_id = ?').run(nickname, teamId, agentId);
243
+ }
244
+ export function getTeamMember(teamId, agentId) {
245
+ return getDB().prepare('SELECT * FROM team_members WHERE team_id = ? AND agent_id = ?').get(teamId, agentId);
246
+ }
247
+ // Display name for an agent in a team context (nickname || display_name)
248
+ export function getTeamDisplayName(teamId, agentId) {
249
+ const member = getTeamMember(teamId, agentId);
250
+ if (member?.nickname)
251
+ return member.nickname;
252
+ const agent = getAgentById(agentId);
253
+ return agent?.display_name ?? 'unknown';
254
+ }
255
+ // --- Team event queries ---
256
+ export function appendTeamEvent(teamId, type, actorAgentId, payload = {}) {
167
257
  const d = getDB();
168
258
  const ts = nowISO();
169
259
  const payloadJson = JSON.stringify(payload);
170
260
  const insert = d.transaction(() => {
171
- const maxSeq = d.prepare('SELECT COALESCE(MAX(seq), 0) as max_seq FROM room_events WHERE room_id = ?').get(roomId);
261
+ const maxSeq = d.prepare('SELECT COALESCE(MAX(seq), 0) as max_seq FROM team_events WHERE team_id = ?').get(teamId);
172
262
  const seq = maxSeq.max_seq + 1;
173
- const result = d.prepare('INSERT INTO room_events (room_id, seq, type, actor_agent_id, payload_json, ts) VALUES (?, ?, ?, ?, ?, ?)').run(roomId, seq, type, actorAgentId, payloadJson, ts);
263
+ const result = d.prepare('INSERT INTO team_events (team_id, seq, type, actor_agent_id, payload_json, ts) VALUES (?, ?, ?, ?, ?, ?)').run(teamId, seq, type, actorAgentId, payloadJson, ts);
174
264
  return { id: result.lastInsertRowid, seq };
175
265
  });
176
266
  const { id, seq } = insert();
177
- return { id, room_id: roomId, seq, type, actor_agent_id: actorAgentId, payload_json: payloadJson, ts };
178
- }
179
- export function getRoomEvents(roomId, since = 0, limit = 50) {
180
- return getDB().prepare('SELECT * FROM room_events WHERE room_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?').all(roomId, since, limit);
181
- }
182
- export function getLastRoomEventTs(roomId) {
183
- const row = getDB().prepare('SELECT ts FROM room_events WHERE room_id = ? ORDER BY seq DESC LIMIT 1').get(roomId);
184
- return row?.ts ?? null;
185
- }
186
- export function getLatestRoomEvents(roomId, limit = 10) {
187
- return getDB().prepare('SELECT * FROM (SELECT * FROM room_events WHERE room_id = ? ORDER BY seq DESC LIMIT ?) ORDER BY seq ASC').all(roomId, limit);
188
- }
189
- export function getRoomMembers(roomId) {
190
- const events = getDB().prepare("SELECT type, actor_agent_id FROM room_events WHERE room_id = ? AND type IN ('join', 'leave') ORDER BY seq ASC").all(roomId);
191
- const members = new Set();
192
- for (const e of events) {
193
- if (e.type === 'join')
194
- members.add(e.actor_agent_id);
195
- if (e.type === 'leave')
196
- members.delete(e.actor_agent_id);
197
- }
198
- return [...members];
199
- }
200
- export function isMember(roomId, agentId) {
201
- return getRoomMembers(roomId).includes(agentId);
202
- }
203
- export function getAgentRooms(agentId, kind, activeOnly) {
204
- let sql = `
205
- SELECT DISTINCT r.* FROM rooms r
206
- JOIN room_events e ON e.room_id = r.id AND e.type = 'join' AND e.actor_agent_id = ?
207
- WHERE NOT EXISTS (
208
- SELECT 1 FROM room_events e2
209
- WHERE e2.room_id = r.id AND e2.type = 'leave' AND e2.actor_agent_id = ? AND e2.seq > e.seq
210
- )
211
- `;
212
- const params = [agentId, agentId];
213
- if (kind) {
214
- sql += ' AND r.kind = ?';
215
- params.push(kind);
216
- }
217
- if (activeOnly) {
218
- sql += ' AND r.closed_at IS NULL';
219
- }
220
- return getDB().prepare(sql).all(...params);
267
+ return { id, team_id: teamId, seq, type, actor_agent_id: actorAgentId, payload_json: payloadJson, ts };
268
+ }
269
+ export function getTeamEvents(teamId, since = 0, limit = 50) {
270
+ return getDB().prepare('SELECT * FROM team_events WHERE team_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?').all(teamId, since, limit);
271
+ }
272
+ export function getLatestTeamEvents(teamId, limit = 10) {
273
+ return getDB().prepare('SELECT * FROM (SELECT * FROM team_events WHERE team_id = ? ORDER BY seq DESC LIMIT ?) ORDER BY seq ASC').all(teamId, limit);
274
+ }
275
+ // --- DM queries ---
276
+ export function appendDM(fromAgentId, toAgentId, content) {
277
+ const d = getDB();
278
+ const ts = nowISO();
279
+ const insert = d.transaction(() => {
280
+ const maxSeq = d.prepare('SELECT COALESCE(MAX(seq), 0) as max_seq FROM dm_messages WHERE to_agent_id = ?').get(toAgentId);
281
+ const seq = maxSeq.max_seq + 1;
282
+ const result = d.prepare('INSERT INTO dm_messages (seq, from_agent_id, to_agent_id, content, ts) VALUES (?, ?, ?, ?, ?)').run(seq, fromAgentId, toAgentId, content, ts);
283
+ return { id: result.lastInsertRowid, seq };
284
+ });
285
+ const { id, seq } = insert();
286
+ return { id, seq, from_agent_id: fromAgentId, to_agent_id: toAgentId, content, ts };
287
+ }
288
+ export function getMaxIncomingDMId(toAgentId, fromAgentId) {
289
+ const row = getDB().prepare('SELECT MAX(id) AS max_id FROM dm_messages WHERE to_agent_id = ? AND from_agent_id = ?').get(toAgentId, fromAgentId);
290
+ return row?.max_id ?? 0;
291
+ }
292
+ export function getDMConversation(agentA, agentB, since = 0, limit = 50) {
293
+ return getDB().prepare(`
294
+ SELECT * FROM dm_messages
295
+ WHERE ((from_agent_id = ? AND to_agent_id = ?) OR (from_agent_id = ? AND to_agent_id = ?))
296
+ AND id > ?
297
+ ORDER BY id ASC LIMIT ?
298
+ `).all(agentA, agentB, agentB, agentA, since, limit);
221
299
  }
222
300
  // --- Task queries ---
223
- export function createTask(title, creatorId, assigneeId, sourceRoomId, input) {
301
+ export function createTask(title, creatorId, assigneeId, sourceTeamId, input) {
224
302
  const task = {
225
303
  id: ulid(), title, creator_agent_id: creatorId,
226
304
  assignee_agent_id: assigneeId ?? null,
227
305
  status: 'created', workflow_json: null, current_step: 0,
228
- source_room_id: sourceRoomId ?? null,
306
+ source_team_id: sourceTeamId ?? null,
229
307
  input_json: JSON.stringify(input ?? {}),
230
308
  created_at: nowISO(), completed_at: null,
231
309
  };
232
310
  getDB().prepare(`
233
- INSERT INTO tasks (id, title, creator_agent_id, assignee_agent_id, status, workflow_json, current_step, source_room_id, input_json, created_at, completed_at)
234
- VALUES (@id, @title, @creator_agent_id, @assignee_agent_id, @status, @workflow_json, @current_step, @source_room_id, @input_json, @created_at, @completed_at)
311
+ INSERT INTO tasks (id, title, creator_agent_id, assignee_agent_id, status, workflow_json, current_step, source_team_id, input_json, created_at, completed_at)
312
+ VALUES (@id, @title, @creator_agent_id, @assignee_agent_id, @status, @workflow_json, @current_step, @source_team_id, @input_json, @created_at, @completed_at)
235
313
  `).run(task);
236
314
  return task;
237
315
  }
@@ -291,29 +369,38 @@ export function setReadCursor(agentId, targetType, targetId, seq) {
291
369
  }
292
370
  export function getUnreadForAgent(agentId) {
293
371
  const result = [];
294
- // Room unread
295
- const rooms = getAgentRooms(agentId, undefined, true);
296
- for (const room of rooms) {
297
- const cursor = getReadCursor(agentId, 'room', room.id);
298
- const unread = getDB().prepare('SELECT * FROM room_events WHERE room_id = ? AND seq > ? AND actor_agent_id != ? ORDER BY seq ASC LIMIT 10').all(room.id, cursor, agentId);
372
+ const d = getDB();
373
+ // Team unread
374
+ const teams = getAgentTeams(agentId, true);
375
+ for (const team of teams) {
376
+ const cursor = getReadCursor(agentId, 'team', team.id);
377
+ const unread = d.prepare('SELECT * FROM team_events WHERE team_id = ? AND seq > ? AND actor_agent_id != ? ORDER BY seq ASC LIMIT 10').all(team.id, cursor, agentId);
299
378
  if (unread.length === 0)
300
379
  continue;
301
- const totalUnread = getDB().prepare('SELECT COUNT(*) as cnt FROM room_events WHERE room_id = ? AND seq > ? AND actor_agent_id != ?').get(room.id, cursor, agentId).cnt;
302
- const latest = unread.slice(-5).map(e => {
303
- const actor = getAgentById(e.actor_agent_id ?? '');
304
- let preview = '';
305
- try {
306
- const p = JSON.parse(e.payload_json);
307
- preview = p.content || '';
308
- }
309
- catch { }
310
- return {
311
- from: actor?.display_name ?? 'unknown', type: e.type,
312
- preview: preview.length > 200 ? preview.slice(0, 200) + ' [summary this is a preview, not a broken message]' : preview,
313
- ts: e.ts,
314
- };
315
- });
316
- result.push({ type: 'room', id: room.id, name: room.name, kind: room.kind, unread_count: totalUnread, latest });
380
+ const totalUnread = d.prepare('SELECT COUNT(*) as cnt FROM team_events WHERE team_id = ? AND seq > ? AND actor_agent_id != ?').get(team.id, cursor, agentId).cnt;
381
+ const latest = unread.slice(-5).map(e => ({
382
+ from: e.actor_agent_id ? getTeamDisplayName(team.id, e.actor_agent_id) : 'system',
383
+ type: e.type,
384
+ preview: previewFromPayload(e.payload_json),
385
+ ts: e.ts,
386
+ }));
387
+ result.push({ type: 'team', id: team.id, name: team.name, kind: 'team', unread_count: totalUnread, latest });
388
+ }
389
+ // DM unread (per-sender cursor: target_id = sender_agent_id)
390
+ const senders = d.prepare('SELECT DISTINCT from_agent_id FROM dm_messages WHERE to_agent_id = ?').all(agentId);
391
+ for (const { from_agent_id: senderId } of senders) {
392
+ const cursor = getReadCursor(agentId, 'dm', senderId);
393
+ const msgs = d.prepare('SELECT * FROM dm_messages WHERE to_agent_id = ? AND from_agent_id = ? AND id > ? ORDER BY id ASC').all(agentId, senderId, cursor);
394
+ if (msgs.length === 0)
395
+ continue;
396
+ const sender = getAgentById(senderId);
397
+ const latest = msgs.slice(-5).map(m => ({
398
+ from: sender?.display_name ?? 'unknown',
399
+ type: 'dm',
400
+ preview: m.content.length > 200 ? m.content.slice(0, 200) + ' [summary]' : m.content,
401
+ ts: m.ts,
402
+ }));
403
+ result.push({ type: 'dm', id: senderId, name: sender?.display_name ?? null, kind: 'dm', unread_count: msgs.length, latest });
317
404
  }
318
405
  // Task unread
319
406
  const tasks = getAgentTasks(agentId);
@@ -321,12 +408,12 @@ export function getUnreadForAgent(agentId) {
321
408
  if (['completed', 'failed', 'canceled'].includes(task.status))
322
409
  continue;
323
410
  const cursor = getReadCursor(agentId, 'task', task.id);
324
- const unread = getDB().prepare('SELECT * FROM task_events WHERE task_id = ? AND seq > ? AND actor_agent_id != ? ORDER BY seq ASC LIMIT 10').all(task.id, cursor, agentId);
411
+ const unread = d.prepare('SELECT * FROM task_events WHERE task_id = ? AND seq > ? AND actor_agent_id != ? ORDER BY seq ASC LIMIT 10').all(task.id, cursor, agentId);
325
412
  if (unread.length === 0)
326
413
  continue;
327
- const totalUnread = getDB().prepare('SELECT COUNT(*) as cnt FROM task_events WHERE task_id = ? AND seq > ? AND actor_agent_id != ?').get(task.id, cursor, agentId).cnt;
414
+ const totalUnread = d.prepare('SELECT COUNT(*) as cnt FROM task_events WHERE task_id = ? AND seq > ? AND actor_agent_id != ?').get(task.id, cursor, agentId).cnt;
328
415
  const latest = unread.slice(-5).map(e => {
329
- const actor = getAgentById(e.actor_agent_id ?? '');
416
+ const actor = e.actor_agent_id ? getAgentById(e.actor_agent_id) : null;
330
417
  let preview = '';
331
418
  try {
332
419
  const p = JSON.parse(e.payload_json);
@@ -343,6 +430,16 @@ export function getUnreadForAgent(agentId) {
343
430
  }
344
431
  return result;
345
432
  }
433
+ function previewFromPayload(payloadJson) {
434
+ try {
435
+ const p = JSON.parse(payloadJson);
436
+ const text = p.content || p.message || p.preview || '';
437
+ return text.length > 200 ? text.slice(0, 200) + ' [summary]' : text;
438
+ }
439
+ catch {
440
+ return '';
441
+ }
442
+ }
346
443
  export function addPeer(name, url, secret, exposed = '') {
347
444
  const peer = {
348
445
  id: ulid(), name, url, secret, exposed, status: 'active',