opencode-swarm-plugin 0.42.9 → 0.44.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 (48) hide show
  1. package/.hive/issues.jsonl +14 -0
  2. package/.turbo/turbo-build.log +2 -2
  3. package/CHANGELOG.md +110 -0
  4. package/README.md +296 -6
  5. package/bin/cass.characterization.test.ts +422 -0
  6. package/bin/swarm.test.ts +683 -0
  7. package/bin/swarm.ts +501 -0
  8. package/dist/contributor-tools.d.ts +42 -0
  9. package/dist/contributor-tools.d.ts.map +1 -0
  10. package/dist/dashboard.d.ts +83 -0
  11. package/dist/dashboard.d.ts.map +1 -0
  12. package/dist/error-enrichment.d.ts +49 -0
  13. package/dist/error-enrichment.d.ts.map +1 -0
  14. package/dist/export-tools.d.ts +76 -0
  15. package/dist/export-tools.d.ts.map +1 -0
  16. package/dist/index.d.ts +14 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +95 -2
  19. package/dist/observability-tools.d.ts +2 -2
  20. package/dist/plugin.js +95 -2
  21. package/dist/query-tools.d.ts +59 -0
  22. package/dist/query-tools.d.ts.map +1 -0
  23. package/dist/replay-tools.d.ts +28 -0
  24. package/dist/replay-tools.d.ts.map +1 -0
  25. package/dist/sessions/agent-discovery.d.ts +59 -0
  26. package/dist/sessions/agent-discovery.d.ts.map +1 -0
  27. package/dist/sessions/index.d.ts +10 -0
  28. package/dist/sessions/index.d.ts.map +1 -0
  29. package/docs/planning/ADR-010-cass-inhousing.md +1215 -0
  30. package/evals/fixtures/cass-baseline.ts +217 -0
  31. package/examples/plugin-wrapper-template.ts +89 -0
  32. package/package.json +1 -1
  33. package/src/contributor-tools.test.ts +133 -0
  34. package/src/contributor-tools.ts +201 -0
  35. package/src/dashboard.test.ts +611 -0
  36. package/src/dashboard.ts +462 -0
  37. package/src/error-enrichment.test.ts +403 -0
  38. package/src/error-enrichment.ts +219 -0
  39. package/src/export-tools.test.ts +476 -0
  40. package/src/export-tools.ts +257 -0
  41. package/src/index.ts +8 -3
  42. package/src/query-tools.test.ts +636 -0
  43. package/src/query-tools.ts +324 -0
  44. package/src/replay-tools.test.ts +496 -0
  45. package/src/replay-tools.ts +240 -0
  46. package/src/sessions/agent-discovery.test.ts +137 -0
  47. package/src/sessions/agent-discovery.ts +112 -0
  48. package/src/sessions/index.ts +15 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * CASS Baseline Response Fixtures
