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.
- package/.hive/issues.jsonl +14 -0
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +110 -0
- package/README.md +296 -6
- package/bin/cass.characterization.test.ts +422 -0
- package/bin/swarm.test.ts +683 -0
- package/bin/swarm.ts +501 -0
- package/dist/contributor-tools.d.ts +42 -0
- package/dist/contributor-tools.d.ts.map +1 -0
- package/dist/dashboard.d.ts +83 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/error-enrichment.d.ts +49 -0
- package/dist/error-enrichment.d.ts.map +1 -0
- package/dist/export-tools.d.ts +76 -0
- package/dist/export-tools.d.ts.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -2
- package/dist/observability-tools.d.ts +2 -2
- package/dist/plugin.js +95 -2
- package/dist/query-tools.d.ts +59 -0
- package/dist/query-tools.d.ts.map +1 -0
- package/dist/replay-tools.d.ts +28 -0
- package/dist/replay-tools.d.ts.map +1 -0
- package/dist/sessions/agent-discovery.d.ts +59 -0
- package/dist/sessions/agent-discovery.d.ts.map +1 -0
- package/dist/sessions/index.d.ts +10 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/docs/planning/ADR-010-cass-inhousing.md +1215 -0
- package/evals/fixtures/cass-baseline.ts +217 -0
- package/examples/plugin-wrapper-template.ts +89 -0
- package/package.json +1 -1
- package/src/contributor-tools.test.ts +133 -0
- package/src/contributor-tools.ts +201 -0
- package/src/dashboard.test.ts +611 -0
- package/src/dashboard.ts +462 -0
- package/src/error-enrichment.test.ts +403 -0
- package/src/error-enrichment.ts +219 -0
- package/src/export-tools.test.ts +476 -0
- package/src/export-tools.ts +257 -0
- package/src/index.ts +8 -3
- package/src/query-tools.test.ts +636 -0
- package/src/query-tools.ts +324 -0
- package/src/replay-tools.test.ts +496 -0
- package/src/replay-tools.ts +240 -0
- package/src/sessions/agent-discovery.test.ts +137 -0
- package/src/sessions/agent-discovery.ts +112 -0
- 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
|
@@ -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;
|