@pkgseer/cli 0.2.4 → 0.3.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/README.md CHANGED
@@ -2,120 +2,215 @@
2
2
 
3
3
  CLI and MCP server for [PkgSeer](https://pkgseer.dev) — package intelligence for developers and AI assistants.
4
4
 
5
- Get insights about packages across npm, PyPI, and Hex registries: metadata, security vulnerabilities, dependencies, and quality metrics. Works standalone or as an MCP server for AI assistants like Claude and Cursor.
5
+ Get insights about packages across npm, PyPI, Hex, and crates.io registries: search code and documentation, check security vulnerabilities, analyze dependencies, and compare packages. Works standalone or as an MCP server for AI assistants like Claude and Cursor.
6
6
 
7
7
  ## Installation
8
8
 
9
- The easiest way to get started is using npx no installation required:
9
+ Use npx without installation:
10
10
 
11
11
  ```bash
12
12
  npx @pkgseer/cli --help
13
13
  ```
14
14
 
15
- For frequent use, install globally:
15
+ Or install globally:
16
16
 
17
17
  ```bash
18
18
  npm install -g @pkgseer/cli
19
19
  ```
20
20
 
21
- ## Getting Started
21
+ ## Quick Start
22
22
 
23
- ### 1. Authenticate (Recommended)
23
+ ```bash
24
+ # Interactive setup (recommended for first-time users)
25
+ pkgseer init
26
+
27
+ # Or set up manually:
28
+ pkgseer login # Authenticate with your PkgSeer account
29
+ pkgseer skill init # Install as AI assistant skill
30
+ ```
31
+
32
+ ## AI Assistant Integration
33
+
34
+ PkgSeer works with AI assistants in two ways:
35
+
36
+ ### Skills
24
37
 
25
- While you can use PkgSeer without authentication, logging in gives you higher rate limits and access to all features:
38
+ Skills teach your AI assistant to use PkgSeer CLI commands through natural language:
26
39
 
27
40
  ```bash
28
- pkgseer login
41
+ pkgseer skill init
29
42
  ```
30
43
 
31
- This opens your browser to authenticate with your PkgSeer account. Your credentials are stored securely in `~/.pkgseer/`.
44
+ This installs a skill definition for Claude Code or Codex CLI. The AI runs CLI commands and reads the output.
32
45
 
33
- To check your authentication status:
46
+ ### MCP Server
47
+
48
+ MCP provides structured tools that AI assistants can call programmatically:
34
49
 
35
50
  ```bash
36
- pkgseer auth status
51
+ pkgseer mcp init
37
52
  ```
38
53
 
39
- ### 2. Use with AI Assistants
40
-
41
- The main use case for this CLI is as an MCP (Model Context Protocol) server that gives AI assistants access to package intelligence.
54
+ This provides structured tools that AI assistants can call programmatically. Configuration varies by assistant:
42
55
 
43
- #### Cursor IDE
56
+ **Claude Code / Codex CLI**: The `mcp init` command configures these automatically.
44
57
 
45
- Add this to your `.cursor/mcp.json` file:
58
+ **Cursor IDE**: Add to `.cursor/mcp.json`:
46
59
 
47
60
  ```json
48
61
  {
49
62
  "mcpServers": {
50
63
  "pkgseer": {
51
64
  "command": "npx",
52
- "args": ["-y", "@pkgseer/cli", "mcp"]
65
+ "args": ["-y", "@pkgseer/cli", "mcp", "start"]
53
66
  }
54
67
  }
55
68
  }
56
69
  ```
57
70
 
58
- #### Claude Desktop
59
-
60
- Add this to your Claude Desktop configuration file:
61
-
62
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
63
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
71
+ **Claude Desktop**: Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
64
72
 
65
73
  ```json
66
74
  {
67
75
  "mcpServers": {
68
76
  "pkgseer": {
69
77
  "command": "npx",
70
- "args": ["-y", "@pkgseer/cli", "mcp"]
78
+ "args": ["-y", "@pkgseer/cli", "mcp", "start"]
71
79
  }
72
80
  }
73
81
  }
74
82
  ```
75
83
 
76
- Once configured, your AI assistant can use tools to answer questions like:
84
+ ## CLI Commands
77
85
 
78
- - "What security vulnerabilities does lodash have?"
79
- - "Compare react vs preact vs solid-js"
80
- - "What are the dependencies of express?"
86
+ ### Search
81
87
 
82
- #### Prompt for AI Assistants
88
+ Search code and documentation across packages:
83
89
 
84
- To help your AI assistant use PkgSeer proactively, add this to your project rules (`.cursor/rules`, `AGENTS.md`, or similar):
90
+ ```bash
91
+ # Search in specific packages
92
+ pkgseer search "authentication" -P express,passport
93
+ pkgseer search "http client" -P pypi:requests,pypi:httpx
94
+ pkgseer search "json parsing" -P hex:jason,hex:poison
95
+
96
+ # Search modes
97
+ pkgseer search "auth" -P lodash --mode code # Code only
98
+ pkgseer search "auth" -P lodash --mode docs # Docs only
99
+
100
+ # Wait options (for packages that need indexing)
101
+ pkgseer search "api" -P new-package --wait 60000 # Wait up to 60s
102
+ pkgseer search "api" -P new-package --no-wait # Return immediately
103
+ ```
104
+
105
+ If packages haven't been indexed yet, the search will wait up to 30 seconds by default. Use `--wait <ms>` to customize or `--no-wait` to return immediately with progress info.
85
106
 
86
- ```markdown
87
- When working with dependencies:
107
+ ### Package Commands
88
108
 
89
- - Use PkgSeer MCP tools to check for security vulnerabilities before adding new packages
90
- - Compare package alternatives with compare_packages when multiple options exist
91
- - Review package quality metrics to ensure dependencies are well-maintained
109
+ ```bash
110
+ pkgseer pkg info lodash # Package summary and metadata
111
+ pkgseer pkg vulns lodash@4.17.21 # Security vulnerabilities
112
+ pkgseer pkg quality express # Quality score (0-100)
113
+ pkgseer pkg deps express # Direct dependencies
114
+ pkgseer pkg deps express --transitive # Include transitive deps
115
+ pkgseer pkg compare axios got fetch-h2 # Compare packages
92
116
  ```
93
117
 
94
- ## Available Tools
118
+ Package format: `[registry:]name[@version]`
119
+ - `lodash` — npm (default registry)
120
+ - `pypi:requests` — PyPI
121
+ - `hex:phoenix` — Hex
122
+ - `crates:serde` — crates.io
123
+ - `lodash@4.17.21` — specific version
124
+
125
+ ### Documentation Commands
95
126
 
96
- When running as an MCP server, the following tools are available to AI assistants:
127
+ ```bash
128
+ pkgseer docs list pypi:requests # List available doc pages
129
+ pkgseer docs get lodash/chunk # Fetch specific doc page
130
+ pkgseer docs search "routing" -P express # Search docs only
131
+ ```
97
132
 
98
- | Tool | What it does |
99
- | ------------------------- | ----------------------------------------------------------------------------------- |
100
- | `package_summary` | Get package metadata, latest versions, security advisories, and quickstart examples |
101
- | `package_vulnerabilities` | Find known security vulnerabilities affecting a package |
102
- | `package_dependencies` | Explore the dependency tree (direct and transitive) |
103
- | `package_quality` | View quality metrics and maintenance scores |
104
- | `compare_packages` | Compare multiple packages side-by-side |
133
+ ### Project Commands
105
134
 
106
- All tools work with **npm**, **PyPI**, and **Hex** registries.
135
+ ```bash
136
+ pkgseer project init # Create pkgseer.yml config
137
+ pkgseer project detect # Detect package manifests
138
+ pkgseer project upload # Upload project to PkgSeer
139
+ ```
107
140
 
108
- ## CLI Commands
141
+ ### Authentication
109
142
 
110
143
  ```bash
111
- pkgseer --help # Show all available commands
112
- pkgseer --version # Show version number
144
+ pkgseer login # Authenticate via browser
145
+ pkgseer logout # Sign out
146
+ pkgseer auth status # Check authentication state
147
+ ```
148
+
149
+ ### Configuration
150
+
151
+ ```bash
152
+ pkgseer config show # Display current configuration
153
+ ```
154
+
155
+ ## MCP Tools
156
+
157
+ When running as an MCP server, these tools are available:
158
+
159
+ | Tool | Description |
160
+ |------|-------------|
161
+ | `package_summary` | Package metadata, versions, quickstart examples |
162
+ | `package_vulnerabilities` | Security advisories and CVEs |
163
+ | `package_dependencies` | Dependency tree (direct and transitive) |
164
+ | `package_quality` | Quality score with category breakdown |
165
+ | `compare_packages` | Side-by-side comparison of packages |
166
+ | `list_package_docs` | Available documentation pages |
167
+ | `fetch_package_doc` | Full content of a documentation page |
168
+ | `search` | Search code and docs across packages |
169
+ | `fetch_code_context` | Fetch code from search results |
170
+ | `search_project_docs` | Search docs for packages in current project |
171
+
172
+ ## Configuration
173
+
174
+ ### Project Configuration
113
175
 
114
- pkgseer login # Authenticate with your PkgSeer account
115
- pkgseer logout # Sign out and clear stored credentials
116
- pkgseer auth status # Check if you're logged in and token validity
176
+ Create `pkgseer.yml` in your project root:
117
177
 
118
- pkgseer mcp # Start the MCP server (for AI assistant integration)
178
+ ```yaml
179
+ project: my-project-name
180
+
181
+ # Optional: limit which tools are available
182
+ enabled_tools:
183
+ - package_summary
184
+ - package_vulnerabilities
185
+ - search_project_docs
186
+ ```
187
+
188
+ ### Environment Variables
189
+
190
+ | Variable | Description |
191
+ |----------|-------------|
192
+ | `PKGSEER_API_TOKEN` | API token (alternative to `pkgseer login`) |
193
+ | `PKGSEER_URL` | Base URL for PkgSeer (for development/testing) |
194
+
195
+ ## Documentation
196
+
197
+ - [Architecture Overview](docs/implementation/architecture.md)
198
+ - [MCP Installation Guide](docs/implementation/mcp-installation.md)
199
+ - [Creating MCP Tools](docs/implementation/tools.md)
200
+ - [Authentication Flow](docs/implementation/auth.md)
201
+ - [Skills System](docs/implementation/skills.md)
202
+ - [Configuration Reference](docs/implementation/configuration.md)
203
+
204
+ ## Development
205
+
206
+ See [CLAUDE.md](CLAUDE.md) for development guidelines.
207
+
208
+ ```bash
209
+ bun install # Install dependencies
210
+ bun run dev # Development mode
211
+ bun test # Run tests
212
+ bun run build # Build for production
213
+ bun run codegen # Regenerate GraphQL types
119
214
  ```
120
215
 
121
216
  ## Need Help?
@@ -125,4 +220,4 @@ pkgseer mcp # Start the MCP server (for AI assistant integration)
125
220
 
126
221
  ## License
127
222
 
128
- MIT © [Juha Litola](https://github.com/pkgseer)
223
+ (c) 2025-2026 Juha Litola
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-48mwa8wt.js";
4
+ } from "./shared/chunk-9yar14cw.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -211,36 +211,52 @@ var CliDocsListDocument = gql`
211
211
  }
