opencode-swarm-plugin 0.1.0 → 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.
package/src/storage.ts ADDED
@@ -0,0 +1,679 @@
1
+ /**
2
+ * Storage Module - Pluggable persistence for learning data
3
+ *
4
+ * Provides a unified storage interface with multiple backends:
5
+ * - semantic-memory (default) - Persistent with semantic search
6
+ * - in-memory - For testing and ephemeral sessions
7
+ *
8
+ * The semantic-memory backend uses collections:
9
+ * - `swarm-feedback` - Criterion feedback events
10
+ * - `swarm-patterns` - Decomposition patterns and anti-patterns
11
+ * - `swarm-maturity` - Pattern maturity tracking
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Use default semantic-memory storage
16
+ * const storage = createStorage();
17
+ *
18
+ * // Or configure explicitly
19
+ * const storage = createStorage({
20
+ * backend: "semantic-memory",
21
+ * collections: {
22
+ * feedback: "my-feedback",
23
+ * patterns: "my-patterns",
24
+ * maturity: "my-maturity",
25
+ * },
26
+ * });
27
+ *
28
+ * // Or use in-memory for testing
29
+ * const storage = createStorage({ backend: "memory" });
30
+ * ```
31
+ */
32
+
33
+ import type { FeedbackEvent } from "./learning";
34
+ import type { DecompositionPattern } from "./anti-patterns";
35
+ import type { PatternMaturity, MaturityFeedback } from "./pattern-maturity";
36
+ import { InMemoryFeedbackStorage } from "./learning";
37
+ import { InMemoryPatternStorage } from "./anti-patterns";
38
+ import { InMemoryMaturityStorage } from "./pattern-maturity";
39
+
40
+ // ============================================================================
41
+ // Command Resolution
42
+ // ============================================================================
43
+
44
+ /**
45
+ * Cached semantic-memory command (native or bunx fallback)
46
+ */
47
+ let cachedCommand: string[] | null = null;
48
+
49
+ /**
50
+ * Resolve the semantic-memory command
51
+ *
52
+ * Checks for native install first, falls back to bunx.
53
+ * Result is cached for the session.
54
+ */
55
+ async function resolveSemanticMemoryCommand(): Promise<string[]> {
56
+ if (cachedCommand) return cachedCommand;
57
+
58
+ // Try native install first
59
+ const nativeResult = await Bun.$`which semantic-memory`.quiet().nothrow();
60
+ if (nativeResult.exitCode === 0) {
61
+ cachedCommand = ["semantic-memory"];
62
+ return cachedCommand;
63
+ }
64
+
65
+ // Fall back to bunx
66
+ cachedCommand = ["bunx", "semantic-memory"];
67
+ return cachedCommand;
68
+ }
69
+
70
+ /**
71
+ * Execute semantic-memory command with args
72
+ */
73
+ async function execSemanticMemory(
74
+ args: string[],
75
+ ): Promise<{ exitCode: number; stdout: Buffer; stderr: Buffer }> {
76
+ const cmd = await resolveSemanticMemoryCommand();
77
+ const fullCmd = [...cmd, ...args];
78
+
79
+ // Use Bun.spawn for dynamic command arrays
80
+ const proc = Bun.spawn(fullCmd, {
81
+ stdout: "pipe",
82
+ stderr: "pipe",
83
+ });
84
+
85
+ const stdout = Buffer.from(await new Response(proc.stdout).arrayBuffer());
86
+ const stderr = Buffer.from(await new Response(proc.stderr).arrayBuffer());
87
+ const exitCode = await proc.exited;
88
+
89
+ return { exitCode, stdout, stderr };
90
+ }
91
+
92
+ /**
93
+ * Reset the cached command (for testing)
94
+ */
95
+ export function resetCommandCache(): void {
96
+ cachedCommand = null;
97
+ }
98
+
99
+ // ============================================================================
100
+ // Configuration
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Storage backend type
105
+ */
106
+ export type StorageBackend = "semantic-memory" | "memory";
107
+
108
+ /**
109
+ * Collection names for semantic-memory
110
+ */
111
+ export interface StorageCollections {
112
+ feedback: string;
113
+ patterns: string;
114
+ maturity: string;
115
+ }
116
+
117
+ /**
118
+ * Storage configuration
119
+ */
120
+ export interface StorageConfig {
121
+ /** Backend to use (default: "semantic-memory") */
122
+ backend: StorageBackend;
123
+ /** Collection names for semantic-memory backend */
124
+ collections: StorageCollections;
125
+ /** Whether to use semantic search for queries (default: true) */
126
+ useSemanticSearch: boolean;
127
+ }
128
+
129
+ export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
130
+ backend: "semantic-memory",
131
+ collections: {
132
+ feedback: "swarm-feedback",
133
+ patterns: "swarm-patterns",
134
+ maturity: "swarm-maturity",
135
+ },
136
+ useSemanticSearch: true,
137
+ };
138
+
139
+ // ============================================================================
140
+ // Unified Storage Interface
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Unified storage interface for all learning data
145
+ */
146
+ export interface LearningStorage {
147
+ // Feedback operations
148
+ storeFeedback(event: FeedbackEvent): Promise<void>;
149
+ getFeedbackByCriterion(criterion: string): Promise<FeedbackEvent[]>;
150
+ getFeedbackByBead(beadId: string): Promise<FeedbackEvent[]>;
151
+ getAllFeedback(): Promise<FeedbackEvent[]>;
152
+ findSimilarFeedback(query: string, limit?: number): Promise<FeedbackEvent[]>;
153
+
154
+ // Pattern operations
155
+ storePattern(pattern: DecompositionPattern): Promise<void>;
156
+ getPattern(id: string): Promise<DecompositionPattern | null>;
157
+ getAllPatterns(): Promise<DecompositionPattern[]>;
158
+ getAntiPatterns(): Promise<DecompositionPattern[]>;
159
+ getPatternsByTag(tag: string): Promise<DecompositionPattern[]>;
160
+ findSimilarPatterns(
161
+ query: string,
162
+ limit?: number,
163
+ ): Promise<DecompositionPattern[]>;
164
+
165
+ // Maturity operations
166
+ storeMaturity(maturity: PatternMaturity): Promise<void>;
167
+ getMaturity(patternId: string): Promise<PatternMaturity | null>;
168
+ getAllMaturity(): Promise<PatternMaturity[]>;
169
+ getMaturityByState(state: string): Promise<PatternMaturity[]>;
170
+ storeMaturityFeedback(feedback: MaturityFeedback): Promise<void>;
171
+ getMaturityFeedback(patternId: string): Promise<MaturityFeedback[]>;
172
+
173
+ // Lifecycle
174
+ close(): Promise<void>;
175
+ }
176
+
177
+ // ============================================================================
178
+ // Semantic Memory Storage Implementation
179
+ // ============================================================================
180
+
181
+ /**
182
+ * Semantic-memory backed storage
183
+ *
184
+ * Uses the semantic-memory CLI for persistence with semantic search.
185
+ * Data survives across sessions and can be searched by meaning.
186
+ */
187
+ export class SemanticMemoryStorage implements LearningStorage {
188
+ private config: StorageConfig;
189
+
190
+ constructor(config: Partial<StorageConfig> = {}) {
191
+ this.config = { ...DEFAULT_STORAGE_CONFIG, ...config };
192
+ }
193
+
194
+ // -------------------------------------------------------------------------
195
+ // Helpers
196
+ // -------------------------------------------------------------------------
197
+
198
+ private async store(
199
+ collection: string,
200
+ data: unknown,
201
+ metadata?: Record<string, unknown>,
202
+ ): Promise<void> {
203
+ const content = typeof data === "string" ? data : JSON.stringify(data);
204
+ const args = ["store", content, "--collection", collection];
205
+
206
+ if (metadata) {
207
+ args.push("--metadata", JSON.stringify(metadata));
208
+ }
209
+
210
+ await execSemanticMemory(args);
211
+ }
212
+
213
+ private async find<T>(
214
+ collection: string,
215
+ query: string,
216
+ limit: number = 10,
217
+ useFts: boolean = false,
218
+ ): Promise<T[]> {
219
+ const args = [
220
+ "find",
221
+ query,
222
+ "--collection",
223
+ collection,
224
+ "--limit",
225
+ String(limit),
226
+ "--json",
227
+ ];
228
+
229
+ if (useFts) {
230
+ args.push("--fts");
231
+ }
232
+
233
+ const result = await execSemanticMemory(args);
234
+
235
+ if (result.exitCode !== 0) {
236
+ return [];
237
+ }
238
+
239
+ try {
240
+ const output = result.stdout.toString().trim();
241
+ if (!output) return [];
242
+
243
+ const parsed = JSON.parse(output);
244
+ // semantic-memory returns { results: [...] } or just [...]
245
+ const results = Array.isArray(parsed) ? parsed : parsed.results || [];
246
+
247
+ // Extract the stored content from each result
248
+ return results.map((r: { content?: string; information?: string }) => {
249
+ const content = r.content || r.information || "";
250
+ try {
251
+ return JSON.parse(content);
252
+ } catch {
253
+ return content;
254
+ }
255
+ });
256
+ } catch {
257
+ return [];
258
+ }
259
+ }
260
+
261
+ private async list<T>(collection: string): Promise<T[]> {
262
+ const result = await execSemanticMemory([
263
+ "list",
264
+ "--collection",
265
+ collection,
266
+ "--json",
267
+ ]);
268
+
269
+ if (result.exitCode !== 0) {
270
+ return [];
271
+ }
272
+
273
+ try {
274
+ const output = result.stdout.toString().trim();
275
+ if (!output) return [];
276
+
277
+ const parsed = JSON.parse(output);
278
+ const items = Array.isArray(parsed) ? parsed : parsed.items || [];
279
+
280
+ return items.map((item: { content?: string; information?: string }) => {
281
+ const content = item.content || item.information || "";
282
+ try {
283
+ return JSON.parse(content);
284
+ } catch {
285
+ return content;
286
+ }
287
+ });
288
+ } catch {
289
+ return [];
290
+ }
291
+ }
292
+
293
+ // -------------------------------------------------------------------------
294
+ // Feedback Operations
295
+ // -------------------------------------------------------------------------
296
+
297
+ async storeFeedback(event: FeedbackEvent): Promise<void> {
298
+ await this.store(this.config.collections.feedback, event, {
299
+ criterion: event.criterion,
300
+ type: event.type,
301
+ bead_id: event.bead_id || "",
302
+ timestamp: event.timestamp,
303
+ });
304
+ }
305
+
306
+ async getFeedbackByCriterion(criterion: string): Promise<FeedbackEvent[]> {
307
+ // Use FTS for exact criterion match
308
+ return this.find<FeedbackEvent>(
309
+ this.config.collections.feedback,
310
+ criterion,
311
+ 100,
312
+ true, // FTS for exact match
313
+ );
314
+ }
315
+
316
+ async getFeedbackByBead(beadId: string): Promise<FeedbackEvent[]> {
317
+ return this.find<FeedbackEvent>(
318
+ this.config.collections.feedback,
319
+ beadId,
320
+ 100,
321
+ true,
322
+ );
323
+ }
324
+
325
+ async getAllFeedback(): Promise<FeedbackEvent[]> {
326
+ return this.list<FeedbackEvent>(this.config.collections.feedback);
327
+ }
328
+
329
+ async findSimilarFeedback(
330
+ query: string,
331
+ limit: number = 10,
332
+ ): Promise<FeedbackEvent[]> {
333
+ return this.find<FeedbackEvent>(
334
+ this.config.collections.feedback,
335
+ query,
336
+ limit,
337
+ !this.config.useSemanticSearch,
338
+ );
339
+ }
340
+
341
+ // -------------------------------------------------------------------------
342
+ // Pattern Operations
343
+ // -------------------------------------------------------------------------
344
+
345
+ async storePattern(pattern: DecompositionPattern): Promise<void> {
346
+ await this.store(this.config.collections.patterns, pattern, {
347
+ id: pattern.id,
348
+ kind: pattern.kind,
349
+ is_negative: pattern.is_negative,
350
+ tags: pattern.tags.join(","),
351
+ });
352
+ }
353
+
354
+ async getPattern(id: string): Promise<DecompositionPattern | null> {
355
+ // List all and filter by ID - FTS search by ID is unreliable
356
+ const all = await this.list<DecompositionPattern>(
357
+ this.config.collections.patterns,
358
+ );
359
+ return all.find((p) => p.id === id) || null;
360
+ }
361
+
362
+ async getAllPatterns(): Promise<DecompositionPattern[]> {
363
+ return this.list<DecompositionPattern>(this.config.collections.patterns);
364
+ }
365
+
366
+ async getAntiPatterns(): Promise<DecompositionPattern[]> {
367
+ const all = await this.getAllPatterns();
368
+ return all.filter((p) => p.kind === "anti_pattern");
369
+ }
370
+
371
+ async getPatternsByTag(tag: string): Promise<DecompositionPattern[]> {
372
+ const results = await this.find<DecompositionPattern>(
373
+ this.config.collections.patterns,
374
+ tag,
375
+ 100,
376
+ true,
377
+ );
378
+ return results.filter((p) => p.tags.includes(tag));
379
+ }
380
+
381
+ async findSimilarPatterns(
382
+ query: string,
383
+ limit: number = 10,
384
+ ): Promise<DecompositionPattern[]> {
385
+ return this.find<DecompositionPattern>(
386
+ this.config.collections.patterns,
387
+ query,
388
+ limit,
389
+ !this.config.useSemanticSearch,
390
+ );
391
+ }
392
+
393
+ // -------------------------------------------------------------------------
394
+ // Maturity Operations
395
+ // -------------------------------------------------------------------------
396
+
397
+ async storeMaturity(maturity: PatternMaturity): Promise<void> {
398
+ await this.store(this.config.collections.maturity, maturity, {
399
+ pattern_id: maturity.pattern_id,
400
+ state: maturity.state,
401
+ });
402
+ }
403
+
404
+ async getMaturity(patternId: string): Promise<PatternMaturity | null> {
405
+ // List all and filter by pattern_id - FTS search by ID is unreliable
406
+ const all = await this.list<PatternMaturity>(
407
+ this.config.collections.maturity,
408
+ );
409
+ return all.find((m) => m.pattern_id === patternId) || null;
410
+ }
411
+
412
+ async getAllMaturity(): Promise<PatternMaturity[]> {
413
+ return this.list<PatternMaturity>(this.config.collections.maturity);
414
+ }
415
+
416
+ async getMaturityByState(state: string): Promise<PatternMaturity[]> {
417
+ const all = await this.getAllMaturity();
418
+ return all.filter((m) => m.state === state);
419
+ }
420
+
421
+ async storeMaturityFeedback(feedback: MaturityFeedback): Promise<void> {
422
+ await this.store(this.config.collections.maturity + "-feedback", feedback, {
423
+ pattern_id: feedback.pattern_id,
424
+ type: feedback.type,
425
+ timestamp: feedback.timestamp,
426
+ });
427
+ }
428
+
429
+ async getMaturityFeedback(patternId: string): Promise<MaturityFeedback[]> {
430
+ // List all and filter by pattern_id - FTS search by ID is unreliable
431
+ const all = await this.list<MaturityFeedback>(
432
+ this.config.collections.maturity + "-feedback",
433
+ );
434
+ return all.filter((f) => f.pattern_id === patternId);
435
+ }
436
+
437
+ async close(): Promise<void> {
438
+ // No cleanup needed for CLI-based storage
439
+ }
440
+ }
441
+
442
+ // ============================================================================
443
+ // In-Memory Storage Implementation
444
+ // ============================================================================
445
+
446
+ /**
447
+ * In-memory storage adapter
448
+ *
449
+ * Wraps the existing in-memory implementations into the unified interface.
450
+ * Useful for testing and ephemeral sessions.
451
+ */
452
+ export class InMemoryStorage implements LearningStorage {
453
+ private feedback: InMemoryFeedbackStorage;
454
+ private patterns: InMemoryPatternStorage;
455
+ private maturity: InMemoryMaturityStorage;
456
+
457
+ constructor() {
458
+ this.feedback = new InMemoryFeedbackStorage();
459
+ this.patterns = new InMemoryPatternStorage();
460
+ this.maturity = new InMemoryMaturityStorage();
461
+ }
462
+
463
+ // Feedback
464
+ async storeFeedback(event: FeedbackEvent): Promise<void> {
465
+ return this.feedback.store(event);
466
+ }
467
+
468
+ async getFeedbackByCriterion(criterion: string): Promise<FeedbackEvent[]> {
469
+ return this.feedback.getByCriterion(criterion);
470
+ }
471
+
472
+ async getFeedbackByBead(beadId: string): Promise<FeedbackEvent[]> {
473
+ return this.feedback.getByBead(beadId);
474
+ }
475
+
476
+ async getAllFeedback(): Promise<FeedbackEvent[]> {
477
+ return this.feedback.getAll();
478
+ }
479
+
480
+ async findSimilarFeedback(
481
+ query: string,
482
+ limit: number = 10,
483
+ ): Promise<FeedbackEvent[]> {
484
+ // In-memory doesn't support semantic search, filter by query string match
485
+ const all = await this.feedback.getAll();
486
+ const lowerQuery = query.toLowerCase();
487
+ const filtered = all.filter(
488
+ (event) =>
489
+ event.criterion.toLowerCase().includes(lowerQuery) ||
490
+ (event.bead_id && event.bead_id.toLowerCase().includes(lowerQuery)) ||
491
+ (event.context && event.context.toLowerCase().includes(lowerQuery)),
492
+ );
493
+ return filtered.slice(0, limit);
494
+ }
495
+
496
+ // Patterns
497
+ async storePattern(pattern: DecompositionPattern): Promise<void> {
498
+ return this.patterns.store(pattern);
499
+ }
500
+
501
+ async getPattern(id: string): Promise<DecompositionPattern | null> {
502
+ return this.patterns.get(id);
503
+ }
504
+
505
+ async getAllPatterns(): Promise<DecompositionPattern[]> {
506
+ return this.patterns.getAll();
507
+ }
508
+
509
+ async getAntiPatterns(): Promise<DecompositionPattern[]> {
510
+ return this.patterns.getAntiPatterns();
511
+ }
512
+
513
+ async getPatternsByTag(tag: string): Promise<DecompositionPattern[]> {
514
+ return this.patterns.getByTag(tag);
515
+ }
516
+
517
+ async findSimilarPatterns(
518
+ query: string,
519
+ limit: number = 10,
520
+ ): Promise<DecompositionPattern[]> {
521
+ const results = await this.patterns.findByContent(query);
522
+ return results.slice(0, limit);
523
+ }
524
+
525
+ // Maturity
526
+ async storeMaturity(maturity: PatternMaturity): Promise<void> {
527
+ return this.maturity.store(maturity);
528
+ }
529
+
530
+ async getMaturity(patternId: string): Promise<PatternMaturity | null> {
531
+ return this.maturity.get(patternId);
532
+ }
533
+
534
+ async getAllMaturity(): Promise<PatternMaturity[]> {
535
+ return this.maturity.getAll();
536
+ }
537
+
538
+ async getMaturityByState(state: string): Promise<PatternMaturity[]> {
539
+ return this.maturity.getByState(state as any);
540
+ }
541
+
542
+ async storeMaturityFeedback(feedback: MaturityFeedback): Promise<void> {
543
+ return this.maturity.storeFeedback(feedback);
544
+ }
545
+
546
+ async getMaturityFeedback(patternId: string): Promise<MaturityFeedback[]> {
547
+ return this.maturity.getFeedback(patternId);
548
+ }
549
+
550
+ async close(): Promise<void> {
551
+ // No cleanup needed
552
+ }
553
+ }
554
+
555
+ // ============================================================================
556
+ // Factory
557
+ // ============================================================================
558
+
559
+ /**
560
+ * Create a storage instance
561
+ *
562
+ * @param config - Storage configuration (default: semantic-memory)
563
+ * @returns Configured storage instance
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * // Default semantic-memory storage
568
+ * const storage = createStorage();
569
+ *
570
+ * // In-memory for testing
571
+ * const storage = createStorage({ backend: "memory" });
572
+ *
573
+ * // Custom collections
574
+ * const storage = createStorage({
575
+ * backend: "semantic-memory",
576
+ * collections: {
577
+ * feedback: "my-project-feedback",
578
+ * patterns: "my-project-patterns",
579
+ * maturity: "my-project-maturity",
580
+ * },
581
+ * });
582
+ * ```
583
+ */
584
+ export function createStorage(
585
+ config: Partial<StorageConfig> = {},
586
+ ): LearningStorage {
587
+ const fullConfig = { ...DEFAULT_STORAGE_CONFIG, ...config };
588
+
589
+ switch (fullConfig.backend) {
590
+ case "semantic-memory":
591
+ return new SemanticMemoryStorage(fullConfig);
592
+ case "memory":
593
+ return new InMemoryStorage();
594
+ default:
595
+ throw new Error(`Unknown storage backend: ${fullConfig.backend}`);
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Check if semantic-memory is available (native or via bunx)
601
+ */
602
+ export async function isSemanticMemoryAvailable(): Promise<boolean> {
603
+ try {
604
+ const result = await execSemanticMemory(["stats"]);
605
+ return result.exitCode === 0;
606
+ } catch {
607
+ return false;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Get the resolved semantic-memory command (for debugging/logging)
613
+ */
614
+ export async function getResolvedCommand(): Promise<string[]> {
615
+ return resolveSemanticMemoryCommand();
616
+ }
617
+
618
+ /**
619
+ * Create storage with automatic fallback
620
+ *
621
+ * Uses semantic-memory if available, otherwise falls back to in-memory.
622
+ *
623
+ * @param config - Storage configuration
624
+ * @returns Storage instance
625
+ */
626
+ export async function createStorageWithFallback(
627
+ config: Partial<StorageConfig> = {},
628
+ ): Promise<LearningStorage> {
629
+ if (config.backend === "memory") {
630
+ return new InMemoryStorage();
631
+ }
632
+
633
+ const available = await isSemanticMemoryAvailable();
634
+ if (available) {
635
+ return new SemanticMemoryStorage(config);
636
+ }
637
+
638
+ console.warn(
639
+ "semantic-memory not available, falling back to in-memory storage",
640
+ );
641
+ return new InMemoryStorage();
642
+ }
643
+
644
+ // ============================================================================
645
+ // Global Storage Instance
646
+ // ============================================================================
647
+
648
+ let globalStorage: LearningStorage | null = null;
649
+
650
+ /**
651
+ * Get or create the global storage instance
652
+ *
653
+ * Uses semantic-memory by default, with automatic fallback to in-memory.
654
+ */
655
+ export async function getStorage(): Promise<LearningStorage> {
656
+ if (!globalStorage) {
657
+ globalStorage = await createStorageWithFallback();
658
+ }
659
+ return globalStorage;
660
+ }
661
+
662
+ /**
663
+ * Set the global storage instance
664
+ *
665
+ * Useful for testing or custom configurations.
666
+ */
667
+ export function setStorage(storage: LearningStorage): void {
668
+ globalStorage = storage;
669
+ }
670
+
671
+ /**
672
+ * Reset the global storage instance
673
+ */
674
+ export async function resetStorage(): Promise<void> {
675
+ if (globalStorage) {
676
+ await globalStorage.close();
677
+ globalStorage = null;
678
+ }
679
+ }