@veedubin/boomerang-v3 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 (53) hide show
  1. package/.github/workflows/npm-publish.yml +58 -0
  2. package/.opencode/skills/boomerang-agent-builder/SKILL.md +226 -0
  3. package/.opencode/skills/boomerang-architect/SKILL.md +252 -0
  4. package/.opencode/skills/boomerang-coder/SKILL.md +283 -0
  5. package/.opencode/skills/boomerang-explorer/SKILL.md +58 -0
  6. package/.opencode/skills/boomerang-git/SKILL.md +115 -0
  7. package/.opencode/skills/boomerang-handoff/SKILL.md +209 -0
  8. package/.opencode/skills/boomerang-init/SKILL.md +117 -0
  9. package/.opencode/skills/boomerang-linter/SKILL.md +92 -0
  10. package/.opencode/skills/boomerang-orchestrator/SKILL.md +401 -0
  11. package/.opencode/skills/boomerang-release/SKILL.md +116 -0
  12. package/.opencode/skills/boomerang-scraper/SKILL.md +105 -0
  13. package/.opencode/skills/boomerang-tester/SKILL.md +107 -0
  14. package/.opencode/skills/boomerang-writer/SKILL.md +93 -0
  15. package/.opencode/skills/mcp-specialist/SKILL.md +130 -0
  16. package/.opencode/skills/researcher/SKILL.md +118 -0
  17. package/AGENTS.md +333 -0
  18. package/README.md +305 -0
  19. package/dist/index.js +13 -0
  20. package/dist/memini-client/index.js +560 -0
  21. package/dist/memini-client/schema.js +13 -0
  22. package/dist/memory/contradictions.js +119 -0
  23. package/dist/memory/graph.js +86 -0
  24. package/dist/memory/index.js +314 -0
  25. package/dist/memory/kg.js +111 -0
  26. package/dist/memory/schema.js +10 -0
  27. package/dist/memory/tiered.js +104 -0
  28. package/dist/memory/trust.js +148 -0
  29. package/dist/protocol/types.js +6 -0
  30. package/package.json +41 -0
  31. package/packages/opencode-plugin/src/asset-loader.ts +201 -0
  32. package/packages/opencode-plugin/src/git.ts +77 -0
  33. package/packages/opencode-plugin/src/index.ts +346 -0
  34. package/packages/opencode-plugin/src/memory.ts +109 -0
  35. package/packages/opencode-plugin/src/orchestrator.ts +263 -0
  36. package/packages/opencode-plugin/src/quality-gates.ts +75 -0
  37. package/packages/opencode-plugin/src/types.ts +141 -0
  38. package/src/index.ts +16 -0
  39. package/src/memini-client/index.ts +762 -0
  40. package/src/memini-client/schema.ts +60 -0
  41. package/src/memory/contradictions.ts +164 -0
  42. package/src/memory/graph.ts +116 -0
  43. package/src/memory/index.ts +422 -0
  44. package/src/memory/kg.ts +166 -0
  45. package/src/memory/schema.ts +274 -0
  46. package/src/memory/tiered.ts +133 -0
  47. package/src/memory/trust.ts +218 -0
  48. package/src/protocol/types.ts +79 -0
  49. package/tests/index.test.ts +58 -0
  50. package/tests/memini-client.test.ts +321 -0
  51. package/tests/memory/index.test.ts +214 -0
  52. package/tsconfig.json +17 -0
  53. package/vitest.config.ts +19 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Memory graph integration for memini-ai