212
212
  `;
213
213
  var CombinedSearchDocument = gql`
214
- query CombinedSearch($packages: [SearchPackageInput!]!, $query: String!, $mode: SearchMode, $limit: Int) {
215
- combinedSearch(packages: $packages, query: $query, mode: $mode, limit: $limit) {
216
- query
217
- mode
218
- totalResults
219
- entries {
220
- id
221
- type
222
- title
223
- subtitle
224
- packageName
225
- registry
226
- score
227
- snippet
228
- filePath
229
- startLine
230
- endLine
231
- language
232
- chunkType
233
- repoUrl
234
- gitRef
235
- pageId
236
- sourceUrl
214
+ query CombinedSearch($packages: [SearchPackageInput!]!, $query: String!, $mode: SearchMode, $limit: Int, $waitTimeoutMs: Int) {
215
+ combinedSearch(
216
+ packages: $packages
217
+ query: $query
218
+ mode: $mode
219
+ limit: $limit
220
+ waitTimeoutMs: $waitTimeoutMs
221
+ ) {
222
+ completed
223
+ searchRef
224
+ result {
225
+ query
226
+ mode
227
+ totalResults
228
+ entries {
229
+ id
230
+ type
231
+ title
232
+ subtitle
233
+ packageName
234
+ registry
235
+ score
236
+ snippet
237
+ filePath
238
+ startLine
239
+ endLine
240
+ language
241
+ chunkType
242
+ repoUrl
243
+ gitRef
244
+ pageId
245
+ sourceUrl
246
+ }
247
+ indexingStatus {
248
+ registry
249
+ packageName
250
+ version
251
+ codeStatus
252
+ docsStatus
253
+ }
237
254
  }
238
- indexingStatus {
239
- registry
240
- packageName
241
- version
242
- codeStatus
243
- docsStatus
255
+ progress {
256
+ status
257
+ packagesTotal
258
+ packagesReady
259
+ elapsedMs
244
260
  }
245
261
  }
246
262
  }
@@ -1419,7 +1435,8 @@ class PkgseerServiceImpl {
1419
1435
  packages,
1420
1436
  query,
1421
1437
  mode: options?.mode,
1422
- limit: options?.limit
1438
+ limit: options?.limit,
1439
+ waitTimeoutMs: options?.waitTimeoutMs
1423
1440
  });
1424
1441
  return { data: result.data, errors: result.errors };
1425
1442
  }
@@ -1789,7 +1806,7 @@ function parsePackageSpec(spec) {
1789
1806
  if (spec.includes(":")) {
1790
1807
  const colonIndex = spec.indexOf(":");
1791
1808
  const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
1792
- if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
1809
+ if (["npm", "pypi", "hex", "crates"].includes(potentialRegistry)) {
1793
1810
  registry = potentialRegistry;
1794
1811
  rest = spec.slice(colonIndex + 1);
1795
1812
  }
@@ -1808,7 +1825,8 @@ function toGraphQLRegistry(registry) {
1808
1825
  const map = {
1809
1826
  npm: "NPM",
1810
1827
  pypi: "PYPI",
1811
- hex: "HEX"
1828
+ hex: "HEX",
1829
+ crates: "CRATES"
1812
1830
  };
1813
1831
  return map[registry.toLowerCase()] || "NPM";
1814
1832
  }
@@ -2341,6 +2359,7 @@ function registerDocsListCommand(program) {
2341
2359
  });
2342
2360
  }
2343
2361
  // src/commands/search.ts
2362
+ var DEFAULT_WAIT_TIMEOUT_MS = 30000;
2344
2363
  var colors = {
2345
2364
  reset: "\x1B[0m",
2346
2365
  bold: "\x1B[1m",
@@ -2559,7 +2578,37 @@ function abbrevLang(lang) {
2559
2578
  function truncate(text, maxLen) {
2560
2579
  if (text.length <= maxLen)
2561
2580
  return text;
2562
- return text.slice(0, maxLen - 3) + "...";
2581
+ return `${text.slice(0, maxLen - 3)}...`;
2582
+ }
2583
+ function isSubstantiveLine(line) {
2584
+ const trimmed = line.trim();
2585
+ if (trimmed.length === 0)
2586
+ return false;
2587
+ if (/^[(){}[\];,]+$/.test(trimmed))
2588
+ return false;
2589
+ if (trimmed.length <= 3 && /^[^a-zA-Z0-9]*$/.test(trimmed))
2590
+ return false;
2591
+ return true;
2592
+ }
2593
+ function findBestSnippetLine(snippet) {
2594
+ const lines = snippet.split(`
2595
+ `).map((l) => l.trim());
2596
+ if (lines.length === 0)
2597
+ return null;
2598
+ const middleIndex = Math.floor((lines.length - 1) / 2);
2599
+ for (let offset = 0;offset <= lines.length; offset++) {
2600
+ const beforeIdx = middleIndex - offset;
2601
+ if (beforeIdx >= 0 && isSubstantiveLine(lines[beforeIdx])) {
2602
+ return lines[beforeIdx];
2603
+ }
2604
+ if (offset > 0) {
2605
+ const afterIdx = middleIndex + offset;
2606
+ if (afterIdx < lines.length && isSubstantiveLine(lines[afterIdx])) {
2607
+ return lines[afterIdx];
2608
+ }
2609
+ }
2610
+ }
2611
+ return null;
2563
2612
  }
2564
2613
  function formatEntryCompact(entry, useColors) {
2565
2614
  if (!entry)
@@ -2578,12 +2627,11 @@ function formatEntryCompact(entry, useColors) {
2578
2627
  }
2579
2628
  let snippet = "";
2580
2629
  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)}`;
2630
+ const bestLine = findBestSnippetLine(entry.snippet);
2631
+ if (bestLine) {
2632
+ snippet = ` ${truncate(bestLine, 80)}`;
2585
2633
  if (useColors) {
2586
- snippet = ` ${colors.dim}${truncate(firstLine, 80)}${colors.reset}`;
2634
+ snippet = ` ${colors.dim}${truncate(bestLine, 80)}${colors.reset}`;
2587
2635
  }
2588
2636
  }
2589
2637
  }
@@ -2661,6 +2709,50 @@ function summarizeSearchResults(results) {
2661
2709
  return lines.join(`
2662
2710
  `);
2663
2711
  }
2712
+ function formatSearchProgress(progress, searchRef, useColors) {
2713
+ const lines = [];
2714
+ if (useColors) {
2715
+ lines.push(`${colors.yellow}Search in progress...${colors.reset}`);
2716
+ } else {
2717
+ lines.push("Search in progress...");
2718
+ }
2719
+ if (progress) {
2720
+ const statusText = progress.status?.toLowerCase() ?? "unknown";
2721
+ lines.push(`Status: ${statusText}`);
2722
+ if (progress.packagesTotal != null && progress.packagesReady != null) {
2723
+ lines.push(`Packages: ${progress.packagesReady}/${progress.packagesTotal} ready`);
2724
+ }
2725
+ if (progress.elapsedMs != null) {
2726
+ lines.push(`Elapsed: ${(progress.elapsedMs / 1000).toFixed(1)}s`);
2727
+ }
2728
+ }
2729
+ lines.push("");
2730
+ lines.push("Some packages may still be indexing.");
2731
+ lines.push("Run again in a moment for complete results.");
2732
+ if (searchRef) {
2733
+ lines.push("");
2734
+ if (useColors) {
2735
+ lines.push(`${colors.dim}Search ref: ${searchRef}${colors.reset}`);
2736
+ } else {
2737
+ lines.push(`Search ref: ${searchRef}`);
2738
+ }
2739
+ }
2740
+ return lines.join(`
2741
+ `);
2742
+ }
2743
+ function getWaitTimeoutMs(options) {
2744
+ if (options.noWait) {
2745
+ return 0;
2746
+ }
2747
+ if (options.wait) {
2748
+ const parsed = Number.parseInt(options.wait, 10);
2749
+ if (!Number.isNaN(parsed) && parsed >= 0) {
2750
+ return parsed;
2751
+ }
2752
+ console.warn(`Warning: Invalid --wait value "${options.wait}". Using default (${DEFAULT_WAIT_TIMEOUT_MS}ms).`);
2753
+ }
2754
+ return DEFAULT_WAIT_TIMEOUT_MS;
2755
+ }
2664
2756
  async function searchAction(queryArg, options, deps, defaultMode = "ALL") {
2665
2757
  const { pkgseerService } = deps;
2666
2758
  const query = Array.isArray(queryArg) ? queryArg.join(" ") : queryArg ?? "";
@@ -2680,16 +2772,37 @@ async function searchAction(queryArg, options, deps, defaultMode = "ALL") {
2680
2772
  const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
2681
2773
  const mode = options.mode ? toSearchMode(options.mode) : defaultMode;
2682
2774
  const useColors = shouldUseColors(options.noColor);
2775
+ const waitTimeoutMs = getWaitTimeoutMs(options);
2683
2776
  const result = await pkgseerService.combinedSearch(packages, query, {
2684
2777
  mode,
2685
- limit
2778
+ limit,
2779
+ waitTimeoutMs
2686
2780
  });
2687
2781
  handleErrors(result.errors, options.json ?? false);
2688
- if (!result.data.combinedSearch) {
2782
+ const asyncResult = result.data.combinedSearch;
2783
+ if (!asyncResult) {
2689
2784
  outputError("No results returned. Check package names and registries.", options.json ?? false);
2690
2785
  return;
2691
2786
  }
2692
- outputResults(result.data.combinedSearch, options, useColors);
2787
+ if (!asyncResult.completed) {
2788
+ if (options.json) {
2789
+ output({
2790
+ completed: false,
2791
+ searchRef: asyncResult.searchRef,
2792
+ progress: asyncResult.progress,
2793
+ message: "Search is still indexing. Retry for complete results."
2794
+ }, true);
2795
+ } else {
2796
+ console.log(formatSearchProgress(asyncResult.progress, asyncResult.searchRef, useColors));
2797
+ }
2798
+ return;
2799
+ }
2800
+ const searchResults = asyncResult.result;
2801
+ if (!searchResults) {
2802
+ outputError("Search completed but no results returned.", options.json ?? false);
2803
+ return;
2804
+ }
2805
+ outputResults(searchResults, options, useColors);
2693
2806
  }
2694
2807
  function outputResults(results, options, useColors) {
2695
2808
  const format = options.json ? "json" : options.format ?? "human";
@@ -2740,7 +2853,7 @@ Examples:
2740
2853
  # JSON output
2741
2854
  pkgseer search "error" -P express --json`;
2742
2855
  function addSearchOptions(cmd) {
2743
- 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");
2856
+ 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").option("--wait <ms>", "Max milliseconds to wait for indexing (default: 30000)").option("--no-wait", "Return immediately without waiting for indexing");
2744
2857
  }
2745
2858
  function registerSearchCommand(program) {
2746
2859
  const cmd = program.command("search [query...]").summary("Search code and documentation across packages").description(SEARCH_DESCRIPTION);
@@ -2781,7 +2894,7 @@ Examples:
2781
2894
 
2782
2895
  Note: For code search, use 'pkgseer search --mode code' instead.`;
2783
2896
  function addDocsSearchOptions(cmd) {
2784
- return cmd.option("-P, --packages <packages>", "Packages to search (comma-separated). Format: [registry:]name[@version]. Examples: express | express,lodash | pypi:django@4.2").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");
2897
+ return cmd.option("-P, --packages <packages>", "Packages to search (comma-separated). Format: [registry:]name[@version]. Examples: express | express,lodash | pypi:django@4.2").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").option("--wait <ms>", "Max milliseconds to wait for indexing (default: 30000)").option("--no-wait", "Return immediately without waiting for indexing");
2785
2898
  }
2786
2899
  function registerDocsSearchCommand(program) {
2787
2900
  const cmd = program.command("search [query...]").summary("Search documentation").description(DOCS_SEARCH_DESCRIPTION);
@@ -3319,7 +3432,7 @@ function shouldIgnorePath(relativePath, patterns) {
3319
3432
  return ignored;
3320
3433
  }
3321
3434
  function shouldIgnoreDirectory(relativeDirPath, patterns) {
3322
- const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
3435
+ const normalized = `${relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "")}/`;
3323
3436
  return shouldIgnorePath(normalized, patterns);
3324
3437
  }
3325
3438
 
@@ -3352,6 +3465,10 @@ var MANIFEST_TYPES = [
3352
3465
  {
3353
3466
  type: "hex",
3354
3467
  filenames: ["mix.exs", "mix.lock"]
3468
+ },
3469
+ {
3470
+ type: "crates",
3471
+ filenames: ["Cargo.toml", "Cargo.lock"]
3355
3472
  }
3356
3473
  ];
3357
3474
  function suggestLabel(relativePath) {
@@ -3360,7 +3477,7 @@ function suggestLabel(relativePath) {
3360
3477
  if (parts.length === 1) {
3361
3478
  return "root";
3362
3479
  }
3363
- return parts[parts.length - 2];
3480
+ return parts[parts.length - 2] ?? "root";
3364
3481
  }
3365
3482
  async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
3366
3483
  const detected = [];
@@ -3426,12 +3543,12 @@ function filterRedundantPackageJson(manifests) {
3426
3543
  if (!dirToManifests.has(dir)) {
3427
3544
  dirToManifests.set(dir, []);
3428
3545
  }
3429
- dirToManifests.get(dir).push(manifest);
3546
+ dirToManifests.get(dir)?.push(manifest);
3430
3547
  }
3431
3548
  const filtered = [];
3432
- for (const [dir, dirManifests] of dirToManifests.entries()) {
3549
+ for (const [_dir, dirManifests] of dirToManifests.entries()) {
3433
3550
  const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
3434
- const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3551
+ const _hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3435
3552
  for (const manifest of dirManifests) {
3436
3553
  if (manifest.filename === "package.json" && hasLockFile) {
3437
3554
  continue;
@@ -4295,9 +4412,9 @@ Project already configured: ${highlight(existingConfig.config.project, useColors
4295
4412
  setupProject = false;
4296
4413
  } else {
4297
4414
  console.log(`
4298
- ` + "=".repeat(50));
4415
+ ${"=".repeat(50)}`);
4299
4416
  console.log("Project Configuration Setup");
4300
- console.log("=".repeat(50) + `
4417
+ console.log(`${"=".repeat(50)}
4301
4418
  `);
4302
4419
  await projectInit({}, {
4303
4420
  projectService: deps.projectService,
@@ -4318,9 +4435,9 @@ ${highlight("✓", useColors)} Project setup complete!
4318
4435
  }
4319
4436
  if (aiIntegration === "skill") {
4320
4437
  console.log(`
4321
- ` + "=".repeat(50));
4438
+ ${"=".repeat(50)}`);
4322
4439
  console.log("Skill Setup");
4323
- console.log("=".repeat(50) + `
4440
+ console.log(`${"=".repeat(50)}
4324
4441
  `);
4325
4442
  await skillInit({
4326
4443
  fileSystemService: deps.fileSystemService,
@@ -4334,9 +4451,9 @@ ${highlight("✓", useColors)} Skill setup complete!
4334
4451
  const currentConfig = await deps.configService.loadProjectConfig();
4335
4452
  const hasProjectNow = currentConfig?.config.project !== undefined;
4336
4453
  console.log(`
4337
- ` + "=".repeat(50));
4454
+ ${"=".repeat(50)}`);
4338
4455
  console.log("MCP Server Setup");
4339
- console.log("=".repeat(50) + `
4456
+ console.log(`${"=".repeat(50)}
4340
4457
  `);
4341
4458
  await mcpInit({
4342
4459
  fileSystemService: deps.fileSystemService,
@@ -4352,7 +4469,7 @@ ${highlight("✓", useColors)} MCP setup complete!
4352
4469
  }
4353
4470
  console.log("=".repeat(50));
4354
4471
  console.log("Setup Complete!");
4355
- console.log("=".repeat(50) + `
4472
+ console.log(`${"=".repeat(50)}
4356
4473
  `);
4357
4474
  if (setupProject) {
4358
4475
  const finalConfig = await deps.configService.loadProjectConfig();
@@ -4393,8 +4510,7 @@ function showCliUsage(useColors) {
4393
4510
  console.log(` ${highlight("pkgseer docs get <pkg>/<page>", useColors)} Fetch doc content`);
4394
4511
  console.log(` ${highlight("pkgseer docs search <query>", useColors)} Search documentation
4395
4512
  `);
4396
- console.log(dim(`All commands support --json for structured output.
4397
- ` + "Tip: Run 'pkgseer quickstart' for a quick reference guide.", useColors));
4513
+ console.log(dim("All commands support --json for structured output.", useColors));
4398
4514
  }
4399
4515
  function registerInitCommand(program) {
4400
4516
  program.command("init").summary("Set up project and AI integration").description(`Set up PkgSeer for your project.
@@ -4463,9 +4579,11 @@ async function loginAction(options, deps) {
4463
4579
  let callback;
4464
4580
  try {
4465
4581
  callback = await Promise.race([serverPromise, timeoutPromise]);
4466
- clearTimeout(timeoutId);
4582
+ if (timeoutId)
4583
+ clearTimeout(timeoutId);
4467
4584
  } catch (error2) {
4468
- clearTimeout(timeoutId);
4585
+ if (timeoutId)
4586
+ clearTimeout(timeoutId);
4469
4587
  if (error2 instanceof Error) {
4470
4588
  console.log(`${error2.message}.
4471
4589
  `);
@@ -4582,12 +4700,13 @@ function toGraphQLRegistry2(registry) {
4582
4700
  const map = {
4583
4701
  npm: "NPM",
4584
4702
  pypi: "PYPI",
4585
- hex: "HEX"
4703
+ hex: "HEX",
4704
+ crates: "CRATES"
4586
4705
  };
4587
4706
  return map[registry.toLowerCase()] || "NPM";
4588
4707
  }
4589
4708
  var schemas = {
4590
- registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
4709
+ registry: z2.enum(["npm", "pypi", "hex", "crates"]).describe("Package registry (npm, pypi, hex, or crates)"),
4591
4710
  packageName: z2.string().max(255).describe("Name of the package"),
4592
4711
  version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
4593
4712
  };
@@ -4630,7 +4749,7 @@ function notFoundError(packageName, registry) {
4630
4749
 
4631
4750
  // src/tools/compare-packages.ts
4632
4751
  var packageInputSchema = z3.object({
4633
- registry: z3.enum(["npm", "pypi", "hex"]),
4752
+ registry: z3.enum(["npm", "pypi", "hex", "crates"]),
4634
4753
  name: z3.string().max(255),
4635
4754
  version: z3.string().max(100).optional()
4636
4755
  });
@@ -4640,7 +4759,7 @@ var argsSchema = {
4640
4759
  function createComparePackagesTool(pkgseerService) {
4641
4760
  return {
4642
4761
  name: "compare_packages",
4643
- 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"}]',
4762
+ 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, crates). " + 'Format: [{"registry":"npm","name":"lodash"},{"registry":"npm","name":"underscore"}]',
4644
4763
  schema: argsSchema,
4645
4764
  handler: async ({ packages }, _extra) => {
4646
4765
  return withErrorHandling("compare packages", async () => {
@@ -4944,11 +5063,13 @@ var packageInputSchema2 = z7.object({
4944
5063
  name: z7.string().min(1).describe("Package name"),
4945
5064
  version: z7.string().optional().describe("Specific version (defaults to latest)")
4946
5065
  });
5066
+ var DEFAULT_AGENT_WAIT_TIMEOUT_MS = 1e4;
4947
5067
  var argsSchema9 = {
4948
5068
  packages: z7.array(packageInputSchema2).min(1).max(20).describe("Packages to search (1-20). Each package needs registry and name."),
4949
5069
  query: z7.string().min(1).describe("Search query - natural language or keywords"),
4950
5070
  mode: z7.enum(["all", "code", "docs"]).optional().describe('Search mode: "all" (default), "code" only, or "docs" only'),
4951
- limit: z7.number().int().min(1).max(100).optional().describe("Maximum results (default: 20)")
5071
+ limit: z7.number().int().min(1).max(100).optional().describe("Maximum results (default: 20)"),
5072
+ waitTimeoutMs: z7.number().int().min(0).max(60000).optional().describe("Max milliseconds to wait for indexing (default: 10000, 0=immediate)")
4952
5073
  };
4953
5074
  function toSearchMode2(mode) {
4954
5075
  if (!mode)
@@ -4986,9 +5107,9 @@ Results may be incomplete. Retry later for more complete results.`;
4986
5107
  function createSearchTool(pkgseerService) {
4987
5108
  return {
4988
5109
  name: "search",
4989
- 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.",
5110
+ 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. If packages need indexing, the search will wait up to waitTimeoutMs " + "(default 10s). If not complete, returns progress info with searchRef for follow-up.",
4990
5111
  schema: argsSchema9,
4991
- handler: async ({ packages, query, mode, limit }, _extra) => {
5112
+ handler: async ({ packages, query, mode, limit, waitTimeoutMs }, _extra) => {
4992
5113
  return withErrorHandling("search packages", async () => {
4993
5114
  const normalizedQuery = query.trim();
4994
5115
  if (normalizedQuery.length === 0) {
@@ -5001,15 +5122,29 @@ function createSearchTool(pkgseerService) {
5001
5122
  }));
5002
5123
  const result = await pkgseerService.combinedSearch(graphqlPackages, normalizedQuery, {
5003
5124
  mode: toSearchMode2(mode),
5004
- limit
5125
+ limit,
5126
+ waitTimeoutMs: waitTimeoutMs ?? DEFAULT_AGENT_WAIT_TIMEOUT_MS
5005
5127
  });
5006
5128
  const graphqlError = handleGraphQLErrors(result.errors);
5007
5129
  if (graphqlError)
5008
5130
  return graphqlError;
5009
- if (!result.data.combinedSearch) {
5131
+ const asyncResult = result.data.combinedSearch;
5132
+ if (!asyncResult) {
5010
5133
  return errorResult("No search results returned. Check package names and registries.");
5011
5134
  }
5012
- const searchResult = result.data.combinedSearch;
5135
+ if (!asyncResult.completed) {
5136
+ const progressInfo = {
5137
+ completed: false,
5138
+ searchRef: asyncResult.searchRef,
5139
+ progress: asyncResult.progress,
5140
+ message: "Search is still indexing. Retry with same query for results, " + "or increase waitTimeoutMs to wait longer."
5141
+ };
5142
+ return textResult(JSON.stringify(progressInfo, null, 2));
5143
+ }
5144
+ const searchResult = asyncResult.result;
5145
+ if (!searchResult) {
5146
+ return errorResult("Search completed but no results returned.");
5147
+ }
5013
5148
  const entries = searchResult.entries ?? [];
5014
5149
  if (entries.length === 0) {
5015
5150
  return errorResult(`No results found for "${normalizedQuery}". ` + "Try broader terms or different packages.");
@@ -5173,7 +5308,7 @@ function showMcpSetupInstructions(deps) {
5173
5308
  console.log(` ${highlight(`${deps.baseUrl}/docs/mcp-server`, useColors)}
5174
5309
  `);
5175
5310
  console.log("Alternative: Use CLI directly (no MCP setup needed)");
5176
- console.log(` ${highlight("pkgseer quickstart", useColors)}`);
5311
+ console.log(` ${highlight("pkgseer pkg info <package>", useColors)}`);
5177
5312
  }
5178
5313
  function registerMcpCommand(program) {
5179
5314
  const mcpCommand = program.command("mcp").summary("Show setup instructions or start MCP server").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
@@ -5255,10 +5390,10 @@ async function pkgCompareAction(packages, options, deps) {
5255
5390
  if (options.json) {
5256
5391
  const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
5257
5392
  const slim = pkgs.map((p) => ({
5258
- package: `${p.packageName}@${p.version}`,
5259
- quality: p.quality?.score,
5260
- downloads: p.downloadsLastMonth,
5261
- vulnerabilities: p.vulnerabilityCount
5393
+ package: `${p?.packageName}@${p?.version}`,
5394
+ quality: p?.quality?.score,
5395
+ downloads: p?.downloadsLastMonth,
5396
+ vulnerabilities: p?.vulnerabilityCount
5262
5397
  }));
5263
5398
  output(slim, true);
5264
5399
  } else {
@@ -5527,8 +5662,8 @@ async function pkgQualityAction(packageArg, options, deps) {
5527
5662
  score: quality?.overallScore,
5528
5663
  grade: quality?.grade,
5529
5664
  categories: quality?.categories?.filter((c) => c).map((c) => ({
5530
- name: c.category,
5531
- score: c.score
5665
+ name: c?.category,
5666
+ score: c?.score
5532
5667
  }))
5533
5668
  };
5534
5669
  output(slim, true);
@@ -5629,10 +5764,10 @@ async function pkgVulnsAction(packageArg, options, deps) {
5629
5764
  package: `${data.package?.name}@${data.package?.version}`,
5630
5765
  count: data.security?.vulnerabilityCount ?? 0,
5631
5766
  vulnerabilities: vulns.filter((v) => v).map((v) => ({
5632
- id: v.osvId,
5633
- severity: v.severityScore,
5634
- summary: v.summary,
5635
- fixed: v.fixedInVersions
5767
+ id: v?.osvId,
5768
+ severity: v?.severityScore,
5769
+ summary: v?.summary,
5770
+ fixed: v?.fixedInVersions
5636
5771
  }))
5637
5772
  };
5638
5773
  output(slim, true);
@@ -5673,7 +5808,7 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5673
5808
  });
5674
5809
  }
5675
5810
  }
5676
- const labelToFiles = new Map;
5811
+ const _labelToFiles = new Map;
5677
5812
  const labelToConfig = new Map;
5678
5813
  for (const detectedGroup of detectedGroups) {
5679
5814
  for (const manifest of detectedGroup.manifests) {
@@ -5686,13 +5821,14 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5686
5821
  label = existingConfig?.label ?? detectedGroup.label;
5687
5822
  }
5688
5823
  const allowMixDeps = existingConfig?.allow_mix_deps;
5689
- if (!labelToConfig.has(label)) {
5690
- labelToConfig.set(label, {
5824
+ let config = labelToConfig.get(label);
5825
+ if (!config) {
5826
+ config = {
5691
5827
  files: new Set,
5692
5828
  allow_mix_deps: allowMixDeps
5693
- });
5829
+ };
5830
+ labelToConfig.set(label, config);
5694
5831
  }
5695
- const config = labelToConfig.get(label);
5696
5832
  config.files.add(manifest.relativePath);
5697
5833
  if (allowMixDeps) {
5698
5834
  config.allow_mix_deps = true;
@@ -5729,7 +5865,7 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5729
5865
  });
5730
5866
  }
5731
5867
  async function projectDetectAction(options, deps) {
5732
- const { configService, fileSystemService, promptService, shellService } = deps;
5868
+ const { configService, fileSystemService, promptService } = deps;
5733
5869
  const projectConfig = await configService.loadProjectConfig();
5734
5870
  if (!projectConfig?.config.project) {
5735
5871
  console.error(`✗ No project is configured in pkgseer.yml`);
@@ -5786,7 +5922,7 @@ Suggested configuration:`);
5786
5922
  console.log(`${prefix}${file}`);
5787
5923
  }
5788
5924
  }
5789
- const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
5925
+ const _hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
5790
5926
  const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
5791
5927
  let allowMixDeps = false;
5792
5928
  if (hasHexInSuggested) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "./shared/chunk-48mwa8wt.js";
3
+ } from "./shared/chunk-9yar14cw.js";
4
4
  export {
5
5
  version
6
6
  };
@@ -1,4 +1,4 @@
1
1
  // package.json
2
- var version = "0.2.4";
2
+ var version = "0.3.0";
3
3
 
4
4
  export { version };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pkgseer/cli",
3
3
  "description": "CLI companion for PkgSeer - package intelligence for developers and AI assistants",
4
- "version": "0.2.4",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",