xtrm-tools 0.7.1 → 0.7.2
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/CHANGELOG.md +5 -1
- package/README.md +18 -3
- package/cli/dist/index.cjs +76 -19
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +5 -3
- package/package.json +4 -2
- package/scripts/ghgrep.mjs +358 -0
package/cli/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtrm-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"xtrm": "dist/index.cjs",
|
|
9
|
-
"xt": "dist/index.cjs"
|
|
9
|
+
"xt": "dist/index.cjs",
|
|
10
|
+
"ghgrep": "scripts/ghgrep.mjs"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"dist",
|
|
13
|
-
"config"
|
|
14
|
+
"config",
|
|
15
|
+
"scripts/ghgrep.mjs"
|
|
14
16
|
],
|
|
15
17
|
"scripts": {
|
|
16
18
|
"prebuild": "node -e \"if(process.cwd().includes('/.xtrm/worktrees/')){console.error('ERROR: Do not run npm run build from a worktree — dist paths will be contaminated.\\nRun from the main repo: cd <repo-root>/cli && npm run build');process.exit(1)}\"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtrm-tools",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
],
|
|
11
11
|
"bin": {
|
|
12
12
|
"xtrm": "cli/dist/index.cjs",
|
|
13
|
-
"xt": "cli/dist/index.cjs"
|
|
13
|
+
"xt": "cli/dist/index.cjs",
|
|
14
|
+
"ghgrep": "scripts/ghgrep.mjs"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"README.md",
|
|
17
18
|
"CHANGELOG.md",
|
|
18
19
|
"cli/dist",
|
|
19
20
|
"cli/package.json",
|
|
21
|
+
"scripts/ghgrep.mjs",
|
|
20
22
|
".xtrm/config",
|
|
21
23
|
".xtrm/hooks",
|
|
22
24
|
".xtrm/extensions",
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const ENDPOINT = 'https://mcp.grep.app';
|
|
4
|
+
const DEFAULT_LIMIT = 10;
|
|
5
|
+
|
|
6
|
+
function printUsage() {
|
|
7
|
+
console.log(`ghgrep <query> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--lang <langs> Filter by language (comma-separated): TypeScript,TSX,Python
|
|
11
|
+
--repo <repo> Filter by repo: facebook/react
|
|
12
|
+
--path <path> Filter by file path pattern
|
|
13
|
+
--regexp Treat query as regex (auto-prefixes (?s) for multiline)
|
|
14
|
+
--case Case-sensitive match
|
|
15
|
+
--words Match whole words only
|
|
16
|
+
--json Raw JSON output
|
|
17
|
+
--limit <n> Max results (default: 10)
|
|
18
|
+
-h, --help Show help
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseCliArgs(argv) {
|
|
23
|
+
const options = {
|
|
24
|
+
caseSensitive: false,
|
|
25
|
+
json: false,
|
|
26
|
+
languages: [],
|
|
27
|
+
limit: DEFAULT_LIMIT,
|
|
28
|
+
path: undefined,
|
|
29
|
+
regexp: false,
|
|
30
|
+
repo: undefined,
|
|
31
|
+
wholeWords: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const queryParts = [];
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
|
|
39
|
+
if (arg === '-h' || arg === '--help') {
|
|
40
|
+
options.help = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (arg === '--json') {
|
|
45
|
+
options.json = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (arg === '--regexp') {
|
|
50
|
+
options.regexp = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arg === '--case') {
|
|
55
|
+
options.caseSensitive = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (arg === '--words') {
|
|
60
|
+
options.wholeWords = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (arg === '--lang' || arg === '--repo' || arg === '--path' || arg === '--limit') {
|
|
65
|
+
const value = argv[index + 1];
|
|
66
|
+
if (!value || value.startsWith('-')) {
|
|
67
|
+
throw new Error(`Missing value for ${arg}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg === '--lang') {
|
|
71
|
+
options.languages = value
|
|
72
|
+
.split(',')
|
|
73
|
+
.map((item) => item.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (arg === '--repo') {
|
|
78
|
+
options.repo = value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (arg === '--path') {
|
|
82
|
+
options.path = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (arg === '--limit') {
|
|
86
|
+
const parsedLimit = Number.parseInt(value, 10);
|
|
87
|
+
if (!Number.isFinite(parsedLimit) || parsedLimit <= 0) {
|
|
88
|
+
throw new Error(`Invalid --limit value: ${value}`);
|
|
89
|
+
}
|
|
90
|
+
options.limit = parsedLimit;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (arg.startsWith('-')) {
|
|
98
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
queryParts.push(arg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
options,
|
|
106
|
+
query: queryParts.join(' ').trim(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseSseChunk(chunk, state, events) {
|
|
111
|
+
state.buffer += chunk;
|
|
112
|
+
const lines = state.buffer.split(/\r?\n/);
|
|
113
|
+
state.buffer = lines.pop() ?? '';
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (line === '') {
|
|
117
|
+
if (state.eventData.length > 0) {
|
|
118
|
+
const payload = state.eventData.join('\n').trim();
|
|
119
|
+
if (payload && payload !== '[DONE]') {
|
|
120
|
+
events.push(payload);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
state.eventData = [];
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (line.startsWith('data:')) {
|
|
128
|
+
state.eventData.push(line.slice(5).trimStart());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function parseMcpResponse(response) {
|
|
134
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
135
|
+
|
|
136
|
+
if (contentType.includes('application/json')) {
|
|
137
|
+
return [await response.json()];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!contentType.includes('text/event-stream')) {
|
|
141
|
+
const body = await response.text();
|
|
142
|
+
throw new Error(`Unexpected response type: ${contentType || 'unknown'}\n${body}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const reader = response.body?.getReader();
|
|
146
|
+
if (!reader) {
|
|
147
|
+
throw new Error('Response stream is empty');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const decoder = new TextDecoder();
|
|
151
|
+
const events = [];
|
|
152
|
+
const state = { buffer: '', eventData: [] };
|
|
153
|
+
|
|
154
|
+
while (true) {
|
|
155
|
+
const { done, value } = await reader.read();
|
|
156
|
+
if (done) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parseSseChunk(decoder.decode(value, { stream: true }), state, events);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const trailingChunk = decoder.decode();
|
|
164
|
+
if (trailingChunk) {
|
|
165
|
+
parseSseChunk(trailingChunk, state, events);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (state.eventData.length > 0) {
|
|
169
|
+
const payload = state.eventData.join('\n').trim();
|
|
170
|
+
if (payload && payload !== '[DONE]') {
|
|
171
|
+
events.push(payload);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return events.map((event) => {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(event);
|
|
178
|
+
} catch {
|
|
179
|
+
return { raw: event };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function callMcp(method, params) {
|
|
185
|
+
const response = await fetch(ENDPOINT, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
Accept: 'application/json, text/event-stream',
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
id: Date.now(),
|
|
193
|
+
jsonrpc: '2.0',
|
|
194
|
+
method,
|
|
195
|
+
params,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const payloads = await parseMcpResponse(response);
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return payloads;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseSnippetSections(snippetsText) {
|
|
208
|
+
const sections = snippetsText.split(/--- Snippet \d+ \(Line (\d+)\) ---\n/g);
|
|
209
|
+
const snippets = [];
|
|
210
|
+
|
|
211
|
+
for (let index = 1; index < sections.length; index += 2) {
|
|
212
|
+
const line = Number.parseInt(sections[index], 10);
|
|
213
|
+
const code = (sections[index + 1] ?? '').trimEnd();
|
|
214
|
+
snippets.push({
|
|
215
|
+
code,
|
|
216
|
+
line: Number.isFinite(line) ? line : undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return snippets;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseResultBlock(text) {
|
|
224
|
+
const match = text.match(
|
|
225
|
+
/^Repository:\s*(.+)\nPath:\s*(.+)\nURL:\s*(.+)\nLicense:\s*(.+)\n\nSnippets:\n([\s\S]*)$/,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (!match) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
license: match[4].trim(),
|
|
234
|
+
path: match[2].trim(),
|
|
235
|
+
repo: match[1].trim(),
|
|
236
|
+
snippets: parseSnippetSections(match[5]),
|
|
237
|
+
url: match[3].trim(),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatEntries(entries) {
|
|
242
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
243
|
+
const entry = entries[index];
|
|
244
|
+
const firstSnippetLine = entry.snippets[0]?.line;
|
|
245
|
+
const location = firstSnippetLine ? `${entry.repo}/${entry.path}:${firstSnippetLine}` : `${entry.repo}/${entry.path}`;
|
|
246
|
+
|
|
247
|
+
console.log(`${index + 1}. ${location}`);
|
|
248
|
+
console.log(` ${entry.url}`);
|
|
249
|
+
console.log(` License: ${entry.license}`);
|
|
250
|
+
|
|
251
|
+
for (const snippet of entry.snippets) {
|
|
252
|
+
const lineLabel = snippet.line ? `Line ${snippet.line}` : 'Snippet';
|
|
253
|
+
console.log(`\n --- ${lineLabel} ---`);
|
|
254
|
+
const snippetLines = snippet.code.split('\n');
|
|
255
|
+
for (const snippetLine of snippetLines) {
|
|
256
|
+
console.log(` ${snippetLine}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (index < entries.length - 1) {
|
|
261
|
+
console.log('\n' + '-'.repeat(80) + '\n');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getTextContent(resultPayload) {
|
|
267
|
+
const content = resultPayload?.result?.content;
|
|
268
|
+
if (!Array.isArray(content)) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return content
|
|
273
|
+
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
|
|
274
|
+
.map((item) => item.text);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function run() {
|
|
278
|
+
const { options, query } = parseCliArgs(process.argv.slice(2));
|
|
279
|
+
|
|
280
|
+
if (options.help || !query) {
|
|
281
|
+
printUsage();
|
|
282
|
+
process.exit(options.help ? 0 : 1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const requestArguments = {
|
|
286
|
+
query: options.regexp && !query.startsWith('(?s)') ? `(?s)${query}` : query,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (options.caseSensitive) {
|
|
290
|
+
requestArguments.matchCase = true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (options.wholeWords) {
|
|
294
|
+
requestArguments.matchWholeWords = true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options.regexp) {
|
|
298
|
+
requestArguments.useRegexp = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (options.repo) {
|
|
302
|
+
requestArguments.repo = options.repo;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.path) {
|
|
306
|
+
requestArguments.path = options.path;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (options.languages.length > 0) {
|
|
310
|
+
requestArguments.language = options.languages;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const payloads = await callMcp('tools/call', {
|
|
314
|
+
arguments: requestArguments,
|
|
315
|
+
name: 'searchGitHub',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (options.json) {
|
|
319
|
+
console.log(JSON.stringify(payloads, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resultPayload = payloads.find((payload) => payload?.result || payload?.error) ?? payloads[0];
|
|
324
|
+
|
|
325
|
+
if (resultPayload?.error) {
|
|
326
|
+
throw new Error(resultPayload.error.message ?? JSON.stringify(resultPayload.error));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const textBlocks = getTextContent(resultPayload);
|
|
330
|
+
const isError = resultPayload?.result?.isError === true;
|
|
331
|
+
|
|
332
|
+
if (isError) {
|
|
333
|
+
throw new Error(textBlocks.join('\n\n') || 'searchGitHub returned an error');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const parsedEntries = textBlocks
|
|
337
|
+
.map((block) => parseResultBlock(block))
|
|
338
|
+
.filter((entry) => entry !== null)
|
|
339
|
+
.slice(0, options.limit);
|
|
340
|
+
|
|
341
|
+
if (parsedEntries.length > 0) {
|
|
342
|
+
formatEntries(parsedEntries);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const plainText = textBlocks.join('\n\n').trim();
|
|
347
|
+
if (!plainText) {
|
|
348
|
+
console.log('No results.');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(plainText);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
run().catch((error) => {
|
|
356
|
+
console.error(`ghgrep error: ${error.message}`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
});
|