@zhijiewang/openharness 1.2.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
@@ -635,51 +635,88 @@ function setPinned(args, ctx, pinned) {
635
635
  }
636
636
  register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
637
637
  register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
638
- register("plugins", "List installed plugins and discover new ones", (args) => {
638
+ register("plugins", "Manage plugins: list, search, install, uninstall, marketplace", (args) => {
639
639
  const { discoverPlugins, discoverSkills } = require('../harness/plugins.js');
640
- const query = args.trim();
641
- if (query === 'search' || query.startsWith('search ')) {
642
- // npm registry search
643
- const keyword = query.replace(/^search\s*/, '').trim() || 'openharness-plugin';
644
- return {
645
- 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.`,
646
- handled: true,
647
- };
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 };
682
+ }
683
+ // /plugins uninstall <name>
684
+ if (subcommand === 'uninstall' && rest) {
685
+ return { output: uninstallPlugin(rest) ? `Uninstalled "${rest}"` : `Plugin "${rest}" not found`, handled: true };
648
686
  }
649
- // List installed
687
+ // /plugins (no args) — show everything
650
688
  const plugins = discoverPlugins();
651
689
  const skills = discoverSkills();
690
+ const marketplacePlugins = getInstalledPlugins();
652
691
  const lines = [];
692
+ if (marketplacePlugins.length > 0) {
693
+ lines.push(formatInstalledPlugins(marketplacePlugins));
694
+ lines.push('');
695
+ }
653
696
  if (plugins.length > 0) {
654
- lines.push(`Installed Plugins (${plugins.length}):`);
697
+ lines.push(`Local Plugins (${plugins.length}):`);
655
698
  for (const p of plugins) {
656
699
  lines.push(` ${p.name}@${p.version} — ${p.description || 'no description'}`);
657
- if (p.skills?.length)
658
- lines.push(` Skills: ${p.skills.length}`);
659
- if (p.mcpServers?.length)
660
- lines.push(` MCP servers: ${p.mcpServers.map((s) => s.name).join(', ')}`);
661
700
  }
662
701
  lines.push('');
663
702
  }
664
703
  if (skills.length > 0) {
665
- lines.push(`Available Skills (${skills.length}):`);
666
- const bySource = {};
704
+ lines.push(`Skills (${skills.length}):`);
667
705
  for (const s of skills) {
668
- (bySource[s.source] ??= []).push(s);
669
- }
670
- for (const [source, sourceSkills] of Object.entries(bySource)) {
671
- lines.push(` ${source}:`);
672
- for (const s of sourceSkills) {
673
- lines.push(` ${s.name} — ${s.description}${s.trigger ? ` (trigger: "${s.trigger}")` : ''}`);
674
- }
706
+ lines.push(` ${s.source}:${s.name} ${s.description || ''}`);
675
707
  }
708
+ lines.push('');
676
709
  }
677
- else if (plugins.length === 0) {
710
+ if (lines.length === 0) {
678
711
  lines.push('No plugins or skills installed.');
679
- lines.push('');
680
- lines.push('Create skills in .oh/skills/ or ~/.oh/skills/');
681
- lines.push('Run /plugins search to find npm packages.');
682
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');
683
720
  return { output: lines.join('\n'), handled: true };
684
721
  });
685
722
  // ── Command Parser ──
@@ -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) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {