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 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.13.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. Success
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
- // Warn about hooks/mcpServers if present
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.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Install verified Claude Code skills and agents from the Oath Bound registry",
5
- "license": "BUSL-1.1",
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
  }