3
+ *
4
+ * These fixtures capture the ACTUAL behavior of the CASS binary tools.
5
+ * DO NOT modify to match desired behavior - these document what the binary DOES.
6
+ *
7
+ * Purpose: Characterization tests for ADR-010 (CASS inhousing).
8
+ * These ensure our inhouse implementation matches the binary's behavior.
9
+ */
10
+
11
+ /**
12
+ * cass stats --json
13
+ * Captured: 2025-12-25
14
+ */
15
+ export const cassStatsBaseline = {
16
+ by_agent: [
17
+ {
18
+ agent: "claude_code",
19
+ count: 137,
20
+ },
21
+ {
22
+ agent: "cursor",
23
+ count: 23,
24
+ },
25
+ {
26
+ agent: "codex",
27
+ count: 2,
28
+ },
29
+ ],
30
+ conversations: 162,
31
+ date_range: {
32
+ newest: "2025-12-08T04:20:36.526+00:00",
33
+ oldest: "2025-07-14T01:14:44.997+00:00",
34
+ },
35
+ db_path:
36
+ "/Users/joel/Library/Application Support/com.coding-agent-search.coding-agent-search/agent_search.db",
37
+ messages: 4213,
38
+ top_workspaces: [
39
+ {
40
+ count: 28,
41
+ workspace:
42
+ "/Users/joel/Code/vercel/academy-vectr-workflow-course-content/external/workflow-builder-starter",
43
+ },
44
+ {
45
+ count: 22,
46
+ workspace: "/Users/joel/Code/vercel/slack-agents-course",
47
+ },
48
+ ],
49
+ } as const;
50
+
51
+ /**
52
+ * cass search "swarm" --limit 2 --json
53
+ * Captured: 2025-12-25
54
+ */
55
+ export const cassSearchBaseline = {
56
+ count: 2,
57
+ cursor: null,
58
+ hits: [
59
+ {
60
+ agent: "claude_code",
61
+ content:
62
+ 'Fixed. The `plugins` key is invalid - OpenCode auto-loads plugins from directories instead.\n\n**Changes:**\n1. ✅ Removed invalid `plugins` array from `opencode.jsonc`\n2. ✅ Created `~/.config/opencode/plugin/` directory\n3. ✅ Symlinked your swarm plugin → `~/.config/opencode/plugin/swarm.js`\n\nThe plugin will now auto-load on startup. Restart OpenCode to pick it up.\n\nSources:\n- [OpenCode Plugins Documentation](https://opencode.ai/docs/plugins/)\n- [OpenCode Config Documentation](https://opencode.ai/docs/config/)',
63
+ created_at: 1765161767083,
64
+ line_number: 9,
65
+ match_type: "exact",
66
+ score: 15.536974906921387,
67
+ snippet:
68
+ "Symlinked your swarm plugin → `~/.config/opencode/plugin/swarm.js`\n\nThe plugin will now auto-load on startup. Restart OpenCode to pick it up.\n\nSources:\n- [OpenC…",
69
+ source_path:
70
+ "/Users/joel/.claude/projects/-Users-joel--config-opencode/ccd64ac6-bca7-40e5-9150-cea58c3788ae.jsonl",
71
+ title:
72
+ "@opencode.jsonc has an invalid plugins key https://opencode.ai/docs/plugins/ https://opencode.ai/doc",
73
+ workspace: "/Users/joel/.config/opencode",
74
+ },
75
+ {
76
+ agent: "claude_code",
77
+ content:
78
+ "I'm ready to help you explore the codebase and design implementation plans. I'm in **READ-ONLY mode** - I can explore files, understand architecture, and create detailed plans, but I cannot and will not modify any files.\n\nI have access to the beads issue tracker (`bd` commands) and can see your current working directory is `/Users/joel/.config/opencode`.\n\n**Current git status shows:**\n- Modified: `.beads/issues.jsonl`, `AGENTS.md`, `command/swarm.md`, `opencode.jsonc`\n- Untracked: `command/swarm-collect.md`, `command/swarm-status.md`, `plugin/`\n\n**What would you like me to explore and plan?**\n\nCommon scenarios I can help with:\n- Designing new command implementations\n- Planning plugin architecture\n- Exploring existing patterns for feature additions\n- Creating implementation strategies for beads issues\n\nLet me know what you need, and I'll dive into the codebase, understand the current architecture, and provide a detailed implementation plan.",
79
+ created_at: 1765161814722,
80
+ line_number: 1,
81
+ match_type: "exact",
82
+ score: 14.522254943847656,
83
+ snippet:
84
+ ".md`, `command/swarm.md`, `opencode.jsonc`\n- Untracked: `command/swarm-collect.md`, `command/swarm-status.md`, `plugin/`\n\n**What would you like me to explore an…",
85
+ source_path:
86
+ "/Users/joel/.claude/projects/-Users-joel--config-opencode/agent-ee2a73ee.jsonl",
87
+ title: "opencode",
88
+ workspace: "/Users/joel/.config/opencode",
89
+ },
90
+ ],
91
+ hits_clamped: false,
92
+ limit: 2,
93
+ max_tokens: null,
94
+ offset: 0,
95
+ query: "swarm",
96
+ request_id: null,
97
+ total_matches: 2,
98
+ } as const;
99
+
100
+ /**
101
+ * cass health (human-readable output)
102
+ * Captured: 2025-12-25
103
+ */
104
+ export const cassHealthHumanBaseline = `✓ Healthy (3ms)
105
+ Note: index stale (older than 300s)`;
106
+
107
+ /**
108
+ * cass stats (human-readable output)
109
+ * Captured: 2025-12-25
110
+ */
111
+ export const cassStatsHumanBaseline = `CASS Index Statistics
112
+ =====================
113
+ Database: /Users/joel/Library/Application Support/com.coding-agent-search.coding-agent-search/agent_search.db
114
+
115
+ Totals:
116
+ Conversations: 162
117
+ Messages: 4213
118
+
119
+ By Agent:
120
+ claude_code: 137
121
+ cursor: 23
122
+ codex: 2
123
+
124
+ Top Workspaces:
125
+ /Users/joel/Code/vercel/academy-vectr-workflow-course-content/external/workflow-builder-starter: 28
126
+ /Users/joel/Code/vercel/slack-agents-course: 22
127
+ /Users/joel/Code/vercel/academy-vectr-workflow-course-content: 22
128
+ /Users/joel/Code/vercel/academy-content: 13
129
+ /Users/joel/Code/joelhooks/trt-buddy: 13
130
+ /Users/joel/Code/vercel/front: 11
131
+ /Users/joel/.config/opencode: 9
132
+ /Users/joel: 6
133
+ /Users/joel/Code/badass-courses/course-builder/apps/ai-hero: 5
134
+ /Users/joel/Code/vercel/front/apps/vercel-academy: 4
135
+
136
+ Date Range: 2025-07-14 to 2025-12-08`;
137
+
138
+ /**
139
+ * cass view <file> -n <line>
140
+ * Captured: 2025-12-25
141
+ *
142
+ * Format: File path header, line indicator with context window, separator, content with line numbers
143
+ */
144
+ export const cassViewBaseline = `File: /Users/joel/.config/swarm-tools/sessions/ses_19yz2iaMpHxY1ddvVq2voC.jsonl
145
+ Line: 1 (context: 5)
146
+ ----------------------------------------
147
+ > 1 | {"session_id":"ses_19yz2iaMpHxY1ddvVq2voC","epic_id":"cell-f2p61v-mjko4d89zdt","timestamp":"2025-12-24T23:51:52.896Z","event_type":"OUTCOME","outcome_type":"subtask_success","payload":{"bead_id":"cell-f2p61v-mjko4d89zdt","duration_ms":0,"files_touched":[],"verification_passed":false,"verification_skipped":true}}
148
+ ----------------------------------------`;
149
+
150
+ /**
151
+ * Error responses (captured from actual failures)
152
+ */
153
+ export const cassErrorBaseline = {
154
+ fileNotFound: {
155
+ error: {
156
+ code: 3,
157
+ hint: null,
158
+ kind: "file-not-found",
159
+ message:
160
+ "File not found: /Users/joel/.config/swarm-tools/sessions/ses_fRrFb7WrNr9K89JBCKd6GV.jsonl",
161
+ retryable: false,
162
+ },
163
+ },
164
+ invalidArgument: {
165
+ error: {
166
+ code: 2,
167
+ hint: {
168
+ common_mistakes: [
169
+ {
170
+ correct: "cass robot-docs",
171
+ wrong: "cass --robot-docs",
172
+ },
173
+ {
174
+ correct: "cass robot-docs commands",
175
+ wrong: "cass --robot-docs=commands",
176
+ },
177
+ {
178
+ correct: "cass robot-docs",
179
+ wrong: "cass robot-docs --robot",
180
+ },
181
+ ],
182
+ error:
183
+ "error: unexpected argument '--robot' found\\n\\nUsage: cass stats [OPTIONS]\\n\\nFor more information, try '--help'.\\n",
184
+ examples: [
185
+ "cass robot-docs commands",
186
+ "cass robot-docs schemas",
187
+ "cass robot-docs examples",
188
+ "cass --robot-help",
189
+ ],
190
+ flag_syntax: {
191
+ correct: ["--limit 5", "--robot", "--json"],
192
+ incorrect: ["-limit 5", "limit=5", "--Limit"],
193
+ },
194
+ hints: [
195
+ "For get robot-mode documentation, try: cass --robot-help",
196
+ ],
197
+ kind: "argument_parsing",
198
+ status: "error",
199
+ },
200
+ kind: "usage",
201
+ message: "Could not parse arguments",
202
+ retryable: false,
203
+ },
204
+ },
205
+ } as const;
206
+
207
+ /**
208
+ * Schema definitions extracted from actual responses
209
+ */
210
+ export type CassStatsResponse = typeof cassStatsBaseline;
211
+ export type CassSearchResponse = typeof cassSearchBaseline;
212
+ export type CassSearchHit = CassSearchResponse["hits"][number];
213
+ export type CassAgentStats = CassStatsResponse["by_agent"][number];
214
+ export type CassWorkspaceStats = CassStatsResponse["top_workspaces"][number];
215
+ export type CassError =
216
+ | typeof cassErrorBaseline.fileNotFound
217
+ | typeof cassErrorBaseline.invalidArgument;
@@ -1028,6 +1028,88 @@ const swarm_get_pattern_insights = tool({
1028
1028
  execute: (args, ctx) => execTool("swarm_get_pattern_insights", args, ctx),
1029
1029
  });
