@pkgseer/cli 0.2.3 → 0.2.5

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,209 @@
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:
24
35
 
25
- While you can use PkgSeer without authentication, logging in gives you higher rate limits and access to all features:
36
+ ### Skills
37
+
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.
45
+
46
+ ### MCP Server
32
47
 
33
- To check your authentication status:
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
+ ```
85
100
 
86
- ```markdown
87
- When working with dependencies:
101
+ ### Package Commands
88
102
 
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
103
+ ```bash
104
+ pkgseer pkg info lodash # Package summary and metadata
105
+ pkgseer pkg vulns lodash@4.17.21 # Security vulnerabilities
106
+ pkgseer pkg quality express # Quality score (0-100)
107
+ pkgseer pkg deps express # Direct dependencies
108
+ pkgseer pkg deps express --transitive # Include transitive deps
109
+ pkgseer pkg compare axios got fetch-h2 # Compare packages
92
110
  ```
93
111
 
94
- ## Available Tools
112
+ Package format: `[registry:]name[@version]`
113
+ - `lodash` — npm (default registry)
114
+ - `pypi:requests` — PyPI
115
+ - `hex:phoenix` — Hex
116
+ - `crates:serde` — crates.io
117
+ - `lodash@4.17.21` — specific version
118
+
119
+ ### Documentation Commands
95
120
 
96
- When running as an MCP server, the following tools are available to AI assistants:
121
+ ```bash
122
+ pkgseer docs list pypi:requests # List available doc pages
123
+ pkgseer docs get lodash/chunk # Fetch specific doc page
124
+ pkgseer docs search "routing" -P express # Search docs only
125
+ ```
97
126
 
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 |
127
+ ### Project Commands
105
128
 
106
- All tools work with **npm**, **PyPI**, and **Hex** registries.
129
+ ```bash
130
+ pkgseer project init # Create pkgseer.yml config
131
+ pkgseer project detect # Detect package manifests
132
+ pkgseer project upload # Upload project to PkgSeer
133
+ ```
107
134
 
108
- ## CLI Commands
135
+ ### Authentication
109
136
 
110
137
  ```bash
111
- pkgseer --help # Show all available commands
112
- pkgseer --version # Show version number
138
+ pkgseer login # Authenticate via browser
139
+ pkgseer logout # Sign out
140
+ pkgseer auth status # Check authentication state
141
+ ```
142
+
143
+ ### Configuration
144
+
145
+ ```bash
146
+ pkgseer config show # Display current configuration
147
+ ```
148
+
149
+ ## MCP Tools
150
+
151
+ When running as an MCP server, these tools are available:
152
+
153
+ | Tool | Description |
154
+ |------|-------------|
155
+ | `package_summary` | Package metadata, versions, quickstart examples |
156
+ | `package_vulnerabilities` | Security advisories and CVEs |
157
+ | `package_dependencies` | Dependency tree (direct and transitive) |
158
+ | `package_quality` | Quality score with category breakdown |
159
+ | `compare_packages` | Side-by-side comparison of packages |
160
+ | `list_package_docs` | Available documentation pages |
161
+ | `fetch_package_doc` | Full content of a documentation page |
162
+ | `search` | Search code and docs across packages |
163
+ | `fetch_code_context` | Fetch code from search results |
164
+ | `search_project_docs` | Search docs for packages in current project |
165
+
166
+ ## Configuration
167
+
168
+ ### Project Configuration
113
169
 
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
170
+ Create `pkgseer.yml` in your project root:
117
171
 
118
- pkgseer mcp # Start the MCP server (for AI assistant integration)
172
+ ```yaml
173
+ project: my-project-name
174
+
175
+ # Optional: limit which tools are available
176
+ enabled_tools:
177
+ - package_summary
178
+ - package_vulnerabilities
179
+ - search_project_docs
180
+ ```
181
+
182
+ ### Environment Variables
183
+
184
+ | Variable | Description |
185
+ |----------|-------------|
186
+ | `PKGSEER_API_TOKEN` | API token (alternative to `pkgseer login`) |
187
+ | `PKGSEER_URL` | Base URL for PkgSeer (for development/testing) |
188
+
189
+ ## Documentation
190
+
191
+ - [Architecture Overview](docs/implementation/architecture.md)
192
+ - [MCP Installation Guide](docs/implementation/mcp-installation.md)
193
+ - [Creating MCP Tools](docs/implementation/tools.md)
194
+ - [Authentication Flow](docs/implementation/auth.md)
195
+ - [Skills System](docs/implementation/skills.md)
196
+ - [Configuration Reference](docs/implementation/configuration.md)
197
+
198
+ ## Development
199
+
200
+ See [CLAUDE.md](CLAUDE.md) for development guidelines.
201
+
202
+ ```bash
203
+ bun install # Install dependencies
204
+ bun run dev # Development mode
205
+ bun test # Run tests
206
+ bun run build # Build for production
207
+ bun run codegen # Regenerate GraphQL types
119
208
  ```
120
209
 
121
210
  ## Need Help?
@@ -125,4 +214,4 @@ pkgseer mcp # Start the MCP server (for AI assistant integration)
125
214
 
126
215
  ## License
127
216
 
128
- MIT © [Juha Litola](https://github.com/pkgseer)
217
+ (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-zaq9c2d8.js";
4
+ } from "./shared/chunk-drz16bhv.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -23,6 +23,7 @@ var CliPackageInfoDocument = gql`
23
23
  license
24
24
  homepage
25
25
  repositoryUrl
26
+ downloadsLastMonth
26
27
  }
