@pkgseer/cli 0.1.5 → 0.2.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.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  version
4
- } from "./shared/chunk-0fnprry7.js";
4
+ } from "./shared/chunk-1m9g9ehr.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -205,11 +205,65 @@ var CliDocsListDocument = gql`
205
205
  pages {
206
206
  id
207
207
  title
208
- words
209
208
  }
210
209
  }
211
210
  }
212
211
  `;
212
+ var CombinedSearchDocument = gql`
213
+ query CombinedSearch($packages: [SearchPackageInput!]!, $query: String!, $mode: SearchMode, $limit: Int) {
214
+ combinedSearch(packages: $packages, query: $query, mode: $mode, limit: $limit) {
215
+ query
216
+ mode
217
+ totalResults
218
+ entries {
219
+ id
220
+ type
221
+ title
222
+ subtitle
223
+ packageName
224
+ registry
225
+ score
226
+ snippet
227
+ filePath
228
+ startLine
229
+ endLine
230
+ language
231
+ chunkType
232
+ repoUrl
233
+ gitRef
234
+ pageId
235
+ sourceUrl
236
+ }
237
+ indexingStatus {
238
+ registry
239
+ packageName
240
+ version
241
+ codeStatus
242
+ docsStatus
243
+ }
244
+ }
245
+ }
246
+ `;
247
+ var FetchCodeContextDocument = gql`
248
+ query FetchCodeContext($repoUrl: String!, $gitRef: String!, $filePath: String!, $startLine: Int, $endLine: Int) {
249
+ fetchCodeContext(
250
+ repoUrl: $repoUrl
251
+ gitRef: $gitRef
252
+ filePath: $filePath
253
+ startLine: $startLine
254
+ endLine: $endLine
255
+ ) {
256
+ content
257
+ filePath
258
+ language
259
+ totalLines
260
+ startLine
261
+ endLine
262
+ repoUrl
263
+ gitRef
264
+ }
265
+ }
266
+ `;
213
267
  var CliDocsGetDocument = gql`
