@codexstar/bug-hunter 3.0.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/LICENSE +21 -0
  3. package/README.md +665 -0
  4. package/SKILL.md +624 -0
  5. package/bin/bug-hunter +222 -0
  6. package/evals/evals.json +362 -0
  7. package/modes/_dispatch.md +121 -0
  8. package/modes/extended.md +94 -0
  9. package/modes/fix-loop.md +115 -0
  10. package/modes/fix-pipeline.md +384 -0
  11. package/modes/large-codebase.md +212 -0
  12. package/modes/local-sequential.md +143 -0
  13. package/modes/loop.md +125 -0
  14. package/modes/parallel.md +113 -0
  15. package/modes/scaled.md +76 -0
  16. package/modes/single-file.md +38 -0
  17. package/modes/small.md +86 -0
  18. package/package.json +56 -0
  19. package/prompts/doc-lookup.md +44 -0
  20. package/prompts/examples/hunter-examples.md +131 -0
  21. package/prompts/examples/skeptic-examples.md +87 -0
  22. package/prompts/fixer.md +103 -0
  23. package/prompts/hunter.md +146 -0
  24. package/prompts/recon.md +159 -0
  25. package/prompts/referee.md +122 -0
  26. package/prompts/skeptic.md +143 -0
  27. package/prompts/threat-model.md +122 -0
  28. package/scripts/bug-hunter-state.cjs +537 -0
  29. package/scripts/code-index.cjs +541 -0
  30. package/scripts/context7-api.cjs +133 -0
  31. package/scripts/delta-mode.cjs +219 -0
  32. package/scripts/dep-scan.cjs +343 -0
  33. package/scripts/doc-lookup.cjs +316 -0
  34. package/scripts/fix-lock.cjs +167 -0
  35. package/scripts/init-test-fixture.sh +19 -0
  36. package/scripts/payload-guard.cjs +197 -0
  37. package/scripts/run-bug-hunter.cjs +892 -0
  38. package/scripts/tests/bug-hunter-state.test.cjs +87 -0
  39. package/scripts/tests/code-index.test.cjs +57 -0
  40. package/scripts/tests/delta-mode.test.cjs +47 -0
  41. package/scripts/tests/fix-lock.test.cjs +36 -0
  42. package/scripts/tests/fixtures/flaky-worker.cjs +63 -0
  43. package/scripts/tests/fixtures/low-confidence-worker.cjs +73 -0
  44. package/scripts/tests/fixtures/success-worker.cjs +42 -0
  45. package/scripts/tests/payload-guard.test.cjs +41 -0
  46. package/scripts/tests/run-bug-hunter.test.cjs +403 -0
  47. package/scripts/tests/test-utils.cjs +59 -0
  48. package/scripts/tests/worktree-harvest.test.cjs +297 -0
  49. package/scripts/triage.cjs +528 -0
  50. package/scripts/worktree-harvest.cjs +516 -0
  51. package/templates/subagent-wrapper.md +109 -0
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env node
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const SOURCE_EXTENSIONS = [
8
+ '.ts',
9
+ '.tsx',
10
+ '.js',
11
+ '.jsx',
12
+ '.mjs',
13
+ '.cjs',
14
+ '.py',
15
+ '.go',
16
+ '.rs',
17
+ '.java',
18
+ '.kt',
19
+ '.rb',
20
+ '.php'
21
+ ];
22
+
23
+ const JS_CALL_KEYWORDS = new Set([
24
+ 'if',
25
+ 'for',
26
+ 'while',
27
+ 'switch',
28
+ 'catch',
29
+ 'return',
30
+ 'new',
31
+ 'typeof',
32
+ 'await'
33
+ ]);
34
+
35
+ function usage() {
36
+ console.error('Usage:');
37
+ console.error(' code-index.cjs build <indexPath> <filesJsonPath> [repoRoot]');
38
+ console.error(' code-index.cjs status <indexPath>');
39
+ console.error(' code-index.cjs deps <indexPath> <filePath>');
40
+ console.error(' code-index.cjs reverse-deps <indexPath> <filePath>');
41
+ console.error(' code-index.cjs query <indexPath> <seedFilesJsonPath> [hops]');
42
+ console.error(' code-index.cjs query-bugs <indexPath> <bugsJsonPath> [hops]');
43
+ }
44
+
45
+ function nowIso() {
46
+ return new Date().toISOString();
47
+ }
48
+
49
+ function ensureDir(dirPath) {
50
+ fs.mkdirSync(dirPath, { recursive: true });
51
+ }
52
+
53
+ function readJson(filePath) {
54
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
55
+ }
56
+
57
+ function writeJson(filePath, value) {
58
+ ensureDir(path.dirname(filePath));
59
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
60
+ }
61
+
62
+ function sha256(input) {
63
+ return crypto.createHash('sha256').update(input).digest('hex');
64
+ }
65
+
66
+ function isSupportedSource(filePath) {
67
+ return SOURCE_EXTENSIONS.includes(path.extname(filePath));
68
+ }
69
+
70
+ function isTestFile(filePath) {
71
+ const normalized = filePath.replace(/\\/g, '/');
72
+ return (
73
+ normalized.includes('/__tests__/') ||
74
+ normalized.includes('/tests/') ||
75
+ normalized.endsWith('.test.ts') ||
76
+ normalized.endsWith('.test.tsx') ||
77
+ normalized.endsWith('.test.js') ||
78
+ normalized.endsWith('.spec.ts') ||
79
+ normalized.endsWith('.spec.tsx') ||
80
+ normalized.endsWith('.spec.js')
81
+ );
82
+ }
83
+
84
+ function inferRiskHint(relativePath) {
85
+ const normalized = relativePath.toLowerCase();
86
+ if (isTestFile(relativePath)) {
87
+ return 'context-only';
88
+ }
89
+ if (/(auth|middleware|route|router|api|controller|handler|server|webhook|payment|billing)/.test(normalized)) {
90
+ return 'critical';
91
+ }
92
+ if (/(service|state|store|db|repository|queue|worker|cron|job|model)/.test(normalized)) {
93
+ return 'high';
94
+ }
95
+ return 'medium';
96
+ }
97
+
98
+ function inferTrustBoundaries(relativePath, content) {
99
+ const boundaries = new Set();
100
+ const normalizedPath = relativePath.toLowerCase();
101
+ const normalizedContent = content.toLowerCase();
102
+
103
+ if (/(route|router|api|controller|handler|webhook)/.test(normalizedPath)) {
104
+ boundaries.add('external-input');
105
+ }
106
+ if (/(auth|middleware|session|token|jwt|permission|acl)/.test(normalizedPath + normalizedContent)) {
107
+ boundaries.add('auth');
108
+ }
109
+ if (/(db|model|repository|query|prisma|sql|mongo|redis)/.test(normalizedPath + normalizedContent)) {
110
+ boundaries.add('data-store');
111
+ }
112
+ if (/(queue|worker|cron|job|kafka|rabbitmq|sqs|pubsub)/.test(normalizedPath + normalizedContent)) {
113
+ boundaries.add('async-boundary');
114
+ }
115
+
116
+ return [...boundaries].sort();
117
+ }
118
+
119
+ function extractImports(content, extension) {
120
+ const imports = new Set();
121
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(extension)) {
122
+ const regexes = [
123
+ /import\s+[^'"]*?from\s+['"]([^'"]+)['"]/g,
124
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
125
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
126
+ /export\s+[^'"]*?from\s+['"]([^'"]+)['"]/g
127
+ ];
128
+ for (const regex of regexes) {
129
+ let match = regex.exec(content);
130
+ while (match) {
131
+ imports.add(match[1]);
132
+ match = regex.exec(content);
133
+ }
134
+ }
135
+ } else if (extension === '.py') {
136
+ const importRegex = /^\s*import\s+([a-zA-Z0-9_\.]+)/gm;
137
+ const fromRegex = /^\s*from\s+([a-zA-Z0-9_\.]+)\s+import\s+/gm;
138
+ let match = importRegex.exec(content);
139
+ while (match) {
140
+ imports.add(match[1]);
141
+ match = importRegex.exec(content);
142
+ }
143
+ match = fromRegex.exec(content);
144
+ while (match) {
145
+ imports.add(match[1]);
146
+ match = fromRegex.exec(content);
147
+ }
148
+ }
149
+ return [...imports];
150
+ }
151
+
152
+ function extractSymbols(content, extension) {
153
+ const symbols = new Set();
154
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(extension)) {
155
+ const patterns = [
156
+ /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g,
157
+ /(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)\s*/g,
158
+ /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:async\s*)?\(/g,
159
+ /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g
160
+ ];
161
+ for (const pattern of patterns) {
162
+ let match = pattern.exec(content);
163
+ while (match) {
164
+ symbols.add(match[1]);
165
+ match = pattern.exec(content);
166
+ }
167
+ }
168
+ } else if (extension === '.py') {
169
+ const patterns = [
170
+ /^\s*def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/gm,
171
+ /^\s*class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[:\(]/gm
172
+ ];
173
+ for (const pattern of patterns) {
174
+ let match = pattern.exec(content);
175
+ while (match) {
176
+ symbols.add(match[1]);
177
+ match = pattern.exec(content);
178
+ }
179
+ }
180
+ }
181
+ return [...symbols].sort();
182
+ }
183
+
184
+ function extractCalls(content, extension) {
185
+ const calls = new Set();
186
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(extension)) {
187
+ const callPattern = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
188
+ let match = callPattern.exec(content);
189
+ while (match) {
190
+ const name = match[1];
191
+ if (!JS_CALL_KEYWORDS.has(name)) {
192
+ calls.add(name);
193
+ }
194
+ match = callPattern.exec(content);
195
+ }
196
+ } else if (extension === '.py') {
197
+ const callPattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
198
+ let match = callPattern.exec(content);
199
+ while (match) {
200
+ calls.add(match[1]);
201
+ match = callPattern.exec(content);
202
+ }
203
+ }
204
+ return [...calls].sort();
205
+ }
206
+
207
+ function resolveRelativeImport(specifier, fromFilePath, fileSet) {
208
+ if (!specifier.startsWith('.')) {
209
+ return null;
210
+ }
211
+ const fromDir = path.dirname(fromFilePath);
212
+ const base = path.resolve(fromDir, specifier);
213
+ const candidates = [
214
+ base,
215
+ ...SOURCE_EXTENSIONS.map((ext) => `${base}${ext}`),
216
+ ...SOURCE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
217
+ ];
218
+ for (const candidate of candidates) {
219
+ if (fileSet.has(candidate)) {
220
+ return candidate;
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+
226
+ function normalizeFilePath(filePath) {
227
+ return path.resolve(filePath);
228
+ }
229
+
230
+ function buildCallGraph(filesIndex, symbolDefinitions) {
231
+ const graph = {};
232
+ for (const [filePath, entry] of Object.entries(filesIndex)) {
233
+ const callees = new Set();
234
+ const unresolved = new Set();
235
+ for (const callName of entry.calls) {
236
+ const targets = symbolDefinitions[callName] || [];
237
+ if (targets.length === 1) {
238
+ callees.add(targets[0]);
239
+ continue;
240
+ }
241
+ if (targets.length > 1) {
242
+ const externalTargets = targets.filter((targetFile) => targetFile !== filePath);
243
+ for (const target of externalTargets) {
244
+ callees.add(target);
245
+ }
246
+ continue;
247
+ }
248
+ unresolved.add(callName);
249
+ }
250
+ graph[filePath] = {
251
+ callees: [...callees].sort(),
252
+ unresolvedCalls: [...unresolved].sort()
253
+ };
254
+ }
255
+ return graph;
256
+ }
257
+
258
+ function expandByHops({ seeds, index, hops }) {
259
+ const selected = new Set(seeds);
260
+ let frontier = new Set(seeds);
261
+ for (let hop = 0; hop < hops; hop += 1) {
262
+ const next = new Set();
263
+ for (const filePath of frontier) {
264
+ const deps = (index.files[filePath] && index.files[filePath].dependencies) || [];
265
+ const reverse = index.reverseDependencies[filePath] || [];
266
+ for (const neighbor of [...deps, ...reverse]) {
267
+ if (selected.has(neighbor)) {
268
+ continue;
269
+ }
270
+ selected.add(neighbor);
271
+ next.add(neighbor);
272
+ }
273
+ }
274
+ if (next.size === 0) {
275
+ break;
276
+ }
277
+ frontier = next;
278
+ }
279
+ return [...selected].sort();
280
+ }
281
+
282
+ function buildIndex(indexPath, filesJsonPath, repoRootInput) {
283
+ const filesRaw = readJson(filesJsonPath);
284
+ if (!Array.isArray(filesRaw)) {
285
+ throw new Error('filesJsonPath must contain an array');
286
+ }
287
+ const repoRoot = path.resolve(repoRootInput || process.cwd());
288
+ const files = [...new Set(filesRaw.map((filePath) => normalizeFilePath(filePath)))]
289
+ .filter((filePath) => fs.existsSync(filePath))
290
+ .filter((filePath) => isSupportedSource(filePath))
291
+ .sort();
292
+ const fileSet = new Set(files);
293
+ const filesIndex = {};
294
+ const reverseDepsMap = new Map();
295
+ const symbolDefinitions = {};
296
+
297
+ for (const filePath of files) {
298
+ const content = fs.readFileSync(filePath, 'utf8');
299
+ const extension = path.extname(filePath);
300
+ const importsRaw = extractImports(content, extension);
301
+ const symbols = extractSymbols(content, extension);
302
+ const calls = extractCalls(content, extension);
303
+ const resolvedDeps = [];
304
+ const unresolvedDeps = [];
305
+
306
+ for (const importSpecifier of importsRaw) {
307
+ const resolved = resolveRelativeImport(importSpecifier, filePath, fileSet);
308
+ if (resolved) {
309
+ resolvedDeps.push(resolved);
310
+ if (!reverseDepsMap.has(resolved)) {
311
+ reverseDepsMap.set(resolved, new Set());
312
+ }
313
+ reverseDepsMap.get(resolved).add(filePath);
314
+ } else {
315
+ unresolvedDeps.push(importSpecifier);
316
+ }
317
+ }
318
+
319
+ const relativePath = path.relative(repoRoot, filePath) || path.basename(filePath);
320
+ const trustBoundaries = inferTrustBoundaries(relativePath, content);
321
+ filesIndex[filePath] = {
322
+ relativePath,
323
+ extension,
324
+ hash: sha256(content),
325
+ lineCount: content.split('\n').length,
326
+ importsRaw,
327
+ symbols,
328
+ calls,
329
+ dependencies: [...new Set(resolvedDeps)].sort(),
330
+ unresolvedDependencies: [...new Set(unresolvedDeps)].sort(),
331
+ riskHint: inferRiskHint(relativePath),
332
+ trustBoundaries,
333
+ isTest: isTestFile(relativePath)
334
+ };
335
+
336
+ for (const symbol of symbols) {
337
+ if (!symbolDefinitions[symbol]) {
338
+ symbolDefinitions[symbol] = [];
339
+ }
340
+ symbolDefinitions[symbol].push(filePath);
341
+ }
342
+ }
343
+
344
+ const reverseDependencies = {};
345
+ for (const [dependencyPath, dependents] of reverseDepsMap.entries()) {
346
+ reverseDependencies[dependencyPath] = [...dependents].sort();
347
+ }
348
+
349
+ const callGraph = buildCallGraph(filesIndex, symbolDefinitions);
350
+ const symbolCount = Object.values(filesIndex).reduce((sum, entry) => {
351
+ return sum + entry.symbols.length;
352
+ }, 0);
353
+ const callEdges = Object.values(callGraph).reduce((sum, entry) => {
354
+ return sum + entry.callees.length;
355
+ }, 0);
356
+ const trustBoundaryFiles = Object.values(filesIndex).filter((entry) => {
357
+ return Array.isArray(entry.trustBoundaries) && entry.trustBoundaries.length > 0;
358
+ }).length;
359
+
360
+ const index = {
361
+ schemaVersion: 2,
362
+ builtAt: nowIso(),
363
+ repoRoot,
364
+ metrics: {
365
+ filesIndexed: Object.keys(filesIndex).length,
366
+ dependencyEdges: Object.values(filesIndex).reduce((sum, entry) => {
367
+ return sum + entry.dependencies.length;
368
+ }, 0),
369
+ symbolsIndexed: symbolCount,
370
+ callEdges,
371
+ trustBoundaryFiles
372
+ },
373
+ files: filesIndex,
374
+ symbolDefinitions,
375
+ reverseDependencies,
376
+ callGraph
377
+ };
378
+
379
+ writeJson(indexPath, index);
380
+ return {
381
+ ok: true,
382
+ indexPath,
383
+ metrics: index.metrics
384
+ };
385
+ }
386
+
387
+ function status(indexPath) {
388
+ const index = readJson(indexPath);
389
+ return {
390
+ ok: true,
391
+ indexPath,
392
+ schemaVersion: index.schemaVersion,
393
+ builtAt: index.builtAt,
394
+ repoRoot: index.repoRoot,
395
+ metrics: index.metrics
396
+ };
397
+ }
398
+
399
+ function getDeps(indexPath, filePath, reverse) {
400
+ const index = readJson(indexPath);
401
+ const normalizedTarget = path.resolve(filePath);
402
+ if (reverse) {
403
+ return {
404
+ ok: true,
405
+ file: normalizedTarget,
406
+ reverseDependencies: index.reverseDependencies[normalizedTarget] || []
407
+ };
408
+ }
409
+ return {
410
+ ok: true,
411
+ file: normalizedTarget,
412
+ dependencies: (index.files[normalizedTarget] && index.files[normalizedTarget].dependencies) || []
413
+ };
414
+ }
415
+
416
+ function query(indexPath, seedFilesJsonPath, hopsRaw) {
417
+ const index = readJson(indexPath);
418
+ const seedFilesRaw = readJson(seedFilesJsonPath);
419
+ if (!Array.isArray(seedFilesRaw)) {
420
+ throw new Error('seedFilesJson must contain an array');
421
+ }
422
+ const hops = Number.isInteger(Number.parseInt(String(hopsRaw || ''), 10))
423
+ ? Number.parseInt(String(hopsRaw || ''), 10)
424
+ : 1;
425
+ const filesInIndex = new Set(Object.keys(index.files || {}));
426
+ const seeds = [...new Set(seedFilesRaw.map((filePath) => path.resolve(String(filePath))))]
427
+ .filter((filePath) => filesInIndex.has(filePath));
428
+ const selected = expandByHops({ seeds, index, hops: hops > 0 ? hops : 1 });
429
+ const trustBoundaryFiles = selected.filter((filePath) => {
430
+ const fileMeta = index.files[filePath];
431
+ return fileMeta && Array.isArray(fileMeta.trustBoundaries) && fileMeta.trustBoundaries.length > 0;
432
+ });
433
+ return {
434
+ ok: true,
435
+ hops: hops > 0 ? hops : 1,
436
+ seeds,
437
+ selected,
438
+ trustBoundaryFiles,
439
+ metrics: {
440
+ seedCount: seeds.length,
441
+ selectedCount: selected.length,
442
+ trustBoundaryCount: trustBoundaryFiles.length
443
+ }
444
+ };
445
+ }
446
+
447
+ function queryBugs(indexPath, bugsJsonPath, hopsRaw) {
448
+ const bugs = readJson(bugsJsonPath);
449
+ if (!Array.isArray(bugs)) {
450
+ throw new Error('bugsJsonPath must contain an array');
451
+ }
452
+ const seedFiles = bugs
453
+ .map((bug) => String((bug && bug.file) || '').trim())
454
+ .filter(Boolean)
455
+ .map((filePath) => path.resolve(filePath));
456
+ const tempSeedPath = path.join(path.dirname(path.resolve(bugsJsonPath)), '.seed-files.tmp.json');
457
+ writeJson(tempSeedPath, seedFiles);
458
+ const result = query(indexPath, tempSeedPath, hopsRaw);
459
+ fs.unlinkSync(tempSeedPath);
460
+ return result;
461
+ }
462
+
463
+ function main() {
464
+ const [command, ...args] = process.argv.slice(2);
465
+ if (!command) {
466
+ usage();
467
+ process.exit(1);
468
+ }
469
+
470
+ if (command === 'build') {
471
+ const [indexPath, filesJsonPath, repoRoot] = args;
472
+ if (!indexPath || !filesJsonPath) {
473
+ usage();
474
+ process.exit(1);
475
+ }
476
+ const result = buildIndex(path.resolve(indexPath), path.resolve(filesJsonPath), repoRoot);
477
+ console.log(JSON.stringify(result, null, 2));
478
+ return;
479
+ }
480
+
481
+ if (command === 'status') {
482
+ const [indexPath] = args;
483
+ if (!indexPath) {
484
+ usage();
485
+ process.exit(1);
486
+ }
487
+ console.log(JSON.stringify(status(path.resolve(indexPath)), null, 2));
488
+ return;
489
+ }
490
+
491
+ if (command === 'deps') {
492
+ const [indexPath, filePath] = args;
493
+ if (!indexPath || !filePath) {
494
+ usage();
495
+ process.exit(1);
496
+ }
497
+ console.log(JSON.stringify(getDeps(path.resolve(indexPath), filePath, false), null, 2));
498
+ return;
499
+ }
500
+
501
+ if (command === 'reverse-deps') {
502
+ const [indexPath, filePath] = args;
503
+ if (!indexPath || !filePath) {
504
+ usage();
505
+ process.exit(1);
506
+ }
507
+ console.log(JSON.stringify(getDeps(path.resolve(indexPath), filePath, true), null, 2));
508
+ return;
509
+ }
510
+
511
+ if (command === 'query') {
512
+ const [indexPath, seedFilesJsonPath, hopsRaw] = args;
513
+ if (!indexPath || !seedFilesJsonPath) {
514
+ usage();
515
+ process.exit(1);
516
+ }
517
+ console.log(JSON.stringify(query(path.resolve(indexPath), path.resolve(seedFilesJsonPath), hopsRaw), null, 2));
518
+ return;
519
+ }
520
+
521
+ if (command === 'query-bugs') {
522
+ const [indexPath, bugsJsonPath, hopsRaw] = args;
523
+ if (!indexPath || !bugsJsonPath) {
524
+ usage();
525
+ process.exit(1);
526
+ }
527
+ console.log(JSON.stringify(queryBugs(path.resolve(indexPath), path.resolve(bugsJsonPath), hopsRaw), null, 2));
528
+ return;
529
+ }
530
+
531
+ usage();
532
+ process.exit(1);
533
+ }
534
+
535
+ try {
536
+ main();
537
+ } catch (error) {
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ console.error(message);
540
+ process.exit(1);
541
+ }
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Context7 API Helper Script
5
+ * Provides simple CLI interface to Context7 API for skill integration
6
+ */
7
+
8
+ const https = require('https');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const API_BASE = 'https://context7.com/api/v2';
13
+ const REQUEST_TIMEOUT_MS = 15000;
14
+
15
+ // Load API key from environment variable or .env file
16
+ function loadApiKey() {
17
+ // First try environment variable
18
+ if (process.env.CONTEXT7_API_KEY) {
19
+ return process.env.CONTEXT7_API_KEY;
20
+ }
21
+
22
+ // Then try .env file next to this script
23
+ const envPath = path.join(__dirname, '.env');
24
+ if (fs.existsSync(envPath)) {
25
+ const envContent = fs.readFileSync(envPath, 'utf8');
26
+ const match = envContent.match(/CONTEXT7_API_KEY\s*=\s*(.+)/);
27
+ if (match) {
28
+ return match[1].trim().replace(/^["']|["']$/g, '');
29
+ }
30
+ }
31
+
32
+ return null;
33
+ }
34
+
35
+ const API_KEY = loadApiKey();
36
+
37
+ function makeRequest(path, params = {}) {
38
+ return new Promise((resolve, reject) => {
39
+ const queryString = new URLSearchParams(params).toString();
40
+ const url = `${API_BASE}${path}?${queryString}`;
41
+
42
+ const headers = {
43
+ 'User-Agent': 'Context7-Skill/1.0'
44
+ };
45
+ if (API_KEY) {
46
+ headers['Authorization'] = `Bearer ${API_KEY}`;
47
+ }
48
+
49
+ const options = {
50
+ headers
51
+ };
52
+
53
+ const req = https.get(url, options, (res) => {
54
+ let data = '';
55
+
56
+ res.on('data', (chunk) => {
57
+ data += chunk;
58
+ });
59
+
60
+ res.on('end', () => {
61
+ if (res.statusCode === 200) {
62
+ try {
63
+ resolve(JSON.parse(data));
64
+ } catch (e) {
65
+ resolve(data);
66
+ }
67
+ } else {
68
+ reject(new Error(`API Error ${res.statusCode}: ${data}`));
69
+ }
70
+ });
71
+ });
72
+
73
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
74
+ req.destroy(new Error(`Request timeout after ${REQUEST_TIMEOUT_MS}ms`));
75
+ });
76
+
77
+ req.on('error', reject);
78
+ });
79
+ }
80
+
81
+ async function searchLibrary(libraryName, query) {
82
+ try {
83
+ const result = await makeRequest('/libs/search', {
84
+ libraryName,
85
+ query
86
+ });
87
+ return result;
88
+ } catch (error) {
89
+ console.error(`Error searching library: ${error.message}`);
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async function getContext(libraryId, query) {
95
+ try {
96
+ const result = await makeRequest('/context', {
97
+ libraryId,
98
+ query,
99
+ type: 'json'
100
+ });
101
+ return result;
102
+ } catch (error) {
103
+ console.error(`Error getting context: ${error.message}`);
104
+ return null;
105
+ }
106
+ }
107
+
108
+ // CLI Interface
109
+ const command = process.argv[2];
110
+ const args = process.argv.slice(3);
111
+
112
+ (async () => {
113
+ if (command === 'search') {
114
+ const [libraryName, query] = args;
115
+ if (!libraryName || !query) {
116
+ console.error('Usage: context7-api.cjs search <libraryName> <query>');
117
+ process.exit(1);
118
+ }
119
+ const result = await searchLibrary(libraryName, query);
120
+ console.log(JSON.stringify(result, null, 2));
121
+ } else if (command === 'context') {
122
+ const [libraryId, query] = args;
123
+ if (!libraryId || !query) {
124
+ console.error('Usage: context7-api.cjs context <libraryId> <query>');
125
+ process.exit(1);
126
+ }
127
+ const result = await getContext(libraryId, query);
128
+ console.log(JSON.stringify(result, null, 2));
129
+ } else {
130
+ console.error('Usage: context7-api.cjs <search|context> <args...>');
131
+ process.exit(1);
132
+ }
133
+ })();