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.
@@ -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
- | 2 | context7 | Official library docs | Fast |
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
- - context7 returns a doc link worth fetching
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 = "Iv1.b507a08c87ecfe98";
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
- export const CopilotAuthPlugin: Plugin = async ({ client }) => {
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.body === "string"
105
+ typeof init?.body === "string"
124
106
  ? JSON.parse(init.body)
125
- : init.body;
126
- if (body?.messages) {
127
- isAgentCall = body.messages.some(
128
- (msg: any) =>
129
- msg.role && ["tool", "assistant"].includes(msg.role),
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
- const lastInput = body.input[body.input.length - 1];
140
-
141
- const isAssistant = lastInput?.role === "assistant";
142
- const hasAgentType = lastInput?.type
143
- ? RESPONSES_API_ALTERNATE_INPUT_TYPES.includes(lastInput.type)
144
- : false;
145
- isAgentCall = isAssistant || hasAgentType;
146
-
147
- isVisionRequest =
148
- Array.isArray(lastInput?.content) &&
149
- lastInput.content.some(
150
- (part: any) => part.type === "input_image",
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
- ...init.headers,
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.access}`,
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 new Promise((resolve) =>
303
- setTimeout(resolve, deviceData.interval * 1000),
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 new Promise((resolve) =>
311
- setTimeout(resolve, deviceData.interval * 1000),
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 | Best For | Source Code Adds |
366
- | -------------- | -------------------------- | ------------------------------ |
367
- | **Context7** | API docs, official guides | Implementation details |
368
- | **codesearch** | Usage patterns in the wild | Canonical implementation |
369
- | **gh_grep** | 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 |
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/gh_grep - See how others use it
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 | Use When |
184
- | -------------- | ------------------------------------------------------- |
185
- | **context7** | Library docs (try first). Fast, external APIs. |
186
- | **websearch** | Docs not in Context7, recent releases, troubleshooting. |
187
- | **codesearch** | Real implementation patterns from GitHub. |
188
- | **webfetch** | Specific URL user provided. |
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(/&lt;/g, "<")
112
+ .replace(/&gt;/g, ">")
113
+ .replace(/&amp;/g, "&")
114
+ .replace(/&quot;/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.15.11",
3
+ "version": "0.15.13",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": ["agents", "cli", "mcp", "opencode", "opencodekit", "template"],
6
6
  "license": "MIT",