27
28
  security {
28
29
  vulnerabilityCount
@@ -1782,11 +1783,33 @@ function registerConfigShowCommand(program) {
1782
1783
  }
1783
1784
 
1784
1785
  // src/commands/shared.ts
1786
+ function parsePackageSpec(spec) {
1787
+ let registry = "npm";
1788
+ let rest = spec;
1789
+ if (spec.includes(":")) {
1790
+ const colonIndex = spec.indexOf(":");
1791
+ const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
1792
+ if (["npm", "pypi", "hex", "crates"].includes(potentialRegistry)) {
1793
+ registry = potentialRegistry;
1794
+ rest = spec.slice(colonIndex + 1);
1795
+ }
1796
+ }
1797
+ const atIndex = rest.lastIndexOf("@");
1798
+ if (atIndex > 0) {
1799
+ return {
1800
+ registry,
1801
+ name: rest.slice(0, atIndex),
1802
+ version: rest.slice(atIndex + 1)
1803
+ };
1804
+ }
1805
+ return { registry, name: rest };
1806
+ }
1785
1807
  function toGraphQLRegistry(registry) {
1786
1808
  const map = {
1787
1809
  npm: "NPM",
1788
1810
  pypi: "PYPI",
1789
- hex: "HEX"
1811
+ hex: "HEX",
1812
+ crates: "CRATES"
1790
1813
  };
1791
1814
  return map[registry.toLowerCase()] || "NPM";
1792
1815
  }
@@ -2242,7 +2265,7 @@ Examples:
2242
2265
  # Multiple pages
2243
2266
  pkgseer docs get 24293-shared-plugins-during-build-2 npm/express/4.18.2/readme`;
2244
2267
  function registerDocsGetCommand(program) {
2245
- program.command("get <refs...>").summary("Fetch documentation page(s)").description(GET_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").option("--verbose", "Include metadata (ID, format, source, links, etc.)").option("--preview-lines <n>", "Lines of content to show (0 for full content; default 20)").action(async (refs, options) => {
2268
+ program.command("get <refs...>").summary("Fetch documentation page(s)").description(GET_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-V, --pkg-version <version>", "Package version").option("--json", "Output as JSON").option("--verbose", "Include metadata (ID, format, source, links, etc.)").option("--preview-lines <n>", "Lines of content to show (0 for full content; default 20)").action(async (refs, options) => {
2246
2269
  await withCliErrorHandling(options.json ?? false, async () => {
2247
2270
  const deps = await createContainer();
2248
2271
  await docsGetAction(refs, options, deps);
@@ -2272,13 +2295,15 @@ function formatDocsList(docs) {
2272
2295
  return lines.join(`
2273
2296
  `);
2274
2297
  }
2275
- async function docsListAction(packageName, options, deps) {
2298
+ async function docsListAction(packageArg, options, deps) {
2276
2299
  const { pkgseerService } = deps;
2277
- const registry = toGraphQLRegistry(options.registry);
2278
- const result = await pkgseerService.cliDocsList(registry, packageName, options.pkgVersion);
2300
+ const parsed = parsePackageSpec(packageArg);
2301
+ const registry = toGraphQLRegistry(parsed.registry !== "npm" ? parsed.registry : options.registry);
2302
+ const version2 = parsed.version ?? options.pkgVersion;
2303
+ const result = await pkgseerService.cliDocsList(registry, parsed.name, version2);
2279
2304
  handleErrors(result.errors, options.json ?? false);
2280
2305
  if (!result.data.listPackageDocs) {
2281
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
2306
+ outputError(`Package not found: ${parsed.name} in ${parsed.registry}`, options.json ?? false);
2282
2307
  return;
2283
2308
  }
2284
2309
  if (options.json) {
@@ -2302,19 +2327,21 @@ Shows all documentation pages with titles, globally unique page IDs,
2302
2327
  word counts, and descriptions. Use the page ID with 'docs get'
2303
2328
  to fetch the full content of a specific page.
2304
2329
 
2330
+ Package format: [registry:]name[@version]
2331
+
2305
2332
  Examples:
2306
2333
  pkgseer docs list lodash
2307
- pkgseer docs list requests --registry pypi
2308
- pkgseer docs list phoenix --registry hex --version 1.7.0 --json`;
2334
+ pkgseer docs list pypi:requests
2335
+ pkgseer docs list hex:phoenix@1.7.0 --json`;
2309
2336
  function registerDocsListCommand(program) {
2310
- program.command("list <package>").summary("List documentation pages").description(LIST_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
2337
+ program.command("list <package>").summary("List documentation pages").description(LIST_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-V, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
2311
2338
  await withCliErrorHandling(options.json ?? false, async () => {
2312
2339
  const deps = await createContainer();
2313
2340
  await docsListAction(packageName, options, deps);
2314
2341
  });
2315
2342
  });
2316
2343
  }
2317
- // src/commands/docs/search.ts
2344
+ // src/commands/search.ts
2318
2345
  var colors = {
2319
2346
  reset: "\x1B[0m",
2320
2347
  bold: "\x1B[1m",
@@ -2344,37 +2371,11 @@ function toSearchMode(mode) {
2344
2371
  }
2345
2372
  function parsePackageList(input2) {
2346
2373
  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
+ const parsed = parsePackageSpec(spec.trim());
2374
2375
  return {
2375
- registry: toGraphQLRegistry(beforeSlash),
2376
- name: afterSlash,
2377
- version: version2
2376
+ registry: toGraphQLRegistry(parsed.registry),
2377
+ name: parsed.name,
2378
+ version: parsed.version
2378
2379
  };
2379
2380
  });
2380
2381
  }
@@ -2394,7 +2395,7 @@ function formatEntry(entry, useColors) {
2394
2395
  if (entry.type === "CODE") {
2395
2396
  const title = entry.title ?? "Unknown";
2396
2397
  const location = entry.filePath ? `${entry.filePath}:${entry.startLine ?? "?"}-${entry.endLine ?? "?"}` : "";
2397
- const lang = entry.language ? ` ${entry.language}` : "";
2398
+ const lang = entry.language ? ` * ${entry.language}` : "";
2398
2399
  if (useColors) {
2399
2400
  lines.push(`${badge} ${colors.bold}${title}${colors.reset}`);
2400
2401
  if (location) {
@@ -2446,7 +2447,7 @@ function formatPackageHeader(entry, prevEntry, useColors) {
2446
2447
  return "";
2447
2448
  }
2448
2449
  const header = `${pkg} (${registry})`;
2449
- const separator = "".repeat(Math.min(50, header.length + 10));
2450
+ const separator = "=".repeat(Math.min(50, header.length + 10));
2450
2451
  if (useColors) {
2451
2452
  return `
2452
2453
  ${colors.yellow}${header}${colors.reset}
@@ -2478,10 +2479,10 @@ function formatSearchResults(results, useColors) {
2478
2479
  const codeCount = entries.filter((e) => e?.type === "CODE").length;
2479
2480
  const docCount = entries.filter((e) => e?.type === "DOC" || e?.type === "REPO_DOC").length;
2480
2481
  if (useColors) {
2481
- lines.push(`${colors.dim}───────────────────────────────────────────────────${colors.reset}`);
2482
+ lines.push(`${colors.dim}---------------------------------------------------${colors.reset}`);
2482
2483
  lines.push(`${colors.dim}Results: ${entries.length} total (${codeCount} code, ${docCount} docs)${colors.reset}`);
2483
2484
  } else {
2484
- lines.push("───────────────────────────────────────────────────");
2485
+ lines.push("---------------------------------------------------");
2485
2486
  lines.push(`Results: ${entries.length} total (${codeCount} code, ${docCount} docs)`);
2486
2487
  }
2487
2488
  const indexingWarnings = formatIndexingWarnings(results.indexingStatus);
@@ -2559,7 +2560,7 @@ function abbrevLang(lang) {
2559
2560
  function truncate(text, maxLen) {
2560
2561
  if (text.length <= maxLen)
2561
2562
  return text;
2562
- return text.slice(0, maxLen - 3) + "...";
2563
+ return `${text.slice(0, maxLen - 3)}...`;
2563
2564
  }
2564
2565
  function formatEntryCompact(entry, useColors) {
2565
2566
  if (!entry)
@@ -2661,7 +2662,7 @@ function summarizeSearchResults(results) {
2661
2662
  return lines.join(`
2662
2663
  `);
2663
2664
  }
2664
- async function docsSearchAction(queryArg, options, deps) {
2665
+ async function searchAction(queryArg, options, deps, defaultMode = "ALL") {
2665
2666
  const { pkgseerService } = deps;
2666
2667
  const query = Array.isArray(queryArg) ? queryArg.join(" ") : queryArg ?? "";
2667
2668
  if (!query.trim()) {
@@ -2678,7 +2679,7 @@ async function docsSearchAction(queryArg, options, deps) {
2678
2679
  }
2679
2680
  const packages = parsePackageList(options.packages);
2680
2681
  const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
2681
- const mode = toSearchMode(options.mode);
2682
+ const mode = options.mode ? toSearchMode(options.mode) : defaultMode;
2682
2683
  const useColors = shouldUseColors(options.noColor);
2683
2684
  const result = await pkgseerService.combinedSearch(packages, query, {
2684
2685
  mode,
@@ -2713,36 +2714,79 @@ Searches both code (functions, classes) and documentation pages
2713
2714
  using the combined search endpoint. Returns results with snippets
2714
2715
  showing matches.
2715
2716
 
2717
+ Package format: [registry:]name[@version]
2718
+
2716
2719
  Examples:
2717
2720
  # Search a single package (code + docs)
2718
- pkgseer docs search "error handling" -P express
2721
+ pkgseer search "error handling" -P express
2719
2722
 
2720
2723
  # Search code only
2721
- pkgseer docs search "Router.use" -P express --mode code
2724
+ pkgseer search "Router.use" -P express --mode code
2722
2725
 
2723
2726
  # Search docs only
2724
- pkgseer docs search "middleware" -P express --mode docs
2727
+ pkgseer search "middleware" -P express --mode docs
2725
2728
 
2726
2729
  # Search multiple packages
2727
- pkgseer docs search "auth" -P express,passport
2730
+ pkgseer search "auth" -P express,passport
2728
2731
 
2729
2732
  # With registry prefix (for non-npm packages)
2730
- pkgseer docs search "orm" -P pypi/django,pypi/sqlalchemy
2733
+ pkgseer search "orm" -P pypi:django,pypi:sqlalchemy
2731
2734
 
2732
2735
  # With specific version
2733
- pkgseer docs search "routing" -P express@4.18.2
2736
+ pkgseer search "routing" -P express@4.18.2
2734
2737
 
2735
2738
  # Output for piping
2736
- pkgseer docs search "routing" -P express --refs-only
2739
+ pkgseer search "routing" -P express --refs-only
2737
2740
 
2738
2741
  # JSON output
2739
- pkgseer docs search "error" -P express --json`;
2742
+ pkgseer search "error" -P express --json`;
2740
2743
  function addSearchOptions(cmd) {
2741
2744
  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");
2742
2745
  }
2743
- function registerDocsSearchCommand(program) {
2744
- const cmd = program.command("search [query...]").summary("Search code and documentation").description(SEARCH_DESCRIPTION);
2746
+ function registerSearchCommand(program) {
2747
+ const cmd = program.command("search [query...]").summary("Search code and documentation across packages").description(SEARCH_DESCRIPTION);
2745
2748
  addSearchOptions(cmd).action(async (query, options) => {
2749
+ await withCliErrorHandling(options.json ?? false, async () => {
2750
+ const deps = await createContainer();
2751
+ await searchAction(query, options, deps);
2752
+ });
2753
+ });
2754
+ }
2755
+
2756
+ // src/commands/docs/search.ts
2757
+ async function docsSearchAction(queryArg, options, deps) {
2758
+ await searchAction(queryArg, options, deps, "DOCS");
2759
+ }
2760
+ var DOCS_SEARCH_DESCRIPTION = `Search documentation across packages.
2761
+
2762
+ Searches documentation pages in package docs. For searching both
2763
+ code and docs, use the top-level 'pkgseer search' command instead.
2764
+
2765
+ Package format: [registry:]name[@version]
2766
+
2767
+ Examples:
2768
+ # Search documentation
2769
+ pkgseer docs search "middleware" -P express
2770
+
2771
+ # Search multiple packages
2772
+ pkgseer docs search "auth" -P express,passport
2773
+
2774
+ # With registry prefix (for non-npm packages)
2775
+ pkgseer docs search "orm" -P pypi:django,pypi:sqlalchemy
2776
+
2777
+ # With specific version
2778
+ pkgseer docs search "routing" -P express@4.18.2
2779
+
2780
+ # JSON output
2781
+ pkgseer docs search "error" -P express --json
2782
+
2783
+ Note: For code search, use 'pkgseer search --mode code' instead.`;
2784
+ function addDocsSearchOptions(cmd) {
2785
+ 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");
2786
+ }
2787
+ function registerDocsSearchCommand(program) {
2788
+ const cmd = program.command("search [query...]").summary("Search documentation").description(DOCS_SEARCH_DESCRIPTION);
2789
+ addDocsSearchOptions(cmd).action(async (query, options) => {
2746
2790
  await withCliErrorHandling(options.json ?? false, async () => {
2747
2791
  const deps = await createContainer();
2748
2792
  await docsSearchAction(query, options, deps);
@@ -3276,7 +3320,7 @@ function shouldIgnorePath(relativePath, patterns) {
3276
3320
  return ignored;
3277
3321
  }
3278
3322
  function shouldIgnoreDirectory(relativeDirPath, patterns) {
3279
- const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
3323
+ const normalized = `${relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "")}/`;
3280
3324
  return shouldIgnorePath(normalized, patterns);
3281
3325
  }
3282
3326
 
@@ -3309,6 +3353,10 @@ var MANIFEST_TYPES = [
3309
3353
  {
3310
3354
  type: "hex",
3311
3355
  filenames: ["mix.exs", "mix.lock"]
3356
+ },
3357
+ {
3358
+ type: "crates",
3359
+ filenames: ["Cargo.toml", "Cargo.lock"]
3312
3360
  }
3313
3361
  ];
3314
3362
  function suggestLabel(relativePath) {
@@ -3317,7 +3365,7 @@ function suggestLabel(relativePath) {
3317
3365
  if (parts.length === 1) {
3318
3366
  return "root";
3319
3367
  }
3320
- return parts[parts.length - 2];
3368
+ return parts[parts.length - 2] ?? "root";
3321
3369
  }
3322
3370
  async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
3323
3371
  const detected = [];
@@ -3383,12 +3431,12 @@ function filterRedundantPackageJson(manifests) {
3383
3431
  if (!dirToManifests.has(dir)) {
3384
3432
  dirToManifests.set(dir, []);
3385
3433
  }
3386
- dirToManifests.get(dir).push(manifest);
3434
+ dirToManifests.get(dir)?.push(manifest);
3387
3435
  }
3388
3436
  const filtered = [];
3389
- for (const [dir, dirManifests] of dirToManifests.entries()) {
3437
+ for (const [_dir, dirManifests] of dirToManifests.entries()) {
3390
3438
  const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
3391
- const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3439
+ const _hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3392
3440
  for (const manifest of dirManifests) {
3393
3441
  if (manifest.filename === "package.json" && hasLockFile) {
3394
3442
  continue;
@@ -3869,50 +3917,55 @@ Search and analyze packages across npm, PyPI, and Hex. All commands support \`--
3869
3917
 
3870
3918
  \`\`\`bash
3871
3919
  # Search code and docs across packages
3872
- ${invocation} search "<query>" -P lodash,express # npm packages
3873
- ${invocation} search "<query>" -P requests -r pypi # PyPI packages
3874
- ${invocation} search "authentication" -P phoenix,plug -r hex
3920
+ ${invocation} search "<query>" -P lodash,express # npm packages
3921
+ ${invocation} search "<query>" -P pypi:requests # PyPI packages
3922
+ ${invocation} search "authentication" -P hex:phoenix,hex:plug
3875
3923
 
3876
3924
  # Search modes
3877
- ${invocation} search "<query>" -P <packages> --code # Code only
3878
- ${invocation} search "<query>" -P <packages> --docs # Docs only
3925
+ ${invocation} search "<query>" -P <packages> --mode code # Code only
3926
+ ${invocation} search "<query>" -P <packages> --mode docs # Docs only
3879
3927
 
3880
- # Search project dependencies (requires pkgseer.yml)
3881
- ${invocation} docs search "<query>"
3928
+ # Docs-only search (shorthand)
3929
+ ${invocation} docs search "<query>" -P <packages>
3882
3930
  \`\`\`
3883
3931
 
3884
3932
  ## Package Analysis
3885
3933
 
3934
+ Package format: \`[registry:]name[@version]\`
3935
+
3886
3936
  \`\`\`bash
3887
3937
  # Overview: metadata, versions, quickstart
3888
- ${invocation} pkg info <package> [-r npm|pypi|hex]
3938
+ ${invocation} pkg info lodash
3939
+ ${invocation} pkg info pypi:requests
3889
3940
 
3890
3941
  # Quality score (0-100) with category breakdown
3891
- ${invocation} pkg quality <package> [-r registry] [-v version]
3942
+ ${invocation} pkg quality express@4.18.0
3943
+ ${invocation} pkg quality pypi:django@4.2
3892
3944
 
3893
3945
  # Security: CVEs, severity, upgrade paths
3894
- ${invocation} pkg vulns <package> [-r registry] [-v version]
3946
+ ${invocation} pkg vulns lodash@4.17.21
3895
3947
 
3896
3948
  # Dependencies: direct, transitive, tree view
3897
- ${invocation} pkg deps <package> [-r registry] [-t] [-d depth]
3949
+ ${invocation} pkg deps express --transitive
3898
3950
 
3899
3951
  # Compare up to 10 packages
3900
3952
  ${invocation} pkg compare lodash underscore ramda
3901
- ${invocation} pkg compare npm:axios pypi:httpx # cross-registry
3953
+ ${invocation} pkg compare axios pypi:httpx # cross-registry
3902
3954
  \`\`\`
3903
3955
 
3904
3956
  ## Documentation
3905
3957
 
3906
3958
  \`\`\`bash
3907
- ${invocation} docs list <package> [-r registry] # List pages
3908
- ${invocation} docs get <package>/<page-id> # Fetch content
3959
+ ${invocation} docs list pypi:requests # List pages
3960
+ ${invocation} docs get <package>/<page-id> # Fetch content
3961
+ ${invocation} docs search "<query>" -P <packages> # Search docs only
3909
3962
  \`\`\`
3910
3963
 
3911
3964
  ## Tips
3912
3965
 
3913
- - Default registry: npm. Use \`-r pypi\` or \`-r hex\` for others
3966
+ - Package format: \`[registry:]name[@version]\` (e.g., \`pypi:django@4.2\`)
3967
+ - Default registry: npm
3914
3968
  - Use \`--json\` for structured output when parsing
3915
- - Version defaults to latest; use \`-v\` for specific
3916
3969
  `;
3917
3970
  }
3918
3971
  function extractSkillVersion(content) {
@@ -4247,9 +4300,9 @@ Project already configured: ${highlight(existingConfig.config.project, useColors
4247
4300
  setupProject = false;
4248
4301
  } else {
4249
4302
  console.log(`
4250
- ` + "=".repeat(50));
4303
+ ${"=".repeat(50)}`);
4251
4304
  console.log("Project Configuration Setup");
4252
- console.log("=".repeat(50) + `
4305
+ console.log(`${"=".repeat(50)}
4253
4306
  `);
4254
4307
  await projectInit({}, {
4255
4308
  projectService: deps.projectService,
@@ -4270,9 +4323,9 @@ ${highlight("✓", useColors)} Project setup complete!
4270
4323
  }
4271
4324
  if (aiIntegration === "skill") {
4272
4325
  console.log(`
4273
- ` + "=".repeat(50));
4326
+ ${"=".repeat(50)}`);
4274
4327
  console.log("Skill Setup");
4275
- console.log("=".repeat(50) + `
4328
+ console.log(`${"=".repeat(50)}
4276
4329
  `);
4277
4330
  await skillInit({
4278
4331
  fileSystemService: deps.fileSystemService,
@@ -4286,9 +4339,9 @@ ${highlight("✓", useColors)} Skill setup complete!
4286
4339
  const currentConfig = await deps.configService.loadProjectConfig();
4287
4340
  const hasProjectNow = currentConfig?.config.project !== undefined;
4288
4341
  console.log(`
4289
- ` + "=".repeat(50));
4342
+ ${"=".repeat(50)}`);
4290
4343
  console.log("MCP Server Setup");
4291
- console.log("=".repeat(50) + `
4344
+ console.log(`${"=".repeat(50)}
4292
4345
  `);
4293
4346
  await mcpInit({
4294
4347
  fileSystemService: deps.fileSystemService,
@@ -4304,7 +4357,7 @@ ${highlight("✓", useColors)} MCP setup complete!
4304
4357
  }
4305
4358
  console.log("=".repeat(50));
4306
4359
  console.log("Setup Complete!");
4307
- console.log("=".repeat(50) + `
4360
+ console.log(`${"=".repeat(50)}
4308
4361
  `);
4309
4362
  if (setupProject) {
4310
4363
  const finalConfig = await deps.configService.loadProjectConfig();
@@ -4345,8 +4398,7 @@ function showCliUsage(useColors) {
4345
4398
  console.log(` ${highlight("pkgseer docs get <pkg>/<page>", useColors)} Fetch doc content`);
4346
4399
  console.log(` ${highlight("pkgseer docs search <query>", useColors)} Search documentation
4347
4400
  `);
4348
- console.log(dim(`All commands support --json for structured output.
4349
- ` + "Tip: Run 'pkgseer quickstart' for a quick reference guide.", useColors));
4401
+ console.log(dim("All commands support --json for structured output.", useColors));
4350
4402
  }
4351
4403
  function registerInitCommand(program) {
4352
4404
  program.command("init").summary("Set up project and AI integration").description(`Set up PkgSeer for your project.
@@ -4415,9 +4467,11 @@ async function loginAction(options, deps) {
4415
4467
  let callback;
4416
4468
  try {
4417
4469
  callback = await Promise.race([serverPromise, timeoutPromise]);
4418
- clearTimeout(timeoutId);
4470
+ if (timeoutId)
4471
+ clearTimeout(timeoutId);
4419
4472
  } catch (error2) {
4420
- clearTimeout(timeoutId);
4473
+ if (timeoutId)
4474
+ clearTimeout(timeoutId);
4421
4475
  if (error2 instanceof Error) {
4422
4476
  console.log(`${error2.message}.
4423
4477
  `);
@@ -4534,12 +4588,13 @@ function toGraphQLRegistry2(registry) {
4534
4588
  const map = {
4535
4589
  npm: "NPM",
4536
4590
  pypi: "PYPI",
4537
- hex: "HEX"
4591
+ hex: "HEX",
4592
+ crates: "CRATES"
4538
4593
  };
4539
4594
  return map[registry.toLowerCase()] || "NPM";
4540
4595
  }
4541
4596
  var schemas = {
4542
- registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
4597
+ registry: z2.enum(["npm", "pypi", "hex", "crates"]).describe("Package registry (npm, pypi, hex, or crates)"),
4543
4598
  packageName: z2.string().max(255).describe("Name of the package"),
4544
4599
  version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
4545
4600
  };
@@ -4582,7 +4637,7 @@ function notFoundError(packageName, registry) {
4582
4637
 
4583
4638
  // src/tools/compare-packages.ts
4584
4639
  var packageInputSchema = z3.object({
4585
- registry: z3.enum(["npm", "pypi", "hex"]),
4640
+ registry: z3.enum(["npm", "pypi", "hex", "crates"]),
4586
4641
  name: z3.string().max(255),
4587
4642
  version: z3.string().max(100).optional()
4588
4643
  });
@@ -4592,7 +4647,7 @@ var argsSchema = {
4592
4647
  function createComparePackagesTool(pkgseerService) {
4593
4648
  return {
4594
4649
  name: "compare_packages",
4595
- 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"}]',
4650
+ 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"}]',
4596
4651
  schema: argsSchema,
4597
4652
  handler: async ({ packages }, _extra) => {
4598
4653
  return withErrorHandling("compare packages", async () => {
@@ -5125,7 +5180,7 @@ function showMcpSetupInstructions(deps) {
5125
5180
  console.log(` ${highlight(`${deps.baseUrl}/docs/mcp-server`, useColors)}
5126
5181
  `);
5127
5182
  console.log("Alternative: Use CLI directly (no MCP setup needed)");
5128
- console.log(` ${highlight("pkgseer quickstart", useColors)}`);
5183
+ console.log(` ${highlight("pkgseer pkg info <package>", useColors)}`);
5129
5184
  }
5130
5185
  function registerMcpCommand(program) {
5131
5186
  const mcpCommand = program.command("mcp").summary("Show setup instructions or start MCP server").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
@@ -5155,27 +5210,6 @@ in MCP configuration files. Use 'pkgseer mcp' for interactive setup.`).action(as
5155
5210
  }
5156
5211
 
5157
5212
  // src/commands/pkg/compare.ts
5158
- function parsePackageSpec(spec) {
5159
- let registry = "npm";
5160
- let rest = spec;
5161
- if (spec.includes(":")) {
5162
- const colonIndex = spec.indexOf(":");
5163
- const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
5164
- if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
5165
- registry = potentialRegistry;
5166
- rest = spec.slice(colonIndex + 1);
5167
- }
5168
- }
5169
- const atIndex = rest.lastIndexOf("@");
5170
- if (atIndex > 0) {
5171
- return {
5172
- registry,
5173
- name: rest.slice(0, atIndex),
5174
- version: rest.slice(atIndex + 1)
5175
- };
5176
- }
5177
- return { registry, name: rest };
5178
- }
5179
5213
  function formatPackageComparison(comparison) {
5180
5214
  const lines = [];
5181
5215
  lines.push("\uD83D\uDCCA Package Comparison");
@@ -5228,10 +5262,10 @@ async function pkgCompareAction(packages, options, deps) {
5228
5262
  if (options.json) {
5229
5263
  const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
5230
5264
  const slim = pkgs.map((p) => ({
5231
- package: `${p.packageName}@${p.version}`,
5232
- quality: p.quality?.score,
5233
- downloads: p.downloadsLastMonth,
5234
- vulnerabilities: p.vulnerabilityCount
5265
+ package: `${p?.packageName}@${p?.version}`,
5266
+ quality: p?.quality?.score,
5267
+ downloads: p?.downloadsLastMonth,
5268
+ vulnerabilities: p?.vulnerabilityCount
5235
5269
  }));
5236
5270
  output(slim, true);
5237
5271
  } else {
@@ -5296,14 +5330,16 @@ function formatPackageDependencies(data, transitiveRequested) {
5296
5330
  return lines.join(`
5297
5331
  `);
5298
5332
  }
5299
- async function pkgDepsAction(packageName, options, deps) {
5333
+ async function pkgDepsAction(packageArg, options, deps) {
5300
5334
  const { pkgseerService } = deps;
5301
- const registry = toGraphQLRegistry(options.registry);
5335
+ const parsed = parsePackageSpec(packageArg);
5336
+ const registry = toGraphQLRegistry(parsed.registry !== "npm" ? parsed.registry : options.registry);
5337
+ const version2 = parsed.version ?? options.pkgVersion;
5302
5338
  const transitiveRequested = options.transitive ?? false;
5303
- const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive, options.maxDepth ? Number.parseInt(options.maxDepth, 10) : undefined);
5339
+ const result = await pkgseerService.cliPackageDeps(registry, parsed.name, version2, options.transitive, options.maxDepth ? Number.parseInt(options.maxDepth, 10) : undefined);
5304
5340
  handleErrors(result.errors, options.json ?? false);
5305
5341
  if (!result.data.packageDependencies) {
5306
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
5342
+ outputError(`Package not found: ${parsed.name} in ${parsed.registry}`, options.json ?? false);
5307
5343
  return;
5308
5344
  }
5309
5345
  const format = options.json ? "json" : options.format ?? "human";
@@ -5342,12 +5378,14 @@ var DEPS_DESCRIPTION = `Get package dependencies.
5342
5378
  Lists direct dependencies and shows version constraints and
5343
5379
  dependency types (runtime, dev, optional).
5344
5380
 
5381
+ Package format: [registry:]name[@version]
5382
+
5345
5383
  Examples:
5346
5384
  pkgseer pkg deps express
5347
- pkgseer pkg deps lodash --transitive
5348
- pkgseer pkg deps requests --registry pypi --json`;
5385
+ pkgseer pkg deps lodash@4.17.21 --transitive
5386
+ pkgseer pkg deps pypi:requests@2.28.0 --json`;
5349
5387
  function registerPkgDepsCommand(program) {
5350
- program.command("deps <package>").summary("Get package dependencies").description(DEPS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("-t, --transitive", "Include transitive dependencies").option("--max-depth <n>", "Maximum transitive depth (1-10, defaults to server)").option("--format <format>", "Output format: human|summary|json", "human").option("--json", "Output as JSON").action(async (packageName, options) => {
5388
+ program.command("deps <package>").summary("Get package dependencies").description(DEPS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-V, --pkg-version <version>", "Package version").option("-t, --transitive", "Include transitive dependencies").option("--max-depth <n>", "Maximum transitive depth (1-10, defaults to server)").option("--format <format>", "Output format: human|summary|json", "human").option("--json", "Output as JSON").action(async (packageName, options) => {
5351
5389
  await withCliErrorHandling(options.json ?? false, async () => {
5352
5390
  const deps = await createContainer();
5353
5391
  await pkgDepsAction(packageName, options, deps);
@@ -5355,6 +5393,15 @@ function registerPkgDepsCommand(program) {
5355
5393
  });
5356
5394
  }
5357
5395
  // src/commands/pkg/info.ts
5396
+ function formatDownloads(count) {
5397
+ if (count >= 1e6) {
5398
+ return `${(count / 1e6).toFixed(1)}M`;
5399
+ }
5400
+ if (count >= 1000) {
5401
+ return `${(count / 1000).toFixed(1)}K`;
5402
+ }
5403
+ return count.toLocaleString();
5404
+ }
5358
5405
  function formatPackageSummary(data) {
5359
5406
  const lines = [];
5360
5407
  const pkg = data.package;
@@ -5374,6 +5421,11 @@ function formatPackageSummary(data) {
5374
5421
  ["License", pkg.license]
5375
5422
  ]));
5376
5423
  lines.push("");
5424
+ if (pkg.downloadsLastMonth != null && pkg.downloadsLastMonth > 0) {
5425
+ lines.push("Downloads:");
5426
+ lines.push(keyValueTable([["Last Month", formatDownloads(pkg.downloadsLastMonth)]]));
5427
+ lines.push("");
5428
+ }
5377
5429
  if (pkg.homepage || pkg.repositoryUrl) {
5378
5430
  lines.push("Links:");
5379
5431
  const links = [];
@@ -5396,13 +5448,14 @@ function formatPackageSummary(data) {
5396
5448
  return lines.join(`
5397
5449
  `);
5398
5450
  }
5399
- async function pkgInfoAction(packageName, options, deps) {
5451
+ async function pkgInfoAction(packageArg, options, deps) {
5400
5452
  const { pkgseerService } = deps;
5401
- const registry = toGraphQLRegistry(options.registry);
5402
- const result = await pkgseerService.cliPackageInfo(registry, packageName);
5453
+ const parsed = parsePackageSpec(packageArg);
5454
+ const registry = toGraphQLRegistry(parsed.registry !== "npm" ? parsed.registry : options.registry);
5455
+ const result = await pkgseerService.cliPackageInfo(registry, parsed.name);
5403
5456
  handleErrors(result.errors, options.json ?? false);
5404
5457
  if (!result.data.packageSummary) {
5405
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
5458
+ outputError(`Package not found: ${parsed.name} in ${parsed.registry}`, options.json ?? false);
5406
5459
  return;
5407
5460
  }
5408
5461
  if (options.json) {
@@ -5423,16 +5476,18 @@ async function pkgInfoAction(packageName, options, deps) {
5423
5476
  }
5424
5477
  var INFO_DESCRIPTION = `Get package summary and metadata.
5425
5478
 
5426
- Displays comprehensive information about a package including:
5479
+ Displays information about a package's latest version including:
5427
5480
  - Basic metadata (version, license, description)
5428
5481
  - Download statistics
5429
- - Security advisories
5430
5482
  - Quick start instructions
5431
5483
 
5484
+ Note: This command shows the latest version only.
5485
+ For version-specific info, use 'pkg vulns', 'pkg quality', or 'pkg deps'.
5486
+
5432
5487
  Examples:
5433
5488
  pkgseer pkg info lodash
5434
- pkgseer pkg info requests --registry pypi
5435
- pkgseer pkg info phoenix --registry hex --json`;
5489
+ pkgseer pkg info pypi:requests
5490
+ pkgseer pkg info hex:phoenix --json`;
5436
5491
  function registerPkgInfoCommand(program) {
5437
5492
  program.command("info <package>").summary("Get package summary and metadata").description(INFO_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("--json", "Output as JSON").action(async (packageName, options) => {
5438
5493
  await withCliErrorHandling(options.json ?? false, async () => {
@@ -5460,13 +5515,15 @@ function formatPackageQuality(data) {
5460
5515
  return lines.join(`
5461
5516
  `);
5462
5517
  }
5463
- async function pkgQualityAction(packageName, options, deps) {
5518
+ async function pkgQualityAction(packageArg, options, deps) {
5464
5519
  const { pkgseerService } = deps;
5465
- const registry = toGraphQLRegistry(options.registry);
5466
- const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
5520
+ const parsed = parsePackageSpec(packageArg);
5521
+ const registry = toGraphQLRegistry(parsed.registry !== "npm" ? parsed.registry : options.registry);
5522
+ const version2 = parsed.version ?? options.pkgVersion;
5523
+ const result = await pkgseerService.cliPackageQuality(registry, parsed.name, version2);
5467
5524
  handleErrors(result.errors, options.json ?? false);
5468
5525
  if (!result.data.packageQuality) {
5469
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
5526
+ outputError(`Package not found: ${parsed.name} in ${parsed.registry}`, options.json ?? false);
5470
5527
  return;
5471
5528
  }
5472
5529
  const format = options.json ? "json" : options.format ?? "human";
@@ -5477,8 +5534,8 @@ async function pkgQualityAction(packageName, options, deps) {
5477
5534
  score: quality?.overallScore,
5478
5535
  grade: quality?.grade,
5479
5536
  categories: quality?.categories?.filter((c) => c).map((c) => ({
5480
- name: c.category,
5481
- score: c.score
5537
+ name: c?.category,
5538
+ score: c?.score
5482
5539
  }))
5483
5540
  };
5484
5541
  output(slim, true);
@@ -5494,12 +5551,14 @@ Analyzes package quality across multiple dimensions:
5494
5551
  - Security practices
5495
5552
  - Community engagement
5496
5553
 
5554
+ Package format: [registry:]name[@version]
5555
+
5497
5556
  Examples:
5498
5557
  pkgseer pkg quality lodash
5499
- pkgseer pkg quality express -v 4.18.0
5500
- pkgseer pkg quality requests --registry pypi --json`;
5558
+ pkgseer pkg quality express@4.18.0
5559
+ pkgseer pkg quality pypi:requests@2.28.0 --json`;
5501
5560
  function registerPkgQualityCommand(program) {
5502
- program.command("quality <package>").summary("Get package quality score").description(QUALITY_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--format <format>", "Output format: human|summary|json", "human").option("--json", "Output as JSON").action(async (packageName, options) => {
5561
+ program.command("quality <package>").summary("Get package quality score").description(QUALITY_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-V, --pkg-version <version>", "Package version").option("--format <format>", "Output format: human|summary|json", "human").option("--json", "Output as JSON").action(async (packageName, options) => {
5503
5562
  await withCliErrorHandling(options.json ?? false, async () => {
5504
5563
  const deps = await createContainer();
5505
5564
  await pkgQualityAction(packageName, options, deps);
@@ -5559,13 +5618,15 @@ function formatPackageVulnerabilities(data) {
5559
5618
  return lines.join(`
5560
5619
  `);
5561
5620
  }
5562
- async function pkgVulnsAction(packageName, options, deps) {
5621
+ async function pkgVulnsAction(packageArg, options, deps) {
5563
5622
  const { pkgseerService } = deps;
5564
- const registry = toGraphQLRegistry(options.registry);
5565
- const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
5623
+ const parsed = parsePackageSpec(packageArg);
5624
+ const registry = toGraphQLRegistry(parsed.registry !== "npm" ? parsed.registry : options.registry);
5625
+ const version2 = parsed.version ?? options.pkgVersion;
5626
+ const result = await pkgseerService.cliPackageVulns(registry, parsed.name, version2);
5566
5627
  handleErrors(result.errors, options.json ?? false);
5567
5628
  if (!result.data.packageVulnerabilities) {
5568
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
5629
+ outputError(`Package not found: ${parsed.name} in ${parsed.registry}`, options.json ?? false);
5569
5630
  return;
5570
5631
  }
5571
5632
  if (options.json) {
@@ -5575,10 +5636,10 @@ async function pkgVulnsAction(packageName, options, deps) {
5575
5636
  package: `${data.package?.name}@${data.package?.version}`,
5576
5637
  count: data.security?.vulnerabilityCount ?? 0,
5577
5638
  vulnerabilities: vulns.filter((v) => v).map((v) => ({
5578
- id: v.osvId,
5579
- severity: v.severityScore,
5580
- summary: v.summary,
5581
- fixed: v.fixedInVersions
5639
+ id: v?.osvId,
5640
+ severity: v?.severityScore,
5641
+ summary: v?.summary,
5642
+ fixed: v?.fixedInVersions
5582
5643
  }))
5583
5644
  };
5584
5645
  output(slim, true);
@@ -5594,12 +5655,14 @@ Scans for known security vulnerabilities and provides:
5594
5655
  - Affected version ranges
5595
5656
  - Upgrade recommendations
5596
5657
 
5658
+ Package format: [registry:]name[@version]
5659
+
5597
5660
  Examples:
5598
5661
  pkgseer pkg vulns lodash
5599
- pkgseer pkg vulns express -v 4.17.0
5600
- pkgseer pkg vulns requests --registry pypi --json`;
5662
+ pkgseer pkg vulns express@4.17.0
5663
+ pkgseer pkg vulns pypi:requests@2.28.0 --json`;
5601
5664
  function registerPkgVulnsCommand(program) {
5602
- program.command("vulns <package>").summary("Check for security vulnerabilities").description(VULNS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
5665
+ program.command("vulns <package>").summary("Check for security vulnerabilities").description(VULNS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-V, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
5603
5666
  await withCliErrorHandling(options.json ?? false, async () => {
5604
5667
  const deps = await createContainer();
5605
5668
  await pkgVulnsAction(packageName, options, deps);
@@ -5617,7 +5680,7 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5617
5680
  });
5618
5681
  }
5619
5682
  }
5620
- const labelToFiles = new Map;
5683
+ const _labelToFiles = new Map;
5621
5684
  const labelToConfig = new Map;
5622
5685
  for (const detectedGroup of detectedGroups) {
5623
5686
  for (const manifest of detectedGroup.manifests) {
@@ -5630,13 +5693,14 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5630
5693
  label = existingConfig?.label ?? detectedGroup.label;
5631
5694
  }
5632
5695
  const allowMixDeps = existingConfig?.allow_mix_deps;
5633
- if (!labelToConfig.has(label)) {
5634
- labelToConfig.set(label, {
5696
+ let config = labelToConfig.get(label);
5697
+ if (!config) {
5698
+ config = {
5635
5699
  files: new Set,
5636
5700
  allow_mix_deps: allowMixDeps
5637
- });
5701
+ };
5702
+ labelToConfig.set(label, config);
5638
5703
  }
5639
- const config = labelToConfig.get(label);
5640
5704
  config.files.add(manifest.relativePath);
5641
5705
  if (allowMixDeps) {
5642
5706
  config.allow_mix_deps = true;
@@ -5673,7 +5737,7 @@ function matchManifestsWithConfig(detectedGroups, existingManifests) {
5673
5737
  });
5674
5738
  }
5675
5739
  async function projectDetectAction(options, deps) {
5676
- const { configService, fileSystemService, promptService, shellService } = deps;
5740
+ const { configService, fileSystemService, promptService } = deps;
5677
5741
  const projectConfig = await configService.loadProjectConfig();
5678
5742
  if (!projectConfig?.config.project) {
5679
5743
  console.error(`✗ No project is configured in pkgseer.yml`);
@@ -5730,7 +5794,7 @@ Suggested configuration:`);
5730
5794
  console.log(`${prefix}${file}`);
5731
5795
  }
5732
5796
  }
5733
- const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
5797
+ const _hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
5734
5798
  const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
5735
5799
  let allowMixDeps = false;
5736
5800
  if (hasHexInSuggested) {
@@ -6038,6 +6102,9 @@ Getting started:
6038
6102
  pkgseer login Authenticate with your account
6039
6103
  pkgseer skill init Install AI agent skill
6040
6104
 
6105
+ Search:
6106
+ pkgseer search <query> -P <packages> Search code and docs
6107
+
6041
6108
  Package commands:
6042
6109
  pkgseer pkg info <package> Get package summary
6043
6110
  pkgseer pkg vulns <package> Check vulnerabilities
@@ -6048,7 +6115,7 @@ Package commands:
6048
6115
  Documentation commands:
6049
6116
  pkgseer docs list <package> List doc pages
6050
6117
  pkgseer docs get <pkg> <page> Fetch a doc page
6051
- pkgseer docs search <query> Search documentation
6118
+ pkgseer docs search <query> Search docs only
6052
6119
 
6053
6120
  Learn more at https://pkgseer.dev`);
6054
6121
  registerInitCommand(program);
@@ -6056,6 +6123,7 @@ registerMcpCommand(program);
6056
6123
  registerSkillCommand(program);
6057
6124
  registerLoginCommand(program);
6058
6125
  registerLogoutCommand(program);
6126
+ registerSearchCommand(program);
6059
6127
  var auth = program.command("auth").description("View and manage authentication");
6060
6128
  registerAuthStatusCommand(auth);
6061
6129
  var config = program.command("config").description("View and manage configuration");
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "./shared/chunk-zaq9c2d8.js";
3
+ } from "./shared/chunk-drz16bhv.js";
4
4
  export {
5
5
  version
6
6
  };
@@ -1,4 +1,4 @@
1
1
  // package.json
2
- var version = "0.2.3";
2
+ var version = "0.2.5";
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.3",
4
+ "version": "0.2.5",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",