3
+ *
4
+ * Manages relationships between memory entries
5
+ */
6
+ import { getClient } from '../memini-client/index.js';
7
+ /**
8
+ * Find memories related to a given memory
9
+ */
10
+ export async function findRelatedMemories(memoryId, relationshipType, limit = 10, client) {
11
+ const mc = client ?? getClient();
12
+ // findRelatedMemories returns MemoryEntry[], but API shows MemoryEntry[] directly
13
+ const entries = await mc.findRelatedMemories(memoryId, relationshipType, limit);
14
+ return entries.map((e) => ({
15
+ id: e.id,
16
+ text: e.text,
17
+ vector: Array.from(e.vector || []),
18
+ sourceType: reverseMapSourceType(e.sourceType),
19
+ sourcePath: e.sourcePath || '',
20
+ timestamp: e.timestamp,
21
+ contentHash: e.contentHash || '',
22
+ }));
23
+ }
24
+ /**
25
+ * Create a relationship between two memories
26
+ */
27
+ export async function createRelationship(sourceId, targetId, type, confidence = 1.0, client) {
28
+ const mc = client ?? getClient();
29
+ await mc.createRelationship(sourceId, targetId, type, confidence);
30
+ return {
31
+ success: true,
32
+ sourceId,
33
+ targetId,
34
+ relationshipType: type,
35
+ confidence,
36
+ };
37
+ }
38
+ /**
39
+ * Get summary of all relationships for a memory
40
+ */
41
+ export async function getRelationshipSummary(memoryId, client) {
42
+ const mc = client ?? getClient();
43
+ const result = await mc.getRelationshipSummary(memoryId);
44
+ if (!result) {
45
+ return {
46
+ memoryId,
47
+ totalRelationships: 0,
48
+ byType: {
49
+ SUPERSEDES: 0,
50
+ RELATED_TO: 0,
51
+ CONTRADICTS: 0,
52
+ DERIVED_FROM: 0,
53
+ },
54
+ };
55
+ }
56
+ // Ensure all relationship types are present
57
+ const allTypes = ['SUPERSEDES', 'RELATED_TO', 'CONTRADICTS', 'DERIVED_FROM'];
58
+ const completeByType = {};
59
+ for (const t of allTypes) {
60
+ completeByType[t] = result.byType?.[t] ?? 0;
61
+ }
62
+ return {
63
+ memoryId: result.memoryId,
64
+ totalRelationships: result.totalRelationships,
65
+ byType: completeByType,
66
+ };
67
+ }
68
+ /**
69
+ * Map memini-ai source type back to boomerang source type
70
+ */
71
+ function reverseMapSourceType(type) {
72
+ switch (type) {
73
+ case 'session':
74
+ return 'conversation';
75
+ case 'file':
76
+ return 'file';
77
+ case 'web':
78
+ return 'web';
79
+ case 'boomerang':
80
+ return 'manual';
81
+ case 'project':
82
+ return 'manual';
83
+ default:
84
+ return type;
85
+ }
86
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * MemorySystem - Singleton wrapper for memini-ai MCP client
3
+ *
4
+ * Phase 1: Wraps memini-ai client while preserving the original API surface.
5
+ */
6
+ import { initializeClient, } from '../memini-client/index.js';
7
+ import { DEFAULT_SEARCH_OPTIONS } from './schema.js';
8
+ const PROJECT_ID = process.env.BOOMERANG_PROJECT_ID || 'boomerang-default';
9
+ class MemorySystem {
10
+ static instance = null;
11
+ _initialized = false;
12
+ _client = null;
13
+ _status = null;
14
+ constructor() { }
15
+ /**
16
+ * Get the MemorySystem singleton instance
17
+ */
18
+ static getInstance() {
19
+ if (!MemorySystem.instance) {
20
+ MemorySystem.instance = new MemorySystem();
21
+ }
22
+ return MemorySystem.instance;
23
+ }
24
+ /**
25
+ * Check if the system has been initialized
26
+ */
27
+ isInitialized() {
28
+ return this._initialized;
29
+ }
30
+ /**
31
+ * Initialize the memory system (lazy initialization)
32
+ * Connects to memini-ai Python MCP server
33
+ */
34
+ async initialize() {
35
+ if (this._initialized) {
36
+ return;
37
+ }
38
+ const pythonPath = process.env.PYTHON_PATH || 'python3';
39
+ const serverModule = process.env.MEMINI_SERVER_MODULE || 'memini_ai.server';
40
+ this._client = await initializeClient(pythonPath, serverModule);
41
+ // Get initial status
42
+ this._status = (await this._client.getStatus());
43
+ this._initialized = true;
44
+ }
45
+ /**
46
+ * Get the underlying MCP client
47
+ */
48
+ getClient() {
49
+ if (!this._client) {
50
+ throw new Error('MemorySystem not initialized. Call initialize() first.');
51
+ }
52
+ return this._client;
53
+ }
54
+ /**
55
+ * Add a new memory entry
56
+ */
57
+ async addMemory(entry) {
58
+ this.ensureInitialized();
59
+ // Map source type to memini-ai format
60
+ const sourceType = mapSourceType(entry.sourceType);
61
+ const result = await this._client.addMemory({
62
+ text: entry.text,
63
+ sourceType,
64
+ sourcePath: entry.sourcePath,
65
+ metadataJson: entry.metadataJson,
66
+ });
67
+ // result is already a MemoryEntry from memini-client
68
+ const meminiEntry = result;
69
+ return adaptMemoryEntry(meminiEntry);
70
+ }
71
+ /**
72
+ * Get a memory entry by ID
73
+ */
74
+ async getMemory(id) {
75
+ this.ensureInitialized();
76
+ // Search for the memory by ID
77
+ const results = await this._client.search(`id:${id}`, { topK: 1 });
78
+ const match = results.find((m) => m.entry.id === id);
79
+ if (match) {
80
+ return adaptMemoryEntry(match.entry);
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Delete a memory entry by ID
86
+ * Note: memini-ai doesn't have direct delete, use adjustTrust with archive
87
+ */
88
+ async deleteMemory(id) {
89
+ this.ensureInitialized();
90
+ try {
91
+ // Archive the memory instead of deleting
92
+ await this._client.adjustTrust(id, 'agent_ignored');
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * List unique source paths, optionally filtered by source type
101
+ */
102
+ async listSources(sourceType) {
103
+ this.ensureInitialized();
104
+ // This is a simplified implementation
105
+ // In production, you'd want a dedicated list_sources tool
106
+ const results = await this._client.search('', { topK: 100 });
107
+ const pathSet = new Set();
108
+ for (const result of results) {
109
+ const entry = result.entry;
110
+ const entrySourceType = reverseMapSourceType(entry.sourceType);
111
+ if (entry.sourcePath && (!sourceType || entrySourceType === sourceType)) {
112
+ pathSet.add(entry.sourcePath);
113
+ }
114
+ }
115
+ return Array.from(pathSet);
116
+ }
117
+ /**
118
+ * Save a context entry for a session
119
+ */
120
+ async saveContext(sessionId, context) {
121
+ this.ensureInitialized();
122
+ return this.addMemory({
123
+ text: context,
124
+ sourceType: 'conversation',
125
+ sourcePath: `session://${sessionId}`,
126
+ metadataJson: JSON.stringify({ type: 'context' }),
127
+ sessionId,
128
+ });
129
+ }
130
+ /**
131
+ * Get the most recent context entry for a session
132
+ */
133
+ async getContext(sessionId) {
134
+ this.ensureInitialized();
135
+ const results = await this._client.search(`session:${sessionId}`, { topK: 100 });
136
+ // Find the most recent context entry
137
+ let latestContext = null;
138
+ let latestTimestamp = 0;
139
+ for (const result of results) {
140
+ const entry = result.entry;
141
+ if (entry.metadataJson) {
142
+ try {
143
+ const meta = JSON.parse(entry.metadataJson);
144
+ if (meta.type === 'context') {
145
+ const ts = entry.timestamp;
146
+ if (ts > latestTimestamp) {
147
+ latestTimestamp = ts;
148
+ latestContext = adaptMemoryEntry(entry);
149
+ }
150
+ }
151
+ }
152
+ catch {
153
+ // Not a context entry
154
+ }
155
+ }
156
+ }
157
+ return latestContext;
158
+ }
159
+ /**
160
+ * Search memories using query string with optional search options
161
+ */
162
+ async search(query, options) {
163
+ this.ensureInitialized();
164
+ const opts = {
165
+ ...DEFAULT_SEARCH_OPTIONS,
166
+ ...options,
167
+ };
168
+ const results = await this._client.search(query, { topK: opts.topK });
169
+ return results.map((r) => ({
170
+ entry: adaptMemoryEntry(r.entry),
171
+ score: r.score,
172
+ }));
173
+ }
174
+ /**
175
+ * Ensure the system is initialized before performing operations
176
+ */
177
+ ensureInitialized() {
178
+ if (!this.isInitialized()) {
179
+ throw new Error('MemorySystem not initialized. Call initialize() first.');
180
+ }
181
+ }
182
+ /**
183
+ * Get server status
184
+ */
185
+ async getStatus() {
186
+ if (!this._client) {
187
+ return null;
188
+ }
189
+ try {
190
+ this._status = (await this._client.getStatus());
191
+ return this._status;
192
+ }
193
+ catch {
194
+ return this._status;
195
+ }
196
+ }
197
+ /**
198
+ * Check if the memory system is ready
199
+ */
200
+ async isReady() {
201
+ const status = await this.getStatus();
202
+ return status?.memoryReady ?? false;
203
+ }
204
+ /**
205
+ * Check if content already exists (deduplication)
206
+ */
207
+ async contentExists(text) {
208
+ this.ensureInitialized();
209
+ const results = await this._client.search(text, { topK: 1 });
210
+ return results.length > 0;
211
+ }
212
+ /**
213
+ * Search memories using a pre-computed vector
214
+ * Note: memini-ai handles embedding internally
215
+ */
216
+ async searchWithVector(vector, options) {
217
+ this.ensureInitialized();
218
+ // memini-ai doesn't expose vector search directly
219
+ // Fall back to text search
220
+ const opts = {
221
+ ...DEFAULT_SEARCH_OPTIONS,
222
+ ...options,
223
+ };
224
+ // Use vector as seed for approximate search
225
+ const results = await this._client.search(`vector:${vector.slice(0, 10).join(',')}`, { topK: opts.topK });
226
+ return results.map((r) => ({
227
+ entry: adaptMemoryEntry(r.entry),
228
+ score: r.score,
229
+ }));
230
+ }
231
+ /**
232
+ * Get memories similar to a given memory
233
+ */
234
+ async getSimilar(memoryId, options) {
235
+ this.ensureInitialized();
236
+ const memory = await this.getMemory(memoryId);
237
+ if (!memory) {
238
+ return [];
239
+ }
240
+ return this.search(memory.text, options);
241
+ }
242
+ /**
243
+ * Get memory statistics
244
+ */
245
+ async getStats() {
246
+ this.ensureInitialized();
247
+ const status = await this.getStatus();
248
+ return {
249
+ count: status ? Object.keys(status).length : 0, // Placeholder
250
+ };
251
+ }
252
+ }
253
+ /**
254
+ * Map boomerang source type to memini-ai source type
255
+ */
256
+ function mapSourceType(type) {
257
+ switch (type) {
258
+ case 'conversation':
259
+ return 'session';
260
+ case 'file':
261
+ return 'file';
262
+ case 'web':
263
+ return 'web';
264
+ case 'manual':
265
+ return 'boomerang';
266
+ default:
267
+ return 'boomerang';
268
+ }
269
+ }
270
+ /**
271
+ * Map memini-ai source type back to boomerang source type
272
+ */
273
+ function reverseMapSourceType(type) {
274
+ switch (type) {
275
+ case 'session':
276
+ return 'conversation';
277
+ case 'file':
278
+ return 'file';
279
+ case 'web':
280
+ return 'web';
281
+ case 'boomerang':
282
+ return 'manual';
283
+ case 'project':
284
+ return 'manual';
285
+ default:
286
+ return type;
287
+ }
288
+ }
289
+ /**
290
+ * Adapt a raw memory from memini-ai to our MemoryEntry type
291
+ */
292
+ function adaptMemoryEntry(meminiEntry) {
293
+ return {
294
+ id: meminiEntry.id,
295
+ text: meminiEntry.text,
296
+ vector: Array.from(meminiEntry.vector || []),
297
+ sourceType: reverseMapSourceType(meminiEntry.sourceType),
298
+ sourcePath: meminiEntry.sourcePath || '',
299
+ timestamp: meminiEntry.timestamp,
300
+ contentHash: meminiEntry.contentHash || '',
301
+ metadataJson: meminiEntry.metadataJson,
302
+ sessionId: meminiEntry.sessionId,
303
+ projectId: meminiEntry.projectId,
304
+ score: meminiEntry.score,
305
+ trustScore: meminiEntry.trustScore,
306
+ };
307
+ }
308
+ /**
309
+ * Factory function to get the MemorySystem singleton
310
+ */
311
+ export function getMemorySystem() {
312
+ return MemorySystem.getInstance();
313
+ }
314
+ export { MemorySystem };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Knowledge graph integration for memini-ai (Phase 3)
3
+ *
4
+ * Entity extraction, graph queries, and inference chains
5
+ */
6
+ import { getClient } from '../memini-client/index.js';
7
+ /**
8
+ * Execute a formal knowledge graph query
9
+ */
10
+ export async function queryKG(query, client) {
11
+ const mc = client ?? getClient();
12
+ const result = await mc.queryKG({
13
+ entity_a: query.entityA,
14
+ entity_b: query.entityB,
15
+ relationship_types: query.relationshipTypes,
16
+ inference_depth: query.inferenceDepth ?? 1,
17
+ limit: query.limit ?? 100,
18
+ });
19
+ if (!result) {
20
+ return {
21
+ success: false,
22
+ count: 0,
23
+ results: [],
24
+ error: 'Query failed',
25
+ };
26
+ }
27
+ return {
28
+ success: true,
29
+ count: result.entities.length,
30
+ results: result.entities,
31
+ error: undefined,
32
+ };
33
+ }
34
+ /**
35
+ * Extract entities from a specific memory
36
+ */
37
+ export async function extractEntities(memoryId, client) {
38
+ const mc = client ?? getClient();
39
+ const result = await mc.extractEntities(memoryId);
40
+ return {
41
+ success: true,
42
+ memoryId,
43
+ entities: result.map((e) => ({
44
+ id: e.id,
45
+ name: e.name,
46
+ entityType: e.type ?? 'unknown',
47
+ metadata: e.properties ?? {},
48
+ })),
49
+ count: result.length,
50
+ };
51
+ }
52
+ /**
53
+ * Get all connections to/from an entity
54
+ */
55
+ export async function getEntityGraph(entityId, depth = 1, client) {
56
+ const mc = client ?? getClient();
57
+ const result = await mc.getEntityGraph(entityId, depth);
58
+ if (!result) {
59
+ return null;
60
+ }
61
+ return {
62
+ entityId: result.entity.id,
63
+ incoming: result.connections.map((c) => c.target),
64
+ outgoing: result.connections.map((c) => c.target),
65
+ inferred: [],
66
+ };
67
+ }
68
+ /**
69
+ * Find inference paths between two entities
70
+ */
71
+ export async function getInferenceChain(startEntity, endEntity, maxDepth = 3, client) {
72
+ const mc = client ?? getClient();
73
+ const result = await mc.getInferenceChain(startEntity, endEntity, maxDepth);
74
+ return {
75
+ success: true,
76
+ startEntity,
77
+ endEntity,
78
+ paths: result.map((p) => ({
79
+ path: p.path.map((step) => step.entity),
80
+ confidence: p.confidence,
81
+ depth: p.depth,
82
+ })),
83
+ count: result.length,
84
+ error: undefined,
85
+ };
86
+ }
87
+ /**
88
+ * Search for entities by name
89
+ */
90
+ export async function searchEntities(name, limit = 10, client) {
91
+ const mc = client ?? getClient();
92
+ const result = await mc.searchEntities(name, limit);
93
+ return {
94
+ success: true,
95
+ query: name,
96
+ entities: result.map((e) => ({
97
+ id: e.id,
98
+ name: e.name,
99
+ entityType: e.type ?? 'unknown',
100
+ metadata: e.properties ?? {},
101
+ })),
102
+ count: result.length,
103
+ };
104
+ }
105
+ /**
106
+ * Get HTML visualization of the knowledge graph
107
+ */
108
+ export async function getGraphVisualization(limit = 100, client) {
109
+ const mc = client ?? getClient();
110
+ return mc.getGraphVisualization(limit);
111
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Memory entry and search types for memini-ai integration
3
+ * Updated to include trust engine fields from memini-ai
4
+ */
5
+ /** Default search options */
6
+ export const DEFAULT_SEARCH_OPTIONS = {
7
+ strategy: 'tiered',
8
+ topK: 10,
9
+ threshold: 0.7,
10
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tiered loading integration for memini-ai
3
+ *
4
+ * L0: ~100 tokens - project summary for session start
5
+ * L1: ~2K tokens - key decisions for planning tasks
6
+ */
7
+ import { getClient } from '../memini-client/index.js';
8
+ /**
9
+ * Get L0 project summary (~100 tokens)
10
+ *
11
+ * Uses high-trust memories (trust >= 0.5) to generate a concise
12
+ * project summary suitable for session start auto-injection.
13
+ */
14
+ export async function getTier0Summary(forceRefresh = false, client) {
15
+ const mc = client ?? getClient();
16
+ const result = await mc.getTier0Summary(forceRefresh);
17
+ if (!result) {
18
+ return {
19
+ tier: 'L0',
20
+ content: null,
21
+ tokenCount: 0,
22
+ cacheHit: false,
23
+ sourceCount: 0,
24
+ generatedAt: null,
25
+ error: 'Failed to generate summary',
26
+ };
27
+ }
28
+ return {
29
+ tier: 'L0',
30
+ content: result.content,
31
+ tokenCount: result.memoryCount,
32
+ cacheHit: false,
33
+ sourceCount: result.memoryCount,
34
+ generatedAt: result.generatedAt,
35
+ error: undefined,
36
+ };
37
+ }
38
+ /**
39
+ * Get L1 key decisions summary (~2K tokens)
40
+ *
41
+ * Uses promoted memories (trust >= 0.8) to generate a structured
42
+ * summary of key decisions and patterns for planning tasks.
43
+ */
44
+ export async function getTier1Summary(forceRefresh = false, client) {
45
+ const mc = client ?? getClient();
46
+ const result = await mc.getTier1Summary(forceRefresh);
47
+ if (!result) {
48
+ return {
49
+ tier: 'L1',
50
+ content: null,
51
+ tokenCount: 0,
52
+ cacheHit: false,
53
+ sourceCount: 0,
54
+ generatedAt: null,
55
+ error: 'Failed to generate summary',
56
+ };
57
+ }
58
+ return {
59
+ tier: 'L1',
60
+ content: result.content,
61
+ tokenCount: result.memoryCount,
62
+ cacheHit: false,
63
+ sourceCount: result.memoryCount,
64
+ generatedAt: result.generatedAt,
65
+ error: undefined,
66
+ };
67
+ }
68
+ /**
69
+ * Trigger memory extraction manually
70
+ *
71
+ * Extracts facts/decisions/preferences from conversation text
72
+ * using LLM-based automatic extraction.
73
+ */
74
+ export async function triggerExtraction(conversation, client) {
75
+ const mc = client ?? getClient();
76
+ const result = await mc.triggerExtraction(conversation);
77
+ return {
78
+ success: true,
79
+ count: result.length,
80
+ memoryIds: result,
81
+ };
82
+ }
83
+ /**
84
+ * Pre-compression extraction
85
+ *
86
+ * Captures current context and extracts memories before context
87
+ * compression/compaction. Helps preserve important details.
88
+ */
89
+ export async function preconpressExtraction(context, client) {
90
+ const mc = client ?? getClient();
91
+ const result = await mc.preconpressExtraction(context);
92
+ if (!result) {
93
+ return {
94
+ extractionCount: 0,
95
+ memoriesCreated: [],
96
+ contextCaptured: context ?? '',
97
+ };
98
+ }
99
+ return {
100
+ extractionCount: result.extractedMemories.length,
101
+ memoriesCreated: result.extractedMemories,
102
+ contextCaptured: context ?? '',
103
+ };
104
+ }