zmarketplace 0.4.7 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,84 +1,116 @@
1
1
  # zmarketplace
2
2
 
3
- Cross-agent marketplace search plugin. Type `/zmarketplace` in pi/omp to search, inspect, audit, and install packages across npm, Claude marketplace, and Gemini extensions.
3
+ Cross-agent package marketplace search. One `/zmarketplace` command searches npm, Claude marketplace, Gemini extensions, and the official MCP registry then browse, audit, and install.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- # pi / omp (slash command)
9
8
  pi install npm:zmarketplace
10
- # or link locally:
11
- omp plugin link ./search_marketplace
12
-
13
- # then in omp TUI:
14
- /reload
15
- /zmarketplace search mcp
16
9
  ```
17
10
 
18
- ## Usage
19
-
20
- ```
21
- /zmarketplace search <query> [--type=<type>] [--eco=<ecosystem>] [--limit=<n>]
22
- /zmarketplace detail <id|name>
23
- /zmarketplace audit <id|name>
24
- /zmarketplace install <id|name>
11
+ Or omp:
12
+ ```bash
13
+ omp plugin install npm:zmarketplace
25
14
  ```
26
15
 
27
- ### Search
16
+ Then `/reload` and type `/zmarketplace`.
17
+
18
+ ## Quick Start
28
19
 
29
20
  ```
30
- /zmarketplace search mcp
31
- /zmarketplace search subagent --eco=pi
32
- /zmarketplace search theme --type=theme --limit=10
21
+ /zmarketplace
22
+ → Choose limit: 25 / 50 / 150 / All (paged)
23
+ Type query: "mcp"
24
+ → Browse 50 results per page (← Previous / → Next)
25
+ → Enter on package → detail + README
26
+ → Enter on ⬇ Install → choose ecosystem → command at bottom
33
27
  ```
34
28
 
35
- Opens an interactive select list. Scroll up/down, type to filter, enter to pick.
29
+ ## Commands
36
30
 
37
- ### Detail + README
31
+ | Command | What |
32
+ |---|---|
33
+ | `/zmarketplace` | Interactive search prompt |
34
+ | `/zmarketplace search <query>` | Search directly |
35
+ | `/zmarketplace search mcp --eco=pi` | Filter by ecosystem |
36
+ | `/zmarketplace search theme --type=theme` | Filter by type |
37
+ | `/zmarketplace detail <name>` | Show package detail |
38
+ | `/zmarketplace audit <name>` | Security scan |
39
+ | `/zmarketplace install <name>` | Install flow |
38
40
 
39
- Pick a package → select "📋 Details + README" → full README in a scrollable list.
41
+ ## Detail View
40
42
 
41
- - **Enter on a URL line** (🖼 IMAGE, 🔗 link) → opens in your browser
42
- - **Enter on text** → stays in detail view
43
- - **Type to search** → filters README lines
44
- - **ESC or "Back to menu"** → returns to action menu
43
+ ```
44
+ learnship Details
45
+
46
+ Install (audit first)
47
+ 🔒 Audit only
48
+ ↩ Back to results
49
+ 📦 learnship v2.4.0 — MIT · 0 deps · 1296KB
50
+ Learn as you build. Build with intent.
51
+ 🔗 https://www.npmjs.com/package/learnship
52
+ 🔗 https://github.com/FavioVazquez/learnship
53
+ ━━━ README ━━━
54
+ # learnship
55
+ ...40 lines of README...
56
+ ...(see npm for full README)
57
+ ✅ Run: pi install npm:learnship
58
+ ```
59
+
60
+ - **Actions at top** — Install / Audit / Back
61
+ - **README below** — 40 lines, scrollable
62
+ - **Enter on 🔗 or 🖼** — opens URL in browser
63
+ - **Enter on text** — stays in detail
64
+ - **Selected command at bottom** — after picking install option
45
65
 
46
- ### Install
66
+ ## Install Options
47
67
 
48
- Pick "⬇ Install" choose ecosystem:
68
+ After audit, choose how to install:
49
69
 
50
70
  | Option | Command |
51
71
  |---|---|
52
- | 🥧 pi install | `pi install npm:<name>` |
53
- | ⌥ omp install | `omp plugin install npm:<name>` |
54
- | 🤖 claude install | `claude plugin install npm:<name>` |
55
- | 🔓 opencode install | `opencode plugin <name>` |
56
- | 💎 gemini install | `gemini extension install <url>` |
57
- | 🔲 codex install | `codex plugin add npm:<name>` |
58
- | 📦 npm install | `npm install <name>` |
72
+ | 🥧 pi | `pi install npm:<name>` |
73
+ | ⌥ omp | `omp plugin install npm:<name>` |
74
+ | 🤖 claude | `claude plugin install npm:<name>` |
75
+ | 🔓 opencode | `opencode plugin <name>` |
76
+ | 💎 gemini | `gemini extension install <url>` |
77
+ | 🔲 codex | `codex plugin add npm:<name>` |
78
+ | 📦 npm | `npm install <name>` |
79
+ | 🟤 bun | `bun add <name>` |
80
+ | 📦 pnpm | `pnpm add <name>` |
59
81
  | ⚡ bunx | `bunx <name>` |
60
82
 
61
- Quick security check runs before showing the command. High-risk packages require confirmation.
83
+ High-risk packages require confirmation before showing the command.
62
84
 
63
- ### Audit
85
+ ## Security Audit
64
86
 
65
- ```
66
- /zmarketplace audit pi-mcp-adapter
67
- ```
87
+ | Layer | What it checks |
88
+ |---|---|
89
+ | Metadata | Dependency count, file count, size, license |
90
+ | Source scan | Downloads tarball, scans `.ts`/`.js` for `eval()`, `execSync()`, `rm -rf`, `child_process`, `process.env`, HTTP calls |
68
91
 
