@zhijiewang/openharness 1.0.0 → 1.3.0

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.
@@ -16,9 +16,11 @@ export type AgentRole = {
16
16
  /** Suggested tools to include (empty = all tools) */
17
17
  suggestedTools?: string[];
18
18
  };
19
- /** Get a role by ID */
19
+ /** Discover markdown agent roles from .oh/agents/ and ~/.oh/agents/ */
20
+ export declare function discoverMarkdownAgents(): AgentRole[];
21
+ /** Get a role by ID (checks built-in first, then markdown agents) */
20
22
  export declare function getRole(id: string): AgentRole | undefined;
21
- /** List all available roles */
23
+ /** List all available roles (built-in + markdown) */
22
24
  export declare function listRoles(): AgentRole[];
23
25
  /** Get role IDs */
24
26
  export declare function getRoleIds(): string[];
@@ -157,16 +157,80 @@ Work methodically: search exhaustively, change incrementally, test after each ba
157
157
  suggestedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'],
158
158
  },
159
159
  ];
160
- /** Get a role by ID */
160
+ // ── Markdown Agent Discovery ──
161
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
162
+ import { join, basename } from 'node:path';
163
+ import { homedir } from 'node:os';
164
+ const PROJECT_AGENTS_DIR = join('.oh', 'agents');
165
+ const GLOBAL_AGENTS_DIR = join(homedir(), '.oh', 'agents');
166
+ /**
167
+ * Parse a markdown agent file into an AgentRole.
168
+ *
169
+ * Format:
170
+ * ---
171
+ * name: My Agent
172
+ * description: What it does
173
+ * tools: [Read, Grep, Bash]
174
+ * ---
175
+ *
176
+ * System prompt content...
177
+ */
178
+ function parseAgentMarkdown(raw, filePath) {
179
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
180
+ if (!fmMatch)
181
+ return null;
182
+ const fm = fmMatch[1];
183
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
184
+ const descMatch = fm.match(/^description:\s*(.+)$/m);
185
+ const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
186
+ const fmEnd = raw.indexOf('---', raw.indexOf('---') + 3);
187
+ const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : '';
188
+ const id = basename(filePath, '.md').toLowerCase().replace(/[^a-z0-9]+/g, '-');
189
+ return {
190
+ id,
191
+ name: nameMatch?.[1]?.trim() ?? id,
192
+ description: descMatch?.[1]?.trim() ?? '',
193
+ systemPromptSupplement: content,
194
+ suggestedTools: toolsMatch
195
+ ? toolsMatch[1].split(',').map(t => t.trim())
196
+ : undefined,
197
+ };
198
+ }
199
+ /** Load agent roles from a directory of .md files */
200
+ function loadAgentsFromDir(dir) {
201
+ if (!existsSync(dir))
202
+ return [];
203
+ return readdirSync(dir)
204
+ .filter(f => f.endsWith('.md'))
205
+ .map(f => {
206
+ try {
207
+ const raw = readFileSync(join(dir, f), 'utf-8');
208
+ return parseAgentMarkdown(raw, f);
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ })
214
+ .filter((r) => r !== null);
215
+ }
216
+ /** Discover markdown agent roles from .oh/agents/ and ~/.oh/agents/ */
217
+ export function discoverMarkdownAgents() {
218
+ return [
219
+ ...loadAgentsFromDir(PROJECT_AGENTS_DIR),
220
+ ...loadAgentsFromDir(GLOBAL_AGENTS_DIR),
221
+ ];
222
+ }
223
+ /** Get a role by ID (checks built-in first, then markdown agents) */
161
224
  export function getRole(id) {
162
- return roles.find(r => r.id === id);
225
+ return roles.find(r => r.id === id)
226
+ ?? discoverMarkdownAgents().find(r => r.id === id);
163
227
  }
164
- /** List all available roles */
228
+ /** List all available roles (built-in + markdown) */
165
229
  export function listRoles() {
166
- return [...roles];
230
+ return [...roles, ...discoverMarkdownAgents()];
167
231
  }
168
232
  /** Get role IDs */
169
233
  export function getRoleIds() {
170
- return roles.map(r => r.id);
234
+ return listRoles().map(r => r.id);
171
235
  }
172
236
  //# sourceMappingURL=roles.js.map
@@ -29,7 +29,7 @@ register("help", "Show available commands", () => {
29
29
  'Git': ['diff', 'undo', 'rewind', 'commit', 'log'],
30
30
  'Info': ['help', 'cost', 'status', 'config', 'files', 'model', 'memory', 'doctor', 'context', 'mcp', 'mcp-registry'],
31
31
  'Settings': ['theme', 'vim', 'companion', 'fast', 'keys'],
32
- 'AI': ['plan', 'review', 'roles'],
32
+ 'AI': ['plan', 'review', 'roles', 'agents', 'plugins'],
33
33
  'Pet': ['cybergotchi'],
34
34
  };
35
35
  const lines = [];
@@ -354,6 +354,27 @@ register("roles", "List available agent specialization roles", () => {
354
354
  lines.push("Usage: Agent({ subagent_type: 'code-reviewer', prompt: '...' })");
355
355
  return { output: lines.join("\n"), handled: true };
356
356
  });
357
+ register("agents", "Discover running openHarness agents on this machine", () => {
358
+ const { discoverAgents } = require('../services/a2a.js');
359
+ const agents = discoverAgents();
360
+ if (agents.length === 0) {
361
+ return { output: "No other openHarness agents running on this machine.\n\nOther oh sessions will appear here automatically via the A2A protocol.", handled: true };
362
+ }
363
+ const lines = [`Running Agents (${agents.length}):\n`];
364
+ for (const agent of agents) {
365
+ const age = Math.round((Date.now() - agent.registeredAt) / 60_000);
366
+ lines.push(` ${agent.name}`);
367
+ lines.push(` ID: ${agent.id}`);
368
+ lines.push(` Provider: ${agent.provider ?? 'unknown'} / ${agent.model ?? 'unknown'}`);
369
+ lines.push(` Dir: ${agent.workingDir ?? 'unknown'}`);
370
+ lines.push(` Endpoint: ${agent.endpoint.type}${agent.endpoint.port ? ':' + agent.endpoint.port : ''}`);
371
+ lines.push(` Uptime: ${age}m`);
372
+ lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(', ')}`);
373
+ lines.push('');
374
+ }
375
+ lines.push("Send messages with: Agent({ prompt: 'ask the other agent...', allowed_tools: ['SendMessage'] })");
376
+ return { output: lines.join("\n"), handled: true };
377
+ });
357
378
  register("fast", "Toggle fast mode (optimized for speed)", () => {
358
379
  return { output: "", handled: true, toggleFastMode: true };
359
380
  });
@@ -614,51 +635,88 @@ function setPinned(args, ctx, pinned) {
614
635
  }
615
636
  register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
616
637
  register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
617
- register("plugins", "List installed plugins and discover new ones", (args) => {
638
+ register("plugins", "Manage plugins: list, search, install, uninstall, marketplace", (args) => {
618
639
  const { discoverPlugins, discoverSkills } = require('../harness/plugins.js');
619
- const query = args.trim();
620
- if (query === 'search' || query.startsWith('search ')) {
621
- // npm registry search
622
- const keyword = query.replace(/^search\s*/, '').trim() || 'openharness-plugin';
623
- return {
624
- output: `To discover plugins, search npm:\n\n npm search openharness-plugin${keyword !== 'openharness-plugin' ? ' ' + keyword : ''}\n\nInstall with:\n npm install <package-name>\n\nPlugins are auto-discovered from node_modules/ if they contain openharness-plugin.json.`,
625
- handled: true,
626
- };
640
+ const { searchMarketplace, installPlugin, uninstallPlugin, getInstalledPlugins, listMarketplaces, addMarketplace, removeMarketplace, formatMarketplaceSearch, formatInstalledPlugins, } = require('../harness/marketplace.js');
641
+ const parts = args.trim().split(/\s+/);
642
+ const subcommand = parts[0] ?? '';
643
+ const rest = parts.slice(1).join(' ');
644
+ // /plugins marketplace add <source>
645
+ if (subcommand === 'marketplace') {
646
+ const action = parts[1];
647
+ const source = parts.slice(2).join(' ');
648
+ if (action === 'add' && source) {
649
+ const mp = addMarketplace(source);
650
+ if (mp)
651
+ return { output: `Added marketplace "${mp.name}" (${mp.plugins.length} plugins)`, handled: true };
652
+ return { output: `Failed to add marketplace from "${source}"`, handled: true };
653
+ }
654
+ if (action === 'remove' && source) {
655
+ return { output: removeMarketplace(source) ? `Removed marketplace "${source}"` : `Marketplace "${source}" not found`, handled: true };
656
+ }
657
+ // List marketplaces
658
+ const mps = listMarketplaces();
659
+ if (mps.length === 0) {
660
+ return { output: 'No marketplaces configured.\n\nAdd one:\n /plugins marketplace add owner/repo\n /plugins marketplace add https://example.com/plugins', handled: true };
661
+ }
662
+ const lines = [`Marketplaces (${mps.length}):\n`];
663
+ for (const mp of mps) {
664
+ lines.push(` ${mp.name} — ${mp.plugins.length} plugins`);
665
+ }
666
+ return { output: lines.join('\n'), handled: true };
667
+ }
668
+ // /plugins search <query>
669
+ if (subcommand === 'search') {
670
+ const query = rest || 'all';
671
+ const results = searchMarketplace(query === 'all' ? '' : query);
672
+ return { output: formatMarketplaceSearch(results), handled: true };
673
+ }
674
+ // /plugins install <name>
675
+ if (subcommand === 'install' && rest) {
676
+ const [name, marketplace] = rest.split('@');
677
+ const result = installPlugin(name, marketplace);
678
+ if (result) {
679
+ return { output: `Installed ${result.name}@${result.version} from ${result.marketplace}\nCached at: ${result.cachePath}`, handled: true };
680
+ }
681
+ return { output: `Failed to install "${rest}". Is it listed in a marketplace?\nRun /plugins search ${name} to check.`, handled: true };
627
682
  }
628
- // List installed
683
+ // /plugins uninstall <name>
684
+ if (subcommand === 'uninstall' && rest) {
685
+ return { output: uninstallPlugin(rest) ? `Uninstalled "${rest}"` : `Plugin "${rest}" not found`, handled: true };
686
+ }
687
+ // /plugins (no args) — show everything
629
688
  const plugins = discoverPlugins();
630
689
  const skills = discoverSkills();
690
+ const marketplacePlugins = getInstalledPlugins();
631
691
  const lines = [];
692
+ if (marketplacePlugins.length > 0) {
693
+ lines.push(formatInstalledPlugins(marketplacePlugins));
694
+ lines.push('');
695
+ }
632
696
  if (plugins.length > 0) {
633
- lines.push(`Installed Plugins (${plugins.length}):`);
697
+ lines.push(`Local Plugins (${plugins.length}):`);
634
698
  for (const p of plugins) {
635
699
  lines.push(` ${p.name}@${p.version} — ${p.description || 'no description'}`);
636
- if (p.skills?.length)
637
- lines.push(` Skills: ${p.skills.length}`);
638
- if (p.mcpServers?.length)
639
- lines.push(` MCP servers: ${p.mcpServers.map((s) => s.name).join(', ')}`);
640
700
  }
641
701
  lines.push('');
642
702
  }
643
703
  if (skills.length > 0) {
644
- lines.push(`Available Skills (${skills.length}):`);
645
- const bySource = {};
704
+ lines.push(`Skills (${skills.length}):`);
646
705
  for (const s of skills) {
647
- (bySource[s.source] ??= []).push(s);
648
- }
649
- for (const [source, sourceSkills] of Object.entries(bySource)) {
650
- lines.push(` ${source}:`);
651
- for (const s of sourceSkills) {
652
- lines.push(` ${s.name} — ${s.description}${s.trigger ? ` (trigger: "${s.trigger}")` : ''}`);
653
- }
706
+ lines.push(` ${s.source}:${s.name} ${s.description || ''}`);
654
707
  }
708
+ lines.push('');
655
709
  }
656
- else if (plugins.length === 0) {
710
+ if (lines.length === 0) {
657
711
  lines.push('No plugins or skills installed.');
658
- lines.push('');
659
- lines.push('Create skills in .oh/skills/ or ~/.oh/skills/');
660
- lines.push('Run /plugins search to find npm packages.');
661
712
  }
713
+ lines.push('');
714
+ lines.push('Commands:');
715
+ lines.push(' /plugins search <query> Search marketplaces');
716
+ lines.push(' /plugins install <name> Install from marketplace');
717
+ lines.push(' /plugins uninstall <name> Remove a plugin');
718
+ lines.push(' /plugins marketplace add <src> Add a marketplace');
719
+ lines.push(' /plugins marketplace List marketplaces');
662
720
  return { output: lines.join('\n'), handled: true };
663
721
  });
664
722
  // ── Command Parser ──
@@ -51,6 +51,23 @@ export type OhConfig = {
51
51
  memory?: {
52
52
  consolidateOnExit?: boolean;
53
53
  };
54
+ /** Multi-model router — use different models for different task types */
55
+ modelRouter?: {
56
+ fast?: string;
57
+ balanced?: string;
58
+ powerful?: string;
59
+ };
60
+ /** Opt-in telemetry (default: off) */
61
+ telemetry?: {
62
+ enabled?: boolean;
63
+ endpoint?: string;
64
+ };
65
+ /** Remote server security settings */
66
+ remote?: {
67
+ tokens?: string[];
68
+ rateLimit?: number;
69
+ allowedTools?: string[];
70
+ };
54
71
  };
55
72
  /** Clear cached config (call after writes or to force re-read) */
56
73
  export declare function invalidateConfigCache(): void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Plugin Marketplace — discover, install, and manage plugins from curated registries.
3
+ *
4
+ * Marketplaces are JSON files listing available plugins with versions and sources.
5
+ * Plugins are downloaded and cached to ~/.oh/plugins/cache/ for security and versioning.
6
+ *
7
+ * Inspired by Claude Code's marketplace model.
8
+ */
9
+ export type MarketplaceEntry = {
10
+ name: string;
11
+ description: string;
12
+ version: string;
13
+ author?: string;
14
+ source: MarketplaceSource;
15
+ keywords?: string[];
16
+ };
17
+ export type MarketplaceSource = {
18
+ type: 'github';
19
+ repo: string;
20
+ } | {
21
+ type: 'npm';
22
+ package: string;
23
+ } | {
24
+ type: 'url';
25
+ url: string;
26
+ };
27
+ export type Marketplace = {
28
+ name: string;
29
+ version: number;
30
+ description?: string;
31
+ plugins: MarketplaceEntry[];
32
+ };
33
+ export type InstalledPlugin = {
34
+ name: string;
35
+ version: string;
36
+ marketplace: string;
37
+ installedAt: number;
38
+ cachePath: string;
39
+ };
40
+ /** Add a marketplace from a URL, GitHub repo, or local path */
41
+ export declare function addMarketplace(nameOrUrl: string): Marketplace | null;
42
+ /** Remove a marketplace */
43
+ export declare function removeMarketplace(name: string): boolean;
44
+ /** List all configured marketplaces */
45
+ export declare function listMarketplaces(): Marketplace[];
46
+ /** Search all marketplaces for plugins matching a query */
47
+ export declare function searchMarketplace(query: string): Array<MarketplaceEntry & {
48
+ marketplace: string;
49
+ }>;
50
+ /** Install a plugin from a marketplace */
51
+ export declare function installPlugin(pluginName: string, marketplaceName?: string): InstalledPlugin | null;
52
+ /** Uninstall a plugin */
53
+ export declare function uninstallPlugin(name: string): boolean;
54
+ /** Get all installed plugins */
55
+ export declare function getInstalledPlugins(): InstalledPlugin[];
56
+ /** Format marketplace entries for display */
57
+ export declare function formatMarketplaceSearch(results: Array<MarketplaceEntry & {
58
+ marketplace: string;
59
+ }>): string;
60
+ /** Format installed plugins for display */
61
+ export declare function formatInstalledPlugins(plugins: InstalledPlugin[]): string;
62
+ //# sourceMappingURL=marketplace.d.ts.map
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Plugin Marketplace — discover, install, and manage plugins from curated registries.
3
+ *
4
+ * Marketplaces are JSON files listing available plugins with versions and sources.
5
+ * Plugins are downloaded and cached to ~/.oh/plugins/cache/ for security and versioning.
6
+ *
7
+ * Inspired by Claude Code's marketplace model.
8
+ */
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs';
10
+ import { join, basename } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import { execSync } from 'node:child_process';
13
+ const MARKETPLACE_DIR = join(homedir(), '.oh', 'marketplaces');
14
+ const PLUGIN_CACHE_DIR = join(homedir(), '.oh', 'plugins', 'cache');
15
+ const INSTALLED_PLUGINS_FILE = join(homedir(), '.oh', 'plugins', 'installed.json');
16
+ // ── Marketplace Management ──
17
+ /** Add a marketplace from a URL, GitHub repo, or local path */
18
+ export function addMarketplace(nameOrUrl) {
19
+ mkdirSync(MARKETPLACE_DIR, { recursive: true });
20
+ // Fetch marketplace.json
21
+ let data;
22
+ let marketplaceName;
23
+ if (nameOrUrl.startsWith('http')) {
24
+ // URL
25
+ try {
26
+ data = execSync(`curl -sL "${nameOrUrl}/marketplace.json"`, { encoding: 'utf-8', timeout: 10_000 });
27
+ marketplaceName = new URL(nameOrUrl).hostname;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ else if (nameOrUrl.includes('/') && !nameOrUrl.startsWith('.')) {
34
+ // GitHub repo (owner/repo format)
35
+ try {
36
+ const url = `https://raw.githubusercontent.com/${nameOrUrl}/main/marketplace.json`;
37
+ data = execSync(`curl -sL "${url}"`, { encoding: 'utf-8', timeout: 10_000 });
38
+ marketplaceName = nameOrUrl.replace('/', '-');
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ else if (existsSync(join(nameOrUrl, 'marketplace.json'))) {
45
+ // Local path
46
+ data = readFileSync(join(nameOrUrl, 'marketplace.json'), 'utf-8');
47
+ marketplaceName = basename(nameOrUrl);
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+ try {
53
+ const marketplace = JSON.parse(data);
54
+ if (!marketplace.plugins || !Array.isArray(marketplace.plugins))
55
+ return null;
56
+ marketplace.name = marketplace.name ?? marketplaceName;
57
+ writeFileSync(join(MARKETPLACE_DIR, `${marketplace.name}.json`), JSON.stringify(marketplace, null, 2));
58
+ return marketplace;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** Remove a marketplace */
65
+ export function removeMarketplace(name) {
66
+ const path = join(MARKETPLACE_DIR, `${name}.json`);
67
+ if (!existsSync(path))
68
+ return false;
69
+ try {
70
+ rmSync(path);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** List all configured marketplaces */
78
+ export function listMarketplaces() {
79
+ if (!existsSync(MARKETPLACE_DIR))
80
+ return [];
81
+ return readdirSync(MARKETPLACE_DIR)
82
+ .filter(f => f.endsWith('.json'))
83
+ .map(f => {
84
+ try {
85
+ return JSON.parse(readFileSync(join(MARKETPLACE_DIR, f), 'utf-8'));
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ })
91
+ .filter((m) => m !== null);
92
+ }
93
+ /** Search all marketplaces for plugins matching a query */
94
+ export function searchMarketplace(query) {
95
+ const q = query.toLowerCase();
96
+ const results = [];
97
+ for (const mp of listMarketplaces()) {
98
+ for (const plugin of mp.plugins) {
99
+ if (plugin.name.toLowerCase().includes(q) ||
100
+ plugin.description.toLowerCase().includes(q) ||
101
+ plugin.keywords?.some(k => k.toLowerCase().includes(q))) {
102
+ results.push({ ...plugin, marketplace: mp.name });
103
+ }
104
+ }
105
+ }
106
+ return results;
107
+ }
108
+ // ── Plugin Installation ──
109
+ /** Install a plugin from a marketplace */
110
+ export function installPlugin(pluginName, marketplaceName) {
111
+ // Find the plugin in marketplaces
112
+ const marketplaces = listMarketplaces();
113
+ let entry = null;
114
+ let fromMarketplace = '';
115
+ for (const mp of marketplaces) {
116
+ if (marketplaceName && mp.name !== marketplaceName)
117
+ continue;
118
+ const found = mp.plugins.find(p => p.name === pluginName);
119
+ if (found) {
120
+ entry = found;
121
+ fromMarketplace = mp.name;
122
+ break;
123
+ }
124
+ }
125
+ if (!entry)
126
+ return null;
127
+ // Download to cache
128
+ const cacheDir = join(PLUGIN_CACHE_DIR, entry.name, entry.version);
129
+ mkdirSync(cacheDir, { recursive: true });
130
+ try {
131
+ switch (entry.source.type) {
132
+ case 'github': {
133
+ // Clone the repo to cache
134
+ execSync(`git clone --depth 1 "https://github.com/${entry.source.repo}.git" "${cacheDir}"`, { stdio: 'pipe', timeout: 30_000 });
135
+ break;
136
+ }
137
+ case 'npm': {
138
+ // Install npm package to cache
139
+ execSync(`npm pack "${entry.source.package}" --pack-destination "${cacheDir}"`, { stdio: 'pipe', timeout: 30_000 });
140
+ // Extract the tarball
141
+ const tgz = readdirSync(cacheDir).find(f => f.endsWith('.tgz'));
142
+ if (tgz) {
143
+ execSync(`tar xzf "${join(cacheDir, tgz)}" -C "${cacheDir}" --strip-components=1`, { stdio: 'pipe' });
144
+ }
145
+ break;
146
+ }
147
+ case 'url': {
148
+ execSync(`curl -sL "${entry.source.url}" -o "${join(cacheDir, 'plugin.tar.gz')}"`, { stdio: 'pipe', timeout: 30_000 });
149
+ execSync(`tar xzf "${join(cacheDir, 'plugin.tar.gz')}" -C "${cacheDir}"`, { stdio: 'pipe' });
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ catch {
155
+ // Clean up failed install
156
+ try {
157
+ rmSync(cacheDir, { recursive: true });
158
+ }
159
+ catch { /* ignore */ }
160
+ return null;
161
+ }
162
+ // Record installation
163
+ const installed = {
164
+ name: entry.name,
165
+ version: entry.version,
166
+ marketplace: fromMarketplace,
167
+ installedAt: Date.now(),
168
+ cachePath: cacheDir,
169
+ };
170
+ saveInstalledPlugin(installed);
171
+ return installed;
172
+ }
173
+ /** Uninstall a plugin */
174
+ export function uninstallPlugin(name) {
175
+ const installed = getInstalledPlugins();
176
+ const plugin = installed.find(p => p.name === name);
177
+ if (!plugin)
178
+ return false;
179
+ // Remove from cache
180
+ try {
181
+ rmSync(plugin.cachePath, { recursive: true });
182
+ }
183
+ catch { /* ignore */ }
184
+ // Remove from installed list
185
+ const remaining = installed.filter(p => p.name !== name);
186
+ saveInstalledPluginList(remaining);
187
+ return true;
188
+ }
189
+ /** Get all installed plugins */
190
+ export function getInstalledPlugins() {
191
+ if (!existsSync(INSTALLED_PLUGINS_FILE))
192
+ return [];
193
+ try {
194
+ return JSON.parse(readFileSync(INSTALLED_PLUGINS_FILE, 'utf-8'));
195
+ }
196
+ catch {
197
+ return [];
198
+ }
199
+ }
200
+ function saveInstalledPlugin(plugin) {
201
+ const installed = getInstalledPlugins();
202
+ // Replace existing version
203
+ const idx = installed.findIndex(p => p.name === plugin.name);
204
+ if (idx >= 0)
205
+ installed[idx] = plugin;
206
+ else
207
+ installed.push(plugin);
208
+ saveInstalledPluginList(installed);
209
+ }
210
+ function saveInstalledPluginList(plugins) {
211
+ const dir = join(homedir(), '.oh', 'plugins');
212
+ mkdirSync(dir, { recursive: true });
213
+ writeFileSync(INSTALLED_PLUGINS_FILE, JSON.stringify(plugins, null, 2));
214
+ }
215
+ // ── Formatting ──
216
+ /** Format marketplace entries for display */
217
+ export function formatMarketplaceSearch(results) {
218
+ if (results.length === 0)
219
+ return 'No plugins found.';
220
+ const lines = [`Found ${results.length} plugin(s):\n`];
221
+ for (const r of results) {
222
+ lines.push(` ${r.name}@${r.version} [${r.marketplace}]`);
223
+ lines.push(` ${r.description}`);
224
+ if (r.author)
225
+ lines.push(` by ${r.author}`);
226
+ lines.push('');
227
+ }
228
+ lines.push('Install with: /plugin install <name>');
229
+ return lines.join('\n');
230
+ }
231
+ /** Format installed plugins for display */
232
+ export function formatInstalledPlugins(plugins) {
233
+ if (plugins.length === 0)
234
+ return 'No plugins installed from marketplaces.';
235
+ const lines = [`Installed Plugins (${plugins.length}):\n`];
236
+ for (const p of plugins) {
237
+ const age = Math.round((Date.now() - p.installedAt) / (1000 * 60 * 60 * 24));
238
+ lines.push(` ${p.name}@${p.version} [${p.marketplace}] ${age}d ago`);
239
+ }
240
+ return lines.join('\n');
241
+ }
242
+ //# sourceMappingURL=marketplace.js.map
@@ -42,7 +42,7 @@ export type AgentTeamConfig = {
42
42
  tools?: string[];
43
43
  }>;
44
44
  };
45
- /** Discover all available skills from project + global dirs */
45
+ /** Discover all available skills from project + global dirs + installed plugins */
46
46
  export declare function discoverSkills(): SkillMetadata[];
47
47
  /** Find a skill by name (case-insensitive) */
48
48
  export declare function findSkill(name: string): SkillMetadata | null;
@@ -67,11 +67,25 @@ function loadSkillsFromDir(dir, source) {
67
67
  })
68
68
  .filter((s) => s !== null);
69
69
  }
70
- /** Discover all available skills from project + global dirs */
70
+ /** Discover all available skills from project + global dirs + installed plugins */
71
71
  export function discoverSkills() {
72
72
  const skills = [];
73
73
  skills.push(...loadSkillsFromDir(PROJECT_SKILLS_DIR, 'project'));
74
74
  skills.push(...loadSkillsFromDir(GLOBAL_SKILLS_DIR, 'global'));
75
+ // Load skills from installed marketplace plugins (namespaced as plugin-name:skill-name)
76
+ try {
77
+ const { getInstalledPlugins } = require('./marketplace.js');
78
+ for (const plugin of getInstalledPlugins()) {
79
+ const pluginSkillsDir = join(plugin.cachePath, 'skills');
80
+ const pluginSkills = loadSkillsFromDir(pluginSkillsDir, 'plugin');
81
+ // Namespace: prefix skill name with plugin name
82
+ for (const skill of pluginSkills) {
83
+ skill.name = `${plugin.name}:${skill.name}`;
84
+ }
85
+ skills.push(...pluginSkills);
86
+ }
87
+ }
88
+ catch { /* marketplace module may not be loaded yet */ }
75
89
  return skills;
76
90
  }
77
91
  /** Find a skill by name (case-insensitive) */