@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.
- package/README.md +741 -741
- package/dist/agents/roles.d.ts +4 -2
- package/dist/agents/roles.js +69 -5
- package/dist/commands/index.js +87 -29
- package/dist/harness/config.d.ts +17 -0
- package/dist/harness/marketplace.d.ts +62 -0
- package/dist/harness/marketplace.js +242 -0
- package/dist/harness/plugins.d.ts +1 -1
- package/dist/harness/plugins.js +15 -1
- package/dist/harness/telemetry.d.ts +59 -0
- package/dist/harness/telemetry.js +129 -0
- package/dist/main.js +40 -40
- package/dist/providers/router.d.ts +48 -0
- package/dist/providers/router.js +61 -0
- package/dist/query/compress.d.ts +5 -0
- package/dist/query/compress.js +45 -4
- package/dist/remote/auth.d.ts +25 -0
- package/dist/remote/auth.js +73 -0
- package/dist/remote/server.d.ts +18 -2
- package/dist/remote/server.js +168 -39
- package/dist/repl.js +8 -0
- package/dist/services/PipelineExecutor.d.ts +48 -0
- package/dist/services/PipelineExecutor.js +179 -0
- package/dist/services/a2a.d.ts +119 -0
- package/dist/services/a2a.js +176 -0
- package/dist/tools/PipelineTool/index.d.ts +40 -0
- package/dist/tools/PipelineTool/index.js +53 -0
- package/dist/tools/WebFetchTool/index.js +2 -2
- package/dist/tools.js +3 -0
- package/package.json +73 -73
package/dist/agents/roles.d.ts
CHANGED
|
@@ -16,9 +16,11 @@ export type AgentRole = {
|
|
|
16
16
|
/** Suggested tools to include (empty = all tools) */
|
|
17
17
|
suggestedTools?: string[];
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
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[];
|
package/dist/agents/roles.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
234
|
+
return listRoles().map(r => r.id);
|
|
171
235
|
}
|
|
172
236
|
//# sourceMappingURL=roles.js.map
|
package/dist/commands/index.js
CHANGED
|
@@ -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", "
|
|
638
|
+
register("plugins", "Manage plugins: list, search, install, uninstall, marketplace", (args) => {
|
|
618
639
|
const { discoverPlugins, discoverSkills } = require('../harness/plugins.js');
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
//
|
|
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(`
|
|
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(`
|
|
645
|
-
const bySource = {};
|
|
704
|
+
lines.push(`Skills (${skills.length}):`);
|
|
646
705
|
for (const s of skills) {
|
|
647
|
-
(
|
|
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
|
-
|
|
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 ──
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/harness/plugins.js
CHANGED
|
@@ -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) */
|