1030
1030
 
1031
+ // =============================================================================
1032
+ // CASS Tools (Cross-Agent Session Search)
1033
+ // =============================================================================
1034
+
1035
+ const cass_search = tool({
1036
+ description: "Search across all AI coding agent histories (Claude, Codex, Cursor, Gemini, Aider, ChatGPT, Cline, OpenCode, Amp, Pi-Agent). Query BEFORE solving problems from scratch - another agent may have already solved it. Returns matching sessions ranked by relevance.",
1037
+ args: {
1038
+ query: tool.schema.string().describe("Search query (e.g., 'authentication error Next.js')"),
1039
+ agent: tool.schema
1040
+ .string()
1041
+ .optional()
1042
+ .describe("Filter by agent name (e.g., 'claude', 'cursor')"),
1043
+ days: tool.schema
1044
+ .number()
1045
+ .optional()
1046
+ .describe("Only search sessions from last N days"),
1047
+ limit: tool.schema
1048
+ .number()
1049
+ .optional()
1050
+ .describe("Max results to return (default: 5)"),
1051
+ fields: tool.schema
1052
+ .string()
1053
+ .optional()
1054
+ .describe("Field selection: 'minimal' for compact output (path, line, agent only)"),
1055
+ },
1056
+ execute: (args, ctx) => execTool("cass_search", args, ctx),
1057
+ });
1058
+
1059
+ const cass_view = tool({
1060
+ description: "View a specific conversation/session from search results. Use source_path from cass_search output.",
1061
+ args: {
1062
+ path: tool.schema
1063
+ .string()
1064
+ .describe("Path to session file (from cass_search results)"),
1065
+ line: tool.schema
1066
+ .number()
1067
+ .optional()
1068
+ .describe("Jump to specific line number"),
1069
+ },
1070
+ execute: (args, ctx) => execTool("cass_view", args, ctx),
1071
+ });
1072
+
1073
+ const cass_expand = tool({
1074
+ description: "Expand context around a specific line in a session. Shows messages before/after.",
1075
+ args: {
1076
+ path: tool.schema
1077
+ .string()
1078
+ .describe("Path to session file"),
1079
+ line: tool.schema
1080
+ .number()
1081
+ .describe("Line number to expand around"),
1082
+ context: tool.schema
1083
+ .number()
1084
+ .optional()
1085
+ .describe("Number of lines before/after to show (default: 5)"),
1086
+ },
1087
+ execute: (args, ctx) => execTool("cass_expand", args, ctx),
1088
+ });
1089
+
1090
+ const cass_health = tool({
1091
+ description: "Check if cass index is healthy. Exit 0 = ready, Exit 1 = needs indexing. Run this before searching.",
1092
+ args: {},
1093
+ execute: (args, ctx) => execTool("cass_health", args, ctx),
1094
+ });
1095
+
1096
+ const cass_index = tool({
1097
+ description: "Build or rebuild the search index. Run this if health check fails or to pick up new sessions.",
1098
+ args: {
1099
+ full: tool.schema
1100
+ .boolean()
1101
+ .optional()
1102
+ .describe("Force full rebuild (default: incremental)"),
1103
+ },
1104
+ execute: (args, ctx) => execTool("cass_index", args, ctx),
1105
+ });
1106
+
1107
+ const cass_stats = tool({
1108
+ description: "Show index statistics - how many sessions, messages, agents indexed.",
1109
+ args: {},
1110
+ execute: (args, ctx) => execTool("cass_stats", args, ctx),
1111
+ });
1112
+
1031
1113
  // =============================================================================
