zmarketplace 0.5.3 → 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/package.json +1 -1
- package/src/core/installed.ts +75 -0
- package/src/core/search.ts +9 -1
- package/src/core/tui.ts +8 -6
- package/src/core/types.ts +1 -1
- package/src/index.ts +84 -5
- package/src/opencode.ts +66 -81
- package/src/registries/github.ts +63 -0
- package/src/registries/smithery.ts +51 -0
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",
|
|
@@ -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
|
}
|
|
@@ -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
|
|
129
|
-
" /zmarketplace
|
|
130
|
-
" /zmarketplace
|
|
131
|
-
" /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",
|
|
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
|
|
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
|
-
|
|
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)
|
|
205
|
-
|
|
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,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
|
+
}
|
|
@@ -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
|
+
}
|