69
- Two-layer security scan:
70
- - **Layer 1 (metadata):** dependency count, file count, size, license
71
- - **Layer 2 (source):** downloads tarball, scans `.ts`/`.js` for dangerous patterns (`rm -rf`, `eval()`, `execSync()`, `child_process`, etc.)
92
+ Severity levels: 🔴 critical · 🟠 high · 🟡 medium · 🟢 low
72
93
 
73
94
  ## Registries
74
95
 
75
- | Source | Coverage |
96
+ | Registry | Coverage |
76
97
  |---|---|
77
- | npm | `pi-package`, `claude-code`, `opencode`, `gemini-cli`, `codex` keywords |
78
- | Claude marketplace | `anthropics/claude-plugins-official` + community (~800+ plugins) |
79
- | Gemini extensions | `geminicli.com/extensions.json` (~993 extensions) |
98
+ | npm | `pi-package`, `claude-code`, `opencode`, `gemini-cli`, `codex`, `npm`, `bun`, `pnpm` keywords |
99
+ | Claude marketplace | Official + community (~800+ plugins) |
100
+ | Gemini extensions | geminicli.com (~993 extensions) |
101
+ | MCP registry | registry.modelcontextprotocol.io (official) |
102
+
103
+ All queried in parallel, deduplicated, ranked by relevance.
104
+
105
+ ## Filters
106
+
107
+ ```
108
+ --type=extension extension, skill, theme, prompt, plugin, mcp
109
+ --eco=pi pi, claude, opencode, gemini, codex, npm
110
+ --limit=50 25, 50, 150, All (paged at 50/page)
111
+ ```
80
112
 
81
- ## CLI (standalone, no agent needed)
113
+ ## CLI (standalone)
82
114
 
83
115
  ```bash
84
116
  bunx zmarketplace search "mcp" --limit=5
@@ -87,40 +119,44 @@ bunx zmarketplace audit pi-marketplace
87
119
  bunx zmarketplace install pi-marketplace
88
120
  ```
89
121
 
122
+ Works without any agent. Same core, different interface.
123
+
90
124
  ## Architecture
91
125
 
92
126
  ```
93
127
  src/
94
- ├── index.tspi/omp extension: registerCommand("/zmarketplace")
95
- ├── cli.tsstandalone CLI: bunx zmarketplace
96
- ├── opencode.tsopencode plugin entry
128
+ ├── index.ts pi/omp extension (registerCommand)
129
+ ├── cli.ts standalone CLI (bunx zmarketplace)
130
+ ├── opencode.ts opencode plugin entry
97
131
  ├── core/
98
- │ ├── types.tsunified package model
99
- │ ├── search.tscross-registry search + dedup + ranking
100
- │ ├── detail.tsnpm metadata + README fetch
101
- │ ├── audit.ts2-layer security scanner
102
- │ ├── install.tsagent detection + install command dispatch
103
- │ ├── cache.tsresults cache for ID-based reference
104
- │ └── tui.tsicons, formatting, arg parser
132
+ │ ├── types.ts unified package model + ecosystem keywords
133
+ │ ├── search.ts cross-registry search + dedup + ranking
134
+ │ ├── detail.ts npm metadata + README fetch
135
+ │ ├── audit.ts 2-layer security scanner
136
+ │ ├── install.ts agent detection + command dispatch
137
+ │ ├── cache.ts results cache for ID reference
138
+ │ └── tui.ts icons, formatting, arg parser, help text
105
139
  └── registries/
106
- ├── npm.tsnpm registry (parallel per-keyword queries)
107
- ├── claude.tsClaude marketplace JSON
108
- └── gemini.tsGemini CLI extensions registry
140
+ ├── npm.ts npm registry (parallel per-keyword queries)
141
+ ├── claude.ts Claude marketplace JSON
142
+ ├── gemini.ts Gemini CLI extensions registry
143
+ └── mcp.ts Official MCP registry
109
144
  ```
110
145
 
111
- ## Cross-agent manifests
146
+ ## Compatibility
112
147
 
113
- | File | Ecosystem |
114
- |---|---|
115
- | `package.json` (`pi.extensions`, `omp.extensions`) | Pi, OMP |
116
- | `package.json` (`bin`, `exports`) | CLI, OpenCode |
117
- | `.claude-plugin/plugin.json` | Claude Code |
118
- | `gemini-extension.json` | Gemini CLI |
119
- | `.codex-plugin/plugin.json` | Codex |
148
+ | Agent | Status | How |
149
+ |---|---|---|
150
+ | pi | ✅ Works | `pi install npm:zmarketplace` |
151
+ | omp | Works | `omp plugin install npm:zmarketplace` |
152
+ | OpenCode | Plugin entry | `opencode plugin zmarketplace` |
153
+ | Claude Code | CLI | `bunx zmarketplace search "mcp"` |
154
+ | Gemini CLI | CLI | `bunx zmarketplace search "mcp"` |
155
+ | Codex | ✅ CLI | `bunx zmarketplace search "mcp"` |
120
156
 
