oathbound 0.12.0 → 0.13.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/agent-push.ts +170 -0
- package/agent-search.ts +155 -0
- package/cli.ts +134 -2
- package/package.json +4 -2
- package/ui.ts +23 -1
package/agent-push.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { intro, outro } from '@clack/prompts';
|
|
4
|
+
import { parse as yamlParse } from 'yaml';
|
|
5
|
+
import { BRAND, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
6
|
+
import { getAccessToken } from './auth';
|
|
7
|
+
import { isValidSemver } from './semver';
|
|
8
|
+
|
|
9
|
+
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
10
|
+
|
|
11
|
+
/** Parse YAML frontmatter from an agent .md file. */
|
|
12
|
+
function parseAgentFrontmatter(content: string): {
|
|
13
|
+
meta: Record<string, unknown>;
|
|
14
|
+
body: string;
|
|
15
|
+
} {
|
|
16
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
17
|
+
if (!match) return { meta: {}, body: content };
|
|
18
|
+
const parsed = yamlParse(match[1]);
|
|
19
|
+
const meta: Record<string, unknown> =
|
|
20
|
+
parsed && typeof parsed === 'object' ? parsed : {};
|
|
21
|
+
return { meta, body: match[2] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Resolve agent .md file path from user argument or auto-detect. */
|
|
25
|
+
function resolveAgentFile(pathArg?: string): string {
|
|
26
|
+
if (pathArg) {
|
|
27
|
+
const resolved = resolve(pathArg);
|
|
28
|
+
if (!existsSync(resolved)) {
|
|
29
|
+
fail('File not found', resolved);
|
|
30
|
+
}
|
|
31
|
+
if (statSync(resolved).isDirectory()) {
|
|
32
|
+
// Look for a single .md file with agent frontmatter in the directory
|
|
33
|
+
return findAgentInDir(resolved);
|
|
34
|
+
}
|
|
35
|
+
if (!resolved.endsWith('.md')) {
|
|
36
|
+
fail('Invalid file', 'Agent files must be .md files');
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No path — look in cwd for a single .md with agent frontmatter
|
|
42
|
+
return findAgentInDir(process.cwd());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Find a single agent .md file in a directory. */
|
|
46
|
+
function findAgentInDir(dir: string): string {
|
|
47
|
+
const mdFiles = readdirSync(dir)
|
|
48
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
49
|
+
.map(f => join(dir, f));
|
|
50
|
+
|
|
51
|
+
const agents: string[] = [];
|
|
52
|
+
for (const file of mdFiles) {
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(file, 'utf-8');
|
|
55
|
+
const { meta, body } = parseAgentFrontmatter(content);
|
|
56
|
+
if (meta.name && meta.description && body.trim()) {
|
|
57
|
+
agents.push(file);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Skip unreadable files
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (agents.length === 0) {
|
|
65
|
+
fail(
|
|
66
|
+
'No agent file found',
|
|
67
|
+
'Run from a directory with an agent .md file, or pass a path: oathbound agent push ./my-agent.md',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (agents.length > 1) {
|
|
72
|
+
fail(
|
|
73
|
+
'Multiple agent files found',
|
|
74
|
+
`Found ${agents.length} .md files with agent frontmatter. Specify which one: oathbound agent push ./file.md`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return agents[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function agentPush(pathArg?: string, options?: { private?: boolean }): Promise<void> {
|
|
82
|
+
intro(BRAND);
|
|
83
|
+
|
|
84
|
+
const agentFile = resolveAgentFile(pathArg);
|
|
85
|
+
const content = readFileSync(agentFile, 'utf-8');
|
|
86
|
+
const { meta, body } = parseAgentFrontmatter(content);
|
|
87
|
+
|
|
88
|
+
// Validate required fields
|
|
89
|
+
const name = String(meta.name ?? '');
|
|
90
|
+
const description = String(meta.description ?? '');
|
|
91
|
+
const license = String(meta.license ?? '');
|
|
92
|
+
if (!name) fail('Frontmatter missing: name');
|
|
93
|
+
if (!description) fail('Frontmatter missing: description');
|
|
94
|
+
if (!license) fail('Frontmatter missing: license');
|
|
95
|
+
if (!body.trim()) fail('No system prompt (markdown body) after frontmatter');
|
|
96
|
+
|
|
97
|
+
const rawVersion = meta.version != null ? String(meta.version) : null;
|
|
98
|
+
const version = rawVersion && isValidSemver(rawVersion) ? rawVersion : null;
|
|
99
|
+
|
|
100
|
+
console.log(`${DIM} file: ${agentFile}${RESET}`);
|
|
101
|
+
console.log(`${DIM} name: ${name}${RESET}`);
|
|
102
|
+
console.log(`${DIM} version: ${version ?? 'auto (next)'}${RESET}`);
|
|
103
|
+
console.log(`${DIM} license: ${license}${RESET}`);
|
|
104
|
+
if (meta.model) console.log(`${DIM} model: ${meta.model}${RESET}`);
|
|
105
|
+
if (meta.permissionMode) console.log(`${DIM} permissionMode: ${meta.permissionMode}${RESET}`);
|
|
106
|
+
|
|
107
|
+
const visibility = options?.private ? 'private' : 'public';
|
|
108
|
+
if (options?.private) {
|
|
109
|
+
console.log(`${DIM} visibility: ${BOLD}private${RESET}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Authenticate
|
|
113
|
+
const token = await getAccessToken();
|
|
114
|
+
|
|
115
|
+
// Build request body
|
|
116
|
+
const requestBody: Record<string, unknown> = {
|
|
117
|
+
name,
|
|
118
|
+
description,
|
|
119
|
+
license,
|
|
120
|
+
version,
|
|
121
|
+
systemPrompt: body,
|
|
122
|
+
tools: meta.tools != null ? String(meta.tools) : null,
|
|
123
|
+
disallowedTools: meta.disallowedTools != null ? String(meta.disallowedTools) : null,
|
|
124
|
+
model: meta.model != null ? String(meta.model) : null,
|
|
125
|
+
permissionMode: meta.permissionMode != null ? String(meta.permissionMode) : null,
|
|
126
|
+
maxTurns: meta.maxTurns != null ? Number(meta.maxTurns) : null,
|
|
127
|
+
memoryScope: meta.memory != null ? String(meta.memory) : null,
|
|
128
|
+
background: meta.background != null ? Boolean(meta.background) : null,
|
|
129
|
+
effort: meta.effort != null ? String(meta.effort) : null,
|
|
130
|
+
isolation: meta.isolation != null ? String(meta.isolation) : null,
|
|
131
|
+
config: {
|
|
132
|
+
hooks: meta.hooks ?? null,
|
|
133
|
+
mcpServers: meta.mcpServers ?? null,
|
|
134
|
+
skillsRefs: Array.isArray(meta.skills) ? meta.skills : null,
|
|
135
|
+
initialPrompt: meta.initialPrompt != null ? String(meta.initialPrompt) : null,
|
|
136
|
+
},
|
|
137
|
+
compatibility: meta.compatibility != null ? String(meta.compatibility) : null,
|
|
138
|
+
originalAuthor: meta['original-author'] != null ? String(meta['original-author']) : null,
|
|
139
|
+
visibility,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Push to API
|
|
143
|
+
const spin = spinner('Pushing...');
|
|
144
|
+
|
|
145
|
+
const response = await fetch(`${API_BASE}/api/agents`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
Authorization: `Bearer ${token}`,
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify(requestBody),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
spin.stop();
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const resBody = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
158
|
+
const details = Array.isArray(resBody.details)
|
|
159
|
+
? '\n' + resBody.details.map((d: string) => ` - ${d}`).join('\n')
|
|
160
|
+
: '';
|
|
161
|
+
fail(`Push failed (${response.status})`, `${resBody.error}${details}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await response.json();
|
|
165
|
+
|
|
166
|
+
outro(`${GREEN}✓ Published ${BOLD}${result.namespace}/${result.name}${RESET}${GREEN} v${result.version}${RESET}`);
|
|
167
|
+
if (result.suiObjectId) {
|
|
168
|
+
console.log(`${DIM} on-chain: ${result.suiObjectId}${RESET}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
package/agent-search.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { BRAND, TEAL, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
2
|
+
|
|
3
|
+
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
4
|
+
|
|
5
|
+
export interface AgentSearchOptions {
|
|
6
|
+
query?: string;
|
|
7
|
+
namespace?: string;
|
|
8
|
+
sparse?: boolean;
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseAgentSearchArgs(args: string[]): AgentSearchOptions {
|
|
14
|
+
const opts: AgentSearchOptions = {};
|
|
15
|
+
let i = 0;
|
|
16
|
+
|
|
17
|
+
while (i < args.length) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
|
|
20
|
+
if (arg === '--user' || arg === '-u') {
|
|
21
|
+
opts.namespace = args[++i];
|
|
22
|
+
} else if (arg === '--sparse' || arg === '-s') {
|
|
23
|
+
opts.sparse = true;
|
|
24
|
+
} else if (arg === '--limit') {
|
|
25
|
+
opts.limit = parseInt(args[++i], 10);
|
|
26
|
+
} else if (arg === '--offset') {
|
|
27
|
+
opts.offset = parseInt(args[++i], 10);
|
|
28
|
+
} else if (!arg.startsWith('-')) {
|
|
29
|
+
opts.query = arg;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return opts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface AgentAuthor {
|
|
39
|
+
username: string;
|
|
40
|
+
display_name: string | null;
|
|
41
|
+
verified: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface AgentResult {
|
|
45
|
+
name: string;
|
|
46
|
+
namespace: string;
|
|
47
|
+
description: string;
|
|
48
|
+
version: string;
|
|
49
|
+
license?: string;
|
|
50
|
+
visibility?: string;
|
|
51
|
+
model?: string | null;
|
|
52
|
+
tools?: string | null;
|
|
53
|
+
permission_mode?: string | null;
|
|
54
|
+
effort?: string | null;
|
|
55
|
+
author?: AgentAuthor;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AgentSearchResponse {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
agents: AgentResult[];
|
|
61
|
+
total: number;
|
|
62
|
+
limit: number;
|
|
63
|
+
offset: number;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function agentSearch(opts: AgentSearchOptions): Promise<void> {
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
if (opts.query) params.set('q', opts.query);
|
|
70
|
+
if (opts.namespace) params.set('namespace', opts.namespace);
|
|
71
|
+
if (opts.sparse) params.set('sparse', 'true');
|
|
72
|
+
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
73
|
+
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
74
|
+
|
|
75
|
+
const url = `${API_BASE}/api/agents?${params}`;
|
|
76
|
+
|
|
77
|
+
const sp = spinner('Searching agents...');
|
|
78
|
+
|
|
79
|
+
let res: Response;
|
|
80
|
+
try {
|
|
81
|
+
res = await fetch(url);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
sp.stop();
|
|
84
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
85
|
+
fail('Search failed', msg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sp.stop();
|
|
89
|
+
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
let detail = `HTTP ${res.status}`;
|
|
92
|
+
try {
|
|
93
|
+
const body = await res.json() as { error?: string };
|
|
94
|
+
if (body.error) detail = body.error;
|
|
95
|
+
} catch { /* ignore parse errors */ }
|
|
96
|
+
fail('Search failed', detail);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = await res.json() as AgentSearchResponse;
|
|
100
|
+
|
|
101
|
+
if (!data.ok || !data.agents) {
|
|
102
|
+
fail('Search failed', data.error ?? 'Unexpected response');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { agents, total, offset } = data;
|
|
106
|
+
|
|
107
|
+
if (agents.length === 0) {
|
|
108
|
+
console.log(`\n${BRAND} ${DIM}No agents found.${RESET}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const showing = offset > 0
|
|
113
|
+
? `Showing ${offset + 1}–${offset + agents.length} of ${total}`
|
|
114
|
+
: `${total} agent${total === 1 ? '' : 's'} found`;
|
|
115
|
+
|
|
116
|
+
console.log(`\n${BRAND} ${TEAL}${showing}${RESET}\n`);
|
|
117
|
+
|
|
118
|
+
for (const agent of agents) {
|
|
119
|
+
const id = `${agent.namespace}/${agent.name}`;
|
|
120
|
+
const ver = `v${agent.version}`;
|
|
121
|
+
|
|
122
|
+
// Line 1: name + version
|
|
123
|
+
console.log(` ${BOLD}${id}${RESET} ${DIM}${ver}${RESET}`);
|
|
124
|
+
|
|
125
|
+
// Line 2: description
|
|
126
|
+
if (agent.description) {
|
|
127
|
+
console.log(` ${DIM}${agent.description}${RESET}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Line 3: agent-specific metadata (non-sparse only)
|
|
131
|
+
if (!opts.sparse) {
|
|
132
|
+
const parts: string[] = [];
|
|
133
|
+
if (agent.author) {
|
|
134
|
+
const name = agent.author.display_name || agent.author.username;
|
|
135
|
+
parts.push(`by ${name}${agent.author.verified ? ' ✓' : ''}`);
|
|
136
|
+
}
|
|
137
|
+
if (agent.license) parts.push(agent.license);
|
|
138
|
+
if (agent.model) parts.push(`model: ${agent.model}`);
|
|
139
|
+
if (agent.permission_mode) parts.push(`mode: ${agent.permission_mode}`);
|
|
140
|
+
if (agent.effort) parts.push(`effort: ${agent.effort}`);
|
|
141
|
+
if (agent.visibility === 'private') parts.push('private');
|
|
142
|
+
if (parts.length > 0) {
|
|
143
|
+
console.log(` ${DIM}${parts.join(' · ')}${RESET}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(); // blank line between agents
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Pagination hint
|
|
151
|
+
if (offset + agents.length < total) {
|
|
152
|
+
const nextOffset = offset + agents.length;
|
|
153
|
+
console.log(`${DIM} Use --offset ${nextOffset} to see more${RESET}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/cli.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { join, basename } from 'node:path';
|
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { intro, outro, select, confirm, cancel, isCancel } from '@clack/prompts';
|
|
12
12
|
|
|
13
|
-
import { BRAND, TEAL, GREEN, RED, YELLOW, DIM, BOLD, RESET, usage, fail, spinner } from './ui';
|
|
13
|
+
import { BRAND, TEAL, GREEN, RED, YELLOW, DIM, BOLD, RESET, usage, agentUsage, fail, spinner } from './ui';
|
|
14
14
|
import {
|
|
15
15
|
stripJsoncComments, writeOathboundConfig, mergeClaudeSettings,
|
|
16
16
|
type EnforcementLevel, type MergeResult,
|
|
@@ -21,13 +21,15 @@ import { verify, verifyCheck, findSkillsDir } from './verify';
|
|
|
21
21
|
import { login, logout, whoami } from './auth';
|
|
22
22
|
import { push } from './push';
|
|
23
23
|
import { search, parseSearchArgs } from './search';
|
|
24
|
+
import { agentPush } from './agent-push';
|
|
25
|
+
import { agentSearch, parseAgentSearchArgs } from './agent-search';
|
|
24
26
|
|
|
25
27
|
// Re-exports for tests
|
|
26
28
|
export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type MergeResult } from './config';
|
|
27
29
|
export { isNewer } from './update';
|
|
28
30
|
export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult, addTrustedDependency, type TrustedDepResult };
|
|
29
31
|
|
|
30
|
-
const VERSION = '0.
|
|
32
|
+
const VERSION = '0.13.0';
|
|
31
33
|
|
|
32
34
|
// --- Supabase ---
|
|
33
35
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
@@ -350,6 +352,131 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
350
352
|
console.log(`${DIM} → ${join(skillsDir, name)}${RESET}`);
|
|
351
353
|
}
|
|
352
354
|
|
|
355
|
+
// --- Agent types ---
|
|
356
|
+
interface AgentRow {
|
|
357
|
+
name: string;
|
|
358
|
+
namespace: string;
|
|
359
|
+
version: string;
|
|
360
|
+
content_hash: string;
|
|
361
|
+
storage_path: string;
|
|
362
|
+
config: Record<string, unknown> | null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Agent pull ---
|
|
366
|
+
async function agentPull(agentArg: string): Promise<void> {
|
|
367
|
+
const parsed = parseSkillArg(agentArg); // Same namespace/name[@version] format
|
|
368
|
+
if (!parsed) usage();
|
|
369
|
+
const { namespace, name, version } = parsed;
|
|
370
|
+
const fullName = `${namespace}/${name}`;
|
|
371
|
+
|
|
372
|
+
console.log(`\n${BRAND} ${TEAL}↓ Pulling agent ${fullName}${version ? `@${version}` : ''}...${RESET}`);
|
|
373
|
+
|
|
374
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
375
|
+
|
|
376
|
+
// Query for the agent
|
|
377
|
+
let agent: AgentRow;
|
|
378
|
+
|
|
379
|
+
if (version !== null) {
|
|
380
|
+
const { data, error } = await supabase
|
|
381
|
+
.from('agents')
|
|
382
|
+
.select('name, namespace, version, content_hash, storage_path, config')
|
|
383
|
+
.eq('namespace', namespace)
|
|
384
|
+
.eq('name', name)
|
|
385
|
+
.eq('version', version)
|
|
386
|
+
.single<AgentRow>();
|
|
387
|
+
|
|
388
|
+
if (error || !data) {
|
|
389
|
+
fail(`Agent not found: ${fullName}@${version}`);
|
|
390
|
+
}
|
|
391
|
+
agent = data;
|
|
392
|
+
} else {
|
|
393
|
+
const { data, error } = await supabase
|
|
394
|
+
.from('agents')
|
|
395
|
+
.select('name, namespace, version, content_hash, storage_path, config')
|
|
396
|
+
.eq('namespace', namespace)
|
|
397
|
+
.eq('name', name);
|
|
398
|
+
|
|
399
|
+
if (error || !data || data.length === 0) {
|
|
400
|
+
fail(`Agent not found: ${fullName}`);
|
|
401
|
+
}
|
|
402
|
+
agent = (data as AgentRow[]).sort((a, b) => compareSemver(a.version, b.version)).at(-1)!;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Download from storage
|
|
406
|
+
const { data: blob, error: downloadError } = await supabase
|
|
407
|
+
.storage
|
|
408
|
+
.from('agents')
|
|
409
|
+
.download(agent.storage_path);
|
|
410
|
+
|
|
411
|
+
if (downloadError || !blob) {
|
|
412
|
+
fail('Download failed', downloadError?.message ?? 'Unknown storage error');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const content = await blob.text();
|
|
416
|
+
|
|
417
|
+
// Verify content hash
|
|
418
|
+
const verifySpinner = spinner('Verifying...');
|
|
419
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
420
|
+
verifySpinner.stop();
|
|
421
|
+
|
|
422
|
+
console.log(`${DIM} content hash: ${hash}${RESET}`);
|
|
423
|
+
|
|
424
|
+
if (hash !== agent.content_hash) {
|
|
425
|
+
console.log(`${RED} expected: ${agent.content_hash}${RESET}`);
|
|
426
|
+
fail('Verification failed', `Downloaded file does not match expected hash for ${fullName}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Warn about hooks/mcpServers if present
|
|
430
|
+
const config = agent.config;
|
|
431
|
+
if (config?.hooks) {
|
|
432
|
+
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines hooks:${RESET}`);
|
|
433
|
+
console.log(`${DIM}${JSON.stringify(config.hooks, null, 2)}${RESET}\n`);
|
|
434
|
+
}
|
|
435
|
+
if (config?.mcpServers) {
|
|
436
|
+
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines MCP servers:${RESET}`);
|
|
437
|
+
console.log(`${DIM}${JSON.stringify(config.mcpServers, null, 2)}${RESET}\n`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Ensure .claude/agents/ directory exists
|
|
441
|
+
const agentsDir = join(process.cwd(), '.claude', 'agents');
|
|
442
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
443
|
+
|
|
444
|
+
// Write agent file
|
|
445
|
+
const targetPath = join(agentsDir, `${name}.md`);
|
|
446
|
+
writeFileSync(targetPath, content);
|
|
447
|
+
|
|
448
|
+
console.log(`${BOLD}${GREEN} ✓ Agent verified${RESET}`);
|
|
449
|
+
console.log(`${DIM} ${fullName} v${agent.version}${RESET}`);
|
|
450
|
+
console.log(`${DIM} → ${targetPath}${RESET}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --- Agent subcommand router ---
|
|
454
|
+
async function handleAgent(agentArgs: string[]): Promise<void> {
|
|
455
|
+
const agentSub = agentArgs[0];
|
|
456
|
+
|
|
457
|
+
if (!agentSub || agentSub === '--help' || agentSub === '-h') {
|
|
458
|
+
agentUsage(agentSub ? 0 : 1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (agentSub === 'push') {
|
|
462
|
+
const pushArgs = agentArgs.slice(1);
|
|
463
|
+
const isPrivate = pushArgs.includes('--private');
|
|
464
|
+
const pushPath = pushArgs.find(a => !a.startsWith('--'));
|
|
465
|
+
await agentPush(pushPath, { private: isPrivate });
|
|
466
|
+
} else if (agentSub === 'pull' || agentSub === 'install' || agentSub === 'i') {
|
|
467
|
+
const target = agentArgs[1];
|
|
468
|
+
if (!target) {
|
|
469
|
+
fail('Missing agent name', 'Usage: oathbound agent pull <namespace/name[@version]>');
|
|
470
|
+
}
|
|
471
|
+
await agentPull(target);
|
|
472
|
+
} else if (agentSub === 'search' || agentSub === 'list' || agentSub === 'ls') {
|
|
473
|
+
const searchOpts = parseAgentSearchArgs(agentArgs.slice(1));
|
|
474
|
+
await agentSearch(searchOpts);
|
|
475
|
+
} else {
|
|
476
|
+
agentUsage();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
353
480
|
// --- Entry ---
|
|
354
481
|
if (!import.meta.main) {
|
|
355
482
|
// Module imported for testing — skip CLI entry
|
|
@@ -417,6 +544,11 @@ if (subcommand === 'init') {
|
|
|
417
544
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
418
545
|
fail('Search failed', msg);
|
|
419
546
|
});
|
|
547
|
+
} else if (subcommand === 'agent') {
|
|
548
|
+
handleAgent(args.slice(1)).catch((err: unknown) => {
|
|
549
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
550
|
+
fail('Agent command failed', msg);
|
|
551
|
+
});
|
|
420
552
|
} else {
|
|
421
553
|
const PULL_ALIASES = new Set(['pull', 'i', 'install']);
|
|
422
554
|
const skillArg = args[1];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Install verified Claude Code skills from the Oath Bound registry",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Install verified Claude Code skills and agents from the Oath Bound registry",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Josh Anderson",
|
|
7
7
|
"homepage": "https://oathbound.ai",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"auth.ts",
|
|
33
33
|
"push.ts",
|
|
34
34
|
"search.ts",
|
|
35
|
+
"agent-push.ts",
|
|
36
|
+
"agent-search.ts",
|
|
35
37
|
"semver.ts",
|
|
36
38
|
"bin/cli.cjs",
|
|
37
39
|
"install.cjs"
|
package/ui.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const BRAND = brand();
|
|
|
18
18
|
|
|
19
19
|
export function usage(exitCode = 1): never {
|
|
20
20
|
console.log(`
|
|
21
|
-
${BOLD}oathbound${RESET} — install, verify, and publish skills
|
|
21
|
+
${BOLD}oathbound${RESET} — install, verify, and publish skills & agents
|
|
22
22
|
|
|
23
23
|
${DIM}Usage:${RESET}
|
|
24
24
|
oathbound init ${DIM}Setup wizard — configure project${RESET}
|
|
@@ -33,6 +33,12 @@ ${DIM}Usage:${RESET}
|
|
|
33
33
|
oathbound verify ${DIM}SessionStart hook — verify all skills${RESET}
|
|
34
34
|
oathbound verify --check ${DIM}PreToolUse hook — check skill integrity${RESET}
|
|
35
35
|
|
|
36
|
+
${DIM}Agents:${RESET}
|
|
37
|
+
oathbound agent push [path] [--private] ${DIM}Publish an agent .md file${RESET}
|
|
38
|
+
oathbound agent pull <namespace/name[@version]>
|
|
39
|
+
oathbound agent search [query] ${DIM}Search agents in the registry${RESET}
|
|
40
|
+
oathbound agent list ${DIM}List all public agents${RESET}
|
|
41
|
+
|
|
36
42
|
${DIM}Options:${RESET}
|
|
37
43
|
--help, -h Show this help message
|
|
38
44
|
--version, -v Show version
|
|
@@ -40,6 +46,22 @@ ${DIM}Options:${RESET}
|
|
|
40
46
|
process.exit(exitCode);
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export function agentUsage(exitCode = 1): never {
|
|
50
|
+
console.log(`
|
|
51
|
+
${BOLD}oathbound agent${RESET} — manage Claude Code agents
|
|
52
|
+
|
|
53
|
+
${DIM}Usage:${RESET}
|
|
54
|
+
oathbound agent push [path] [--private] ${DIM}Publish an agent .md file to the registry${RESET}
|
|
55
|
+
oathbound agent pull <namespace/name[@version]> ${DIM}Download agent to .claude/agents/${RESET}
|
|
56
|
+
oathbound agent search [query] ${DIM}Search agents in the registry${RESET}
|
|
57
|
+
oathbound agent list ${DIM}List all public agents${RESET}
|
|
58
|
+
|
|
59
|
+
${DIM}Options:${RESET}
|
|
60
|
+
--help, -h Show this help message
|
|
61
|
+
`);
|
|
62
|
+
process.exit(exitCode);
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
export function fail(message: string, detail?: string): never {
|
|
44
66
|
console.log(`\n${BOLD}${RED} ✗ ${message}${RESET}`);
|
|
45
67
|
if (detail) {
|