swarm-mail 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 (36) hide show
  1. package/README.md +201 -0
  2. package/package.json +28 -0
  3. package/src/adapter.ts +306 -0
  4. package/src/index.ts +57 -0
  5. package/src/pglite.ts +189 -0
  6. package/src/streams/agent-mail.test.ts +777 -0
  7. package/src/streams/agent-mail.ts +535 -0
  8. package/src/streams/debug.test.ts +500 -0
  9. package/src/streams/debug.ts +727 -0
  10. package/src/streams/effect/ask.integration.test.ts +314 -0
  11. package/src/streams/effect/ask.ts +202 -0
  12. package/src/streams/effect/cursor.integration.test.ts +418 -0
  13. package/src/streams/effect/cursor.ts +288 -0
  14. package/src/streams/effect/deferred.test.ts +357 -0
  15. package/src/streams/effect/deferred.ts +445 -0
  16. package/src/streams/effect/index.ts +17 -0
  17. package/src/streams/effect/layers.ts +73 -0
  18. package/src/streams/effect/lock.test.ts +385 -0
  19. package/src/streams/effect/lock.ts +399 -0
  20. package/src/streams/effect/mailbox.test.ts +260 -0
  21. package/src/streams/effect/mailbox.ts +318 -0
  22. package/src/streams/events.test.ts +924 -0
  23. package/src/streams/events.ts +329 -0
  24. package/src/streams/index.test.ts +229 -0
  25. package/src/streams/index.ts +578 -0
  26. package/src/streams/migrations.test.ts +359 -0
  27. package/src/streams/migrations.ts +362 -0
  28. package/src/streams/projections.test.ts +611 -0
  29. package/src/streams/projections.ts +564 -0
  30. package/src/streams/store.integration.test.ts +658 -0
  31. package/src/streams/store.ts +1129 -0
  32. package/src/streams/swarm-mail.ts +552 -0
  33. package/src/types/adapter.ts +392 -0
  34. package/src/types/database.ts +127 -0
  35. package/src/types/index.ts +26 -0
  36. package/tsconfig.json +22 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * SwarmMailAdapter - High-level interface for swarm-mail operations
