oathbound 0.13.0 → 0.14.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-search.ts +7 -0
- package/cli.ts +66 -14
- package/package.json +2 -2
- package/search.ts +7 -0
package/agent-search.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface AgentSearchOptions {
|
|
|
6
6
|
query?: string;
|
|
7
7
|
namespace?: string;
|
|
8
8
|
sparse?: boolean;
|
|
9
|
+
sort?: 'downloads';
|
|
9
10
|
limit?: number;
|
|
10
11
|
offset?: number;
|
|
11
12
|
}
|
|
@@ -21,6 +22,9 @@ export function parseAgentSearchArgs(args: string[]): AgentSearchOptions {
|
|
|
21
22
|
opts.namespace = args[++i];
|
|
22
23
|
} else if (arg === '--sparse' || arg === '-s') {
|
|
23
24
|
opts.sparse = true;
|
|
25
|
+
} else if (arg === '--sort') {
|
|
26
|
+
const val = args[++i];
|
|
27
|
+
if (val === 'downloads') opts.sort = 'downloads';
|
|
24
28
|
} else if (arg === '--limit') {
|
|
25
29
|
opts.limit = parseInt(args[++i], 10);
|
|
26
30
|
} else if (arg === '--offset') {
|
|
@@ -53,6 +57,7 @@ interface AgentResult {
|
|
|
53
57
|
permission_mode?: string | null;
|
|
54
58
|
effort?: string | null;
|
|
55
59
|
author?: AgentAuthor;
|
|
60
|
+
download_count?: number;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
interface AgentSearchResponse {
|
|
@@ -69,6 +74,7 @@ export async function agentSearch(opts: AgentSearchOptions): Promise<void> {
|
|
|
69
74
|
if (opts.query) params.set('q', opts.query);
|
|
70
75
|
if (opts.namespace) params.set('namespace', opts.namespace);
|
|
71
76
|
if (opts.sparse) params.set('sparse', 'true');
|
|
77
|
+
if (opts.sort) params.set('sort', opts.sort);
|
|
72
78
|
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
73
79
|
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
74
80
|
|
|
@@ -135,6 +141,7 @@ export async function agentSearch(opts: AgentSearchOptions): Promise<void> {
|
|
|
135
141
|
parts.push(`by ${name}${agent.author.verified ? ' ✓' : ''}`);
|
|
136
142
|
}
|
|
137
143
|
if (agent.license) parts.push(agent.license);
|
|
144
|
+
if (agent.download_count != null) parts.push(`↓ ${agent.download_count}`);
|
|
138
145
|
if (agent.model) parts.push(`model: ${agent.model}`);
|
|
139
146
|
if (agent.permission_mode) parts.push(`mode: ${agent.permission_mode}`);
|
|
140
147
|
if (agent.effort) parts.push(`effort: ${agent.effort}`);
|
package/cli.ts
CHANGED
|
@@ -29,14 +29,16 @@ export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type Mer
|
|
|
29
29
|
export { isNewer } from './update';
|
|
30
30
|
export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult, addTrustedDependency, type TrustedDepResult };
|
|
31
31
|
|
|
32
|
-
const VERSION = '0.
|
|
32
|
+
const VERSION = '0.14.0';
|
|
33
33
|
|
|
34
34
|
// --- Supabase ---
|
|
35
35
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
36
36
|
const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
|
|
37
|
+
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
37
38
|
|
|
38
39
|
// --- Types ---
|
|
39
40
|
interface SkillRow {
|
|
41
|
+
id: string;
|
|
40
42
|
name: string;
|
|
41
43
|
namespace: string;
|
|
42
44
|
version: string;
|
|
@@ -279,7 +281,7 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
279
281
|
if (version !== null) {
|
|
280
282
|
const { data, error } = await supabase
|
|
281
283
|
.from('skills')
|
|
282
|
-
.select('name, namespace, version, tar_hash, storage_path')
|
|
284
|
+
.select('id, name, namespace, version, tar_hash, storage_path')
|
|
283
285
|
.eq('namespace', namespace)
|
|
284
286
|
.eq('name', name)
|
|
285
287
|
.eq('version', version)
|
|
@@ -293,7 +295,7 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
293
295
|
// Fetch all versions, pick highest via semver comparison
|
|
294
296
|
const { data, error } = await supabase
|
|
295
297
|
.from('skills')
|
|
296
|
-
.select('name, namespace, version, tar_hash, storage_path')
|
|
298
|
+
.select('id, name, namespace, version, tar_hash, storage_path')
|
|
297
299
|
.eq('namespace', namespace)
|
|
298
300
|
.eq('name', name);
|
|
299
301
|
|
|
@@ -346,7 +348,21 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
346
348
|
}
|
|
347
349
|
unlinkSync(tarFile);
|
|
348
350
|
|
|
349
|
-
// 5.
|
|
351
|
+
// 5. Record download (non-fatal)
|
|
352
|
+
try {
|
|
353
|
+
const trackRes = await fetch(`${API_BASE}/api/downloads`, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({ skill_id: skill.id, version: skill.version }),
|
|
357
|
+
});
|
|
358
|
+
if (!trackRes.ok) {
|
|
359
|
+
process.stderr.write(`${DIM} [warn] download tracking failed (${trackRes.status})${RESET}\n`);
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Network error — non-fatal
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 6. Success
|
|
350
366
|
console.log(`${BOLD}${GREEN} ✓ Skill verified${RESET}`);
|
|
351
367
|
console.log(`${DIM} ${fullName} v${skill.version}${RESET}`);
|
|
352
368
|
console.log(`${DIM} → ${join(skillsDir, name)}${RESET}`);
|
|
@@ -354,6 +370,7 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
354
370
|
|
|
355
371
|
// --- Agent types ---
|
|
356
372
|
interface AgentRow {
|
|
373
|
+
id: string;
|
|
357
374
|
name: string;
|
|
358
375
|
namespace: string;
|
|
359
376
|
version: string;
|
|
@@ -379,7 +396,7 @@ async function agentPull(agentArg: string): Promise<void> {
|
|
|
379
396
|
if (version !== null) {
|
|
380
397
|
const { data, error } = await supabase
|
|
381
398
|
.from('agents')
|
|
382
|
-
.select('name, namespace, version, content_hash, storage_path, config')
|
|
399
|
+
.select('id, name, namespace, version, content_hash, storage_path, config')
|
|
383
400
|
.eq('namespace', namespace)
|
|
384
401
|
.eq('name', name)
|
|
385
402
|
.eq('version', version)
|
|
@@ -392,7 +409,7 @@ async function agentPull(agentArg: string): Promise<void> {
|
|
|
392
409
|
} else {
|
|
393
410
|
const { data, error } = await supabase
|
|
394
411
|
.from('agents')
|
|
395
|
-
.select('name, namespace, version, content_hash, storage_path, config')
|
|
412
|
+
.select('id, name, namespace, version, content_hash, storage_path, config')
|
|
396
413
|
.eq('namespace', namespace)
|
|
397
414
|
.eq('name', name);
|
|
398
415
|
|
|
@@ -426,25 +443,60 @@ async function agentPull(agentArg: string): Promise<void> {
|
|
|
426
443
|
fail('Verification failed', `Downloaded file does not match expected hash for ${fullName}`);
|
|
427
444
|
}
|
|
428
445
|
|
|
429
|
-
//
|
|
446
|
+
// Validate name has no path traversal characters
|
|
447
|
+
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
|
448
|
+
fail('Invalid agent name', `Name "${name}" contains path traversal characters`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Ensure .claude/agents/ directory exists
|
|
452
|
+
const agentsDir = join(process.cwd(), '.claude', 'agents');
|
|
453
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
454
|
+
|
|
455
|
+
// Validate resolved path stays within agentsDir
|
|
456
|
+
const targetPath = join(agentsDir, `${name}.md`);
|
|
457
|
+
if (!targetPath.startsWith(agentsDir)) {
|
|
458
|
+
fail('Invalid agent name', `Resolved path escapes agents directory`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Warn and confirm if hooks/mcpServers are present
|
|
430
462
|
const config = agent.config;
|
|
463
|
+
let hasDangerous = false;
|
|
431
464
|
if (config?.hooks) {
|
|
432
|
-
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines hooks:${RESET}`);
|
|
465
|
+
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines hooks (arbitrary command execution):${RESET}`);
|
|
433
466
|
console.log(`${DIM}${JSON.stringify(config.hooks, null, 2)}${RESET}\n`);
|
|
467
|
+
hasDangerous = true;
|
|
434
468
|
}
|
|
435
469
|
if (config?.mcpServers) {
|
|
436
|
-
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines MCP servers:${RESET}`);
|
|
470
|
+
console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines MCP servers (external connections):${RESET}`);
|
|
437
471
|
console.log(`${DIM}${JSON.stringify(config.mcpServers, null, 2)}${RESET}\n`);
|
|
472
|
+
hasDangerous = true;
|
|
473
|
+
}
|
|
474
|
+
if (hasDangerous) {
|
|
475
|
+
const answer = await confirm({
|
|
476
|
+
message: 'This agent contains security-sensitive configuration. Install anyway?',
|
|
477
|
+
});
|
|
478
|
+
if (isCancel(answer) || !answer) {
|
|
479
|
+
fail('Aborted', 'Agent not installed');
|
|
480
|
+
}
|
|
438
481
|
}
|
|
439
|
-
|
|
440
|
-
// Ensure .claude/agents/ directory exists
|
|
441
|
-
const agentsDir = join(process.cwd(), '.claude', 'agents');
|
|
442
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
443
482
|
|
|
444
483
|
// Write agent file
|
|
445
|
-
const targetPath = join(agentsDir, `${name}.md`);
|
|
446
484
|
writeFileSync(targetPath, content);
|
|
447
485
|
|
|
486
|
+
// Record download (non-fatal)
|
|
487
|
+
try {
|
|
488
|
+
const trackRes = await fetch(`${API_BASE}/api/downloads`, {
|
|
489
|
+
method: 'POST',
|
|
490
|
+
headers: { 'Content-Type': 'application/json' },
|
|
491
|
+
body: JSON.stringify({ agent_id: agent.id, version: agent.version }),
|
|
492
|
+
});
|
|
493
|
+
if (!trackRes.ok) {
|
|
494
|
+
process.stderr.write(`${DIM} [warn] download tracking failed (${trackRes.status})${RESET}\n`);
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
// Network error — non-fatal
|
|
498
|
+
}
|
|
499
|
+
|
|
448
500
|
console.log(`${BOLD}${GREEN} ✓ Agent verified${RESET}`);
|
|
449
501
|
console.log(`${DIM} ${fullName} v${agent.version}${RESET}`);
|
|
450
502
|
console.log(`${DIM} → ${targetPath}${RESET}`);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Install verified Claude Code skills and agents from the Oath Bound registry",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Josh Anderson",
|
|
7
7
|
"homepage": "https://oathbound.ai",
|
|
8
8
|
"repository": {
|
package/search.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface SearchOptions {
|
|
|
6
6
|
query?: string;
|
|
7
7
|
namespace?: string;
|
|
8
8
|
sparse?: boolean;
|
|
9
|
+
sort?: 'downloads';
|
|
9
10
|
limit?: number;
|
|
10
11
|
offset?: number;
|
|
11
12
|
}
|
|
@@ -21,6 +22,9 @@ export function parseSearchArgs(args: string[]): SearchOptions {
|
|
|
21
22
|
opts.namespace = args[++i];
|
|
22
23
|
} else if (arg === '--sparse' || arg === '-s') {
|
|
23
24
|
opts.sparse = true;
|
|
25
|
+
} else if (arg === '--sort') {
|
|
26
|
+
const val = args[++i];
|
|
27
|
+
if (val === 'downloads') opts.sort = 'downloads';
|
|
24
28
|
} else if (arg === '--limit') {
|
|
25
29
|
opts.limit = parseInt(args[++i], 10);
|
|
26
30
|
} else if (arg === '--offset') {
|
|
@@ -50,6 +54,7 @@ interface SkillResult {
|
|
|
50
54
|
visibility?: string;
|
|
51
55
|
author?: SkillAuthor;
|
|
52
56
|
audit_status?: 'passed' | 'failed' | 'none';
|
|
57
|
+
download_count?: number;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
interface SearchResponse {
|
|
@@ -66,6 +71,7 @@ export async function search(opts: SearchOptions): Promise<void> {
|
|
|
66
71
|
if (opts.query) params.set('q', opts.query);
|
|
67
72
|
if (opts.namespace) params.set('namespace', opts.namespace);
|
|
68
73
|
if (opts.sparse) params.set('sparse', 'true');
|
|
74
|
+
if (opts.sort) params.set('sort', opts.sort);
|
|
69
75
|
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
70
76
|
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
71
77
|
|
|
@@ -132,6 +138,7 @@ export async function search(opts: SearchOptions): Promise<void> {
|
|
|
132
138
|
parts.push(`by ${name}${skill.author.verified ? ' ✓' : ''}`);
|
|
133
139
|
}
|
|
134
140
|
if (skill.license) parts.push(skill.license);
|
|
141
|
+
if (skill.download_count != null) parts.push(`↓ ${skill.download_count}`);
|
|
135
142
|
if (skill.audit_status && skill.audit_status !== 'none') {
|
|
136
143
|
parts.push(skill.audit_status === 'passed' ? `${GREEN}audited${RESET}` : 'audit failed');
|
|
137
144
|
}
|