121
- ## Support
157
+ Zero runtime dependencies. TypeScript with Bun. No Bun-specific APIs (works on pi's jiti/Node loader).
122
158
 
123
- If this plugin helped you, consider supporting:
159
+ ## Support
124
160
 
125
161
  ☕ [ko-fi.com/zicodev](https://ko-fi.com/zicodev)
126
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zmarketplace",
3
- "version": "0.4.7",
3
+ "version": "0.7.2",
4
4
  "description": "Cross-agent marketplace search: find, audit, and install plugins/skills/themes/prompts across pi, omp, claude code, opencode, gemini cli, and codex",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,7 @@
20
20
  "opencode",
21
21
  "gemini-cli",
22
22
  "codex",
23
+ "npm",
23
24
  "marketplace",
24
25
  "plugin",
25
26
  "search",
@@ -31,8 +31,9 @@ function buildCommand(packageName: string, target: InstallTarget): string {
31
31
  case "claude": return `claude plugin install npm:${packageName}`;
32
32
  case "opencode": return `opencode plugin ${packageName}`;
33
33
  case "gemini": return `gemini extension install npm:${packageName}`;
34
- case "codex": return `codex plugin add npm:${packageName}`;
35
- case "auto": return `npm install ${packageName}`;
34
+ case "npm":
35
+ case "auto":
36
+ default: return `npm install ${packageName}`;
36
37
  }
37
38
  }
38
39
 
@@ -49,6 +50,7 @@ async function resolveAutoTarget(packageName: string): Promise<InstallTarget> {
49
50
  if (kws.has("opencode")) return "opencode";
50
51
  if (kws.has("gemini-cli")) return "gemini";
51
52
  if (kws.has("codex")) return "codex";
53
+ if (kws.has("npm") || kws.has("bun") || kws.has("nodejs")) return "npm";
52
54
  return "auto";
53
55
  }
54
56
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Installed packages detector — checks pi and omp for user-installed plugins.
3
+ * Only checks explicit plugin installs, NOT dependency node_modules.
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+
10
+ export interface InstalledPackage {
11
+ name: string;
12
+ version?: string;
13
+ source: "pi" | "omp";
14
+ }
15
+
16
+ let cache: InstalledPackage[] | null = null;
17
+ let cacheTime = 0;
18
+ const CACHE_TTL = 10_000;
19
+
20
+ /** Get user-installed packages from pi and omp (not dependencies). */
21
+ export function getInstalledPackages(): InstalledPackage[] {
22
+ const now = Date.now();
23
+ if (cache && now - cacheTime < CACHE_TTL) return cache;
24
+
25
+ const results: InstalledPackage[] = [];
26
+ const home = homedir();
27
+
28
+ // omp plugins — from lock file (only explicitly enabled plugins)
29
+ const ompLock = join(home, ".omp", "plugins", "omp-plugins.lock.json");
30
+ try {
31
+ if (existsSync(ompLock)) {
32
+ const data = JSON.parse(readFileSync(ompLock, "utf8")) as { plugins?: Record<string, { version?: string; enabled?: boolean }> };
33
+ for (const [name, info] of Object.entries(data.plugins ?? {})) {
34
+ if (info.enabled !== false) {
35
+ results.push({ name, version: info.version, source: "omp" });
36
+ }
37
+ }
38
+ }
39
+ } catch { /* ignore */ }
40
+
41
+ // pi packages — from settings.json packages array (NOT node_modules scan)
42
+ const piSettings = join(home, ".pi", "agent", "settings.json");
43
+ try {
44
+ if (existsSync(piSettings)) {
45
+ const data = JSON.parse(readFileSync(piSettings, "utf8")) as { packages?: string[]; extensions?: string[] };
46
+ for (const pkg of data.packages ?? []) {
47
+ // Format: "npm:@scope/name@version" or "npm:name" or "git:..."
48
+ const cleaned = pkg.replace(/^npm:/, "").replace(/^git:.*\//, "").replace(/@[^@]*$/, "");
49
+ if (cleaned) results.push({ name: cleaned, source: "pi" });
50
+ }
51
+ }
52
+ } catch { /* ignore */ }
53
+
54
+ // Deduplicate (keep first = prefer omp with version info)
55
+ const seen = new Set<string>();
56
+ const deduped = results.filter(r => {
57
+ if (seen.has(r.name)) return false;
58
+ seen.add(r.name);
59
+ return true;
60
+ });
61
+
62
+ cache = deduped;
63
+ cacheTime = now;
64
+ return deduped;
65
+ }
66
+
67
+ /** Check if a package name is installed (user plugin, not dependency). */
68
+ export function isInstalled(name: string): boolean {
69
+ return getInstalledPackages().some(p => p.name === name || p.name === name.replace(/^@[^/]+\//, ""));
70
+ }
71
+
72
+ /** Get installed version of a package, or undefined. */
73
+ export function getInstalledVersion(name: string): string | undefined {
74
+ return getInstalledPackages().find(p => p.name === name)?.version;
75
+ }
@@ -8,11 +8,13 @@ import { searchNpm, detectEcosystems } from "../registries/npm.ts";
8
8
  import { searchClaudeMarketplace } from "../registries/claude.ts";
9
9
  import { searchGeminiExtensions } from "../registries/gemini.ts";
10
10
  import { searchMcpRegistry } from "../registries/mcp.ts";
11
+ import { searchSmithery } from "../registries/smithery.ts";
12
+ import { searchGitHubTopics } from "../registries/github.ts";
11
13
 
12
14
  /** Determine which registries to query based on options. */
13
15
  function registriesForOptions(registry?: RegistrySource | "all"): RegistrySource[] {
14
16
  if (!registry || registry === "all") {
15
- return ["npm", "claude-marketplace", "gemini-extensions", "mcp-registry"];
17
+ return ["npm", "claude-marketplace", "gemini-extensions", "mcp-registry", "smithery", "github"];
16
18
  }
17
19
  return [registry];
18
20
  }
@@ -20,7 +22,6 @@ function registriesForOptions(registry?: RegistrySource | "all"): RegistrySource
20
22
  /** Map a user-facing ecosystem filter to npm ecosystem list for the npm adapter. */
21
23
  function ecosystemsForFilter(ecosystem?: Ecosystem | "all"): Ecosystem[] | undefined {
22
24
  if (!ecosystem || ecosystem === "all") return undefined;
23
- // pi and omp share the same npm keyword
24
25
  if (ecosystem === "omp") return ["pi"];
25
26
  return [ecosystem];
26
27
  }
@@ -83,6 +84,12 @@ export async function search(options: SearchOptions): Promise<PackageResult[]> {
83
84
  if (registries.includes("mcp-registry")) {
84
85
  queries.push(searchMcpRegistry(options.query, limit));
85
86
  }
87
+ if (registries.includes("smithery")) {
88
+ queries.push(searchSmithery(options.query, limit));
89
+ }
90
+ if (registries.includes("github")) {
91
+ queries.push(searchGitHubTopics(options.query, limit));
92
+ }
86
93
 
87
94
  const settled = await Promise.allSettled(queries);
88
95
  let results = settled
package/src/core/tui.ts CHANGED
@@ -24,6 +24,7 @@ const ECO_LABEL: Record<Ecosystem, string> = {
24
24
  opencode: "opencode",
25
25
  gemini: "gemini",
26
26
  codex: "codex",
27
+ npm: "npm",
27
28
  universal: "universal",
28
29
  unknown: "?",
29
30
  };
@@ -124,18 +125,20 @@ export function formatHelp(): string {
124
125
  "📦 zmarketplace — cross-agent package search",
125
126
  "",
126
127
  "Usage:",
127
- " /zmarketplace search <query> Search across all registries",
128
- " /zmarketplace detail <id|name> Show package details",
129
- " /zmarketplace audit <id|name> Run security audit",
130
- " /zmarketplace install <id|name> Audit + install a package",
128
+ " /zmarketplace Interactive search prompt",
129
+ " /zmarketplace search <query> Search across all registries",
130
+ " /zmarketplace popular Browse popular packages",
131
+ " /zmarketplace updates Check installed packages for updates",
132
+ " /zmarketplace detail <id|name> Show package details",
133
+ " /zmarketplace audit <id|name> Run security audit",
134
+ " /zmarketplace install <id|name> Audit + install a package",
131
135
  "",
132
136
  "Options for 'search':",
133
137
  " --type=<type> Filter: extension, skill, theme, prompt, plugin, mcp",
134
- " --eco=<ecosystem> Filter: pi, claude, opencode, gemini, codex",
138
+ " --eco=<ecosystem> Filter: pi, claude, opencode, gemini, codex, npm",
135
139
  " --limit=<n> Max results (default 20)",
136
140
  "",
137
- "Registries: npm + Claude marketplace + Gemini extensions",
138
- "",
141
+ "Registries: npm + Claude + Gemini + MCP + Smithery + GitHub",
139
142
  "Examples:",
140
143
  " /zmarketplace search mcp",
141
144
  " /zmarketplace search subagent --eco=pi",
package/src/core/types.ts CHANGED
@@ -3,13 +3,13 @@
3
3
  */
4
4
 
5
5
  /** Which agent ecosystem a package targets. */
6
- export type Ecosystem = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "universal" | "unknown";
6
+ export type Ecosystem = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "npm" | "universal" | "unknown";
7
7
 
8
8
  /** Package resource type. */
9
9
  export type PackageType = "extension" | "skill" | "theme" | "prompt" | "plugin" | "mcp" | "unknown";
10
10
 
11
11
  /** Which registry a result came from. */
12
- export type RegistrySource = "npm" | "claude-marketplace" | "gemini-extensions" | "pi-dev" | "mcp-registry";
12
+ export type RegistrySource = "npm" | "claude-marketplace" | "gemini-extensions" | "pi-dev" | "mcp-registry" | "smithery" | "github";
13
13
 
14
14
  /** A normalized search result — the common currency across all registries. */
15
15
  export interface PackageResult {
@@ -113,7 +113,7 @@ export interface AuditReport {
113
113
  }
114
114
 
115
115
  /** Install target — which agent's install command to use. */
116
- export type InstallTarget = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "auto";
116
+ export type InstallTarget = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "npm" | "auto";
117
117
 
118
118
  /** Result of an install operation. */
119
119
  export interface InstallResult {
@@ -132,4 +132,5 @@ export const ECOSYSTEM_KEYWORDS: Record<Exclude<Ecosystem, "unknown" | "universa
132
132
  opencode: ["opencode", "opencode-plugin"],
133
133
  gemini: ["gemini-cli", "gemini-extension", "gemini-cli-extension"],
134
134
  codex: ["codex", "codex-plugin", "codex-cli"],
135
+ npm: ["npm", "bun", "pnpm", "yarn", "nodejs", "deno"],
135
136
  };
package/src/index.ts CHANGED
@@ -10,6 +10,10 @@ import { auditPackage } from "./core/audit.ts";
10
10
  import { cacheResults, resolveRef, cacheAudit } from "./core/cache.ts";
11
11
  import { formatResultOption, formatAuditReport, formatHelp, parseArgs } from "./core/tui.ts";
12
12
  import type { PackageResult } from "./core/types.ts";
13
+ import { isInstalled, getInstalledVersion } from "./core/installed.ts";
14
+ import { getInstalledPackages } from "./core/installed.ts";
15
+ import { getNpmPackageMeta } from "./registries/npm.ts";
16
+ import { spawn } from "node:child_process";
13
17
 
14
18
  // ── Types ──────────────────────────────────────────────────────────────────
15
19
 
@@ -26,9 +30,9 @@ interface Ctx { cwd: string; hasUI: boolean; ui: UI }
26
30
  // ── Helpers ────────────────────────────────────────────────────────────────
27
31
 
28
32
  function openUrl(url: string): void {
29
- if (process.platform === "win32") Bun.spawn(["cmd", "/c", "start", "", url], { stdout: "ignore", stderr: "ignore" });
30
- else if (process.platform === "darwin") Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
31
- else Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
33
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
34
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
35
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
32
36
  }
33
37
 
34
38
  function extractUrl(line: string, repoBase?: string): string | null {
@@ -57,8 +61,10 @@ async function browseResults(results: PackageResult[], ctx: Ctx): Promise<void>
57
61
  while (true) {
58
62
  const pageEnd = Math.min(pageStart + PAGE_SIZE, results.length);
59
63
  const page = results.slice(pageStart, pageEnd);
60
-
61
- const options: string[] = page.map((r, i) => formatResultOption(r, pageStart + i).label);
64
+ const options: string[] = page.map((r, i) => {
65
+ const label = formatResultOption(r, pageStart + i).label;
66
+ return isInstalled(r.name) ? `✓ ${label}` : label;
67
+ });
62
68
 
63
69
  if (pageStart > 0) {
64
70
  const ps = Math.max(0, pageStart - PAGE_SIZE) + 1;
@@ -117,9 +123,13 @@ async function packageDetail(pkg: PackageResult, ctx: Ctx): Promise<void> {
117
123
 
118
124
  while (true) {
119
125
  const selected = await ctx.ui.select(`${detail.name} — Details`, lines);
120
- if (!selected) continue; // ESC → stay in detail, don't quit to results
126
+ if (!selected) continue;
121
127
  if (selected.includes("Back to results")) return;
122
- if (selected.includes("Install")) { await doInstall(pkg, ctx); continue; } // stay in detail after install
128
+ if (selected.includes("Install")) {
129
+ const cmd = await doInstall(pkg, ctx);
130
+ if (cmd) lines.push(`✅ Run: ${cmd}`); // show command at BOTTOM
131
+ continue;
132
+ }
123
133
  if (selected.includes("Audit only")) { await doAudit(pkg.name, ctx); continue; }
124
134
  const url = extractUrl(selected, repoBase);
125
135
  if (url) { openUrl(url); ctx.ui.notify("🌐 Opened in browser", "info"); }
@@ -145,7 +155,7 @@ async function doAudit(name: string, ctx: Ctx): Promise<void> {
145
155
 
146
156
  // ── Install (audit FIRST, then options) ────────────────────────────────────
147
157
 
148
- async function doInstall(pkg: PackageResult, ctx: Ctx): Promise<void> {
158
+ async function doInstall(pkg: PackageResult, ctx: Ctx): Promise<string | null> {
149
159
  // Audit first
150
160
  ctx.ui.setStatus?.(`Auditing ${pkg.name} before install...`);
151
161
  const report = await auditPackage(pkg.name, { deepScan: true });
@@ -176,6 +186,12 @@ async function doInstall(pkg: PackageResult, ctx: Ctx): Promise<void> {
176
186
  if (eco === "opencode" && !seen.has("opencode")) { seen.add("opencode"); addCmd("🔓 opencode", `opencode plugin ${pkg.name}`); }
177
187
  if (eco === "gemini" && !seen.has("gemini")) { seen.add("gemini"); addCmd("💎 gemini", `gemini extension install ${pkg.repository ?? pkg.name}`); }
178
188
  if (eco === "codex" && !seen.has("codex")) { seen.add("codex"); addCmd("🔲 codex", `codex plugin add npm:${pkg.name}`); }
189
+ if (eco === "npm" && !seen.has("npm")) {
190
+ seen.add("npm");
191
+ addCmd("📦 npm", `npm install ${pkg.name}`);
192
+ addCmd("🟤 bun add", `bun add ${pkg.name}`);
193
+ addCmd("📦 pnpm add", `pnpm add ${pkg.name}`);
194
+ }
179
195
  }
180
196
  if (!seen.has("npm")) addCmd("📦 npm", `npm install ${pkg.name}`);
181
197
  addCmd("⚡ bunx", `bunx ${pkg.name}`);
@@ -184,24 +200,99 @@ async function doInstall(pkg: PackageResult, ctx: Ctx): Promise<void> {
184
200
  // High risk confirmation
185
201
  if (report.risk === "critical" || report.risk === "high") {
186
202
  const proceed = await ctx.ui.confirm(`${report.risk} risk`, `⚠️ ${pkg.name}: ${report.findings.length} security findings. Install anyway?`);
187
- if (!proceed) { ctx.ui.notify("Cancelled.", "info"); return; }
203
+ if (!proceed) { ctx.ui.notify("Cancelled.", "info"); return null; }
188
204
  }
189
205
 
190
206
  const choice = await ctx.ui.select(`Install ${pkg.name} — Risk: ${report.risk}`, cmds);
191
- if (!choice || choice === "↩ Cancel") return;
207
+ if (!choice || choice === "↩ Cancel") return null;
192
208
  const command = cmdMap.get(choice);
193
- if (command) ctx.ui.notify(`✅ Run:\n ${command}`, "info");
209
+ if (!command) return null;
210
+
211
+ // Auto-install: confirm, then execute
212
+ const doAuto = await ctx.ui.confirm("Install now?", `Run: ${command}`);
213
+ if (!doAuto) {
214
+ ctx.ui.notify(`✅ Command:\n ${command}`, "info");
215
+ return command;
216
+ }
217
+
218
+ ctx.ui.setStatus?.(`Installing ${pkg.name}...`);
219
+ try {
220
+ const result = await new Promise<{ ok: boolean; out: string }>((resolve) => {
221
+ const proc = spawn(command, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
222
+ let out = "";
223
+ proc.stdout?.on("data", (d: Buffer) => { out += d.toString(); });
224
+ proc.stderr?.on("data", (d: Buffer) => { out += d.toString(); });
225
+ proc.on("close", (code: number | null) => resolve({ ok: code === 0, out }));
226
+ proc.on("error", () => resolve({ ok: false, out: "Failed to start" }));
227
+ });
228
+
229
+ if (result.ok) {
230
+ ctx.ui.notify(`✅ Installed ${pkg.name}!\nRun /reload to activate.`, "info");
231
+ } else {
232
+ ctx.ui.notify(`❌ Install failed:\n${result.out.slice(0, 500)}`, "warning");
233
+ ctx.ui.notify(`Manual command:\n ${command}`, "info");
234
+ }
235
+ } catch {
236
+ ctx.ui.notify(`Manual command:\n ${command}`, "info");
237
+ }
238
+ return command;
194
239
  }
195
240
 
196
241
  // ── Factory ────────────────────────────────────────────────────────────────
197
-
198
242
  // Command handler — extracted so it can be registered at the right time.
243
+
244
+ // ── Updates checker ────────────────────────────────────────────────────────
245
+
246
+ async function doUpdates(ctx: Ctx): Promise<void> {
247
+ ctx.ui.setStatus?.("Checking installed packages for updates...");
248
+ const installed = getInstalledPackages();
249
+ if (installed.length === 0) { ctx.ui.notify("No installed packages found.", "info"); return; }
250
+
251
+ const lines: string[] = [];
252
+ let updateCount = 0;
253
+
254
+ for (const pkg of installed) {
255
+ const meta = await getNpmPackageMeta(pkg.name);
256
+ if (!meta) continue;
257
+ const latest = meta["dist-tags"]?.latest;
258
+ if (!latest) continue;
259
+
260
+ if (pkg.version && pkg.version !== latest) {
261
+ lines.push(`⬆ ${pkg.name}: ${pkg.version} → ${latest} [${pkg.source}]`);
262
+ updateCount++;
263
+ } else if (!pkg.version) {
264
+ lines.push(`? ${pkg.name}: latest is ${latest} [${pkg.source}]`);
265
+ } else {
266
+ lines.push(`✓ ${pkg.name}: ${pkg.version} [${pkg.source}]`);
267
+ }
268
+ }
269
+
270
+ if (lines.length === 0) { ctx.ui.notify("No packages to check.", "info"); return; }
271
+ lines.push("↩ Back");
272
+
273
+ const title = updateCount > 0
274
+ ? `${updateCount} update(s) available (${installed.length} packages)`
275
+ : `All ${installed.length} packages up to date`;
276
+ await ctx.ui.select(title, lines);
277
+ }
278
+
279
+ // ── Popular packages ───────────────────────────────────────────────────────
280
+
281
+ async function doPopular(ctx: Ctx): Promise<void> {
282
+ ctx.ui.setStatus?.("Finding popular packages...");
283
+ const results = await search({ query: "", limit: 25, type: "all", ecosystem: "all" });
284
+ if (results.length === 0) { ctx.ui.notify("No packages found.", "info"); return; }
285
+ ctx.ui.notify(`Found ${results.length} popular packages.`, "info");
286
+ await browseResults(results, ctx);
287
+ }
199
288
  const commandDef = {
200
289
  description: "Search, audit, and install packages across agent ecosystems",
201
290
  handler: async (rawArgs: string, ctx: Ctx) => {
202
291
  const args = parseArgs(rawArgs.trim().split(/\s+/).filter(Boolean));
203
292
  switch (args.subcommand) {
204
293
  case "help": { ctx.ui.notify(formatHelp(), "info"); break; }
294
+ case "updates": { await doUpdates(ctx); break; }
295
+ case "popular": { await doPopular(ctx); break; }
205
296
  case "audit":
206
297
  case "a": {
207
298
  const ref = args.positional[0];
package/src/opencode.ts CHANGED
@@ -1,119 +1,104 @@
1
1
  /**
2
2
  * OpenCode plugin entry point.
3
3
  *
4
- * OpenCode plugins are npm packages with `exports["./server"]` pointing here.
5
- * They export a Plugin function that returns hooks.
4
+ * OpenCode plugins export functions that return hooks.
5
+ * This plugin provides a `zmarketplace` tool the agent can call.
6
6
  *
7
- * Install in opencode:
8
- * opencode plugin zmarketplace
7
+ * OpenCode does NOT support slash commands from plugins.
8
+ * For /zmarketplace, create a command file:
9
+ * ~/.config/opencode/commands/zmarketplace.md
9
10
  *
10
- * Or add to opencode.json:
11
- * { "plugin": ["zmarketplace"] }
11
+ * Install: add "zmarketplace" to opencode.json plugin array.
12
12
  */
13
13
 
14
14
  import { search } from "./core/search.ts";
15
15
  import { getDetail } from "./core/detail.ts";
16
16
  import { auditPackage } from "./core/audit.ts";
17
- import { installPackage } from "./core/install.ts";
18
17
  import type { SearchOptions } from "./core/types.ts";
19
18
 
20
- /**
21
- * Minimal OpenCode plugin type shape.
22
- * Not imported from @opencode-ai/plugin to keep zero deps.
23
- */
24
- interface OpenCodeToolContext {
19
+ interface ToolContext {
25
20
  sessionID?: string;
26
21
  messageID?: string;
27
22
  agent?: string;
28
23
  directory?: string;
24
+ worktree?: string;
29
25
  ask?(message: string): Promise<string>;
30
26
  }
31
27
 
32
- interface OpenCodePluginParams {
33
- project?: string;
34
- serverUrl?: string;
35
- // Bun shell `$`
36
- $?: unknown;
28
+ interface ToolDef {
29
+ description: string;
30
+ args: Record<string, unknown>;
31
+ execute(args: Record<string, unknown>, ctx: ToolContext): Promise<string>;
37
32
  }
38
33
 
39
- interface OpenCodeHooks {
40
- "tool.definition"?: (tools: unknown[]) => unknown[];
34
+ interface PluginCtx {
35
+ project?: unknown;
36
+ client?: unknown;
37
+ directory?: string;
38
+ worktree?: string;
41
39
  }
42
40
 
43
- type PluginFn = (params: OpenCodePluginParams) => Promise<OpenCodeHooks>;
44
-
45
- const ZMARKETPLACE_TOOL = {
46
- type: "function" as const,
47
- function: {
48
- name: "zmarketplace",
49
- description:
50
- "Search, inspect, audit, and install packages across agent ecosystems. " +
51
- "Actions: search, detail, audit, install.",
52
- parameters: {
53
- type: "object",
54
- properties: {
55
- action: { type: "string", enum: ["search", "detail", "audit", "install"] },
56
- query: { type: "string" },
57
- name: { type: "string" },
58
- type: { type: "string", enum: ["extension", "skill", "theme", "prompt", "plugin", "mcp", "all"] },
59
- ecosystem: { type: "string", enum: ["pi", "omp", "claude", "opencode", "gemini", "codex", "all"] },
60
- limit: { type: "number" },
61
- deepScan: { type: "boolean" },
62
- skipAudit: { type: "boolean" },
63
- },
64
- required: ["action"],
41
+ /** The zmarketplace tool definition. */
42
+ const zmarketplaceTool: ToolDef = {
43
+ description:
44
+ "Search, inspect, audit, and install packages across agent ecosystems " +
45
+ "(pi, omp, claude code, opencode, gemini cli, codex). " +
46
+ "Actions: search, detail, audit. " +
47
+ "Sources: npm, Claude marketplace, Gemini extensions, MCP registry, Smithery, GitHub.",
48
+ args: {
49
+ action: { type: "string", description: "search, detail, or audit" },
50
+ query: { type: "string", description: "Search query (for search)" },
51
+ name: { type: "string", description: "Package name (for detail/audit)" },
52
+ ecosystem: {
53
+ type: "string",
54
+ description: "Filter: pi, claude, opencode, gemini, codex, npm, all",
65
55
  },
66
- execute: async (args: Record<string, unknown>, _ctx: OpenCodeToolContext): Promise<string> => {
67
- const action = args["action"] as string;
56
+ limit: { type: "number", description: "Max results (default 20)" },
57
+ },
68
58
 
69
- if (action === "search") {
70
- const opts: SearchOptions = {
71
- query: (args["query"] as string) ?? "",
72
- type: (args["type"] as SearchOptions["type"]) ?? "all",
73
- ecosystem: (args["ecosystem"] as SearchOptions["ecosystem"]) ?? "all",
74
- limit: (args["limit"] as number) ?? 20,
75
- };
76
- const results = await search(opts);
77
- return results.length === 0
78
- ? "No packages found."
79
- : results.map(r => `${r.name} [${r.ecosystems.filter(e => e !== "unknown").join(",")}] — ${r.description}`).join("\n");
80
- }
59
+ async execute(args: Record<string, unknown>): Promise<string> {
60
+ const action = args["action"] as string;
81
61
 
82
- if (action === "detail") {
83
- const name = args["name"] as string;
84
- if (!name) return "Error: 'name' required for detail.";
85
- const detail = await getDetail(name);
86
- if (!detail) return `Package "${name}" not found.`;
87
- return `${detail.name} v${detail.version}\n${detail.description}\nDeps: ${detail.dependencyCount} Size: ${detail.size ? (detail.size / 1024).toFixed(1) + " KB" : "?"}\n${detail.npmUrl}`;
88
- }
62
+ if (action === "search") {
63
+ const opts: SearchOptions = {
64
+ query: (args["query"] as string) ?? "",
65
+ ecosystem: (args["ecosystem"] as SearchOptions["ecosystem"]) ?? "all",
66
+ limit: (args["limit"] as number) ?? 20,
67
+ };
68
+ const results = await search(opts);
69
+ if (results.length === 0) return "No packages found.";
70
+ return results.map(r =>
71
+ `${r.name} [${r.ecosystems.filter(e => e !== "unknown").join(",")}] (${r.source})\n ${r.description.slice(0, 100)}\n Install: ${r.installCommand ?? "n/a"}`
72
+ ).join("\n\n");
73
+ }
89
74
 
90
- if (action === "audit") {
91
- const name = args["name"] as string;
92
- if (!name) return "Error: 'name' required for audit.";
93
- const report = await auditPackage(name, { deepScan: (args["deepScan"] as boolean) ?? true });
94
- return report.summary + "\n" + (report.findings.map(f => `[${f.severity.toUpperCase()}] ${f.reason}`).join("\n") || "No findings.");
95
- }
75
+ if (action === "detail") {
76
+ const name = args["name"] as string;
77
+ if (!name) return "Error: name required.";
78
+ const detail = await getDetail(name);
79
+ if (!detail) return `Package "${name}" not found.`;
80
+ return `${detail.name} v${detail.version}\n${detail.description}\nDeps: ${detail.dependencyCount} Size: ${detail.size ? (detail.size / 1024).toFixed(1) + " KB" : "?"}\n${detail.npmUrl ?? ""}`;
81
+ }
96
82
 
97
- if (action === "install") {
98
- const name = args["name"] as string;
99
- if (!name) return "Error: 'name' required for install.";
100
- const result = await installPackage(name, { skipAudit: (args["skipAudit"] as boolean) ?? false });
101
- return result.message;
102
- }
83
+ if (action === "audit") {
84
+ const name = args["name"] as string;
85
+ if (!name) return "Error: name required.";
86
+ const report = await auditPackage(name, { deepScan: true });
87
+ return `${report.summary}\nFindings:\n${report.findings.map(f => `[${f.severity}] ${f.reason}`).join("\n") || "None"}`;
88
+ }
103
89
 
104
- return `Unknown action: ${action}`;
105
- },
90
+ return `Unknown action: ${action}`;
106
91
  },
107
92
  };
108
93
 
109
- const ZmarketplacePlugin: PluginFn = async (_params: OpenCodePluginParams) => {
94
+ /** OpenCode plugin provides zmarketplace as a tool the agent can call. */
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ export const ZmarketplacePlugin = async (_ctx: PluginCtx): Promise<{ tool: Record<string, ToolDef> }> => {
110
97
  return {
111
- "tool.definition": (tools: unknown[]) => {
112
- tools.push(ZMARKETPLACE_TOOL);
113
- return tools;
98
+ tool: {
99
+ zmarketplace: zmarketplaceTool,
114
100
  },
115
101
  };
116
102
  };
117
103
 
118
104
  export default ZmarketplacePlugin;
119
- export { ZmarketplacePlugin };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * GitHub topics adapter — searches GitHub repos by topic for agent plugins.
3
+ * Uses the public GitHub search API (60 requests/hour without auth).
4
+ */
5
+
6
+ import type { PackageResult, PackageType, RegistrySource } from "../core/types.ts";
7
+
8
+ const GITHUB_SEARCH = "https://api.github.com/search/repositories";
9
+
10
+ const AGENT_TOPICS = [
11
+ "claude-code", "agent-plugin", "mcp-server",
12
+ "gemini-cli-extension", "codex-plugin", "pi-package",
13
+ ];
14
+
15
+ interface GitHubRepo {
16
+ name: string;
17
+ full_name: string;
18
+ description: string | null;
19
+ html_url: string;
20
+ stargazers_count: number;
21
+ pushed_at: string;
22
+ owner: { login: string };
23
+ topics: string[];
24
+ license: { key: string } | null;
25
+ }
26
+
27
+ interface GitHubResponse {
28
+ total_count: number;
29
+ items: GitHubRepo[];
30
+ }
31
+
32
+ /** Search GitHub repos by agent-related topics. */
33
+ export async function searchGitHubTopics(query: string, limit = 25): Promise<PackageResult[]> {
34
+ const q = query.trim();
35
+ const topicQuery = AGENT_TOPICS.map(t => `topic:${t}`).join(" ");
36
+ const searchText = q ? `${topicQuery} ${q}` : topicQuery;
37
+ const url = `${GITHUB_SEARCH}?q=${encodeURIComponent(searchText)}&sort=stars&order=desc&per_page=${limit}`;
38
+
39
+ try {
40
+ const resp = await fetch(url, {
41
+ signal: AbortSignal.timeout(10000),
42
+ headers: { "Accept": "application/vnd.github+json" },
43
+ });
44
+ if (!resp.ok) return [];
45
+ const data = await resp.json() as GitHubResponse;
46
+
47
+ return (data.items ?? []).slice(0, limit).map(repo => ({
48
+ name: repo.full_name,
49
+ description: repo.description ?? "",
50
+ author: repo.owner.login,
51
+ ecosystems: ["universal"] as const,
52
+ type: "plugin" as PackageType,
53
+ source: "github" as RegistrySource,
54
+ homepage: repo.html_url,
55
+ repository: repo.html_url,
56
+ license: repo.license?.key,
57
+ publishedAt: repo.pushed_at,
58
+ installCommand: `git clone ${repo.html_url}`,
59
+ }));
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
@@ -54,6 +54,7 @@ export function detectEcosystems(keywords?: string[], name?: string): Ecosystem[
54
54
  if (kws.has("opencode") || kws.has("opencode-plugin") || n.includes("opencode")) result.push("opencode");
55
55
  if (kws.has("gemini-cli") || kws.has("gemini-extension") || kws.has("gemini-cli-extension") || n.includes("gemini")) result.push("gemini");
56
56
  if (kws.has("codex") || kws.has("codex-plugin") || kws.has("codex-cli") || n.includes("codex")) result.push("codex");
57
+ if (kws.has("npm") || kws.has("bun") || kws.has("pnpm") || kws.has("yarn") || kws.has("nodejs") || kws.has("deno")) result.push("npm");
57
58
 
58
59
  if (result.length === 0) result.push("unknown");
59
60
  return result;
@@ -83,7 +84,7 @@ export function installCommandFor(name: string, ecosystem: Ecosystem): string {
83
84
  case "claude": return `claude plugin install npm:${name}`;
84
85
  case "opencode": return `opencode plugin ${name}`;
85
86
  case "gemini": return `gemini extension install npm:${name}`;
86
- case "codex": return `codex plugin add npm:${name}`;
87
+ case "npm": return `npm install ${name}`;
87
88
  default: return `npm install ${name}`;
88
89
  }
89
90
  }
@@ -96,7 +97,7 @@ export async function searchNpm(
96
97
  const limit = options.limit ?? 25;
97
98
  const allEcosystems: Ecosystem[] = options.ecosystems && options.ecosystems.length > 0
98
99
  ? options.ecosystems
99
- : ["pi", "claude", "opencode", "gemini", "codex"];
100
+ : ["pi", "claude", "opencode", "gemini", "codex", "npm"];
100
101
 
101
102
  // Collect unique keywords for the selected ecosystems
102
103
  const keywords = new Set<string>();
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Smithery MCP registry adapter — queries api.smithery.ai for MCP servers.
3
+ */
4
+
5
+ import type { PackageResult, PackageType, RegistrySource } from "../core/types.ts";
6
+
7
+ const SMITHERY_URL = "https://api.smithery.ai/v1/servers";
8
+
9
+ interface SmitheryServer {
10
+ qualifiedName?: string;
11
+ displayName?: string;
12
+ description?: string;
13
+ shortDescription?: string;
14
+ repository?: { url?: string; source?: string };
15
+ latestVersion?: string;
16
+ author?: { name?: string };
17
+ }
18
+
19
+ interface SmitheryResponse {
20
+ servers?: SmitheryServer[];
21
+ }
22
+
23
+ /** Search Smithery MCP registry. */
24
+ export async function searchSmithery(query: string, limit = 25): Promise<PackageResult[]> {
25
+ const q = query.toLowerCase().trim();
26
+ const url = q
27
+ ? `${SMITHERY_URL}?q=${encodeURIComponent(query)}&page=1&pageSize=${limit}`
28
+ : `${SMITHERY_URL}?page=1&pageSize=${limit}`;
29
+
30
+ try {
31
+ const resp = await fetch(url, { signal: AbortSignal.timeout(10000) });
32
+ if (!resp.ok) return [];
33
+ const data = await resp.json() as SmitheryResponse;
34
+ const servers = data.servers ?? [];
35
+
36
+ return servers.slice(0, limit).map(srv => ({
37
+ name: srv.qualifiedName ?? srv.displayName ?? "unknown",
38
+ description: srv.shortDescription ?? srv.description ?? "",
39
+ version: srv.latestVersion,
40
+ author: srv.author?.name,
41
+ ecosystems: ["universal"] as const,
42
+ type: "mcp" as PackageType,
43
+ source: "smithery" as RegistrySource,
44
+ homepage: srv.repository?.url ?? srv.repository?.source,
45
+ repository: srv.repository?.url,
46
+ installCommand: `npx @smithery/cli install ${srv.qualifiedName ?? srv.displayName ?? ""}`,
47
+ }));
48
+ } catch {
49
+ return [];
50
+ }
51
+ }