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 +107 -71
- package/package.json +2 -1
- package/src/core/install.ts +4 -2
- package/src/core/installed.ts +75 -0
- package/src/core/search.ts +9 -2
- package/src/core/tui.ts +10 -7
- package/src/core/types.ts +4 -3
- package/src/index.ts +103 -12
- package/src/opencode.ts +66 -81
- package/src/registries/github.ts +63 -0
- package/src/registries/npm.ts +3 -2
- package/src/registries/smithery.ts +51 -0
package/README.md
CHANGED
|
@@ -1,84 +1,116 @@
|
|
|
1
1
|
# zmarketplace
|
|
2
2
|
|
|
3
|
-
Cross-agent marketplace search
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
Then `/reload` and type `/zmarketplace`.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
28
19
|
|
|
29
20
|
```
|
|
30
|
-
/zmarketplace
|
|
31
|
-
/
|
|
32
|
-
|
|
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
|
-
|
|
29
|
+
## Commands
|
|
36
30
|
|
|
37
|
-
|
|
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
|
-
|
|
41
|
+
## Detail View
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
66
|
+
## Install Options
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
After audit, choose how to install:
|
|
49
69
|
|
|
50
70
|
| Option | Command |
|
|
51
71
|
|---|---|
|
|
52
|
-
| 🥧 pi
|
|
53
|
-
| ⌥ omp
|
|
54
|
-
| 🤖 claude
|
|
55
|
-
| 🔓 opencode
|
|
56
|
-
| 💎 gemini
|
|
57
|
-
| 🔲 codex
|
|
58
|
-
| 📦 npm
|
|
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
|
-
|
|
83
|
+
High-risk packages require confirmation before showing the command.
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
## Security Audit
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
96
|
+
| Registry | Coverage |
|
|
76
97
|
|---|---|
|
|
77
|
-
| npm | `pi-package`, `claude-code`, `opencode`, `gemini-cli`, `codex` keywords |
|
|
78
|
-
| Claude marketplace |
|
|
79
|
-
| Gemini 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
|
|
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.ts
|
|
95
|
-
├── cli.ts
|
|
96
|
-
├── opencode.ts
|
|
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.ts
|
|
99
|
-
│ ├── search.ts
|
|
100
|
-
│ ├── detail.ts
|
|
101
|
-
│ ├── audit.ts
|
|
102
|
-
│ ├── install.ts
|
|
103
|
-
│ ├── cache.ts
|
|
104
|
-
│ └── tui.ts
|
|
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.ts
|
|
107
|
-
├── claude.ts
|
|
108
|
-
|
|
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
|
-
##
|
|
146
|
+
## Compatibility
|
|
112
147
|
|
|
113
|
-
|
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
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
|
-
|
|
157
|
+
Zero runtime dependencies. TypeScript with Bun. No Bun-specific APIs (works on pi's jiti/Node loader).
|
|
122
158
|
|
|
123
|
-
|
|
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.
|
|
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",
|
package/src/core/install.ts
CHANGED
|
@@ -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 "
|
|
35
|
-
case "auto":
|
|
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
|
+
}
|
package/src/core/search.ts
CHANGED
|
@@ -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
|
|
128
|
-
" /zmarketplace
|
|
129
|
-
" /zmarketplace
|
|
130
|
-
" /zmarketplace
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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;
|
|
126
|
+
if (!selected) continue;
|
|
121
127
|
if (selected.includes("Back to results")) return;
|
|
122
|
-
if (selected.includes("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<
|
|
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)
|
|
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
|
|
5
|
-
*
|
|
4
|
+
* OpenCode plugins export functions that return hooks.
|
|
5
|
+
* This plugin provides a `zmarketplace` tool the agent can call.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
|
|
34
|
+
interface PluginCtx {
|
|
35
|
+
project?: unknown;
|
|
36
|
+
client?: unknown;
|
|
37
|
+
directory?: string;
|
|
38
|
+
worktree?: string;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
67
|
-
|
|
56
|
+
limit: { type: "number", description: "Max results (default 20)" },
|
|
57
|
+
},
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
},
|
|
90
|
+
return `Unknown action: ${action}`;
|
|
106
91
|
},
|
|
107
92
|
};
|
|
108
93
|
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
+
}
|
package/src/registries/npm.ts
CHANGED
|
@@ -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 "
|
|
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
|
+
}
|