3
+ *
4
+ * This interface abstracts all swarm-mail operations (events, messaging,
5
+ * reservations, locks) to enable different storage backends.
6
+ *
7
+ * ## Design Goals
8
+ * - Database-agnostic (works with PGLite, SQLite, PostgreSQL, etc.)
9
+ * - Matches existing swarm-mail API surface
10
+ * - No implementation details leak through interface
11
+ *
12
+ * ## Layering
13
+ * - DatabaseAdapter: Low-level SQL execution
14
+ * - SwarmMailAdapter: High-level swarm-mail operations (uses DatabaseAdapter internally)
15
+ * - Plugin tools: Type-safe Zod-validated wrappers (use SwarmMailAdapter)
16
+ */
17
+
18
+ import type {
19
+ AgentEvent,
20
+ AgentRegisteredEvent,
21
+ FileReservedEvent,
22
+ MessageSentEvent,
23
+ } from "../streams/events";
24
+ import type { DatabaseAdapter } from "./database";
25
+
26
+ // ============================================================================
27
+ // Event Store Operations
28
+ // ============================================================================
29
+
30
+ export interface ReadEventsOptions {
31
+ projectKey?: string;
32
+ types?: AgentEvent["type"][];
33
+ since?: number; // timestamp
34
+ until?: number; // timestamp
35
+ afterSequence?: number;
36
+ limit?: number;
37
+ offset?: number;
38
+ }
39
+
40
+ export interface EventStoreAdapter {
41
+ /**
42
+ * Append a single event to the log
43
+ *
44
+ * Updates materialized views automatically.
45
+ */
46
+ appendEvent(
47
+ event: AgentEvent,
48
+ projectPath?: string,
49
+ ): Promise<AgentEvent & { id: number; sequence: number }>;
50
+
51
+ /**
52
+ * Append multiple events in a transaction
53
+ *
54
+ * Atomic - all events succeed or all fail.
55
+ */
56
+ appendEvents(
57
+ events: AgentEvent[],
58
+ projectPath?: string,
59
+ ): Promise<Array<AgentEvent & { id: number; sequence: number }>>;
60
+
61
+ /**
62
+ * Read events with filters
63
+ */
64
+ readEvents(
65
+ options?: ReadEventsOptions,
66
+ projectPath?: string,
67
+ ): Promise<Array<AgentEvent & { id: number; sequence: number }>>;
68
+
69
+ /**
70
+ * Get the latest sequence number for a project
71
+ */
72
+ getLatestSequence(projectKey?: string, projectPath?: string): Promise<number>;
73
+
74
+ /**
75
+ * Replay events to rebuild materialized views
76
+ */
77
+ replayEvents(
78
+ options?: {
79
+ projectKey?: string;
80
+ fromSequence?: number;
81
+ clearViews?: boolean;
82
+ },
83
+ projectPath?: string,
84
+ ): Promise<{ eventsReplayed: number; duration: number }>;
85
+ }
86
+
87
+ // ============================================================================
88
+ // Agent Operations
89
+ // ============================================================================
90
+
91
+ export interface AgentAdapter {
92
+ /**
93
+ * Register an agent for a project
94
+ */
95
+ registerAgent(
96
+ projectKey: string,
97
+ agentName: string,
98
+ options?: {
99
+ program?: string;
100
+ model?: string;
101
+ taskDescription?: string;
102
+ },
103
+ projectPath?: string,
104
+ ): Promise<AgentRegisteredEvent & { id: number; sequence: number }>;
105
+
106
+ /**
107
+ * Get all agents for a project
108
+ */
109
+ getAgents(
110
+ projectKey: string,
111
+ projectPath?: string,
112
+ ): Promise<
113
+ Array<{
114
+ id: number;
115
+ name: string;
116
+ program: string;
117
+ model: string;
118
+ task_description: string | null;
119
+ registered_at: number;
120
+ last_active_at: number;
121
+ }>
122
+ >;
123
+
124
+ /**
125
+ * Get a specific agent by name
126
+ */
127
+ getAgent(
128
+ projectKey: string,
129
+ agentName: string,
130
+ projectPath?: string,
131
+ ): Promise<{
132
+ id: number;
133
+ name: string;
134
+ program: string;
135
+ model: string;
136
+ task_description: string | null;
137
+ registered_at: number;
138
+ last_active_at: number;
139
+ } | null>;
140
+ }
141
+
142
+ // ============================================================================
143
+ // Messaging Operations
144
+ // ============================================================================
145
+
146
+ export interface InboxOptions {
147
+ limit?: number;
148
+ urgentOnly?: boolean;
149
+ unreadOnly?: boolean;
150
+ includeBodies?: boolean;
151
+ sinceTs?: string;
152
+ }
153
+
154
+ export interface Message {
155
+ id: number;
156
+ from_agent: string;
157
+ subject: string;
158
+ body?: string;
159
+ thread_id: string | null;
160
+ importance: string;
161
+ ack_required: boolean;
162
+ created_at: number;
163
+ read_at?: number | null;
164
+ acked_at?: number | null;
165
+ }
166
+
167
+ export interface MessagingAdapter {
168
+ /**
169
+ * Send a message to other agents
170
+ */
171
+ sendMessage(
172
+ projectKey: string,
173
+ fromAgent: string,
174
+ toAgents: string[],
175
+ subject: string,
176
+ body: string,
177
+ options?: {
178
+ threadId?: string;
179
+ importance?: "low" | "normal" | "high" | "urgent";
180
+ ackRequired?: boolean;
181
+ },
182
+ projectPath?: string,
183
+ ): Promise<MessageSentEvent & { id: number; sequence: number }>;
184
+
185
+ /**
186
+ * Get inbox messages for an agent
187
+ */
188
+ getInbox(
189
+ projectKey: string,
190
+ agentName: string,
191
+ options?: InboxOptions,
192
+ projectPath?: string,
193
+ ): Promise<Message[]>;
194
+
195
+ /**
196
+ * Get a single message by ID
197
+ */
198
+ getMessage(
199
+ projectKey: string,
200
+ messageId: number,
201
+ projectPath?: string,
202
+ ): Promise<Message | null>;
203
+
204
+ /**
205
+ * Get all messages in a thread
206
+ */
207
+ getThreadMessages(
208
+ projectKey: string,
209
+ threadId: string,
210
+ projectPath?: string,
211
+ ): Promise<Message[]>;
212
+
213
+ /**
214
+ * Mark a message as read
215
+ */
216
+ markMessageAsRead(
217
+ projectKey: string,
218
+ messageId: number,
219
+ agentName: string,
220
+ projectPath?: string,
221
+ ): Promise<void>;
222
+
223
+ /**
224
+ * Acknowledge a message
225
+ */
226
+ acknowledgeMessage(
227
+ projectKey: string,
228
+ messageId: number,
229
+ agentName: string,
230
+ projectPath?: string,
231
+ ): Promise<void>;
232
+ }
233
+
234
+ // ============================================================================
235
+ // Reservation Operations
236
+ // ============================================================================
237
+
238
+ export interface Reservation {
239
+ id: number;
240
+ agent_name: string;
241
+ path_pattern: string;
242
+ exclusive: boolean;
243
+ reason: string | null;
244
+ created_at: number;
245
+ expires_at: number;
246
+ }
247
+
248
+ export interface Conflict {
249
+ path: string;
250
+ holder: string;
251
+ pattern: string;
252
+ exclusive: boolean;
253
+ }
254
+
255
+ export interface ReservationAdapter {
256
+ /**
257
+ * Reserve files for exclusive editing
258
+ */
259
+ reserveFiles(
260
+ projectKey: string,
261
+ agentName: string,
262
+ paths: string[],
263
+ options?: {
264
+ reason?: string;
265
+ exclusive?: boolean;
266
+ ttlSeconds?: number;
267
+ },
268
+ projectPath?: string,
269
+ ): Promise<FileReservedEvent & { id: number; sequence: number }>;
270
+
271
+ /**
272
+ * Release file reservations
273
+ */
274
+ releaseFiles(
275
+ projectKey: string,
276
+ agentName: string,
277
+ options?: {
278
+ paths?: string[];
279
+ reservationIds?: number[];
280
+ },
281
+ projectPath?: string,
282
+ ): Promise<void>;
283
+
284
+ /**
285
+ * Get active reservations for a project
286
+ */
287
+ getActiveReservations(
288
+ projectKey: string,
289
+ projectPath?: string,
290
+ agentName?: string,
291
+ ): Promise<Reservation[]>;
292
+
293
+ /**
294
+ * Check for conflicts with existing reservations
295
+ */
296
+ checkConflicts(
297
+ projectKey: string,
298
+ agentName: string,
299
+ paths: string[],
300
+ projectPath?: string,
301
+ ): Promise<Conflict[]>;
302
+ }
303
+
304
+ // ============================================================================
305
+ // Schema and Health Operations
306
+ // ============================================================================
307
+
308
+ export interface SchemaAdapter {
309
+ /**
310
+ * Run database migrations
311
+ *
312
+ * Initializes tables, indexes, and constraints.
313
+ */
314
+ runMigrations(projectPath?: string): Promise<void>;
315
+
316
+ /**
317
+ * Check if database is healthy
318
+ */
319
+ healthCheck(projectPath?: string): Promise<boolean>;
320
+
321
+ /**
322
+ * Get database statistics
323
+ */
324
+ getDatabaseStats(projectPath?: string): Promise<{
325
+ events: number;
326
+ agents: number;
327
+ messages: number;
328
+ reservations: number;
329
+ }>;
330
+
331
+ /**
332
+ * Reset database for testing
333
+ *
334
+ * Clears all data but keeps schema.
335
+ */
336
+ resetDatabase(projectPath?: string): Promise<void>;
337
+ }
338
+
339
+ // ============================================================================
340
+ // Combined SwarmMailAdapter Interface
341
+ // ============================================================================
342
+
343
+ /**
344
+ * SwarmMailAdapter - Complete interface for swarm-mail operations
345
+ *
346
+ * Combines all sub-adapters into a single interface.
347
+ * Implementations provide a DatabaseAdapter and implement all operations.
348
+ */
349
+ export interface SwarmMailAdapter
350
+ extends EventStoreAdapter,
351
+ AgentAdapter,
352
+ MessagingAdapter,
353
+ ReservationAdapter,
354
+ SchemaAdapter {
355
+ /**
356
+ * Get the underlying database adapter
357
+ */
358
+ getDatabase(projectPath?: string): Promise<DatabaseAdapter>;
359
+
360
+ /**
361
+ * Close the database connection
362
+ */
363
+ close(projectPath?: string): Promise<void>;
364
+
365
+ /**
366
+ * Close all database connections
367
+ */
368
+ closeAll(): Promise<void>;
369
+ }
370
+
371
+ // ============================================================================
372
+ // Factory Function Type
373
+ // ============================================================================
374
+
375
+ /**
376
+ * SwarmMailAdapterFactory - Function that creates a SwarmMailAdapter instance
377
+ *
378
+ * Adapters export a factory function with this signature.
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * import { createPGLiteAdapter } from '@opencode/swarm-mail/adapters/pglite';
383
+ * import { createSQLiteAdapter } from '@opencode/swarm-mail/adapters/sqlite';
384
+ *
385
+ * const adapter = createPGLiteAdapter({ path: './streams.db' });
386
+ * const adapter2 = createSQLiteAdapter({ path: './streams.db' });
387
+ * ```
388
+ */
389
+ export type SwarmMailAdapterFactory = (config: {
390
+ path?: string;
391
+ timeout?: number;
392
+ }) => SwarmMailAdapter;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * DatabaseAdapter - Database-agnostic interface for swarm-mail
3
+ *
4
+ * Abstracts PGLite-specific operations to support multiple database backends.
5
+ * Based on coursebuilder's adapter-drizzle pattern.
6
+ *
7
+ * ## Design Goals
8
+ * - Zero PGLite types in this interface
9
+ * - Support for PGLite, better-sqlite3, libsql, PostgreSQL
10
+ * - Transaction support optional (some adapters may not support it)
11
+ *
12
+ * ## Implementation Strategy
13
+ * - Accept database instance via dependency injection
14
+ * - Adapters implement this interface for their specific database
15
+ * - Query results use plain objects (no driver-specific types)
16
+ */
17
+
18
+ /**
19
+ * Query result with rows array
20
+ *
21
+ * All database adapters return results in this shape.
22
+ */
23
+ export interface QueryResult<T = unknown> {
24
+ /** Array of result rows */
25
+ rows: T[];
26
+ }
27
+
28
+ /**
29
+ * DatabaseAdapter interface
30
+ *
31
+ * Minimal interface for executing SQL queries and managing transactions.
32
+ * Adapters implement this for PGLite, SQLite, PostgreSQL, etc.
33
+ */
34
+ export interface DatabaseAdapter {
35
+ /**
36
+ * Execute a query and return results
37
+ *
38
+ * @param sql - SQL query string (parameterized)
39
+ * @param params - Query parameters ($1, $2, etc.)
40
+ * @returns Query result with rows array
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const result = await db.query<{ id: number }>(
45
+ * "SELECT id FROM agents WHERE name = $1",
46
+ * ["BlueLake"]
47
+ * );
48
+ * const id = result.rows[0]?.id;
49
+ * ```
50
+ */
51
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
52
+
53
+ /**
54
+ * Execute a SQL statement without returning results
55
+ *
56
+ * Used for DDL (CREATE TABLE, etc.), DML (INSERT/UPDATE/DELETE), and transactions.
57
+ *
58
+ * @param sql - SQL statement(s) to execute
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * await db.exec("BEGIN");
63
+ * await db.exec("COMMIT");
64
+ * await db.exec("CREATE TABLE users (id SERIAL PRIMARY KEY)");
65
+ * ```
66
+ */
67
+ exec(sql: string): Promise<void>;
68
+
69
+ /**
70
+ * Execute a function within a transaction (optional)
71
+ *
72
+ * If the adapter doesn't support transactions, it can omit this method
73
+ * or throw an error. The swarm-mail layer will handle transaction
74
+ * fallback (using manual BEGIN/COMMIT/ROLLBACK).
75
+ *
76
+ * @param fn - Function to execute within transaction context
77
+ * @returns Result of the function
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const result = await db.transaction?.(async (tx) => {
82
+ * await tx.query("INSERT INTO events ...", [...]);
83
+ * await tx.query("UPDATE agents ...", [...]);
84
+ * return { success: true };
85
+ * });
86
+ * ```
87
+ */
88
+ transaction?<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T>;
89
+
90
+ /**
91
+ * Close the database connection (optional)
92
+ *
93
+ * Some adapters (like PGLite) need explicit cleanup.
94
+ * If not provided, swarm-mail assumes connection is managed externally.
95
+ */
96
+ close?(): Promise<void>;
97
+ }
98
+
99
+ /**
100
+ * Database configuration options
101
+ *
102
+ * Passed to adapter factory functions to create DatabaseAdapter instances.
103
+ */
104
+ export interface DatabaseConfig {
105
+ /** Path to database file or connection string */
106
+ path: string;
107
+ /** Optional timeout in milliseconds for queries */
108
+ timeout?: number;
109
+ /** Optional flags for database initialization */
110
+ flags?: {
111
+ /** Create database if it doesn't exist */
112
+ create?: boolean;
113
+ /** Enable foreign key constraints */
114
+ foreignKeys?: boolean;
115
+ /** Enable WAL mode (SQLite) */
116
+ wal?: boolean;
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Type guard to check if adapter supports transactions
122
+ */
123
+ export function supportsTransactions(
124
+ adapter: DatabaseAdapter,
125
+ ): adapter is Required<Pick<DatabaseAdapter, "transaction">> & DatabaseAdapter {
126
+ return typeof adapter.transaction === "function";
127
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Swarm Mail Types - Database-agnostic interfaces
3
+ *
4
+ * Re-exports all adapter interfaces for easy importing:
5
+ *
6
+ * ```typescript
7
+ * import type { DatabaseAdapter, SwarmMailAdapter } from '@opencode/swarm-mail/types';
8
+ * ```
9
+ */
10
+
11
+ export type {
12
+ AgentAdapter,
13
+ Conflict,
14
+ EventStoreAdapter,
15
+ InboxOptions,
16
+ Message,
17
+ MessagingAdapter,
18
+ ReadEventsOptions,
19
+ Reservation,
20
+ ReservationAdapter,
21
+ SchemaAdapter,
22
+ SwarmMailAdapter,
23
+ SwarmMailAdapterFactory,
24
+ } from "./adapter";
25
+ export type { DatabaseAdapter, DatabaseConfig, QueryResult } from "./database";
26
+ export { supportsTransactions } from "./database";
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ESNext"],
7
+ "types": ["bun-types"],
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "emitDeclarationOnly": true,
17
+ "outDir": "./dist",
18
+ "rootDir": "./src"
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
22
+ }