zmarketplace 0.5.3 → 0.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zmarketplace",
3
- "version": "0.5.3",
3
+ "version": "0.7.3",
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",
@@ -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
  }
@@ -82,6 +84,12 @@ export async function search(options: SearchOptions): Promise<PackageResult[]> {
82
84
  if (registries.includes("mcp-registry")) {
83
85
  queries.push(searchMcpRegistry(options.query, limit));
84
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
+ }
85
93
 
86
94
  const settled = await Promise.allSettled(queries);
87
95
  let results = settled
package/src/core/tui.ts CHANGED
@@ -125,18 +125,20 @@ export function formatHelp(): string {
125
125
  "📦 zmarketplace — cross-agent package search",
126
126
  "",
127
127
  "Usage:",
128
- " /zmarketplace search <query> Search across all registries",
129
- " /zmarketplace detail <id|name> Show package details",
130
- " /zmarketplace audit <id|name> Run security audit",
131
- " /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",
132
135
  "",
133
136
  "Options for 'search':",
134
137
  " --type=<type> Filter: extension, skill, theme, prompt, plugin, mcp",
135
138
  " --eco=<ecosystem> Filter: pi, claude, opencode, gemini, codex, npm",
136
139
  " --limit=<n> Max results (default 20)",
137
140
  "",
138
- "Registries: npm + Claude marketplace + Gemini extensions + MCP registry",
139
- "",
141
+ "Registries: npm + Claude + Gemini + MCP + Smithery + GitHub",
140
142
  "Examples:",
141
143
  " /zmarketplace search mcp",
142
144
  " /zmarketplace search subagent --eco=pi",
package/src/core/types.ts CHANGED
@@ -9,7 +9,7 @@ export type Ecosystem = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex
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 {
package/src/index.ts CHANGED
@@ -10,6 +10,9 @@ 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";
13
16
  import { spawn } from "node:child_process";
14
17
 
15
18
  // ── Types ──────────────────────────────────────────────────────────────────
@@ -58,8 +61,10 @@ async function browseResults(results: PackageResult[], ctx: Ctx): Promise<void>
58
61
  while (true) {
59
62
  const pageEnd = Math.min(pageStart + PAGE_SIZE, results.length);
60
63
  const page = results.slice(pageStart, pageEnd);
61
-
62
- 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
+ });
63
68
 
64
69
  if (pageStart > 0) {
65
70
  const ps = Math.max(0, pageStart - PAGE_SIZE) + 1;
@@ -201,19 +206,93 @@ async function doInstall(pkg: PackageResult, ctx: Ctx): Promise<string | null> {
201
206
  const choice = await ctx.ui.select(`Install ${pkg.name} — Risk: ${report.risk}`, cmds);
202
207
  if (!choice || choice === "↩ Cancel") return null;
203
208
  const command = cmdMap.get(choice);
204
- if (command) ctx.ui.notify(`✅ Run:\n ${command}`, "info");
205
- return command ?? null;
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;
206
239
  }
207
240
 
208
241
  // ── Factory ────────────────────────────────────────────────────────────────
209
-
210
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
+ }
211
288
  const commandDef = {
212
289
  description: "Search, audit, and install packages across agent ecosystems",
213
290
  handler: async (rawArgs: string, ctx: Ctx) => {
214
291
  const args = parseArgs(rawArgs.trim().split(/\s+/).filter(Boolean));
215
292
  switch (args.subcommand) {
216
293
  case "help": { ctx.ui.notify(formatHelp(), "info"); break; }
294
+ case "updates": { await doUpdates(ctx); break; }
295
+ case "popular": { await doPopular(ctx); break; }
217
296
  case "audit":
218
297
  case "a": {
219
298
  const ref = args.positional[0];
package/src/opencode.ts CHANGED
@@ -1,119 +1,60 @@
1
1
  /**
2
- * OpenCode plugin entry point.
2
+ * OpenCode plugin provides zmarketplace as a tool.
3
+ * Uses bunx CLI under the hood (no complex logic that can crash).
3
4
  *
4
- * OpenCode plugins are npm packages with `exports["./server"]` pointing here.
5
- * They export a Plugin function that returns hooks.
6
- *
7
- * Install in opencode:
8
- * opencode plugin zmarketplace
9
- *
10
- * Or add to opencode.json:
11
- * { "plugin": ["zmarketplace"] }
5
+ * Install: add "zmarketplace" to opencode.json plugin array.
12
6
  */
13
7
 
14
- import { search } from "./core/search.ts";
15
- import { getDetail } from "./core/detail.ts";
16
- import { auditPackage } from "./core/audit.ts";
17
- import { installPackage } from "./core/install.ts";
18
- import type { SearchOptions } from "./core/types.ts";
19
-
20
- /**
21
- * Minimal OpenCode plugin type shape.
22
- * Not imported from @opencode-ai/plugin to keep zero deps.
23
- */
24
- interface OpenCodeToolContext {
25
- sessionID?: string;
26
- messageID?: string;
27
- agent?: string;
28
- directory?: string;
29
- ask?(message: string): Promise<string>;
8
+ interface ShellAPI {
9
+ // Bun shell template literal
10
+ (strings: TemplateStringsArray, ...values: unknown[]): { quiet(): Promise<{ stdout: Buffer; stderr: Buffer; exitCode: number }> };
30
11
  }
31
12
 
32
- interface OpenCodePluginParams {
33
- project?: string;
34
- serverUrl?: string;
35
- // Bun shell `$`
36
- $?: unknown;
13
+ interface PluginCtx {
14
+ project?: unknown;
15
+ directory?: string;
16
+ worktree?: string;
17
+ $?: ShellAPI;
37
18
  }
38
19
 
39
- interface OpenCodeHooks {
40
- "tool.definition"?: (tools: unknown[]) => unknown[];
20
+ /** Run a shell command and return stdout as string. */
21
+ async function runCmd($: ShellAPI | undefined, cmd: string): Promise<string> {
22
+ if (!$) return "Error: shell not available. Use: bunx zmarketplace " + cmd;
23
+ try {
24
+ const result = await $`bunx zmarketplace ${cmd}`.quiet();
25
+ const stdout = result.stdout?.toString() ?? "";
26
+ const stderr = result.stderr?.toString() ?? "";
27
+ return stdout || stderr || "No output.";
28
+ } catch (err) {
29
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
30
+ }
41
31
  }
42
32
 
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"],
65
- },
66
- execute: async (args: Record<string, unknown>, _ctx: OpenCodeToolContext): Promise<string> => {
67
- const action = args["action"] as string;
68
-
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
- }
81
-
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
- }
89
-
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
- }
96
-
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
- }
103
-
104
- return `Unknown action: ${action}`;
105
- },
106
- },
107
- };
108
-
109
- const ZmarketplacePlugin: PluginFn = async (_params: OpenCodePluginParams) => {
33
+ /** OpenCode plugin registers zmarketplace tool. */
34
+ export const ZmarketplacePlugin = async (ctx: PluginCtx) => {
35
+ const $ = ctx.$;
110
36
  return {
111
- "tool.definition": (tools: unknown[]) => {
112
- tools.push(ZMARKETPLACE_TOOL);
113
- return tools;
37
+ tool: {
38
+ zmarketplace: {
39
+ description:
40
+ "Search, inspect, audit, and install packages across agent ecosystems " +
41
+ "(pi, omp, claude, opencode, gemini, codex, npm). " +
42
+ "Actions: search <query>, detail <name>, audit <name>",
43
+ args: {
44
+ action: { type: "string", description: "search, detail, or audit" },
45
+ query: { type: "string", description: "Search query or package name" },
46
+ },
47
+ async execute(args: { action?: string; query?: string }): Promise<string> {
48
+ const action = args.action ?? "search";
49
+ const query = args.query ?? "";
50
+
51
+ if (action === "detail") return runCmd($, `detail ${query}`);
52
+ if (action === "audit") return runCmd($, `audit ${query}`);
53
+ return runCmd($, `search "${query}" --limit=10`);
54
+ },
55
+ },
114
56
  },
115
57
  };
116
58
  };
117
59
 
118
60
  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
+ }
@@ -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
+ }