opencodekit 0.15.11 → 0.15.13
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/README.md +4 -4
- package/dist/index.js +1 -1
- package/dist/template/.opencode/README.md +7 -4
- package/dist/template/.opencode/agent/scout.md +97 -26
- package/dist/template/.opencode/command/research-ui.md +1 -1
- package/dist/template/.opencode/command/research.md +1 -1
- package/dist/template/.opencode/memory/observations/2026-01-28-decision-gh-grep-mcp-wrapper-vs-native-grep-searc.md +21 -0
- package/dist/template/.opencode/memory/research/opencode-mcp-bug-report.md +5 -2
- package/dist/template/.opencode/opencode.json +39 -155
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plans/1768385996691-silent-wizard.md +12 -2
- package/dist/template/.opencode/plugin/copilot-auth.ts +104 -77
- package/dist/template/.opencode/skill/source-code-research/SKILL.md +8 -8
- package/dist/template/.opencode/skill/tool-priority/SKILL.md +8 -6
- package/dist/template/.opencode/tool/context7-query-docs.ts +89 -0
- package/dist/template/.opencode/tool/context7-resolve-library-id.ts +113 -0
- package/dist/template/.opencode/tool/grep-search.ts +135 -0
- package/package.json +1 -1
|
@@ -38,7 +38,16 @@ If memory returns high-confidence findings on this exact topic, synthesize and r
|
|
|
38
38
|
| Priority | Tool | Use Case | Speed |
|
|
39
39
|
|----------|------|----------|-------|
|
|
40
40
|
| 1 | memory-search | Past research findings | Instant |
|
|
41
|
-
|
|
|
41
|
+
| Priority | Tool | Use Case | Speed |
|
|
42
|
+
|----------|------|----------|-------|
|
|
43
|
+
| 1 | memory-search | Past research findings | Instant |
|
|
44
|
+
| 2 | context7_resolve-library-id | Resolve library names to IDs | Fast |
|
|
45
|
+
| 3 | context7_query-docs | Official library docs | Fast |
|
|
46
|
+
| 4 | codesearch | Exa Code API for SDK/library patterns | Fast |
|
|
47
|
+
| 5 | grep-search | Cross-repo GitHub code search | Medium |
|
|
48
|
+
| 6 | webfetch | Specific doc URLs, READMEs, changelogs | Medium |
|
|
49
|
+
| 7 | opensrc + LSP | Clone & analyze source code | Slow |
|
|
50
|
+
| 8 | websearch | Tutorials, blog posts, recent news | Slow |
|
|
42
51
|
| 3 | codesearch | Usage patterns in real code | Fast |
|
|
43
52
|
| 4 | gh_grep | Cross-repo deep code search | Medium |
|
|
44
53
|
| 5 | webfetch | Specific doc URLs, READMEs, changelogs | Medium |
|
|
@@ -76,7 +85,8 @@ webfetch({ url: "https://docs.example.com/api/authentication", format: "markdown
|
|
|
76
85
|
**When to use:**
|
|
77
86
|
|
|
78
87
|
- User provides a specific URL
|
|
79
|
-
-
|
|
88
|
+
- context7_resolve-library-id returns a library ID
|
|
89
|
+
- context7_query-docs returns a doc link worth fetching
|
|
80
90
|
- Need CHANGELOG or release notes
|
|
81
91
|
- GitHub README has details not in context7
|
|
82
92
|
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
7
7
|
|
|
8
|
-
const CLIENT_ID = "
|
|
8
|
+
const CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
9
|
+
|
|
10
|
+
// Add a small safety buffer when polling to avoid hitting the server
|
|
11
|
+
// slightly too early due to clock skew / timer drift.
|
|
12
|
+
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000; // 3 seconds
|
|
9
13
|
|
|
10
14
|
const HEADERS = {
|
|
11
15
|
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
@@ -40,11 +44,12 @@ function getUrls(domain: string) {
|
|
|
40
44
|
return {
|
|
41
45
|
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
42
46
|
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
43
|
-
COPILOT_API_KEY_URL: `https://api.github.com/copilot_internal/v2/token`,
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
|
|
52
|
+
export const CopilotAuthPlugin: Plugin = async ({ client: _client }) => {
|
|
48
53
|
return {
|
|
49
54
|
auth: {
|
|
50
55
|
provider: "github-copilot",
|
|
@@ -52,6 +57,11 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
52
57
|
const info = await getAuth();
|
|
53
58
|
if (!info || info.type !== "oauth") return {};
|
|
54
59
|
|
|
60
|
+
const enterpriseUrl = info.enterpriseUrl;
|
|
61
|
+
const baseURL = enterpriseUrl
|
|
62
|
+
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
55
65
|
if (provider && provider.models) {
|
|
56
66
|
for (const model of Object.values(provider.models)) {
|
|
57
67
|
model.cost = {
|
|
@@ -62,71 +72,47 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
62
72
|
write: 0,
|
|
63
73
|
},
|
|
64
74
|
};
|
|
75
|
+
|
|
76
|
+
// Sync with official: Handle Claude routing and SDK mapping
|
|
77
|
+
const base =
|
|
78
|
+
baseURL ?? model.api.url ?? "https://api.githubcopilot.com";
|
|
79
|
+
const isClaude = model.id.includes("claude");
|
|
80
|
+
|
|
81
|
+
let url = base;
|
|
82
|
+
if (isClaude) {
|
|
83
|
+
if (!url.endsWith("/v1")) {
|
|
84
|
+
url = url.endsWith("/") ? `${url}v1` : `${url}/v1`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
model.api.url = url;
|
|
89
|
+
model.api.npm = isClaude
|
|
90
|
+
? "@ai-sdk/anthropic"
|
|
91
|
+
: "@ai-sdk/github-copilot";
|
|
65
92
|
}
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
const enterpriseUrl = info.enterpriseUrl;
|
|
69
|
-
const baseURL = enterpriseUrl
|
|
70
|
-
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
71
|
-
: "https://api.githubcopilot.com";
|
|
72
|
-
|
|
73
95
|
return {
|
|
74
|
-
baseURL,
|
|
75
96
|
apiKey: "",
|
|
76
97
|
async fetch(input, init) {
|
|
77
98
|
const info = await getAuth();
|
|
78
|
-
if (info.type !== "oauth") return
|
|
79
|
-
if (!info.access) {
|
|
80
|
-
const domain = info.enterpriseUrl
|
|
81
|
-
? normalizeDomain(info.enterpriseUrl)
|
|
82
|
-
: "github.com";
|
|
83
|
-
const urls = getUrls(domain);
|
|
84
|
-
|
|
85
|
-
const response = await fetch(urls.COPILOT_API_KEY_URL, {
|
|
86
|
-
headers: {
|
|
87
|
-
Accept: "application/json",
|
|
88
|
-
Authorization: `Bearer ${info.refresh}`,
|
|
89
|
-
...HEADERS,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
throw new Error(`Token refresh failed: ${response.status}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const tokenData = await response.json();
|
|
98
|
-
|
|
99
|
-
const saveProviderID = info.enterpriseUrl
|
|
100
|
-
? "github-copilot-enterprise"
|
|
101
|
-
: "github-copilot";
|
|
102
|
-
await client.auth.set({
|
|
103
|
-
path: {
|
|
104
|
-
id: saveProviderID,
|
|
105
|
-
},
|
|
106
|
-
body: {
|
|
107
|
-
type: "oauth",
|
|
108
|
-
refresh: info.refresh,
|
|
109
|
-
access: tokenData.token,
|
|
110
|
-
expires: tokenData.expires_at * 1000 - 5 * 60 * 1000,
|
|
111
|
-
...(info.enterpriseUrl && {
|
|
112
|
-
enterpriseUrl: info.enterpriseUrl,
|
|
113
|
-
}),
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
info.access = tokenData.token;
|
|
117
|
-
}
|
|
99
|
+
if (info.type !== "oauth") return fetch(input, init);
|
|
118
100
|
|
|
119
101
|
let isAgentCall = false;
|
|
120
102
|
let isVisionRequest = false;
|
|
121
103
|
try {
|
|
122
104
|
const body =
|
|
123
|
-
typeof init
|
|
105
|
+
typeof init?.body === "string"
|
|
124
106
|
? JSON.parse(init.body)
|
|
125
|
-
: init
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
: init?.body;
|
|
108
|
+
|
|
109
|
+
const url = input.toString();
|
|
110
|
+
|
|
111
|
+
// Completions API
|
|
112
|
+
if (body?.messages && url.includes("completions")) {
|
|
113
|
+
// Keep local logic: detect if any message is assistant/tool
|
|
114
|
+
isAgentCall = body.messages.some((msg: any) =>
|
|
115
|
+
["tool", "assistant"].includes(msg.role),
|
|
130
116
|
);
|
|
131
117
|
isVisionRequest = body.messages.some(
|
|
132
118
|
(msg: any) =>
|
|
@@ -135,34 +121,58 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
135
121
|
);
|
|
136
122
|
}
|
|
137
123
|
|
|
124
|
+
// Responses API
|
|
138
125
|
if (body?.input) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
126
|
+
isAgentCall = body.input.some(
|
|
127
|
+
(item: any) =>
|
|
128
|
+
item?.role === "assistant" ||
|
|
129
|
+
(item?.type &&
|
|
130
|
+
RESPONSES_API_ALTERNATE_INPUT_TYPES.includes(item.type)),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
isVisionRequest = body.input.some(
|
|
134
|
+
(item: any) =>
|
|
135
|
+
Array.isArray(item?.content) &&
|
|
136
|
+
item.content.some(
|
|
137
|
+
(part: any) => part.type === "input_image",
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Messages API (Anthropic style)
|
|
143
|
+
if (body?.messages && !url.includes("completions")) {
|
|
144
|
+
isAgentCall = body.messages.some((msg: any) =>
|
|
145
|
+
["tool", "assistant"].includes(msg.role),
|
|
146
|
+
);
|
|
147
|
+
isVisionRequest = body.messages.some(
|
|
148
|
+
(item: any) =>
|
|
149
|
+
Array.isArray(item?.content) &&
|
|
150
|
+
item.content.some(
|
|
151
|
+
(part: any) =>
|
|
152
|
+
part?.type === "image" ||
|
|
153
|
+
(part?.type === "tool_result" &&
|
|
154
|
+
Array.isArray(part?.content) &&
|
|
155
|
+
part.content.some(
|
|
156
|
+
(nested: any) => nested?.type === "image",
|
|
157
|
+
)),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
152
160
|
}
|
|
153
161
|
} catch {}
|
|
154
162
|
|
|
155
|
-
const headers = {
|
|
156
|
-
|
|
163
|
+
const headers: Record<string, string> = {
|
|
164
|
+
"x-initiator": isAgentCall ? "agent" : "user",
|
|
165
|
+
...(init?.headers as Record<string, string>),
|
|
157
166
|
...HEADERS,
|
|
158
|
-
Authorization: `Bearer ${info.
|
|
167
|
+
Authorization: `Bearer ${info.refresh}`,
|
|
159
168
|
"Openai-Intent": "conversation-edits",
|
|
160
|
-
"X-Initiator": isAgentCall ? "agent" : "user",
|
|
161
169
|
};
|
|
170
|
+
|
|
162
171
|
if (isVisionRequest) {
|
|
163
172
|
headers["Copilot-Vision-Request"] = "true";
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
// Official only deletes lowercase "authorization"
|
|
166
176
|
delete headers["x-api-key"];
|
|
167
177
|
delete headers["authorization"];
|
|
168
178
|
|
|
@@ -286,7 +296,7 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
286
296
|
} = {
|
|
287
297
|
type: "success",
|
|
288
298
|
refresh: data.access_token,
|
|
289
|
-
access:
|
|
299
|
+
access: data.access_token,
|
|
290
300
|
expires: 0,
|
|
291
301
|
};
|
|
292
302
|
|
|
@@ -299,16 +309,33 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
if (data.error === "authorization_pending") {
|
|
302
|
-
await
|
|
303
|
-
|
|
312
|
+
await sleep(
|
|
313
|
+
deviceData.interval * 1000 +
|
|
314
|
+
OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
304
315
|
);
|
|
305
316
|
continue;
|
|
306
317
|
}
|
|
307
318
|
|
|
319
|
+
if (data.error === "slow_down") {
|
|
320
|
+
// Based on the RFC spec, we must add 5 seconds to our current polling interval.
|
|
321
|
+
let newInterval = (deviceData.interval + 5) * 1000;
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
data.interval &&
|
|
325
|
+
typeof data.interval === "number" &&
|
|
326
|
+
data.interval > 0
|
|
327
|
+
) {
|
|
328
|
+
newInterval = data.interval * 1000;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
308
335
|
if (data.error) return { type: "failed" };
|
|
309
336
|
|
|
310
|
-
await
|
|
311
|
-
|
|
337
|
+
await sleep(
|
|
338
|
+
deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
312
339
|
);
|
|
313
340
|
continue;
|
|
314
341
|
}
|
|
@@ -362,20 +362,20 @@ If opensrc doesn't work:
|
|
|
362
362
|
|
|
363
363
|
Source code research complements other tools:
|
|
364
364
|
|
|
365
|
-
| Method
|
|
366
|
-
|
|
|
367
|
-
| **Context7**
|
|
368
|
-
| **codesearch**
|
|
369
|
-
| **
|
|
370
|
-
| **Web search**
|
|
371
|
-
| **Codebase**
|
|
365
|
+
| Method | Best For | Source Code Adds |
|
|
366
|
+
| --------------- | -------------------------- | ------------------------------ |
|
|
367
|
+
| **Context7** | API docs, official guides | Implementation details |
|
|
368
|
+
| **codesearch** | Usage patterns in the wild | Canonical implementation |
|
|
369
|
+
| **grep_search** | Real-world examples | How library itself works |
|
|
370
|
+
| **Web search** | Tutorials, blog posts | Ground truth from source |
|
|
371
|
+
| **Codebase** | Project-specific patterns | How dependencies actually work |
|
|
372
372
|
|
|
373
373
|
**Recommended flow:**
|
|
374
374
|
|
|
375
375
|
1. Context7 - Check official docs
|
|
376
376
|
2. Codebase - Check existing usage
|
|
377
377
|
3. **Source code** - If still unclear, fetch source
|
|
378
|
-
4. codesearch/
|
|
378
|
+
4. codesearch/grep_search - See how others use it
|
|
379
379
|
5. Web search - Last resort for context
|
|
380
380
|
|
|
381
381
|
## Cleanup
|
|
@@ -180,9 +180,11 @@ glob ["src/**/*.ts", "tests/**/*.ts"]
|
|
|
180
180
|
|
|
181
181
|
## Research Tools
|
|
182
182
|
|
|
183
|
-
| Tool
|
|
184
|
-
|
|
|
185
|
-
| **
|
|
186
|
-
| **
|
|
187
|
-
| **
|
|
188
|
-
| **
|
|
183
|
+
| Tool | Use When |
|
|
184
|
+
| ------------------------------- | ------------------------------------------------------- |
|
|
185
|
+
| **context7_resolve-library-id** | Resolve library names to IDs (try first). |
|
|
186
|
+
| **context7_query-docs** | Query official library documentation. Fast. |
|
|
187
|
+
| **websearch** | Docs not in Context7, recent releases, troubleshooting. |
|
|
188
|
+
| **codesearch** | Real implementation patterns from GitHub. |
|
|
189
|
+
| **grep-search** | Cross-repo code patterns via grep.app. |
|
|
190
|
+
| **webfetch** | Specific URL user provided. |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
// Context7 API v2 - https://context7.com/docs/api-guide
|
|
4
|
+
const CONTEXT7_API = "https://context7.com/api/v2";
|
|
5
|
+
|
|
6
|
+
export default tool({
|
|
7
|
+
description: `Query library documentation from Context7 using a library ID.
|
|
8
|
+
|
|
9
|
+
Use when:
|
|
10
|
+
- You have a library ID (from context7_resolve_library_id)
|
|
11
|
+
- Need specific documentation about a library feature
|
|
12
|
+
- Looking for API reference, examples, or setup instructions
|
|
13
|
+
|
|
14
|
+
Always resolve library name to ID first with context7_resolve_library_id!
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
context7_query_docs({ libraryId: "/reactjs/react.dev", topic: "hooks" })
|
|
18
|
+
context7_query_docs({ libraryId: "/vercel/next.js", topic: "middleware" })
|
|
19
|
+
context7_query_docs({ libraryId: "/microsoft/TypeScript", topic: "generics" })
|
|
20
|
+
`,
|
|
21
|
+
args: {
|
|
22
|
+
libraryId: tool.schema
|
|
23
|
+
.string()
|
|
24
|
+
.describe(
|
|
25
|
+
"Library ID from context7_resolve_library_id (e.g., '/reactjs/react.dev')",
|
|
26
|
+
),
|
|
27
|
+
topic: tool.schema
|
|
28
|
+
.string()
|
|
29
|
+
.describe("Documentation topic or feature to search for"),
|
|
30
|
+
},
|
|
31
|
+
execute: async (args) => {
|
|
32
|
+
const { libraryId, topic } = args;
|
|
33
|
+
|
|
34
|
+
if (!libraryId || libraryId.trim() === "") {
|
|
35
|
+
return "Error: libraryId is required (use context7-resolve-library-id first)";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!topic || topic.trim() === "") {
|
|
39
|
+
return "Error: topic is required (e.g., 'hooks', 'setup', 'API reference')";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Query Context7 documentation - GET /api/v2/context
|
|
44
|
+
// Returns text format by default which is better for LLM consumption
|
|
45
|
+
const url = new URL(`${CONTEXT7_API}/context`);
|
|
46
|
+
url.searchParams.set("libraryId", libraryId);
|
|
47
|
+
url.searchParams.set("query", topic);
|
|
48
|
+
|
|
49
|
+
// Add API key if available (recommended for higher rate limits)
|
|
50
|
+
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
51
|
+
const headers: HeadersInit = {
|
|
52
|
+
Accept: "text/plain",
|
|
53
|
+
"User-Agent": "OpenCode/1.0",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (apiKey) {
|
|
57
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url.toString(), { headers });
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
if (response.status === 401) {
|
|
64
|
+
return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 404) {
|
|
67
|
+
return `Error: Library not found: ${libraryId}\n\nUse context7-resolve-library-id first to find the correct ID.`;
|
|
68
|
+
}
|
|
69
|
+
if (response.status === 429) {
|
|
70
|
+
return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
|
|
71
|
+
}
|
|
72
|
+
return `Error: Context7 API returned ${response.status}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const content = await response.text();
|
|
76
|
+
|
|
77
|
+
if (!content || content.trim() === "") {
|
|
78
|
+
return `No documentation found for "${topic}" in ${libraryId}.\n\nTry:\n- Simpler terms (e.g., "useState" instead of "state management")\n- Different topic spelling\n- Broader topics like "API reference" or "getting started"`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `# Documentation: ${topic} (${libraryId})
|
|
82
|
+
|
|
83
|
+
${content}`;
|
|
84
|
+
} catch (error: unknown) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return `Error querying documentation: ${message}`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
// Context7 API v2 - https://context7.com/docs/api-guide
|
|
4
|
+
const CONTEXT7_API = "https://context7.com/api/v2";
|
|
5
|
+
|
|
6
|
+
interface LibraryInfo {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
totalSnippets?: number;
|
|
11
|
+
trustScore?: number;
|
|
12
|
+
benchmarkScore?: number;
|
|
13
|
+
versions?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SearchResponse {
|
|
17
|
+
results: LibraryInfo[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default tool({
|
|
21
|
+
description: `Resolve a library name to its Context7 ID for documentation lookup.
|
|
22
|
+
|
|
23
|
+
Use when:
|
|
24
|
+
- You need to find the exact library ID format for a package
|
|
25
|
+
- Starting a documentation search (get the ID first, then query docs)
|
|
26
|
+
- Normalizing library names (e.g., "react" → "/reactjs/react.dev")
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
context7_resolve_library_id({ libraryName: "react" })
|
|
30
|
+
context7_resolve_library_id({ libraryName: "vue", query: "composition API" })
|
|
31
|
+
context7_resolve_library_id({ libraryName: "nextjs" })
|
|
32
|
+
`,
|
|
33
|
+
args: {
|
|
34
|
+
libraryName: tool.schema
|
|
35
|
+
.string()
|
|
36
|
+
.describe(
|
|
37
|
+
"Library name to resolve (e.g., 'react', 'lodash', 'typescript')",
|
|
38
|
+
),
|
|
39
|
+
query: tool.schema
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Optional context for the search (improves relevance ranking)"),
|
|
43
|
+
},
|
|
44
|
+
execute: async (args) => {
|
|
45
|
+
const { libraryName, query = "documentation" } = args;
|
|
46
|
+
|
|
47
|
+
if (!libraryName || libraryName.trim() === "") {
|
|
48
|
+
return "Error: libraryName is required";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Query Context7 library search - GET /api/v2/libs/search
|
|
53
|
+
const url = new URL(`${CONTEXT7_API}/libs/search`);
|
|
54
|
+
url.searchParams.set("libraryName", libraryName);
|
|
55
|
+
url.searchParams.set("query", query);
|
|
56
|
+
|
|
57
|
+
// Add API key if available (recommended for higher rate limits)
|
|
58
|
+
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
59
|
+
const headers: HeadersInit = {
|
|
60
|
+
Accept: "application/json",
|
|
61
|
+
"User-Agent": "OpenCode/1.0",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (apiKey) {
|
|
65
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = await fetch(url.toString(), { headers });
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (response.status === 401) {
|
|
72
|
+
return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
|
|
73
|
+
}
|
|
74
|
+
if (response.status === 429) {
|
|
75
|
+
return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
|
|
76
|
+
}
|
|
77
|
+
return `Error: Context7 API returned ${response.status}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = (await response.json()) as SearchResponse;
|
|
81
|
+
const libraries = data.results || [];
|
|
82
|
+
|
|
83
|
+
if (!libraries || libraries.length === 0) {
|
|
84
|
+
return `No libraries found matching: ${libraryName}\n\nTry:\n- Different library name\n- Check spelling\n- Use official package name`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const formatted = libraries
|
|
88
|
+
.slice(0, 5)
|
|
89
|
+
.map((lib, i) => {
|
|
90
|
+
const desc = lib.description
|
|
91
|
+
? `\n ${lib.description.slice(0, 100)}...`
|
|
92
|
+
: "";
|
|
93
|
+
const snippets = lib.totalSnippets
|
|
94
|
+
? ` (${lib.totalSnippets} snippets)`
|
|
95
|
+
: "";
|
|
96
|
+
const score = lib.benchmarkScore
|
|
97
|
+
? ` [score: ${lib.benchmarkScore}]`
|
|
98
|
+
: "";
|
|
99
|
+
return `${i + 1}. **${lib.title}** → \`${lib.id}\`${snippets}${score}${desc}`;
|
|
100
|
+
})
|
|
101
|
+
.join("\n\n");
|
|
102
|
+
|
|
103
|
+
return `Found ${libraries.length} libraries matching "${libraryName}":
|
|
104
|
+
|
|
105
|
+
${formatted}
|
|
106
|
+
|
|
107
|
+
**Next step**: Use \`context7-query-docs({ libraryId: "${libraries[0].id}", topic: "your topic" })\` to fetch documentation.`;
|
|
108
|
+
} catch (error: unknown) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
return `Error resolving library: ${message}`;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
const GREP_APP_API = "https://grep.app/api/search";
|
|
4
|
+
|
|
5
|
+
interface SearchResult {
|
|
6
|
+
repo: string;
|
|
7
|
+
path: string;
|
|
8
|
+
content: { snippet: string };
|
|
9
|
+
total_matches: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface GrepResponse {
|
|
13
|
+
hits: { hits: SearchResult[] };
|
|
14
|
+
time: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default tool({
|
|
18
|
+
description: `Search real-world code examples from GitHub repositories via grep.app.
|
|
19
|
+
|
|
20
|
+
Use when:
|
|
21
|
+
- Implementing unfamiliar APIs - see how others use a library
|
|
22
|
+
- Looking for production patterns - find real-world examples
|
|
23
|
+
- Understanding library integrations - see how things work together
|
|
24
|
+
|
|
25
|
+
IMPORTANT: Search for **literal code patterns**, not keywords:
|
|
26
|
+
✅ Good: "useState(", "import React from", "async function"
|
|
27
|
+
❌ Bad: "react tutorial", "best practices", "how to use"
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
grep_search({ query: "getServerSession", language: "TypeScript" })
|
|
31
|
+
grep_search({ query: "CORS(", language: "Python", repo: "flask" })
|
|
32
|
+
grep_search({ query: "export async function POST", path: "route.ts" })
|
|
33
|
+
`,
|
|
34
|
+
args: {
|
|
35
|
+
query: tool.schema
|
|
36
|
+
.string()
|
|
37
|
+
.describe("Code pattern to search for (literal text)"),
|
|
38
|
+
language: tool.schema
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Filter by language: TypeScript, TSX, Python, Go, Rust, etc."),
|
|
42
|
+
repo: tool.schema
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Filter by repo: 'owner/repo' or partial match"),
|
|
46
|
+
path: tool.schema
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Filter by file path: 'src/', '.test.ts', etc."),
|
|
50
|
+
limit: tool.schema
|
|
51
|
+
.number()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Max results to return (default: 10, max: 20)"),
|
|
54
|
+
},
|
|
55
|
+
execute: async (args) => {
|
|
56
|
+
const { query, language, repo, path, limit = 10 } = args;
|
|
57
|
+
|
|
58
|
+
if (!query || query.trim() === "") {
|
|
59
|
+
return "Error: query is required";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build URL with proper filter parameters
|
|
63
|
+
// grep.app uses filter[lang][0]=TypeScript format, NOT inline lang:TypeScript
|
|
64
|
+
const url = new URL(GREP_APP_API);
|
|
65
|
+
url.searchParams.set("q", query);
|
|
66
|
+
|
|
67
|
+
// Add language filter (grep.app uses filter[lang][0] format)
|
|
68
|
+
if (language) {
|
|
69
|
+
url.searchParams.set("filter[lang][0]", language);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add repo filter
|
|
73
|
+
if (repo) {
|
|
74
|
+
url.searchParams.set("filter[repo][0]", repo);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add path filter
|
|
78
|
+
if (path) {
|
|
79
|
+
url.searchParams.set("filter[path][0]", path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(url.toString(), {
|
|
84
|
+
headers: {
|
|
85
|
+
Accept: "application/json",
|
|
86
|
+
"User-Agent": "OpenCode/1.0",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
return `Error: grep.app API returned ${response.status}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = (await response.json()) as GrepResponse;
|
|
95
|
+
|
|
96
|
+
if (!data.hits?.hits?.length) {
|
|
97
|
+
return `No results found for: ${query}${language ? ` (${language})` : ""}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const maxResults = Math.min(limit, 20);
|
|
101
|
+
const results = data.hits.hits.slice(0, maxResults);
|
|
102
|
+
|
|
103
|
+
const formatted = results.map((hit, i) => {
|
|
104
|
+
const repoName = hit.repo || "unknown";
|
|
105
|
+
const filePath = hit.path || "unknown";
|
|
106
|
+
const snippet = hit.content?.snippet || "";
|
|
107
|
+
|
|
108
|
+
// Clean up HTML from snippet and extract text
|
|
109
|
+
const cleanCode = snippet
|
|
110
|
+
.replace(/<[^>]*>/g, "") // Remove HTML tags
|
|
111
|
+
.replace(/</g, "<")
|
|
112
|
+
.replace(/>/g, ">")
|
|
113
|
+
.replace(/&/g, "&")
|
|
114
|
+
.replace(/"/g, '"')
|
|
115
|
+
.split("\n")
|
|
116
|
+
.slice(0, 8)
|
|
117
|
+
.join("\n")
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
return `## ${i + 1}. ${repoName}
|
|
121
|
+
**File**: ${filePath}
|
|
122
|
+
\`\`\`
|
|
123
|
+
${cleanCode}
|
|
124
|
+
\`\`\``;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return `Found ${data.hits.hits.length} results (showing ${results.length}) in ${data.time}ms:
|
|
128
|
+
|
|
129
|
+
${formatted.join("\n\n")}`;
|
|
130
|
+
} catch (error: unknown) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return `Error searching grep.app: ${message}`;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
package/package.json
CHANGED