@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,148 @@
1
+ /**
2
+ * Trust engine integration for memini-ai
3
+ *
4
+ * Provides trust scoring and adjustment for memory entries
5
+ */
6
+ import { getClient } from '../memini-client/index.js';
7
+ const TRUST_SIGNALS = ['agent_used', 'agent_ignored', 'user_corrected', 'user_confirmed'];
8
+ /**
9
+ * Get trust score for a memory entry
10
+ */
11
+ export async function getTrustScore(memoryId, client) {
12
+ const mc = client ?? getClient();
13
+ const result = await mc.getTrustScore(memoryId);
14
+ return {
15
+ id: memoryId,
16
+ trustScore: result ?? null,
17
+ trustLevel: null,
18
+ retrievalCount: null,
19
+ isArchived: null,
20
+ };
21
+ }
22
+ /**
23
+ * Adjust trust score based on feedback signal
24
+ */
25
+ export async function adjustTrust(memoryId, signal, client) {
26
+ if (!TRUST_SIGNALS.includes(signal)) {
27
+ throw new Error(`Invalid trust signal: ${signal}. Must be one of: ${TRUST_SIGNALS.join(', ')}`);
28
+ }
29
+ const mc = client ?? getClient();
30
+ await mc.adjustTrust(memoryId, signal);
31
+ return {
32
+ success: true,
33
+ memoryId,
34
+ oldScore: null,
35
+ newScore: null,
36
+ signal,
37
+ action: null,
38
+ };
39
+ }
40
+ /**
41
+ * List archived memories (trust below archive threshold)
42
+ */
43
+ export async function listArchived(limit = 50, offset = 0, client) {
44
+ const mc = client ?? getClient();
45
+ const result = await mc.listArchived(limit, offset);
46
+ return result.map((e) => ({
47
+ id: e.id,
48
+ text: e.text,
49
+ vector: Array.from(e.vector || []),
50
+ sourceType: e.sourceType,
51
+ sourcePath: e.sourcePath || '',
52
+ timestamp: e.timestamp,
53
+ contentHash: e.contentHash || '',
54
+ }));
55
+ }
56
+ /**
57
+ * Get decay engine status
58
+ */
59
+ export async function getDecayStatus(client) {
60
+ const mc = client ?? getClient();
61
+ const result = await mc.getDecayStatus();
62
+ if (!result) {
63
+ return {
64
+ enabled: false,
65
+ fadingMemories: [],
66
+ };
67
+ }
68
+ return {
69
+ enabled: result.enabled,
70
+ decayStats: {
71
+ totalMemories: result.totalMemories,
72
+ archivedCount: result.archivedCount,
73
+ fadingCount: result.fadingCount,
74
+ },
75
+ fadingMemories: [],
76
+ };
77
+ }
78
+ /**
79
+ * List fading memories (approaching archive threshold)
80
+ */
81
+ export async function listFadingMemories(limit = 20, client) {
82
+ const mc = client ?? getClient();
83
+ const result = await mc.listFadingMemories(limit);
84
+ return result.map((e) => ({
85
+ id: e.id,
86
+ text: e.text,
87
+ vector: Array.from(e.vector || []),
88
+ sourceType: e.sourceType,
89
+ sourcePath: e.sourcePath || '',
90
+ timestamp: e.timestamp,
91
+ contentHash: e.contentHash || '',
92
+ }));
93
+ }
94
+ /**
95
+ * Adjust decay rate for a specific memory
96
+ */
97
+ export async function adjustDecayRate(memoryId, decayRate, client) {
98
+ // Clamp decay rate to valid range
99
+ const clampedRate = Math.max(0.1, Math.min(10.0, decayRate));
100
+ const mc = client ?? getClient();
101
+ await mc.adjustDecayRate(memoryId, clampedRate);
102
+ return {
103
+ success: true,
104
+ memoryId,
105
+ newDecayRate: clampedRate,
106
+ message: 'Decay rate adjusted',
107
+ };
108
+ }
109
+ /**
110
+ * Trigger memory consolidation
111
+ */
112
+ export async function triggerConsolidation(force = false, client) {
113
+ const mc = client ?? getClient();
114
+ const result = await mc.triggerConsolidation(force);
115
+ if (!result) {
116
+ return {
117
+ success: false,
118
+ pairsFound: 0,
119
+ pairsMerged: 0,
120
+ memoriesConsolidated: 0,
121
+ };
122
+ }
123
+ return {
124
+ success: true,
125
+ pairsFound: 0,
126
+ pairsMerged: result.mergedCount,
127
+ memoriesConsolidated: result.mergedCount,
128
+ };
129
+ }
130
+ /**
131
+ * Adapt a raw memory from memini-ai to our MemoryEntry type
132
+ */
133
+ function adaptMemoryEntry(meminiEntry) {
134
+ return {
135
+ id: meminiEntry.id,
136
+ text: meminiEntry.text,
137
+ vector: Array.from(meminiEntry.vector || []),
138
+ sourceType: meminiEntry.sourceType,
139
+ sourcePath: meminiEntry.sourcePath || '',
140
+ timestamp: meminiEntry.timestamp,
141
+ contentHash: meminiEntry.contentHash || '',
142
+ metadataJson: meminiEntry.metadataJson,
143
+ sessionId: meminiEntry.sessionId,
144
+ projectId: meminiEntry.projectId,
145
+ score: meminiEntry.score,
146
+ trustScore: meminiEntry.trustScore,
147
+ };
148
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Protocol Types for Boomerang v3
3
+ *
4
+ * Extended with memory features: trust scores, contradictions, tiered loading
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@veedubin/boomerang-v3",
3
+ "version": "0.1.0",
4
+ "description": "Multi-agent orchestration plugin for OpenCode with memini-ai memory",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Veedubin/Boomerang-v3"
11
+ },
12
+ "homepage": "https://github.com/Veedubin/Boomerang-v3#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Veedubin/Boomerang-v3/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "lint": "eslint src --ext .ts"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "typescript": "^5.7.0",
36
+ "vitest": "^3.0.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.0.0"
40
+ }
41
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Boomerang Asset Loader v3.0.0
3
+ *
4
+ * Loads agents and skills from the bundled directories.
5
+ * Self-contained - no cross-package imports.
6
+ */
7
+
8
+ import { readFileSync, readdirSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import type { AgentDefinition, SkillDefinition } from './types.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ // Cache for loaded assets
17
+ let agentsCache: AgentDefinition[] | null = null;
18
+ let skillsCache: SkillDefinition[] | null = null;
19
+
20
+ /**
21
+ * Parse YAML frontmatter from markdown content
22
+ */
23
+ function parseFrontmatter(content: string): Record<string, string> {
24
+ const frontmatter: Record<string, string> = {};
25
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
26
+
27
+ if (!match) {
28
+ return frontmatter;
29
+ }
30
+
31
+ const lines = match[1].split('\n');
32
+ let currentKey = '';
33
+ let currentValue = '';
34
+ let inMultiline = false;
35
+
36
+ for (const line of lines) {
37
+ if (inMultiline) {
38
+ if (line.startsWith(' ') || line.startsWith('\t')) {
39
+ currentValue += '\n' + line.replace(/^[ ]{4}|^\t/, '');
40
+ } else {
41
+ frontmatter[currentKey] = currentValue.trim();
42
+ inMultiline = false;
43
+ }
44
+ }
45
+
46
+ const keyMatch = line.match(/^(\w+):\s*(.*)/);
47
+ if (keyMatch) {
48
+ currentKey = keyMatch[1];
49
+ currentValue = keyMatch[2];
50
+
51
+ if (currentValue === '' || currentValue === '|') {
52
+ inMultiline = true;
53
+ } else {
54
+ frontmatter[currentKey] = currentValue.trim();
55
+ }
56
+ }
57
+ }
58
+
59
+ return frontmatter;
60
+ }
61
+
62
+ /**
63
+ * Extract content after frontmatter (the main body)
64
+ */
65
+ function extractContent(content: string): string {
66
+ const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)/);
67
+ return match ? match[1].trim() : content;
68
+ }
69
+
70
+ /**
71
+ * Parse skills array from frontmatter value
72
+ */
73
+ function parseSkillsArray(frontmatterValue: string): string[] {
74
+ const skillsMatch = frontmatterValue.match(/\[([\s\S]*?)\]/);
75
+ if (!skillsMatch) {
76
+ return [];
77
+ }
78
+
79
+ const skillsContent = skillsMatch[1];
80
+ return skillsContent
81
+ .split('\n')
82
+ .map(s => s.replace(/^\s*["']|["']\s*$/g, '').trim())
83
+ .filter(s => s.length > 0);
84
+ }
85
+
86
+ /**
87
+ * Load all agents from the agents/ directory
88
+ */
89
+ export function loadAgents(): AgentDefinition[] {
90
+ if (agentsCache) {
91
+ return agentsCache;
92
+ }
93
+
94
+ const agents: AgentDefinition[] = [];
95
+ const agentsDir = join(__dirname, '..', 'agents');
96
+
97
+ try {
98
+ const files = readdirSync(agentsDir).filter(f => f.endsWith('.md'));
99
+
100
+ for (const file of files) {
101
+ const filePath = join(agentsDir, file);
102
+ const content = readFileSync(filePath, 'utf-8');
103
+ const frontmatter = parseFrontmatter(content);
104
+ const systemPrompt = extractContent(content);
105
+
106
+ const name = file.replace(/\.md$/, '');
107
+ const skills = frontmatter.skills ? parseSkillsArray(frontmatter.skills) : [];
108
+
109
+ agents.push({
110
+ name,
111
+ description: frontmatter.description || '',
112
+ systemPrompt,
113
+ skills
114
+ });
115
+ }
116
+ } catch (error) {
117
+ console.error('Error loading agents:', error);
118
+ }
119
+
120
+ agentsCache = agents;
121
+ return agents;
122
+ }
123
+
124
+ /**
125
+ * Load all skills from the skills/ subdirectories
126
+ */
127
+ export function loadSkills(): SkillDefinition[] {
128
+ if (skillsCache) {
129
+ return skillsCache;
130
+ }
131
+
132
+ const skills: SkillDefinition[] = [];
133
+ const skillsDir = join(__dirname, '..', 'skills');
134
+
135
+ try {
136
+ const skillDirs = readdirSync(skillsDir).filter(f => {
137
+ try {
138
+ return readdirSync(join(skillsDir, f)).some(file => file === 'SKILL.md');
139
+ } catch {
140
+ return false;
141
+ }
142
+ });
143
+
144
+ for (const dir of skillDirs) {
145
+ const skillPath = join(skillsDir, dir, 'SKILL.md');
146
+ const content = readFileSync(skillPath, 'utf-8');
147
+ const frontmatter = parseFrontmatter(content);
148
+ const instructions = extractContent(content);
149
+ const name = frontmatter.name || dir;
150
+
151
+ skills.push({
152
+ name,
153
+ description: frontmatter.description || '',
154
+ instructions
155
+ });
156
+ }
157
+ } catch (error) {
158
+ console.error('Error loading skills:', error);
159
+ }
160
+
161
+ skillsCache = skills;
162
+ return skills;
163
+ }
164
+
165
+ /**
166
+ * Get a specific agent by name
167
+ */
168
+ export function getAgent(name: string): AgentDefinition | undefined {
169
+ const agents = loadAgents();
170
+ return agents.find(a => a.name === name);
171
+ }
172
+
173
+ /**
174
+ * Get a specific skill by name
175
+ */
176
+ export function getSkill(name: string): SkillDefinition | undefined {
177
+ const skills = loadSkills();
178
+ return skills.find(s => s.name === name);
179
+ }
180
+
181
+ /**
182
+ * Clear the cache (useful for testing)
183
+ */
184
+ export function clearCache(): void {
185
+ agentsCache = null;
186
+ skillsCache = null;
187
+ }
188
+
189
+ /**
190
+ * List available agents
191
+ */
192
+ export function listAvailableAgents(): string[] {
193
+ return loadAgents().map(a => a.name);
194
+ }
195
+
196
+ /**
197
+ * List available skills
198
+ */
199
+ export function listAvailableSkills(): string[] {
200
+ return loadSkills().map(s => s.name);
201
+ }
@@ -0,0 +1,77 @@
1
+ import { GitStatus, GitCommitResult } from './types.js';
2
+
3
+ export async function checkGitStatus(
4
+ $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>
5
+ ): Promise<GitStatus> {
6
+ try {
7
+ const statusResult = await $`git status --porcelain`;
8
+ const files = (statusResult as { stdout: string }).stdout
9
+ .split('\n')
10
+ .filter((line: string) => line.trim().length > 0)
11
+ .map((line: string) => line.slice(3));
12
+ const branchResult = await $`git branch --show-current`;
13
+ const branch = (branchResult as { stdout: string }).stdout.trim();
14
+ return {
15
+ isDirty: files.length > 0,
16
+ files,
17
+ branch,
18
+ ahead: 0,
19
+ behind: 0,
20
+ };
21
+ } catch {
22
+ return { isDirty: false, files: [], branch: '', ahead: 0, behind: 0 };
23
+ }
24
+ }
25
+
26
+ export async function commitCheckpoint(
27
+ $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>,
28
+ message = 'wip: checkpoint'
29
+ ): Promise<GitCommitResult> {
30
+ try {
31
+ await $`git add -A`;
32
+ const result = await $`git commit -m ${message}`;
33
+ const hashMatch = (result as { stdout: string }).stdout.match(/\[.+?\s+([a-f0-9]+)\]/);
34
+ return {
35
+ success: true,
36
+ hash: hashMatch?.[1],
37
+ message,
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ success: false,
42
+ error: error instanceof Error ? error.message : String(error),
43
+ };
44
+ }
45
+ }
46
+
47
+ export async function commitWithMessage(
48
+ $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>,
49
+ message: string
50
+ ): Promise<GitCommitResult> {
51
+ try {
52
+ await $`git add -A`;
53
+ const result = await $`git commit -m ${message}`;
54
+ const hashMatch = (result as { stdout: string }).stdout.match(/\[.+?\s+([a-f0-9]+)\]/);
55
+ return {
56
+ success: true,
57
+ hash: hashMatch?.[1],
58
+ message,
59
+ };
60
+ } catch (error) {
61
+ return {
62
+ success: false,
63
+ error: error instanceof Error ? error.message : String(error),
64
+ };
65
+ }
66
+ }
67
+
68
+ export function generateCommitMessage(prompt: string): string {
69
+ const lower = prompt.toLowerCase();
70
+ let prefix = 'feat:';
71
+ if (lower.includes('fix') || lower.includes('bug')) prefix = 'fix:';
72
+ else if (lower.includes('test')) prefix = 'test:';
73
+ else if (lower.includes('doc')) prefix = 'docs:';
74
+ else if (lower.includes('refactor')) prefix = 'refactor:';
75
+ const summary = prompt.split('\n')[0].slice(0, 72);
76
+ return `${prefix} ${summary}`;
77
+ }