214
268
  query CliDocsGet($registry: Registry!, $packageName: String!, $pageId: String!, $version: String) {
215
269
  fetchPackageDoc(
@@ -428,7 +482,6 @@ var ListPackageDocsDocument = gql`
428
482
  slug
429
483
  order
430
484
  linkName
431
- words
432
485
  lastUpdatedAt
433
486
  sourceUrl
434
487
  }
@@ -509,7 +562,6 @@ var SearchPackageDocsDocument = gql`
509
562
  slug
510
563
  order
511
564
  linkName
512
- words
513
565
  lastUpdatedAt
514
566
  sourceUrl
515
567
  matchCount
@@ -540,7 +592,6 @@ var SearchProjectDocsDocument = gql`
540
592
  registry
541
593
  packageName
542
594
  version
543
- words
544
595
  matchCount
545
596
  titleHit
546
597
  score
@@ -560,6 +611,8 @@ var CliComparePackagesDocumentString = print(CliComparePackagesDocument);
560
611
  var CliDocsSearchDocumentString = print(CliDocsSearchDocument);
561
612
  var CliProjectDocsSearchDocumentString = print(CliProjectDocsSearchDocument);
562
613
  var CliDocsListDocumentString = print(CliDocsListDocument);
614
+ var CombinedSearchDocumentString = print(CombinedSearchDocument);
615
+ var FetchCodeContextDocumentString = print(FetchCodeContextDocument);
563
616
  var CliDocsGetDocumentString = print(CliDocsGetDocument);
564
617
  var CreateProjectDocumentString = print(CreateProjectDocument);
565
618
  var PackageSummaryDocumentString = print(PackageSummaryDocument);
@@ -598,6 +651,12 @@ function getSdk(client, withWrapper = defaultWrapper) {
598
651
  CliDocsList(variables, requestHeaders) {
599
652
  return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliDocsListDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliDocsList", "query", variables);
600
653
  },
654
+ CombinedSearch(variables, requestHeaders) {
655
+ return withWrapper((wrappedRequestHeaders) => client.rawRequest(CombinedSearchDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CombinedSearch", "query", variables);
656
+ },
657
+ FetchCodeContext(variables, requestHeaders) {
658
+ return withWrapper((wrappedRequestHeaders) => client.rawRequest(FetchCodeContextDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "FetchCodeContext", "query", variables);
659
+ },
601
660
  CliDocsGet(variables, requestHeaders) {
602
661
  return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliDocsGetDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliDocsGet", "query", variables);
603
662
  },
@@ -960,7 +1019,8 @@ var TOOL_NAMES = [
960
1019
  "compare_packages",
961
1020
  "list_package_docs",
962
1021
  "fetch_package_doc",
963
- "search_package_docs",
1022
+ "search",
1023
+ "fetch_code_context",
964
1024
  "search_project_docs"
965
1025
  ];
966
1026
  var ToolNameSchema = z.enum(TOOL_NAMES);
@@ -1002,7 +1062,10 @@ class ConfigServiceImpl {
1002
1062
  return this.loadAndParseConfig(configPath, GlobalConfigSchema);
1003
1063
  }
1004
1064
  async loadProjectConfig() {
1005
- let currentDir = this.fs.getCwd();
1065
+ return this.loadProjectConfigFrom(this.fs.getCwd());
1066
+ }
1067
+ async loadProjectConfigFrom(startDir) {
1068
+ let currentDir = startDir;
1006
1069
  while (true) {
1007
1070
  const configPath = this.fs.joinPath(currentDir, PROJECT_CONFIG_FILE);
1008
1071
  const config = await this.loadAndParseConfig(configPath, ProjectConfigSchema);
@@ -1350,6 +1413,25 @@ class PkgseerServiceImpl {
1350
1413
  });
1351
1414
  return { data: result.data, errors: result.errors };
1352
1415
  }
1416
+ async combinedSearch(packages, query, options) {
1417
+ const result = await this.client.CombinedSearch({
1418
+ packages,
1419
+ query,
1420
+ mode: options?.mode,
1421
+ limit: options?.limit
1422
+ });
1423
+ return { data: result.data, errors: result.errors };
1424
+ }
1425
+ async fetchCodeContext(repoUrl, gitRef, filePath, options) {
1426
+ const result = await this.client.FetchCodeContext({
1427
+ repoUrl,
1428
+ gitRef,
1429
+ filePath,
1430
+ startLine: options?.startLine,
1431
+ endLine: options?.endLine
1432
+ });
1433
+ return { data: result.data, errors: result.errors };
1434
+ }
1353
1435
  }
1354
1436
  // src/services/project-service.ts
1355
1437
  var PROJECT_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
@@ -2182,8 +2264,7 @@ function formatDocsList(docs) {
2182
2264
  for (const page of docs.pages) {
2183
2265
  if (!page || !page.id)
2184
2266
  continue;
2185
- const words = page.words ? ` (${formatNumber(page.words)} words)` : "";
2186
- lines.push(` ${page.title}${words}`);
2267
+ lines.push(` ${page.title}`);
2187
2268
  lines.push(` ID: ${page.id}`);
2188
2269
  lines.push("");
2189
2270
  }
@@ -2234,51 +2315,6 @@ function registerDocsListCommand(program) {
2234
2315
  });
2235
2316
  }
2236
2317
  // src/commands/docs/search.ts
2237
- function summarizeSearchResults(results) {
2238
- const entries = results.entries ?? [];
2239
- const count = entries.length;
2240
- const totalMatches = entries.reduce((acc, e) => acc + (e?.matchCount ?? 0), 0);
2241
- const lines = [];
2242
- lines.push(`Results: ${count} page${count === 1 ? "" : "s"} | matches: ${totalMatches}`);
2243
- lines.push("");
2244
- for (const entry of entries.filter((e) => !!e)) {
2245
- lines.push(`- ${entry.title ?? entry.slug ?? "untitled"} (${entry.matchCount ?? 0} matches)`);
2246
- }
2247
- if (count === 0) {
2248
- lines.push("No matches. Try broader terms or fewer constraints.");
2249
- }
2250
- return lines.join(`
2251
- `);
2252
- }
2253
- function buildDocRef(entry, searchResult) {
2254
- if (!entry?.slug)
2255
- return "";
2256
- let registry;
2257
- let packageName;
2258
- let version2;
2259
- if ("packageName" in entry && "registry" in entry && "version" in entry) {
2260
- registry = entry.registry;
2261
- packageName = entry.packageName;
2262
- version2 = entry.version;
2263
- } else {
2264
- const rawRegistry = "registry" in searchResult ? searchResult.registry : null;
2265
- registry = rawRegistry ? rawRegistry.toLowerCase() : null;
2266
- packageName = "packageName" in searchResult ? searchResult.packageName : null;
2267
- version2 = "version" in searchResult ? searchResult.version : null;
2268
- }
2269
- const parts = [];
2270
- if (registry)
2271
- parts.push(registry.toLowerCase());
2272
- if (packageName)
2273
- parts.push(packageName);
2274
- if (version2)
2275
- parts.push(version2);
2276
- else if (registry && packageName)
2277
- parts.push("latest");
2278
- if (entry.slug)
2279
- parts.push(entry.slug);
2280
- return parts.join("/");
2281
- }
2282
2318
  var colors = {
2283
2319
  reset: "\x1B[0m",
2284
2320
  bold: "\x1B[1m",
@@ -2286,7 +2322,8 @@ var colors = {
2286
2322
  magenta: "\x1B[35m",
2287
2323
  cyan: "\x1B[36m",
2288
2324
  yellow: "\x1B[33m",
2289
- green: "\x1B[32m"
2325
+ green: "\x1B[32m",
2326
+ blue: "\x1B[34m"
2290
2327
  };
2291
2328
  function shouldUseColors(noColor) {
2292
2329
  if (noColor)
@@ -2295,219 +2332,420 @@ function shouldUseColors(noColor) {
2295
2332
  return false;
2296
2333
  return process.stdout.isTTY ?? false;
2297
2334
  }
2298
- function applyHighlights(line, highlights, useColors) {
2299
- if (!highlights || highlights.length === 0 || !useColors) {
2300
- return line;
2301
- }
2302
- const sorted = [...highlights].filter((h) => h && h.start != null && h.end != null).sort((a, b) => {
2303
- const aStart = a?.start ?? 0;
2304
- const bStart = b?.start ?? 0;
2305
- return bStart - aStart;
2335
+ function toSearchMode(mode) {
2336
+ if (!mode)
2337
+ return "ALL";
2338
+ const map = {
2339
+ all: "ALL",
2340
+ code: "CODE",
2341
+ docs: "DOCS"
2342
+ };
2343
+ return map[mode.toLowerCase()] ?? "ALL";
2344
+ }
2345
+ function parsePackageList(input2) {
2346
+ return input2.split(",").map((spec) => {
2347
+ const trimmed = spec.trim();
2348
+ const atIndex = trimmed.lastIndexOf("@");
2349
+ let regPkg;
2350
+ let version2;
2351
+ if (atIndex > 0) {
2352
+ regPkg = trimmed.slice(0, atIndex);
2353
+ version2 = trimmed.slice(atIndex + 1);
2354
+ } else {
2355
+ regPkg = trimmed;
2356
+ }
2357
+ const slashIndex = regPkg.indexOf("/");
2358
+ if (slashIndex === -1) {
2359
+ return {
2360
+ registry: "NPM",
2361
+ name: regPkg,
2362
+ version: version2
2363
+ };
2364
+ }
2365
+ const beforeSlash = regPkg.slice(0, slashIndex);
2366
+ const afterSlash = regPkg.slice(slashIndex + 1);
2367
+ if (beforeSlash.startsWith("@")) {
2368
+ return {
2369
+ registry: "NPM",
2370
+ name: regPkg,
2371
+ version: version2
2372
+ };
2373
+ }
2374
+ return {
2375
+ registry: toGraphQLRegistry(beforeSlash),
2376
+ name: afterSlash,
2377
+ version: version2
2378
+ };
2306
2379
  });
2307
- let result = line;
2308
- for (const h of sorted) {
2309
- if (!h || h.start == null || h.end == null)
2310
- continue;
2311
- const before = result.slice(0, h.start);
2312
- const match = result.slice(h.start, h.end);
2313
- const after = result.slice(h.end);
2314
- result = `${before}${colors.bold}${colors.magenta}${match}${colors.reset}${after}`;
2380
+ }
2381
+ function formatTypeBadge(type, useColors) {
2382
+ const typeStr = type ?? "DOC";
2383
+ if (useColors) {
2384
+ const color = typeStr === "CODE" ? colors.magenta : colors.cyan;
2385
+ return `${color}[${typeStr}]${colors.reset}`;
2315
2386
  }
2316
- return result;
2387
+ return `[${typeStr}]`;
2317
2388
  }
2318
- function formatEntry(entry, searchResult, useColors) {
2389
+ function formatEntry(entry, useColors) {
2319
2390
  if (!entry)
2320
2391
  return "";
2321
2392
  const lines = [];
2322
- const ref = buildDocRef(entry, searchResult);
2323
- const matchInfo = entry.matchCount && entry.matchCount > 0 ? ` (${entry.matchCount} match${entry.matchCount > 1 ? "es" : ""})` : "";
2324
- if (useColors) {
2325
- lines.push(`${colors.cyan}${ref}${colors.reset}: ${colors.bold}${entry.title}${colors.reset}${colors.dim}${matchInfo}${colors.reset}`);
2326
- } else {
2327
- lines.push(`${ref}: ${entry.title}${matchInfo}`);
2328
- }
2329
- const matches = entry.matches ?? [];
2330
- for (const match of matches) {
2331
- if (!match?.context)
2332
- continue;
2333
- const ctx = match.context;
2334
- for (const beforeLine of ctx.before ?? []) {
2335
- if (beforeLine) {
2336
- const prefix = useColors ? colors.dim : "";
2337
- const suffix = useColors ? colors.reset : "";
2338
- lines.push(` ${prefix}${beforeLine}${suffix}`);
2393
+ const badge = formatTypeBadge(entry.type, useColors);
2394
+ if (entry.type === "CODE") {
2395
+ const title = entry.title ?? "Unknown";
2396
+ const location = entry.filePath ? `${entry.filePath}:${entry.startLine ?? "?"}-${entry.endLine ?? "?"}` : "";
2397
+ const lang = entry.language ? ` • ${entry.language}` : "";
2398
+ if (useColors) {
2399
+ lines.push(`${badge} ${colors.bold}${title}${colors.reset}`);
2400
+ if (location) {
2401
+ lines.push(` ${colors.dim}${location}${lang}${colors.reset}`);
2339
2402
  }
2340
- }
2341
- for (const matchedLine of ctx.matchedLines ?? []) {
2342
- if (matchedLine?.content) {
2343
- const highlighted = applyHighlights(matchedLine.content, matchedLine.highlights, useColors);
2344
- lines.push(` ${highlighted}`);
2403
+ } else {
2404
+ lines.push(`${badge} ${title}`);
2405
+ if (location) {
2406
+ lines.push(` ${location}${lang}`);
2345
2407
  }
2346
2408
  }
2347
- for (const afterLine of ctx.after ?? []) {
2348
- if (afterLine) {
2349
- const prefix = useColors ? colors.dim : "";
2350
- const suffix = useColors ? colors.reset : "";
2351
- lines.push(` ${prefix}${afterLine}${suffix}`);
2409
+ } else {
2410
+ const title = entry.title ?? "Untitled";
2411
+ const ref = entry.pageId ?? entry.sourceUrl ?? "";
2412
+ if (useColors) {
2413
+ lines.push(`${badge} ${colors.bold}${title}${colors.reset}`);
2414
+ if (ref) {
2415
+ lines.push(` ${colors.dim}${ref}${colors.reset}`);
2416
+ }
2417
+ } else {
2418
+ lines.push(`${badge} ${title}`);
2419
+ if (ref) {
2420
+ lines.push(` ${ref}`);
2352
2421
  }
2353
2422
  }
2423
+ }
2424
+ if (entry.snippet) {
2354
2425
  lines.push("");
2426
+ const snippetLines = entry.snippet.split(`
2427
+ `).slice(0, 5);
2428
+ for (const line of snippetLines) {
2429
+ if (useColors) {
2430
+ lines.push(` ${colors.dim}${line}${colors.reset}`);
2431
+ } else {
2432
+ lines.push(` ${line}`);
2433
+ }
2434
+ }
2355
2435
  }
2436
+ lines.push("");
2356
2437
  return lines.join(`
2357
2438
  `);
2358
2439
  }
2440
+ function formatPackageHeader(entry, prevEntry, useColors) {
2441
+ const pkg = entry?.packageName ?? "unknown";
2442
+ const registry = entry?.registry?.toLowerCase() ?? "npm";
2443
+ const prevPkg = prevEntry?.packageName;
2444
+ const prevRegistry = prevEntry?.registry?.toLowerCase();
2445
+ if (pkg === prevPkg && registry === prevRegistry) {
2446
+ return "";
2447
+ }
2448
+ const header = `${pkg} (${registry})`;
2449
+ const separator = "═".repeat(Math.min(50, header.length + 10));
2450
+ if (useColors) {
2451
+ return `
2452
+ ${colors.yellow}${header}${colors.reset}
2453
+ ${colors.dim}${separator}${colors.reset}
2454
+ `;
2455
+ }
2456
+ return `
2457
+ ${header}
2458
+ ${separator}
2459
+ `;
2460
+ }
2359
2461
  function formatSearchResults(results, useColors) {
2360
2462
  const entries = results.entries ?? [];
2361
2463
  if (entries.length === 0) {
2362
- return "No matching documentation found. Try broader terms or fewer constraints.";
2464
+ return "No results found. Try broader terms or different packages.";
2465
+ }
2466
+ const lines = [];
2467
+ let prevEntry = null;
2468
+ for (const entry of entries) {
2469
+ if (!entry)
2470
+ continue;
2471
+ const header = formatPackageHeader(entry, prevEntry, useColors);
2472
+ if (header) {
2473
+ lines.push(header);
2474
+ }
2475
+ lines.push(formatEntry(entry, useColors));
2476
+ prevEntry = entry;
2477
+ }
2478
+ const codeCount = entries.filter((e) => e?.type === "CODE").length;
2479
+ const docCount = entries.filter((e) => e?.type === "DOC" || e?.type === "REPO_DOC").length;
2480
+ if (useColors) {
2481
+ lines.push(`${colors.dim}───────────────────────────────────────────────────${colors.reset}`);
2482
+ lines.push(`${colors.dim}Results: ${entries.length} total (${codeCount} code, ${docCount} docs)${colors.reset}`);
2483
+ } else {
2484
+ lines.push("───────────────────────────────────────────────────");
2485
+ lines.push(`Results: ${entries.length} total (${codeCount} code, ${docCount} docs)`);
2486
+ }
2487
+ const indexingWarnings = formatIndexingWarnings(results.indexingStatus);
2488
+ if (indexingWarnings) {
2489
+ lines.push("");
2490
+ lines.push(indexingWarnings);
2363
2491
  }
2364
- const formatted = entries.filter((e) => e != null).map((entry) => formatEntry(entry, results, useColors));
2365
- return formatted.join(`
2492
+ return lines.join(`
2366
2493
  `);
2367
2494
  }
2368
2495
  function formatRefsOnly(results) {
2369
2496
  const entries = results.entries ?? [];
2370
- return entries.filter((e) => e != null).map((entry) => buildDocRef(entry, results)).join(`
2497
+ return entries.filter((e) => e != null).map((entry) => {
2498
+ if (!entry)
2499
+ return "";
2500
+ if (entry.type === "CODE") {
2501
+ return `${entry.repoUrl ?? ""}:${entry.filePath ?? ""}:${entry.startLine ?? ""}`;
2502
+ }
2503
+ return entry.pageId ?? entry.id ?? "";
2504
+ }).filter((s) => s !== "").join(`
2371
2505
  `);
2372
2506
  }
2373
2507
  function formatCount(results) {
2374
2508
  const entries = results.entries ?? [];
2375
- return entries.filter((e) => e != null).map((entry) => {
2509
+ const counts = new Map;
2510
+ for (const entry of entries) {
2376
2511
  if (!entry)
2377
- return "";
2378
- const ref = buildDocRef(entry, results);
2379
- return `${ref}: ${entry.matchCount ?? 0}`;
2380
- }).filter((s) => s !== "").join(`
2512
+ continue;
2513
+ const key = `${entry.packageName ?? "unknown"}@${entry.registry?.toLowerCase() ?? "npm"}`;
2514
+ const current = counts.get(key) ?? { code: 0, docs: 0 };
2515
+ if (entry.type === "CODE") {
2516
+ current.code++;
2517
+ } else {
2518
+ current.docs++;
2519
+ }
2520
+ counts.set(key, current);
2521
+ }
2522
+ return Array.from(counts.entries()).map(([pkg, { code, docs }]) => `${pkg}: ${code} code, ${docs} docs`).join(`
2381
2523
  `);
2382
2524
  }
2383
- function slimSearchResults(results) {
2525
+ function formatIndexingWarnings(indexingStatus) {
2526
+ if (!indexingStatus || indexingStatus.length === 0)
2527
+ return null;
2528
+ const warnings = [];
2529
+ for (const status of indexingStatus) {
2530
+ if (!status)
2531
+ continue;
2532
+ const pkg = `${status.packageName}@${status.version ?? "latest"}`;
2533
+ if (status.codeStatus && status.codeStatus !== "INDEXED") {
2534
+ warnings.push(` - ${pkg}: code ${status.codeStatus.toLowerCase()}`);
2535
+ }
2536
+ if (status.docsStatus && status.docsStatus !== "INDEXED") {
2537
+ warnings.push(` - ${pkg}: docs ${status.docsStatus.toLowerCase()}`);
2538
+ }
2539
+ }
2540
+ if (warnings.length === 0)
2541
+ return null;
2542
+ return `Note: Some packages are not fully indexed:
2543
+ ` + warnings.join(`
2544
+ `) + `
2545
+ Results may be incomplete.`;
2546
+ }
2547
+ var LANG_ABBREV = {
2548
+ javascript: "js",
2549
+ typescript: "ts",
2550
+ python: "py",
2551
+ markdown: "md"
2552
+ };
2553
+ function abbrevLang(lang) {
2554
+ if (!lang)
2555
+ return "";
2556
+ const lower = lang.toLowerCase();
2557
+ return LANG_ABBREV[lower] ?? lower;
2558
+ }
2559
+ function truncate(text, maxLen) {
2560
+ if (text.length <= maxLen)
2561
+ return text;
2562
+ return text.slice(0, maxLen - 3) + "...";
2563
+ }
2564
+ function formatEntryCompact(entry, useColors) {
2565
+ if (!entry)
2566
+ return "";
2567
+ const badge = formatTypeBadge(entry.type, useColors);
2568
+ let header;
2569
+ if (entry.type === "CODE") {
2570
+ const loc = entry.filePath ? `${entry.filePath}:${entry.startLine ?? "?"}-${entry.endLine ?? "?"}` : "";
2571
+ const lang = abbrevLang(entry.language);
2572
+ const title = entry.title && entry.title !== "Unnamed" ? ` ${entry.title}` : "";
2573
+ header = `${badge} ${loc}${lang ? ` ${lang}` : ""}${title}`;
2574
+ } else {
2575
+ const title = entry.title ?? "Untitled";
2576
+ const ref = entry.pageId ? ` (${entry.pageId})` : "";
2577
+ header = `${badge} ${title}${ref}`;
2578
+ }
2579
+ let snippet = "";
2580
+ if (entry.snippet) {
2581
+ const firstLine = entry.snippet.split(`
2582
+ `).map((l) => l.trim()).find((l) => l.length > 0);
2583
+ if (firstLine) {
2584
+ snippet = ` ${truncate(firstLine, 80)}`;
2585
+ if (useColors) {
2586
+ snippet = ` ${colors.dim}${truncate(firstLine, 80)}${colors.reset}`;
2587
+ }
2588
+ }
2589
+ }
2590
+ return snippet ? `${header}
2591
+ ${snippet}` : header;
2592
+ }
2593
+ function formatPackageHeaderCompact(entry, prevEntry, useColors) {
2594
+ const pkg = entry?.packageName ?? "unknown";
2595
+ const registry = entry?.registry?.toLowerCase() ?? "npm";
2596
+ const prevPkg = prevEntry?.packageName;
2597
+ const prevRegistry = prevEntry?.registry?.toLowerCase();
2598
+ if (pkg === prevPkg && registry === prevRegistry) {
2599
+ return "";
2600
+ }
2601
+ const header = `${pkg} (${registry})`;
2602
+ if (useColors) {
2603
+ return `
2604
+ ${colors.yellow}${header}${colors.reset}`;
2605
+ }
2606
+ return `
2607
+ ${header}`;
2608
+ }
2609
+ function formatSearchResultsCompact(results, useColors) {
2384
2610
  const entries = results.entries ?? [];
2385
- return entries.filter((e) => e != null).map((entry) => {
2611
+ if (entries.length === 0) {
2612
+ return "No results. Try broader terms or different packages.";
2613
+ }
2614
+ const lines = [];
2615
+ let prevEntry = null;
2616
+ for (const entry of entries) {
2386
2617
  if (!entry)
2387
- return null;
2388
- return {
2389
- ref: buildDocRef(entry, results),
2390
- title: entry.title ?? "",
2391
- matchCount: entry.matchCount ?? 0,
2392
- matches: entry.matches?.filter((m) => m?.context).map((m) => {
2393
- if (!m?.context)
2394
- return null;
2395
- return {
2396
- before: (m.context.before ?? []).filter((l) => l != null),
2397
- lines: (m.context.matchedLines ?? []).filter((l) => l?.content).map((l) => l?.content ?? ""),
2398
- after: (m.context.after ?? []).filter((l) => l != null)
2399
- };
2400
- }).filter((m) => m != null)
2401
- };
2402
- }).filter((e) => e != null);
2618
+ continue;
2619
+ const header = formatPackageHeaderCompact(entry, prevEntry, useColors);
2620
+ if (header) {
2621
+ lines.push(header);
2622
+ }
2623
+ lines.push(formatEntryCompact(entry, useColors));
2624
+ prevEntry = entry;
2625
+ }
2626
+ const codeCount = entries.filter((e) => e?.type === "CODE").length;
2627
+ const docCount = entries.filter((e) => e?.type === "DOC" || e?.type === "REPO_DOC").length;
2628
+ lines.push(`
2629
+ ${entries.length} results (${codeCount} code, ${docCount} docs)`);
2630
+ const indexingWarnings = formatIndexingWarnings(results.indexingStatus);
2631
+ if (indexingWarnings) {
2632
+ lines.push(indexingWarnings);
2633
+ }
2634
+ return lines.join(`
2635
+ `);
2636
+ }
2637
+ function summarizeSearchResults(results) {
2638
+ const entries = results.entries ?? [];
2639
+ const codeCount = entries.filter((e) => e?.type === "CODE").length;
2640
+ const docCount = entries.filter((e) => e?.type === "DOC" || e?.type === "REPO_DOC").length;
2641
+ const lines = [];
2642
+ lines.push(`Results: ${entries.length} total | ${codeCount} code | ${docCount} docs`);
2643
+ lines.push("");
2644
+ const byPackage = new Map;
2645
+ for (const entry of entries) {
2646
+ if (!entry)
2647
+ continue;
2648
+ const key = `${entry.packageName ?? "unknown"} (${entry.registry?.toLowerCase() ?? "npm"})`;
2649
+ const list = byPackage.get(key) ?? [];
2650
+ list.push(entry);
2651
+ byPackage.set(key, list);
2652
+ }
2653
+ for (const [pkg, pkgEntries] of byPackage) {
2654
+ const pkgCode = pkgEntries.filter((e) => e?.type === "CODE").length;
2655
+ const pkgDocs = pkgEntries.filter((e) => e?.type === "DOC" || e?.type === "REPO_DOC").length;
2656
+ lines.push(`- ${pkg}: ${pkgCode} code, ${pkgDocs} docs`);
2657
+ }
2658
+ if (entries.length === 0) {
2659
+ lines.push("No results. Try broader terms or different packages.");
2660
+ }
2661
+ return lines.join(`
2662
+ `);
2403
2663
  }
2404
2664
  async function docsSearchAction(queryArg, options, deps) {
2405
- const { pkgseerService, config } = deps;
2406
- const providedTerms = Array.isArray(queryArg) ? queryArg : queryArg ? [queryArg] : [];
2407
- const terms = providedTerms.concat(options.terms ?? []);
2408
- if (!terms.length) {
2409
- outputError("Search terms required. Provide terms as argument or with --terms", options.json ?? false);
2665
+ const { pkgseerService } = deps;
2666
+ const query = Array.isArray(queryArg) ? queryArg.join(" ") : queryArg ?? "";
2667
+ if (!query.trim()) {
2668
+ outputError("Search query required. Provide query as argument.", options.json ?? false);
2410
2669
  return;
2411
2670
  }
2412
- const contextBefore = options.context ? Number.parseInt(options.context, 10) : options.before ? Number.parseInt(options.before, 10) : 2;
2413
- const contextAfter = options.context ? Number.parseInt(options.context, 10) : options.after ? Number.parseInt(options.after, 10) : 2;
2414
- const maxMatches = options.maxMatches ? Number.parseInt(options.maxMatches, 10) : 5;
2415
- const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
2416
- const matchMode = options.match_mode?.toUpperCase() || options.mode?.toUpperCase() || undefined;
2417
- const useColors = shouldUseColors(options.noColor);
2418
- const project = options.project ?? config.project;
2419
- const packageName = options.package;
2420
- if (packageName) {
2421
- const registry = toGraphQLRegistry(options.registry ?? "npm");
2422
- const result = await pkgseerService.cliDocsSearch(registry, packageName, {
2423
- keywords: terms,
2424
- matchMode,
2425
- limit,
2426
- version: options.pkgVersion,
2427
- contextLinesBefore: contextBefore,
2428
- contextLinesAfter: contextAfter,
2429
- maxMatches
2430
- });
2431
- handleErrors(result.errors, options.json ?? false);
2432
- if (!result.data.searchPackageDocs) {
2433
- outputError(`No documentation found for ${packageName} in ${options.registry ?? "npm"}`, options.json ?? false);
2434
- return;
2435
- }
2436
- outputResults(result.data.searchPackageDocs, options, useColors);
2671
+ if (!options.packages) {
2672
+ outputError(`Package(s) required. Use -P or --packages:
2673
+ ` + ` -P express Single package (assumes npm)
2674
+ ` + ` -P express,lodash Multiple packages
2675
+ ` + ` -P pypi/django With registry prefix
2676
+ ` + " -P express@4.18.2 With version", options.json ?? false);
2437
2677
  return;
2438
2678
  }
2439
- if (project) {
2440
- const result = await pkgseerService.cliProjectDocsSearch(project, {
2441
- keywords: terms,
2442
- matchMode,
2443
- limit,
2444
- contextLinesBefore: contextBefore,
2445
- contextLinesAfter: contextAfter,
2446
- maxMatches
2447
- });
2448
- handleErrors(result.errors, options.json ?? false);
2449
- if (!result.data.searchProjectDocs) {
2450
- outputError(`Project not found: ${project}`, options.json ?? false);
2451
- return;
2452
- }
2453
- outputResults(result.data.searchProjectDocs, options, useColors);
2679
+ const packages = parsePackageList(options.packages);
2680
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
2681
+ const mode = toSearchMode(options.mode);
2682
+ const useColors = shouldUseColors(options.noColor);
2683
+ const result = await pkgseerService.combinedSearch(packages, query, {
2684
+ mode,
2685
+ limit
2686
+ });
2687
+ handleErrors(result.errors, options.json ?? false);
2688
+ if (!result.data.combinedSearch) {
2689
+ outputError("No results returned. Check package names and registries.", options.json ?? false);
2454
2690
  return;
2455
2691
  }
2456
- outputError(`No project configured. Either:
2457
- ` + ` - Add project to pkgseer.yml
2458
- ` + ` - Use --project <name> to specify a project
2459
- ` + " - Use --package <name> to search a specific package", options.json ?? false);
2692
+ outputResults(result.data.combinedSearch, options, useColors);
2460
2693
  }
2461
2694
  function outputResults(results, options, useColors) {
2462
2695
  const format = options.json ? "json" : options.format ?? "human";
2463
2696
  if (format === "json") {
2464
- output(slimSearchResults(results), true);
2697
+ output(results, true);
2465
2698
  } else if (options.refsOnly) {
2466
2699
  console.log(formatRefsOnly(results));
2467
2700
  } else if (options.count) {
2468
2701
  console.log(formatCount(results));
2469
2702
  } else if (format === "summary") {
2470
2703
  console.log(summarizeSearchResults(results));
2471
- } else {
2704
+ } else if (options.verbose) {
2472
2705
  console.log(formatSearchResults(results, useColors));
2706
+ } else {
2707
+ console.log(formatSearchResultsCompact(results, useColors));
2473
2708
  }
2474
2709
  }
2475
- var SEARCH_DESCRIPTION = `Search documentation with grep-like output.
2476
-
2477
- Searches across package or project documentation with keyword
2478
- or freeform query support. Shows matching lines with context
2479
- and highlights matched terms.
2710
+ var SEARCH_DESCRIPTION = `Search code and documentation across packages.
2480
2711
 
2481
- By default, searches project documentation if project is
2482
- configured in pkgseer.yml. Use --package to search a specific
2483
- package instead.
2712
+ Searches both code (functions, classes) and documentation pages
2713
+ using the combined search endpoint. Returns results with snippets
2714
+ showing matches.
2484
2715
 
2485
2716
  Examples:
2486
- # Search with context (like grep)
2487
- pkgseer docs search "error handling" --package express
2488
- pkgseer docs search log --package express -C 3
2717
+ # Search a single package (code + docs)
2718
+ pkgseer docs search "error handling" -P express
2489
2719
 
2490
- # Multiple terms (OR by default)
2491
- pkgseer docs search -t "middleware,routing" --package express
2720
+ # Search code only
2721
+ pkgseer docs search "Router.use" -P express --mode code
2492
2722
 
2493
- # Strict matching (AND mode)
2494
- pkgseer docs search -t "error,middleware" --match-mode all --package express
2723
+ # Search docs only
2724
+ pkgseer docs search "middleware" -P express --mode docs
2725
+
2726
+ # Search multiple packages
2727
+ pkgseer docs search "auth" -P express,passport
2728
+
2729
+ # With registry prefix (for non-npm packages)
2730
+ pkgseer docs search "orm" -P pypi/django,pypi/sqlalchemy
2731
+
2732
+ # With specific version
2733
+ pkgseer docs search "routing" -P express@4.18.2
2495
2734
 
2496
2735
  # Output for piping
2497
- pkgseer docs search log --package express --refs-only | \\
2498
- xargs -I{} pkgseer docs get express {}
2736
+ pkgseer docs search "routing" -P express --refs-only
2499
2737
 
2500
- # Count matches
2501
- pkgseer docs search error --package express --count`;
2738
+ # JSON output
2739
+ pkgseer docs search "error" -P express --json`;
2502
2740
  function addSearchOptions(cmd) {
2503
- return cmd.option("-p, --package <name>", "Search specific package (overrides project)").option("-r, --registry <registry>", "Package registry (with --package)", "npm").option("-v, --pkg-version <version>", "Package version (with --package)").option("--project <name>", "Project name (overrides config)").option("-t, --terms <words>", "Comma-separated search terms", (val) => val.split(",").map((s) => s.trim())).option("-l, --limit <n>", "Max results (default: 25)").option("-A, --after <n>", "Lines of context after match (default: 2)").option("-B, --before <n>", "Lines of context before match (default: 2)").option("-C, --context <n>", "Lines of context before and after match").option("--max-matches <n>", "Max matches per page (default: 5)").option("--match-mode <mode>", "How to combine terms: any/or (default) or all/and").option("--refs-only", "Output only page references (for piping)").option("--count", "Output only match counts per page").option("--format <format>", "Output format: human|summary|json", "human").option("--no-color", "Disable colored output").option("--json", "Output as JSON");
2741
+ return cmd.option("-P, --packages <packages>", "Packages to search (comma-separated). Format: name or registry/name[@version]. Examples: express | express,lodash | pypi/django@4.2").option("-m, --mode <mode>", "Search mode: all (default), code, docs").option("-l, --limit <n>", "Max results (default: 25)").option("-v, --verbose", "Expanded output with more details").option("--refs-only", "Output only references (for piping)").option("--count", "Output result counts by package").option("--format <format>", "Output format: human|summary|json", "human").option("--no-color", "Disable colored output").option("--json", "Output as JSON");
2504
2742
  }
2505
2743
  function registerDocsSearchCommand(program) {
2506
- const cmd = program.command("search [terms...]").summary("Search documentation").description(SEARCH_DESCRIPTION);
2507
- addSearchOptions(cmd).action(async (terms, options) => {
2744
+ const cmd = program.command("search [query...]").summary("Search code and documentation").description(SEARCH_DESCRIPTION);
2745
+ addSearchOptions(cmd).action(async (query, options) => {
2508
2746
  await withCliErrorHandling(options.json ?? false, async () => {
2509
2747
  const deps = await createContainer();
2510
- await docsSearchAction(terms, options, deps);
2748
+ await docsSearchAction(query, options, deps);
2511
2749
  });
2512
2750
  });
2513
2751
  }
@@ -2684,6 +2922,56 @@ Note: Create the ${dirName} directory if it doesn't exist.`, useColors));
2684
2922
  console.log(dim(`
2685
2923
  After editing, restart your AI assistant to activate the MCP server.`, useColors));
2686
2924
  }
2925
+ async function isCliAvailable(shellService, command) {
2926
+ try {
2927
+ const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
2928
+ await shellService.execute(checkCommand, process.cwd());
2929
+ return true;
2930
+ } catch {
2931
+ return false;
2932
+ }
2933
+ }
2934
+ async function setupClaudeCodeViaCli(shellService, scope) {
2935
+ try {
2936
+ const command = `claude mcp add --transport stdio --scope ${scope} pkgseer -- npx -y @pkgseer/cli mcp start`;
2937
+ await shellService.execute(command, process.cwd());
2938
+ return { success: true };
2939
+ } catch (err) {
2940
+ return {
2941
+ success: false,
2942
+ error: err instanceof Error ? err.message : String(err)
2943
+ };
2944
+ }
2945
+ }
2946
+ async function setupCodexViaCli(shellService) {
2947
+ try {
2948
+ const command = "codex mcp add pkgseer -- npx -y @pkgseer/cli mcp start";
2949
+ await shellService.execute(command, process.cwd());
2950
+ return { success: true };
2951
+ } catch (err) {
2952
+ return {
2953
+ success: false,
2954
+ error: err instanceof Error ? err.message : String(err)
2955
+ };
2956
+ }
2957
+ }
2958
+ function showAvailableTools(hasProject, useColors) {
2959
+ console.log(`
2960
+ Available MCP tools:`);
2961
+ console.log(" • package_summary - Get package overview");
2962
+ console.log(" • package_vulnerabilities - Check for security issues");
2963
+ console.log(" • package_quality - Get quality score");
2964
+ console.log(" • package_dependencies - List dependencies");
2965
+ console.log(" • compare_packages - Compare multiple packages");
2966
+ console.log(" • list_package_docs - List documentation pages");
2967
+ console.log(" • fetch_package_doc - Fetch documentation content");
2968
+ console.log(" • search_package_docs - Search package documentation");
2969
+ console.log(" • search_project_docs - Search docs across project dependencies");
2970
+ if (!hasProject) {
2971
+ console.log(dim(`
2972
+ Tip: Create pkgseer.yml to enable automatic project detection for search_project_docs.`, useColors));
2973
+ }
2974
+ }
2687
2975
  async function mcpInitAction(deps) {
2688
2976
  const { fileSystemService: fs, promptService, hasProject } = deps;
2689
2977
  const useColors = shouldUseColors2();
@@ -2736,47 +3024,84 @@ Run ${highlight("pkgseer project init", useColors)} first, then ${highlight("pkg
2736
3024
  if (tool === "cursor" || tool === "codex" || tool === "claude-code") {
2737
3025
  const projectPath = tool === "cursor" ? ".cursor/mcp.json" : tool === "codex" ? ".codex/config.toml" : ".mcp.json";
2738
3026
  const globalPath = tool === "cursor" ? "~/.cursor/mcp.json" : tool === "codex" ? "~/.codex/config.toml" : "~/.claude.json";
2739
- if (hasProject) {
2740
- scope = await promptService.select("Where should the MCP config be created?", [
3027
+ if (tool === "codex") {
3028
+ console.log(dim(`
3029
+ Codex uses user-level configuration.
3030
+ `, useColors));
3031
+ scope = "global";
3032
+ } else {
3033
+ scope = await promptService.select("Where should the MCP server be installed?", [
2741
3034
  {
2742
- value: "project",
2743
- name: `Project (${projectPath}) – recommended`,
2744
- description: "Uses project token; enables docs search for your packages"
3035
+ value: "global",
3036
+ name: `User-level (${globalPath}) – recommended`,
3037
+ description: "Works across all projects. Use project_directory param for project features."
2745
3038
  },
2746
3039
  {
2747
- value: "global",
2748
- name: `Global (${globalPath})`,
2749
- description: "Works everywhere but without project-specific features"
3040
+ value: "project",
3041
+ name: `Project-level (${projectPath})`,
3042
+ description: "Scoped to this project only."
2750
3043
  }
2751
3044
  ]);
2752
- } else {
2753
- console.log(dim(`
2754
- Using global config (no pkgseer.yml found).
2755
- `, useColors));
2756
- scope = "global";
2757
3045
  }
2758
3046
  }
2759
3047
  let configPath;
2760
3048
  let backupPath;
2761
- if (tool === "cursor") {
3049
+ const { shellService } = deps;
3050
+ if (tool === "claude-code") {
2762
3051
  if (!scope) {
2763
3052
  scope = "global";
2764
3053
  }
2765
- const paths = getCursorConfigPaths(fs, scope);
3054
+ const claudeAvailable = await isCliAvailable(shellService, "claude");
3055
+ if (claudeAvailable) {
3056
+ console.log(dim(`
3057
+ Using Claude Code CLI to add MCP server...
3058
+ `, useColors));
3059
+ const cliScope = scope === "global" ? "user" : "project";
3060
+ const result = await setupClaudeCodeViaCli(shellService, cliScope);
3061
+ if (result.success) {
3062
+ console.log(success("PkgSeer MCP server configured for Claude Code!", useColors));
3063
+ console.log(dim(`
3064
+ The MCP server has been registered. It should be available immediately.`, useColors));
3065
+ showAvailableTools(hasProject, useColors);
3066
+ return;
3067
+ }
3068
+ console.log(dim(`
3069
+ CLI setup failed: ${result.error}
3070
+ Falling back to file editing...
3071
+ `, useColors));
3072
+ }
3073
+ const paths = getClaudeCodeConfigPaths(fs, scope);
2766
3074
  configPath = paths.configPath;
2767
3075
  backupPath = paths.backupPath;
2768
3076
  } else if (tool === "codex") {
2769
3077
  if (!scope) {
2770
- scope = hasProject ? "project" : "global";
3078
+ scope = "global";
3079
+ }
3080
+ const codexAvailable = await isCliAvailable(shellService, "codex");
3081
+ if (codexAvailable) {
3082
+ console.log(dim(`
3083
+ Using Codex CLI to add MCP server...
3084
+ `, useColors));
3085
+ const result = await setupCodexViaCli(shellService);
3086
+ if (result.success) {
3087
+ console.log(success("PkgSeer MCP server configured for Codex CLI!", useColors));
3088
+ console.log(dim(`
3089
+ The MCP server has been registered. It should be available immediately.`, useColors));
3090
+ showAvailableTools(hasProject, useColors);
3091
+ return;
3092
+ }
3093
+ console.log(dim(`
3094
+ CLI setup failed: ${result.error}
3095
+ `, useColors));
2771
3096
  }
2772
3097
  const paths = getCodexConfigPaths(fs, scope);
2773
3098
  showManualInstructions("codex", scope, paths.configPath, useColors);
2774
3099
  return;
2775
- } else if (tool === "claude-code") {
3100
+ } else if (tool === "cursor") {
2776
3101
  if (!scope) {
2777
3102
  scope = "global";
2778
3103
  }
2779
- const paths = getClaudeCodeConfigPaths(fs, scope);
3104
+ const paths = getCursorConfigPaths(fs, scope);
2780
3105
  configPath = paths.configPath;
2781
3106
  backupPath = paths.backupPath;
2782
3107
  } else {
@@ -2853,31 +3178,19 @@ Config file: ${highlight(configPath, useColors)}`);
2853
3178
  Next steps:
2854
3179
  1. Restart your AI assistant to activate the MCP server
2855
3180
  2. Test by asking your assistant about packages`, useColors));
2856
- console.log(`
2857
- Available MCP tools:`);
2858
- console.log(" • package_summary - Get package overview");
2859
- console.log(" • package_vulnerabilities - Check for security issues");
2860
- console.log(" • package_quality - Get quality score");
2861
- console.log(" • package_dependencies - List dependencies");
2862
- console.log(" • compare_packages - Compare multiple packages");
2863
- console.log(" • list_package_docs - List documentation pages");
2864
- console.log(" • fetch_package_doc - Fetch documentation content");
2865
- console.log(" • search_package_docs - Search package documentation");
2866
- if (hasProject) {
2867
- console.log(" • search_project_docs - Search your project's docs");
2868
- }
3181
+ showAvailableTools(hasProject, useColors);
2869
3182
  }
2870
3183
  var MCP_INIT_DESCRIPTION = `Configure PkgSeer's MCP server for your AI assistant.
2871
3184
 
2872
3185
  Guides you through:
2873
3186
  • Selecting your AI tool (Cursor IDE, Codex CLI, Claude Code)
2874
- • Choosing configuration location (project or global)
2875
- Safely editing config files with automatic backup
3187
+ • Choosing installation scope (user-level or project-level)
3188
+ Setting up via CLI commands or config file editing
2876
3189
 
2877
- Project-level config (recommended):
2878
- Uses your project token for authenticated features
2879
- Enables search_project_docs (search your project's packages)
2880
- Portable with your project`;
3190
+ User-level (recommended):
3191
+ Works across all your projects
3192
+ Use project_directory parameter for project-specific features
3193
+ Automatically updated when using npx`;
2881
3194
  function registerMcpInitCommand(mcpCommand) {
2882
3195
  mcpCommand.command("init").summary("Configure MCP server for AI assistants").description(MCP_INIT_DESCRIPTION).action(async () => {
2883
3196
  const deps = await createContainer();
@@ -2886,6 +3199,7 @@ function registerMcpInitCommand(mcpCommand) {
2886
3199
  fileSystemService: deps.fileSystemService,
2887
3200
  promptService: deps.promptService,
2888
3201
  configService: deps.configService,
3202
+ shellService: deps.shellService,
2889
3203
  baseUrl: deps.baseUrl,
2890
3204
  hasProject
2891
3205
  });
@@ -3618,6 +3932,7 @@ ${highlight("✓", useColors)} Project setup complete!
3618
3932
  fileSystemService: deps.fileSystemService,
3619
3933
  promptService: deps.promptService,
3620
3934
  configService: deps.configService,
3935
+ shellService: deps.shellService,
3621
3936
  baseUrl: deps.baseUrl,
3622
3937
  hasProject: hasProjectNow
3623
3938
  });
@@ -3912,7 +4227,7 @@ var argsSchema = {
3912
4227
  function createComparePackagesTool(pkgseerService) {
3913
4228
  return {
3914
4229
  name: "compare_packages",
3915
- description: 'Compares 2-10 packages across metadata, quality, and security. Example: [{"registry":"npm","name":"react","version":"18.2.0"},{"registry":"pypi","name":"requests"}].',
4230
+ description: "Compare 2-10 packages side-by-side. Use this when evaluating alternatives (e.g., react vs preact vs solid-js). " + "Returns for each package: quality score, download counts, vulnerability count, license, and latest version. " + "Supports cross-registry comparison (npm, pypi, hex). " + 'Format: [{"registry":"npm","name":"lodash"},{"registry":"npm","name":"underscore"}]',
3916
4231
  schema: argsSchema,
3917
4232
  handler: async ({ packages }, _extra) => {
3918
4233
  return withErrorHandling("compare packages", async () => {
@@ -3933,16 +4248,47 @@ function createComparePackagesTool(pkgseerService) {
3933
4248
  }
3934
4249
  };
3935
4250
  }
3936
- // src/tools/fetch-package-doc.ts
4251
+ // src/tools/fetch-code-context.ts
3937
4252
  import { z as z4 } from "zod";
3938
4253
  var argsSchema2 = {
3939
- page_id: z4.string().max(500).describe("Globally unique documentation page identifier (from list_package_docs)")
4254
+ repo_url: z4.string().min(1).describe("Repository URL (GitHub). Example: https://github.com/expressjs/express"),
4255
+ git_ref: z4.string().min(1).describe("Git reference (tag, commit, or branch). Example: v4.18.2"),
4256
+ file_path: z4.string().min(1).describe("Path to file in repository. Example: src/router/index.js"),
4257
+ start_line: z4.number().int().positive().optional().describe("Starting line (1-indexed). If omitted, starts from line 1."),
4258
+ end_line: z4.number().int().positive().optional().describe("Ending line. If omitted, returns to end of file.")
4259
+ };
4260
+ function createFetchCodeContextTool(pkgseerService) {
4261
+ return {
4262
+ name: "fetch_code_context",
4263
+ description: "Fetch code content from a repository. Use this to expand code search results and see " + "more context around a match. Returns full file by default, or a specific line range. " + "Requires repo_url, git_ref (tag/commit/branch), and file_path from search results.",
4264
+ schema: argsSchema2,
4265
+ handler: async ({ repo_url, git_ref, file_path, start_line, end_line }, _extra) => {
4266
+ return withErrorHandling("fetch code context", async () => {
4267
+ const result = await pkgseerService.fetchCodeContext(repo_url, git_ref, file_path, {
4268
+ startLine: start_line,
4269
+ endLine: end_line
4270
+ });
4271
+ const graphqlError = handleGraphQLErrors(result.errors);
4272
+ if (graphqlError)
4273
+ return graphqlError;
4274
+ if (!result.data.fetchCodeContext) {
4275
+ return errorResult(`Code not found: ${file_path} at ${git_ref} in ${repo_url}. ` + "Check that the repository, file path, and git ref are correct.");
4276
+ }
4277
+ return textResult(JSON.stringify(result.data.fetchCodeContext, null, 2));
4278
+ });
4279
+ }
4280
+ };
4281
+ }
4282
+ // src/tools/fetch-package-doc.ts
4283
+ import { z as z5 } from "zod";
4284
+ var argsSchema3 = {
4285
+ page_id: z5.string().max(500).describe("Globally unique documentation page identifier (from list_package_docs)")
3940
4286
  };
3941
4287
  function createFetchPackageDocTool(pkgseerService) {
3942
4288
  return {
3943
4289
  name: "fetch_package_doc",
3944
- description: "Fetches the full content of a documentation page by globally unique page ID (from list_package_docs). Returns full metadata (title, content, format, breadcrumbs, source, base URL, last updated).",
3945
- schema: argsSchema2,
4290
+ description: "Get full content of a documentation page. Requires page_id from list_package_docs. " + "Returns: title, full content (markdown/HTML), format type, navigation breadcrumbs, " + "source URL, and last updated timestamp. Use this when you need complete documentation, " + "not just search snippets.",
4291
+ schema: argsSchema3,
3946
4292
  handler: async ({ page_id }, _extra) => {
3947
4293
  return withErrorHandling("fetch documentation page", async () => {
3948
4294
  const result = await pkgseerService.getDocPage(page_id);
@@ -3958,7 +4304,7 @@ function createFetchPackageDocTool(pkgseerService) {
3958
4304
  };
3959
4305
  }
3960
4306
  // src/tools/list-package-docs.ts
3961
- var argsSchema3 = {
4307
+ var argsSchema4 = {
3962
4308
  registry: schemas.registry,
3963
4309
  package_name: schemas.packageName.describe("Name of the package to list documentation for"),
3964
4310
  version: schemas.version
@@ -3966,8 +4312,8 @@ var argsSchema3 = {
3966
4312
  function createListPackageDocsTool(pkgseerService) {
3967
4313
  return {
3968
4314
  name: "list_package_docs",
3969
- description: "Lists documentation pages for a package version. Returns page titles, globally unique IDs, and metadata. Use this to discover available documentation before fetching specific pages.",
3970
- schema: argsSchema3,
4315
+ description: "Discover available documentation pages for a package. Start here before fetching or searching docs. " + "Returns: page titles, unique IDs (needed for fetch_package_doc), word counts, and update timestamps. " + "Workflow: list_package_docs fetch_package_doc for full content, or search_package_docs to find specific topics.",
4316
+ schema: argsSchema4,
3971
4317
  handler: async ({ registry, package_name, version: version2 }, _extra) => {
3972
4318
  return withErrorHandling("list package documentation", async () => {
3973
4319
  const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
@@ -3983,13 +4329,13 @@ function createListPackageDocsTool(pkgseerService) {
3983
4329
  };
3984
4330
  }
3985
4331
  // src/tools/package-dependencies.ts
3986
- import { z as z5 } from "zod";
3987
- var argsSchema4 = {
4332
+ import { z as z6 } from "zod";
4333
+ var argsSchema5 = {
3988
4334
  registry: schemas.registry,
3989
4335
  package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
3990
4336
  version: schemas.version,
3991
- include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
3992
- max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
4337
+ include_transitive: z6.boolean().optional().describe("Whether to include transitive dependency DAG"),
4338
+ max_depth: z6.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
3993
4339
  };
3994
4340
  function decodeDag(rawDag) {
3995
4341
  if (!rawDag || typeof rawDag !== "object")
@@ -4066,8 +4412,8 @@ function buildEdgeDepths(decodedDag, rootId) {
4066
4412
  function createPackageDependenciesTool(pkgseerService) {
4067
4413
  return {
4068
4414
  name: "package_dependencies",
4069
- description: "Retrieves direct and transitive dependencies for a package version. Options: include_transitive (bool) and max_depth (1-10). Transitive output includes a DAG (`n/e/v` keys) decoded into readable nodes/edges with depth and counts; raw DAG is preserved alongside decoded form.",
4070
- schema: argsSchema4,
4415
+ description: "Analyze package dependencies. By default returns direct dependencies only. " + "Set include_transitive=true to get the full dependency tree (use max_depth to limit large trees). " + "Returns: dependency names, version constraints, and for transitive deps, a graph with depth levels. " + "Use this to understand complexity before adding a package, or to find nested dependencies.",
4416
+ schema: argsSchema5,
4071
4417
  handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
4072
4418
  return withErrorHandling("fetch package dependencies", async () => {
4073
4419
  const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
@@ -4105,7 +4451,7 @@ function createPackageDependenciesTool(pkgseerService) {
4105
4451
  };
4106
4452
  }
4107
4453
  // src/tools/package-quality.ts
4108
- var argsSchema5 = {
4454
+ var argsSchema6 = {
4109
4455
  registry: schemas.registry,
4110
4456
  package_name: schemas.packageName.describe("Name of the package to analyze"),
4111
4457
  version: schemas.version
@@ -4113,8 +4459,8 @@ var argsSchema5 = {
4113
4459
  function createPackageQualityTool(pkgseerService) {
4114
4460
  return {
4115
4461
  name: "package_quality",
4116
- description: "Retrieves quality score and rule-level breakdown for a package",
4117
- schema: argsSchema5,
4462
+ description: "Evaluate package maintenance health and code quality. Use this to assess if a package is well-maintained " + "before adding it as a dependency. Returns: overall score (0-100), category scores (documentation, " + "testing, community, maintenance), and individual rule results with pass/fail status. " + "Useful for comparing quality between alternative packages.",
4463
+ schema: argsSchema6,
4118
4464
  handler: async ({ registry, package_name, version: version2 }, _extra) => {
4119
4465
  return withErrorHandling("fetch package quality", async () => {
4120
4466
  const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
@@ -4130,15 +4476,15 @@ function createPackageQualityTool(pkgseerService) {
4130
4476
  };
4131
4477
  }
4132
4478
  // src/tools/package-summary.ts
4133
- var argsSchema6 = {
4479
+ var argsSchema7 = {
4134
4480
  registry: schemas.registry,
4135
4481
  package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
4136
4482
  };
4137
4483
  function createPackageSummaryTool(pkgseerService) {
4138
4484
  return {
4139
4485
  name: "package_summary",
4140
- description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
4141
- schema: argsSchema6,
4486
+ description: "Get a comprehensive overview of a package. Use this as your first tool when researching a package. " + "Returns: description, latest version, license, repository URL, download stats, " + "active security advisories count, and quickstart/installation instructions. " + "For deeper analysis, follow up with package_quality (maintenance health) or " + "package_vulnerabilities (security details).",
4487
+ schema: argsSchema7,
4142
4488
  handler: async ({ registry, package_name }, _extra) => {
4143
4489
  return withErrorHandling("fetch package summary", async () => {
4144
4490
  const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
@@ -4154,7 +4500,7 @@ function createPackageSummaryTool(pkgseerService) {
4154
4500
  };
4155
4501
  }
4156
4502
  // src/tools/package-vulnerabilities.ts
4157
- var argsSchema7 = {
4503
+ var argsSchema8 = {
4158
4504
  registry: schemas.registry,
4159
4505
  package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
4160
4506
  version: schemas.version
@@ -4162,8 +4508,8 @@ var argsSchema7 = {
4162
4508
  function createPackageVulnerabilitiesTool(pkgseerService) {
4163
4509
  return {
4164
4510
  name: "package_vulnerabilities",
4165
- description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
4166
- schema: argsSchema7,
4511
+ description: "Check security vulnerabilities for a package. Use this before adding dependencies or when auditing existing ones. " + "Returns: list of CVEs/advisories with severity levels, affected version ranges, fixed versions, " + "and upgrade recommendations. If version is specified, shows only vulnerabilities affecting that version.",
4512
+ schema: argsSchema8,
4167
4513
  handler: async ({ registry, package_name, version: version2 }, _extra) => {
4168
4514
  return withErrorHandling("fetch package vulnerabilities", async () => {
4169
4515
  const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
@@ -4178,79 +4524,126 @@ function createPackageVulnerabilitiesTool(pkgseerService) {
4178
4524
  }
4179
4525
  };
4180
4526
  }
4181
- // src/tools/search-package-docs.ts
4182
- import { z as z6 } from "zod";
4183
- var argsSchema8 = {
4527
+ // src/tools/search.ts
4528
+ import { z as z7 } from "zod";
4529
+ var packageInputSchema2 = z7.object({
4184
4530
  registry: schemas.registry,
4185
- package_name: schemas.packageName.describe("Name of the package to search documentation for"),
4186
- terms: z6.array(z6.string()).optional().describe("Search terms to match. Provide a few key words or phrases that should appear in results."),
4187
- match_mode: z6.enum(["any", "or", "all", "and"]).optional().describe('How to combine terms: "any" (OR) or "all" (AND).'),
4188
- include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
4189
- limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
4190
- version: schemas.version
4531
+ name: z7.string().min(1).describe("Package name"),
4532
+ version: z7.string().optional().describe("Specific version (defaults to latest)")
4533
+ });
4534
+ var argsSchema9 = {
4535
+ packages: z7.array(packageInputSchema2).min(1).max(20).describe("Packages to search (1-20). Each package needs registry and name."),
4536
+ query: z7.string().min(1).describe("Search query - natural language or keywords"),
4537
+ mode: z7.enum(["all", "code", "docs"]).optional().describe('Search mode: "all" (default), "code" only, or "docs" only'),
4538
+ limit: z7.number().int().min(1).max(100).optional().describe("Maximum results (default: 20)")
4191
4539
  };
4192
- function createSearchPackageDocsTool(pkgseerService) {
4540
+ function toSearchMode2(mode) {
4541
+ if (!mode)
4542
+ return;
4543
+ const map = {
4544
+ all: "ALL",
4545
+ code: "CODE",
4546
+ docs: "DOCS"
4547
+ };
4548
+ return map[mode.toLowerCase()];
4549
+ }
4550
+ function formatIndexingWarnings2(indexingStatus) {
4551
+ if (!indexingStatus || indexingStatus.length === 0)
4552
+ return null;
4553
+ const warnings = [];
4554
+ for (const status of indexingStatus) {
4555
+ if (!status)
4556
+ continue;
4557
+ const pkg = `${status.packageName}@${status.version ?? "latest"} (${status.registry?.toLowerCase() ?? "unknown"})`;
4558
+ if (status.codeStatus && status.codeStatus !== "INDEXED") {
4559
+ warnings.push(` - ${pkg}: code ${status.codeStatus.toLowerCase()}`);
4560
+ }
4561
+ if (status.docsStatus && status.docsStatus !== "INDEXED") {
4562
+ warnings.push(` - ${pkg}: docs ${status.docsStatus.toLowerCase()}`);
4563
+ }
4564
+ }
4565
+ if (warnings.length === 0)
4566
+ return null;
4567
+ return `
4568
+ Note: Some packages are not fully indexed:
4569
+ ` + warnings.join(`
4570
+ `) + `
4571
+ Results may be incomplete. Retry later for more complete results.`;
4572
+ }
4573
+ function createSearchTool(pkgseerService) {
4193
4574
  return {
4194
- name: "search_package_docs",
4195
- description: "Searches package documentation using search terms and match modes (any/all). Returns ranked results with match context. Defaults to include_snippets=true.",
4196
- schema: argsSchema8,
4197
- handler: async ({
4198
- registry,
4199
- package_name,
4200
- terms,
4201
- match_mode,
4202
- include_snippets,
4203
- limit,
4204
- version: version2
4205
- }, _extra) => {
4206
- return withErrorHandling("search package documentation", async () => {
4207
- const normalizedTerms = terms?.map((term) => term.trim()).filter((term) => term.length > 0) ?? [];
4208
- if (normalizedTerms.length === 0) {
4209
- return errorResult("Search terms are required to run documentation search.");
4575
+ name: "search",
4576
+ description: "Search code and documentation across packages. Returns functions, classes, and documentation pages " + "matching your query. Use mode='code' for code only, mode='docs' for documentation only, or " + "mode='all' (default) for both. Provide 1-20 packages to search. Results include relevance scores " + "and snippets showing matches.",
4577
+ schema: argsSchema9,
4578
+ handler: async ({ packages, query, mode, limit }, _extra) => {
4579
+ return withErrorHandling("search packages", async () => {
4580
+ const normalizedQuery = query.trim();
4581
+ if (normalizedQuery.length === 0) {
4582
+ return errorResult("Search query is required.");
4210
4583
  }
4211
- const matchMode = match_mode === "all" || match_mode === "and" ? "AND" : match_mode === "any" || match_mode === "or" ? "OR" : undefined;
4212
- const includeSnippets = include_snippets ?? true;
4213
- const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
4214
- keywords: normalizedTerms,
4215
- matchMode,
4216
- includeSnippets,
4217
- limit,
4218
- version: version2
4584
+ const graphqlPackages = packages.map((pkg) => ({
4585
+ registry: toGraphQLRegistry2(pkg.registry),
4586
+ name: pkg.name,
4587
+ version: pkg.version
4588
+ }));
4589
+ const result = await pkgseerService.combinedSearch(graphqlPackages, normalizedQuery, {
4590
+ mode: toSearchMode2(mode),
4591
+ limit
4219
4592
  });
4220
4593
  const graphqlError = handleGraphQLErrors(result.errors);
4221
4594
  if (graphqlError)
4222
4595
  return graphqlError;
4223
- if (!result.data.searchPackageDocs) {
4224
- return errorResult(`No documentation found for ${package_name} in ${registry}`);
4596
+ if (!result.data.combinedSearch) {
4597
+ return errorResult("No search results returned. Check package names and registries.");
4225
4598
  }
4226
- if ((result.data.searchPackageDocs.entries ?? []).length === 0 && normalizedTerms.length > 0) {
4227
- return errorResult(`No documentation matched: ${normalizedTerms.join(", ")}. ` + "Try fewer or broader terms, or reduce match constraints.");
4599
+ const searchResult = result.data.combinedSearch;
4600
+ const entries = searchResult.entries ?? [];
4601
+ if (entries.length === 0) {
4602
+ return errorResult(`No results found for "${normalizedQuery}". ` + "Try broader terms or different packages.");
4603
+ }
4604
+ let response = JSON.stringify(searchResult, null, 2);
4605
+ const indexingWarning = formatIndexingWarnings2(searchResult.indexingStatus);
4606
+ if (indexingWarning) {
4607
+ response += indexingWarning;
4228
4608
  }
4229
- return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
4609
+ return textResult(response);
4230
4610
  });
4231
4611
  }
4232
4612
  };
4233
4613
  }
4234
4614
  // src/tools/search-project-docs.ts
4235
- import { z as z7 } from "zod";
4236
- var argsSchema9 = {
4237
- project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
4238
- terms: z7.array(z7.string()).optional().describe("Search terms to match. Provide a few key words or phrases that should appear in results."),
4239
- match_mode: z7.enum(["any", "or", "all", "and"]).optional().describe('How to combine terms: "any" (OR) or "all" (AND).'),
4240
- include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
4241
- limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
4615
+ import { z as z8 } from "zod";
4616
+ var argsSchema10 = {
4617
+ project_directory: z8.string().optional().describe("Directory to search for pkgseer.yml configuration. " + "Use this to specify which project's context to use when the MCP server " + "is installed at user level. Defaults to MCP server's working directory."),
4618
+ project: z8.string().optional().describe("Project name to search. Overrides value from pkgseer.yml if provided."),
4619
+ terms: z8.array(z8.string()).optional().describe("Search terms to match. Provide a few key words or phrases that should appear in results."),
4620
+ match_mode: z8.enum(["any", "or", "all", "and"]).optional().describe('How to combine terms: "any" (OR) or "all" (AND).'),
4621
+ include_snippets: z8.boolean().optional().describe("Include content excerpts around matches"),
4622
+ limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
4242
4623
  };
4243
4624
  function createSearchProjectDocsTool(deps) {
4244
- const { pkgseerService, config } = deps;
4625
+ const { pkgseerService, configService, defaultProjectDir } = deps;
4245
4626
  return {
4246
4627
  name: "search_project_docs",
4247
- description: "Searches documentation across all dependencies in a PkgSeer project using search terms and match modes (any/all). Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
4248
- schema: argsSchema9,
4249
- handler: async ({ project, terms, match_mode, include_snippets, limit }, _extra) => {
4628
+ description: "Searches documentation across all dependencies in a PkgSeer project using search terms and match modes (any/all). Returns ranked results from multiple packages. Use project_directory to specify which project's context to use, or project to search a specific project directly.",
4629
+ schema: argsSchema10,
4630
+ handler: async ({
4631
+ project_directory,
4632
+ project,
4633
+ terms,
4634
+ match_mode,
4635
+ include_snippets,
4636
+ limit
4637
+ }, _extra) => {
4250
4638
  return withErrorHandling("search project documentation", async () => {
4251
- const resolvedProject = project ?? config.project;
4639
+ const targetDir = project_directory ?? defaultProjectDir;
4640
+ const projectConfig = await configService.loadProjectConfigFrom(targetDir);
4641
+ const resolvedProject = project ?? projectConfig?.config.project;
4252
4642
  if (!resolvedProject) {
4253
- return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
4643
+ return errorResult(`No project specified and no pkgseer.yml found. To fix:
4644
+ ` + ` 1. Pass project_directory: '/path/to/project' (folder containing pkgseer.yml)
4645
+ ` + ` 2. Pass project: 'project-name' directly if you know the project name
4646
+ ` + " 3. Run 'pkgseer project init' to create pkgseer.yml in your project");
4254
4647
  }
4255
4648
  const normalizedTerms = terms?.map((term) => term.trim()).filter((term) => term.length > 0) ?? [];
4256
4649
  if (normalizedTerms.length === 0) {
@@ -4287,8 +4680,13 @@ var TOOL_FACTORIES = {
4287
4680
  compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
4288
4681
  list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
4289
4682
  fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
4290
- search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
4291
- search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
4683
+ search: ({ pkgseerService }) => createSearchTool(pkgseerService),
4684
+ fetch_code_context: ({ pkgseerService }) => createFetchCodeContextTool(pkgseerService),
4685
+ search_project_docs: ({ pkgseerService, configService, defaultProjectDir }) => createSearchProjectDocsTool({
4686
+ pkgseerService,
4687
+ configService,
4688
+ defaultProjectDir
4689
+ })
4292
4690
  };
4293
4691
  var PUBLIC_READ_TOOLS = [
4294
4692
  "package_summary",
@@ -4298,21 +4696,29 @@ var PUBLIC_READ_TOOLS = [
4298
4696
  "compare_packages",
4299
4697
  "list_package_docs",
4300
4698
  "fetch_package_doc",
4301
- "search_package_docs"
4699
+ "search",
4700
+ "fetch_code_context"
4302
4701
  ];
4303
4702
  var PROJECT_READ_TOOLS = ["search_project_docs"];
4304
4703
  var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
4305
4704
  function createMcpServer(deps) {
4306
- const { pkgseerService, config } = deps;
4705
+ const { pkgseerService, configService, config, fileSystemService } = deps;
4307
4706
  const server = new McpServer({
4308
4707
  name: "pkgseer",
4309
4708
  version: "0.1.0"
4310
4709
  });
4710
+ const defaultProjectDir = fileSystemService.getCwd();
4311
4711
  const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
4312
4712
  const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
4713
+ const factoryDeps = {
4714
+ pkgseerService,
4715
+ configService,
4716
+ config,
4717
+ defaultProjectDir
4718
+ };
4313
4719
  for (const toolName of toolsToRegister) {
4314
4720
  const factory = TOOL_FACTORIES[toolName];
4315
- const tool = factory({ pkgseerService, config });
4721
+ const tool = factory(factoryDeps);
4316
4722
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
4317
4723
  }
4318
4724
  return server;