@selesai/code 0.5.8 → 0.5.10
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.
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"cacheWrite": 0
|
|
24
24
|
},
|
|
25
25
|
"compat": {
|
|
26
|
+
"supportsDeveloperRole": false,
|
|
26
27
|
"requiresReasoningContentOnAssistantMessages": true,
|
|
27
28
|
"thinkingFormat": "deepseek"
|
|
28
29
|
}
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"cacheWrite": 0
|
|
41
42
|
},
|
|
42
43
|
"compat": {
|
|
44
|
+
"supportsDeveloperRole": false,
|
|
43
45
|
"supportsReasoningEffort": false
|
|
44
46
|
}
|
|
45
47
|
},
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
"cacheWrite": 0
|
|
57
59
|
},
|
|
58
60
|
"compat": {
|
|
61
|
+
"supportsDeveloperRole": false,
|
|
59
62
|
"supportsReasoningEffort": false
|
|
60
63
|
}
|
|
61
64
|
},
|
|
@@ -72,6 +75,7 @@
|
|
|
72
75
|
"cacheWrite": 0
|
|
73
76
|
},
|
|
74
77
|
"compat": {
|
|
78
|
+
"supportsDeveloperRole": false,
|
|
75
79
|
"supportsReasoningEffort": false
|
|
76
80
|
}
|
|
77
81
|
},
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
"cacheWrite": 0
|
|
89
93
|
},
|
|
90
94
|
"compat": {
|
|
95
|
+
"supportsDeveloperRole": false,
|
|
91
96
|
"supportsReasoningEffort": false
|
|
92
97
|
}
|
|
93
98
|
},
|
|
@@ -120,6 +125,7 @@
|
|
|
120
125
|
"cacheWrite": 0
|
|
121
126
|
},
|
|
122
127
|
"compat": {
|
|
128
|
+
"supportsDeveloperRole": false,
|
|
123
129
|
"supportsReasoningEffort": true
|
|
124
130
|
}
|
|
125
131
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSearchUrl, formatSearchResponse } from "./index.ts";
|
|
3
|
+
|
|
4
|
+
describe("grep-app extension", () => {
|
|
5
|
+
it("maps search filters to grep.app parameters", () => {
|
|
6
|
+
const url = buildSearchUrl({
|
|
7
|
+
query: "useEffect cleanup",
|
|
8
|
+
page: 2,
|
|
9
|
+
caseSensitive: true,
|
|
10
|
+
useRegex: true,
|
|
11
|
+
repo: "facebook/react",
|
|
12
|
+
path: "packages/",
|
|
13
|
+
language: "TypeScript",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(Object.fromEntries(url.searchParams)).toEqual({
|
|
17
|
+
q: "useEffect cleanup",
|
|
18
|
+
page: "2",
|
|
19
|
+
case: "true",
|
|
20
|
+
regexp: "true",
|
|
21
|
+
"f.repo.pattern": "facebook/react",
|
|
22
|
+
"f.path.pattern": "packages/",
|
|
23
|
+
"f.lang": "TypeScript",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("turns grep.app HTML snippets into readable numbered source", () => {
|
|
28
|
+
const output = formatSearchResponse(
|
|
29
|
+
{
|
|
30
|
+
hits: {
|
|
31
|
+
total: 42,
|
|
32
|
+
hits: [
|
|
33
|
+
{
|
|
34
|
+
repo: "owner/repo",
|
|
35
|
+
branch: "main",
|
|
36
|
+
path: "src/a file.ts",
|
|
37
|
+
content: {
|
|
38
|
+
snippet: '<table><tr data-line="7"><td><pre>const x = <mark>;</pre></td></tr></table>',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
3,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(output).toContain("42 total matches; page 3");
|
|
48
|
+
expect(output).toContain("L7: const x = <mark>;");
|
|
49
|
+
expect(output).toContain("https://github.com/owner/repo/blob/main/src/a%20file.ts#L7");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Native Selesai extension inspired by ai-tools-all/grep_app_mcp (ISC). */
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_MAX_BYTES,
|
|
8
|
+
DEFAULT_MAX_LINES,
|
|
9
|
+
formatSize,
|
|
10
|
+
truncateHead,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
withFileMutationQueue,
|
|
13
|
+
} from "@selesai/code";
|
|
14
|
+
import { load } from "cheerio";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
|
|
17
|
+
const API_URL = "https://grep.app/api/search";
|
|
18
|
+
const RAW_GITHUB_URL = "https://raw.githubusercontent.com";
|
|
19
|
+
|
|
20
|
+
interface GrepAppHit {
|
|
21
|
+
repo: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
path: string;
|
|
24
|
+
content: { snippet: string };
|
|
25
|
+
total_matches?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface GrepAppResponse {
|
|
29
|
+
hits: { total: number; hits: GrepAppHit[] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SearchParams {
|
|
33
|
+
query: string;
|
|
34
|
+
page?: number;
|
|
35
|
+
caseSensitive?: boolean;
|
|
36
|
+
useRegex?: boolean;
|
|
37
|
+
wholeWords?: boolean;
|
|
38
|
+
repo?: string;
|
|
39
|
+
path?: string;
|
|
40
|
+
language?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildSearchUrl(params: SearchParams): URL {
|
|
44
|
+
const url = new URL(API_URL);
|
|
45
|
+
url.searchParams.set("q", params.query);
|
|
46
|
+
url.searchParams.set("page", String(params.page ?? 1));
|
|
47
|
+
if (params.caseSensitive) url.searchParams.set("case", "true");
|
|
48
|
+
if (params.useRegex) url.searchParams.set("regexp", "true");
|
|
49
|
+
if (params.wholeWords) url.searchParams.set("words", "true");
|
|
50
|
+
if (params.repo) url.searchParams.set("f.repo.pattern", params.repo);
|
|
51
|
+
if (params.path) url.searchParams.set("f.path.pattern", params.path);
|
|
52
|
+
if (params.language) url.searchParams.set("f.lang", params.language);
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function snippetLines(snippet: string): Array<{ line: string; text: string }> {
|
|
57
|
+
const $ = load(snippet);
|
|
58
|
+
return $("tr")
|
|
59
|
+
.toArray()
|
|
60
|
+
.map((row) => ({
|
|
61
|
+
line: $(row).attr("data-line") ?? "?",
|
|
62
|
+
text: $(row).find("pre").text().replace(/\s+$/, ""),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function encodePath(value: string): string {
|
|
67
|
+
return value
|
|
68
|
+
.split("/")
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.map(encodeURIComponent)
|
|
71
|
+
.join("/");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatSearchResponse(response: GrepAppResponse, page: number): string {
|
|
75
|
+
if (response.hits.hits.length === 0) return "No matches found.";
|
|
76
|
+
|
|
77
|
+
const results = response.hits.hits.map((hit, index) => {
|
|
78
|
+
const lines = snippetLines(hit.content.snippet);
|
|
79
|
+
const firstLine = lines.find((line) => line.line !== "?")?.line;
|
|
80
|
+
const url = `https://github.com/${hit.repo}/blob/${encodePath(hit.branch)}/${encodePath(hit.path)}${firstLine ? `#L${firstLine}` : ""}`;
|
|
81
|
+
const body = lines.map(({ line, text }) => ` L${line}: ${text}`).join("\n");
|
|
82
|
+
return `${index + 1}. ${hit.repo}:${hit.path} (${hit.branch})\n${body}\n ${url}`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return `grep.app: ${response.hits.total} total matches; page ${page}\n\n${results.join("\n\n")}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchWithBrowser(url: URL | string, signal: AbortSignal | undefined): Promise<string> {
|
|
89
|
+
const { chromium } = await import("playwright");
|
|
90
|
+
const browser = await chromium.launch({ headless: true });
|
|
91
|
+
const abort = () => void browser.close();
|
|
92
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
93
|
+
try {
|
|
94
|
+
const page = await browser.newPage();
|
|
95
|
+
await page.goto(String(url), { waitUntil: "networkidle", timeout: 30_000 });
|
|
96
|
+
await page.waitForFunction(() => document.body.innerText.trimStart().startsWith("{"), undefined, { timeout: 15_000 });
|
|
97
|
+
return await page.locator("body").innerText();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Cancelled");
|
|
100
|
+
throw error;
|
|
101
|
+
} finally {
|
|
102
|
+
signal?.removeEventListener("abort", abort);
|
|
103
|
+
await browser.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchText(url: URL | string, signal: AbortSignal | undefined, browserFallback = false): Promise<string> {
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
signal,
|
|
110
|
+
headers: { Accept: "application/json, text/plain;q=0.9", "User-Agent": "selesai-grep-app-extension" },
|
|
111
|
+
});
|
|
112
|
+
if (browserFallback && response.status === 429 && response.headers.get("x-vercel-mitigated") === "challenge") {
|
|
113
|
+
return fetchWithBrowser(url, signal);
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
116
|
+
return response.text();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function truncateOutput(output: string): Promise<{ text: string; fullOutputPath?: string }> {
|
|
120
|
+
const truncated = truncateHead(output, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
|
|
121
|
+
if (!truncated.truncated) return { text: output };
|
|
122
|
+
|
|
123
|
+
const directory = await mkdtemp(join(tmpdir(), "selesai-grep-app-"));
|
|
124
|
+
const fullOutputPath = join(directory, "output.txt");
|
|
125
|
+
await withFileMutationQueue(fullOutputPath, () => writeFile(fullOutputPath, output, "utf8"));
|
|
126
|
+
return {
|
|
127
|
+
text: `${truncated.content}\n\n[Output truncated to ${truncated.outputLines} lines / ${formatSize(truncated.outputBytes)}. Full output: ${fullOutputPath}]`,
|
|
128
|
+
fullOutputPath,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default function grepAppExtension(pi: ExtensionAPI): void {
|
|
133
|
+
pi.registerTool({
|
|
134
|
+
name: "grep_app_search",
|
|
135
|
+
label: "grep.app Search",
|
|
136
|
+
description: "Search public GitHub code through grep.app. Returns one page (up to 10 files); use page to continue.",
|
|
137
|
+
promptSnippet: "Search public GitHub code through grep.app",
|
|
138
|
+
promptGuidelines: ["Use grep_app_search to find real public-code implementations and usage examples across GitHub."],
|
|
139
|
+
parameters: Type.Object({
|
|
140
|
+
query: Type.String({ minLength: 1, description: "Code or pattern to search for." }),
|
|
141
|
+
page: Type.Optional(Type.Integer({ minimum: 1, description: "Result page; defaults to 1." })),
|
|
142
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Match case." })),
|
|
143
|
+
useRegex: Type.Optional(Type.Boolean({ description: "Treat query as a regular expression." })),
|
|
144
|
+
wholeWords: Type.Optional(Type.Boolean({ description: "Match whole words. Cannot be combined with useRegex." })),
|
|
145
|
+
repo: Type.Optional(Type.String({ description: "Repository filter, for example facebook/react." })),
|
|
146
|
+
path: Type.Optional(Type.String({ description: "File path filter, for example src/components/." })),
|
|
147
|
+
language: Type.Optional(Type.String({ description: "Language filter, for example TypeScript." })),
|
|
148
|
+
}),
|
|
149
|
+
async execute(_toolCallId, params, signal) {
|
|
150
|
+
if (params.useRegex && params.wholeWords) throw new Error("useRegex and wholeWords cannot both be true.");
|
|
151
|
+
|
|
152
|
+
const page = params.page ?? 1;
|
|
153
|
+
const raw = await fetchText(buildSearchUrl(params), signal, true);
|
|
154
|
+
let response: GrepAppResponse;
|
|
155
|
+
try {
|
|
156
|
+
response = JSON.parse(raw) as GrepAppResponse;
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error("grep.app returned invalid JSON.");
|
|
159
|
+
}
|
|
160
|
+
if (!response.hits || !Array.isArray(response.hits.hits)) throw new Error("grep.app returned an unexpected response.");
|
|
161
|
+
|
|
162
|
+
const output = await truncateOutput(formatSearchResponse(response, page));
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: output.text }],
|
|
165
|
+
details: { total: response.hits.total, page, resultCount: response.hits.hits.length, fullOutputPath: output.fullOutputPath },
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
pi.registerTool({
|
|
171
|
+
name: "grep_app_fetch",
|
|
172
|
+
label: "GitHub File",
|
|
173
|
+
description: "Fetch a public GitHub file found via grep.app. Supports line ranges; output is truncated to 50KB or 2000 lines.",
|
|
174
|
+
promptSnippet: "Fetch a public GitHub file found via grep.app",
|
|
175
|
+
promptGuidelines: ["Use grep_app_fetch after grep_app_search when the full source context is needed."],
|
|
176
|
+
parameters: Type.Object({
|
|
177
|
+
repo: Type.String({ pattern: "^[^/\\s]+/[^/\\s]+$", description: "owner/repository." }),
|
|
178
|
+
path: Type.String({ minLength: 1, description: "Path inside the repository." }),
|
|
179
|
+
ref: Type.Optional(Type.String({ minLength: 1, description: "Branch, tag, or commit; defaults to HEAD." })),
|
|
180
|
+
startLine: Type.Optional(Type.Integer({ minimum: 1, description: "First line; defaults to 1." })),
|
|
181
|
+
endLine: Type.Optional(Type.Integer({ minimum: 1, description: "Last line; defaults to end of file." })),
|
|
182
|
+
}),
|
|
183
|
+
async execute(_toolCallId, params, signal) {
|
|
184
|
+
const startLine = params.startLine ?? 1;
|
|
185
|
+
if (params.endLine !== undefined && params.endLine < startLine) throw new Error("endLine must be greater than or equal to startLine.");
|
|
186
|
+
|
|
187
|
+
const rawUrl = `${RAW_GITHUB_URL}/${encodePath(params.repo)}/${encodePath(params.ref ?? "HEAD")}/${encodePath(params.path)}`;
|
|
188
|
+
const source = await fetchText(rawUrl, signal);
|
|
189
|
+
const allLines = source.split("\n");
|
|
190
|
+
const endLine = Math.min(params.endLine ?? allLines.length, allLines.length);
|
|
191
|
+
const selected = allLines.slice(startLine - 1, endLine).map((line, index) => `${startLine + index}: ${line}`).join("\n");
|
|
192
|
+
const output = await truncateOutput(selected);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: output.text || "(empty file or range)" }],
|
|
196
|
+
details: { repo: params.repo, path: params.path, ref: params.ref ?? "HEAD", startLine, endLine, totalLines: allLines.length, fullOutputPath: output.fullOutputPath },
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: researcher
|
|
3
|
-
description: Autonomous
|
|
4
|
-
tools: read, web_explore
|
|
3
|
+
description: Autonomous code-first researcher that produces a focused sourced brief
|
|
4
|
+
tools: read, grep_app_search, grep_app_fetch, web_explore
|
|
5
5
|
thinking: medium
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: false
|
|
@@ -10,9 +10,11 @@ defaultContext: fresh
|
|
|
10
10
|
skill: ponytail, caveman
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
You are a
|
|
13
|
+
You are a code-first research subagent. Answer the supplied question with a concise, well-sourced brief. Do not edit project files, write output files, or launch subagents.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Start with `grep_app_search` for public-code evidence and use `grep_app_fetch` only for the most relevant source files. Search 2–4 distinct angles, prefer original implementations and repository-owned examples, then narrow follow-ups only for material gaps.
|
|
16
|
+
|
|
17
|
+
Treat `web_explore` as a last resort: use it only when grep.app cannot answer the question, the task requires non-code sources or official documentation, or grep.app fails. If `web_explore` is unavailable, report the gap instead of fetching URLs through shell commands.
|
|
16
18
|
|
|
17
19
|
Output:
|
|
18
20
|
|