1032
1114
  // Plugin Export
1033
1115
  // =============================================================================
@@ -2064,6 +2146,13 @@ const SwarmPlugin: Plugin = async (
2064
2146
  swarm_get_strategy_insights,
2065
2147
  swarm_get_file_insights,
2066
2148
  swarm_get_pattern_insights,
2149
+ // CASS (Cross-Agent Session Search)
2150
+ cass_search,
2151
+ cass_view,
2152
+ cass_expand,
2153
+ cass_health,
2154
+ cass_index,
2155
+ cass_stats,
2067
2156
  },
2068
2157
 
2069
2158
  // Swarm-aware compaction hook with LLM-powered continuation prompts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.42.9",
3
+ "version": "0.44.0",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Contributor Tools Integration Tests
3
+ *
4
+ * Tests for contributor_lookup tool that fetches GitHub profiles
5
+ * and generates changeset credits.
6
+ */
7
+
8
+ import { afterAll, describe, expect, test } from "bun:test";
9
+ import { closeAllSwarmMail } from "swarm-mail";
10
+ import { contributorTools, resetContributorCache } from "./contributor-tools";
11
+ import { resetMemoryCache } from "./memory-tools";
12
+
13
+ interface ToolContext {
14
+ sessionID: string;
15
+ }
16
+
17
+ describe("contributor tools integration", () => {
18
+ afterAll(async () => {
19
+ resetContributorCache();
20
+ resetMemoryCache();
21
+ await closeAllSwarmMail();
22
+ });
23
+
24
+ test("contributor_lookup tool is registered", () => {
25
+ expect(contributorTools).toHaveProperty("contributor_lookup");
26
+ expect(typeof contributorTools.contributor_lookup.execute).toBe("function");
27
+ });
28
+
29
+ describe("contributor_lookup", () => {
30
+ test("returns formatted credit with name + twitter", async () => {
31
+ const tool = contributorTools.contributor_lookup;
32
+ const result = await tool.execute(
33
+ {
34
+ login: "kentcdodds",
35
+ issue: 42,
36
+ },
37
+ { sessionID: "test-session" } as ToolContext,
38
+ );
39
+
40
+ expect(typeof result).toBe("string");
41
+ const parsed = JSON.parse(result);
42
+
43
+ expect(parsed.login).toBe("kentcdodds");
44
+ expect(parsed.name).toBeDefined();
45
+ expect(parsed.twitter).toBeDefined();
46
+ expect(parsed.credit_line).toContain("Thanks to");
47
+ expect(parsed.credit_line).toContain("reporting #42");
48
+ expect(parsed.credit_line).toContain("@kentcdodds");
49
+ expect(parsed.credit_line).toContain("https://x.com/");
50
+ expect(parsed.memory_stored).toBe(true);
51
+ });
52
+
53
+ test("handles missing twitter gracefully", async () => {
54
+ const tool = contributorTools.contributor_lookup;
55
+
56
+ // Use a user that likely has name but no twitter
57
+ // (we'll test the format logic mainly)
58
+ const result = await tool.execute(
59
+ {
60
+ login: "torvalds", // Linus Torvalds - has name, might not have twitter
61
+ issue: 123,
62
+ },
63
+ { sessionID: "test-session" } as ToolContext,
64
+ );
65
+
66
+ const parsed = JSON.parse(result);
67
+
68
+ expect(parsed.login).toBe("torvalds");
69
+ expect(parsed.name).toBeDefined();
70
+ expect(parsed.credit_line).toContain("Thanks to");
71
+ expect(parsed.credit_line).toContain("reporting #123");
72
+ // Should have GitHub mention if no Twitter
73
+ if (!parsed.twitter) {
74
+ expect(parsed.credit_line).toContain("on GitHub");
75
+ }
76
+ });
77
+
78
+ test("works without issue number", async () => {
79
+ const tool = contributorTools.contributor_lookup;
80
+ const result = await tool.execute(
81
+ {
82
+ login: "kentcdodds",
83
+ },
84
+ { sessionID: "test-session" } as ToolContext,
85
+ );
86
+
87
+ const parsed = JSON.parse(result);
88
+
89
+ expect(parsed.login).toBe("kentcdodds");
90
+ expect(parsed.credit_line).toContain("Thanks to");
91
+ // Should NOT contain "reporting #"
92
+ expect(parsed.credit_line).not.toContain("reporting #");
93
+ });
94
+
95
+ test("stores contributor info in semantic-memory", async () => {
96
+ const tool = contributorTools.contributor_lookup;
97
+ const result = await tool.execute(
98
+ {
99
+ login: "gaearon", // Dan Abramov
100
+ issue: 99,
101
+ },
102
+ { sessionID: "test-session" } as ToolContext,
103
+ );
104
+
105
+ const parsed = JSON.parse(result);
106
+
107
+ // Just verify memory_stored flag - embedding search may be async
108
+ expect(parsed.memory_stored).toBe(true);
109
+ });
110
+
111
+ test("returns all expected fields", async () => {
112
+ const tool = contributorTools.contributor_lookup;
113
+ const result = await tool.execute(
114
+ {
115
+ login: "kentcdodds",
116
+ },
117
+ { sessionID: "test-session" } as ToolContext,
118
+ );
119
+
120
+ const parsed = JSON.parse(result);
121
+
122
+ // Required fields
123
+ expect(parsed).toHaveProperty("login");
124
+ expect(parsed).toHaveProperty("credit_line");
125
+ expect(parsed).toHaveProperty("memory_stored");
126
+
127
+ // Optional fields (may be null but should be present)
128
+ expect(parsed).toHaveProperty("name");
129
+ expect(parsed).toHaveProperty("twitter");
130
+ expect(parsed).toHaveProperty("bio");
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Contributor Tools - GitHub profile extraction for changeset credits
3
+ *
4
+ * Provides contributor_lookup tool for fetching GitHub profiles and
5
+ * generating formatted changeset credit lines. Automatically stores
6
+ * contributor info in semantic-memory for future reference.
7
+ *
8
+ * Based on patterns from gh-issue-triage skill.
9
+ */
10
+
11
+ import { tool } from "@opencode-ai/plugin";
12
+ import { z } from "zod";
13
+ import { getMemoryAdapter } from "./memory-tools";
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ const GitHubUserSchema = z.object({
20
+ login: z.string(),
21
+ name: z.string().nullable(),
22
+ twitter_username: z.string().nullable(),
23
+ blog: z.string().nullable(),
24
+ bio: z.string().nullable(),
25
+ avatar_url: z.string(),
26
+ html_url: z.string(),
27
+ public_repos: z.number().optional(),
28
+ followers: z.number().optional(),
29
+ });
30
+
31
+ type GitHubUser = z.infer<typeof GitHubUserSchema>;
32
+
33
+ interface ContributorResult {
34
+ login: string;
35
+ name: string | null;
36
+ twitter: string | null;
37
+ bio: string | null;
38
+ credit_line: string;
39
+ memory_stored: boolean;
40
+ }
41
+
42
+ interface ToolContext {
43
+ sessionID: string;
44
+ }
45
+
46
+ // ============================================================================
47
+ // Core Functions
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Fetch GitHub user profile via gh CLI
52
+ */
53
+ async function fetchGitHubUser(login: string): Promise<GitHubUser> {
54
+ const result = await Bun.$`gh api users/${login}`.json();
55
+ return GitHubUserSchema.parse(result);
56
+ }
57
+
58
+ /**
59
+ * Format changeset credit line based on available data
60
+ *
61
+ * Hierarchy:
62
+ * 1. Name + Twitter: "Thanks to {Name} ([@twitter](...)) for reporting #{issue}!"
63
+ * 2. Name only: "Thanks to {Name} (@{login} on GitHub) for reporting #{issue}!"
64
+ * 3. Twitter only: "Thanks to [@twitter](...) for reporting #{issue}!"
65
+ * 4. Fallback: "Thanks to @{login} for reporting #{issue}!"
66
+ */
67
+ function formatCreditLine(user: GitHubUser, issueNumber?: number): string {
68
+ const issueText = issueNumber ? `reporting #${issueNumber}` : "the report";
69
+
70
+ // PREFERRED: Full name + Twitter (best for engagement)
71
+ if (user.name && user.twitter_username) {
72
+ return `Thanks to ${user.name} ([@${user.twitter_username}](https://x.com/${user.twitter_username})) for ${issueText}!`;
73
+ }
74
+
75
+ // Twitter only (no name available)
76
+ if (user.twitter_username) {
77
+ return `Thanks to [@${user.twitter_username}](https://x.com/${user.twitter_username}) for ${issueText}!`;
78
+ }
79
+
80
+ // Name only (no Twitter)
81
+ if (user.name) {
82
+ return `Thanks to ${user.name} (@${user.login} on GitHub) for ${issueText}!`;
83
+ }
84
+
85
+ // Fallback: GitHub username only
86
+ return `Thanks to @${user.login} for ${issueText}!`;
87
+ }
88
+
89
+ /**
90
+ * Store contributor info in semantic-memory
91
+ */
92
+ async function storeContributorMemory(
93
+ user: GitHubUser,
94
+ issueNumber?: number,
95
+ ): Promise<boolean> {
96
+ try {
97
+ const adapter = await getMemoryAdapter();
98
+
99
+ const twitterPart = user.twitter_username
100
+ ? ` (@${user.twitter_username} on Twitter)`
101
+ : "";
102
+ const issuePart = issueNumber ? `. Filed issue #${issueNumber}` : "";
103
+ const bioPart = user.bio ? `. Bio: '${user.bio}'` : "";
104
+
105
+ const tags = [
106
+ "contributor",
107
+ user.login,
108
+ issueNumber ? `issue-${issueNumber}` : null,
109
+ ]
110
+ .filter(Boolean)
111
+ .join(",");
112
+
113
+ const information = `Contributor @${user.login}: ${user.name || user.login}${twitterPart}${issuePart}${bioPart}`;
114
+
115
+ await adapter.store({
116
+ information,
117
+ tags,
118
+ });
119
+
120
+ return true;
121
+ } catch (error) {
122
+ console.error("Failed to store contributor memory:", error);
123
+ return false;
124
+ }
125
+ }
126
+
127
+ // ============================================================================
128
+ // Cache Management
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Reset cache for testing
133
+ */
134
+ export function resetContributorCache(): void {
135
+ // Currently no cache, but keeping this for consistency with other tools
136
+ }
137
+
138
+ // ============================================================================
139
+ // Plugin Tools
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Look up GitHub contributor and generate changeset credit
144
+ */
145
+ export const contributor_lookup = tool({
146
+ description:
147
+ "Fetch GitHub contributor profile and generate formatted changeset credit. Automatically stores contributor info in semantic-memory. Returns login, name, twitter, bio, and ready-to-paste credit_line.",
148
+ args: {
149
+ login: tool.schema.string().describe("GitHub username (required)"),
150
+ issue: tool.schema
151
+ .number()
152
+ .optional()
153
+ .describe("Issue number for context (optional)"),
154
+ },
155
+ async execute(
156
+ args: { login: string; issue?: number },
157
+ _ctx: ToolContext,
158
+ ): Promise<string> {
159
+ try {
160
+ // Fetch GitHub profile
161
+ const user = await fetchGitHubUser(args.login);
162
+
163
+ // Format credit line
164
+ const creditLine = formatCreditLine(user, args.issue);
165
+
166
+ // Store in semantic-memory
167
+ const memoryStored = await storeContributorMemory(user, args.issue);
168
+
169
+ // Build result
170
+ const result: ContributorResult = {
171
+ login: user.login,
172
+ name: user.name,
173
+ twitter: user.twitter_username,
174
+ bio: user.bio,
175
+ credit_line: creditLine,
176
+ memory_stored: memoryStored,
177
+ };
178
+
179
+ return JSON.stringify(result, null, 2);
180
+ } catch (error) {
181
+ if (error instanceof Error) {
182
+ return JSON.stringify({
183
+ error: error.message,
184
+ login: args.login,
185
+ });
186
+ }
187
+ return JSON.stringify({
188
+ error: "Unknown error fetching contributor",
189
+ login: args.login,
190
+ });
191
+ }
192
+ },
193
+ });
194
+
195
+ // ============================================================================
196
+ // Exports
197
+ // ============================================================================
198
+
199
+ export const contributorTools = {
200
+ contributor_lookup,
201
+ } as const;