oathbound 0.11.0 → 0.12.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.
Files changed (4) hide show
  1. package/cli.ts +39 -2
  2. package/package.json +2 -1
  3. package/search.ts +152 -0
  4. package/ui.ts +2 -0
package/cli.ts CHANGED
@@ -20,13 +20,14 @@ import { isValidSemver, compareSemver } from './semver';
20
20
  import { verify, verifyCheck, findSkillsDir } from './verify';
21
21
  import { login, logout, whoami } from './auth';
22
22
  import { push } from './push';
23
+ import { search, parseSearchArgs } from './search';
23
24
 
24
25
  // Re-exports for tests
25
26
  export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type MergeResult } from './config';
26
27
  export { isNewer } from './update';
27
- export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult };
28
+ export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult, addTrustedDependency, type TrustedDepResult };
28
29
 
29
- const VERSION = '0.11.0';
30
+ const VERSION = '0.12.0';
30
31
 
31
32
  // --- Supabase ---
32
33
  const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
@@ -106,6 +107,27 @@ function setup(): void {
106
107
  }
107
108
  }
108
109
 
110
+ type TrustedDepResult = 'added' | 'skipped';
111
+
112
+ function addTrustedDependency(): TrustedDepResult {
113
+ const pkgPath = join(process.cwd(), 'package.json');
114
+ if (!existsSync(pkgPath)) return 'skipped';
115
+
116
+ let pkg: Record<string, unknown>;
117
+ try {
118
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
119
+ } catch {
120
+ return 'skipped';
121
+ }
122
+
123
+ const trusted = Array.isArray(pkg.trustedDependencies) ? pkg.trustedDependencies as string[] : [];
124
+ if (trusted.includes('oathbound')) return 'skipped';
125
+
126
+ pkg.trustedDependencies = [...trusted, 'oathbound'];
127
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
128
+ return 'added';
129
+ }
130
+
109
131
  type PrepareResult = 'added' | 'appended' | 'skipped';
110
132
 
111
133
  function addPrepareScript(): PrepareResult {
@@ -194,6 +216,15 @@ async function init(): Promise<void> {
194
216
  process.exit(1);
195
217
  }
196
218
 
219
+ // For bun/pnpm: add trustedDependencies so postinstall runs
220
+ const pm = detectPackageManager();
221
+ if (pm === 'bun' || pm === 'pnpm') {
222
+ const trustResult = addTrustedDependency();
223
+ if (trustResult === 'added') {
224
+ process.stderr.write(`${GREEN} ✓ Added oathbound to trustedDependencies (required by ${pm})${RESET}\n`);
225
+ }
226
+ }
227
+
197
228
  // Add prepare script to package.json
198
229
  const prepareResult = addPrepareScript();
199
230
  if (prepareResult === 'added' || prepareResult === 'appended') {
@@ -380,6 +411,12 @@ if (subcommand === 'init') {
380
411
  const msg = err instanceof Error ? err.message : 'Unknown error';
381
412
  fail('Push failed', msg);
382
413
  });
414
+ } else if (subcommand === 'search' || subcommand === 'list' || subcommand === 'ls') {
415
+ const searchOpts = parseSearchArgs(args.slice(1));
416
+ search(searchOpts).catch((err: unknown) => {
417
+ const msg = err instanceof Error ? err.message : 'Unknown error';
418
+ fail('Search failed', msg);
419
+ });
383
420
  } else {
384
421
  const PULL_ALIASES = new Set(['pull', 'i', 'install']);
385
422
  const skillArg = args[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oathbound",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Install verified Claude Code skills from the Oath Bound registry",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Josh Anderson",
@@ -31,6 +31,7 @@
31
31
  "content-hash.ts",
32
32
  "auth.ts",
33
33
  "push.ts",
34
+ "search.ts",
34
35
  "semver.ts",
35
36
  "bin/cli.cjs",
36
37
  "install.cjs"
package/search.ts ADDED
@@ -0,0 +1,152 @@
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 SearchOptions {
6
+ query?: string;
7
+ namespace?: string;
8
+ sparse?: boolean;
9
+ limit?: number;
10
+ offset?: number;
11
+ }
12
+
13
+ export function parseSearchArgs(args: string[]): SearchOptions {
14
+ const opts: SearchOptions = {};
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 SkillAuthor {
39
+ username: string;
40
+ display_name: string | null;
41
+ verified: boolean;
42
+ }
43
+
44
+ interface SkillResult {
45
+ name: string;
46
+ namespace: string;
47
+ description: string;
48
+ version: string;
49
+ license?: string;
50
+ visibility?: string;
51
+ author?: SkillAuthor;
52
+ audit_status?: 'passed' | 'failed' | 'none';
53
+ }
54
+
55
+ interface SearchResponse {
56
+ ok: boolean;
57
+ skills: SkillResult[];
58
+ total: number;
59
+ limit: number;
60
+ offset: number;
61
+ error?: string;
62
+ }
63
+
64
+ export async function search(opts: SearchOptions): Promise<void> {
65
+ const params = new URLSearchParams();
66
+ if (opts.query) params.set('q', opts.query);
67
+ if (opts.namespace) params.set('namespace', opts.namespace);
68
+ if (opts.sparse) params.set('sparse', 'true');
69
+ if (opts.limit != null) params.set('limit', String(opts.limit));
70
+ if (opts.offset != null) params.set('offset', String(opts.offset));
71
+
72
+ const url = `${API_BASE}/api/skills?${params}`;
73
+
74
+ const sp = spinner('Searching...');
75
+
76
+ let res: Response;
77
+ try {
78
+ res = await fetch(url);
79
+ } catch (err) {
80
+ sp.stop();
81
+ const msg = err instanceof Error ? err.message : 'Unknown error';
82
+ fail('Search failed', msg);
83
+ }
84
+
85
+ sp.stop();
86
+
87
+ if (!res.ok) {
88
+ let detail = `HTTP ${res.status}`;
89
+ try {
90
+ const body = await res.json() as { error?: string };
91
+ if (body.error) detail = body.error;
92
+ } catch { /* ignore parse errors */ }
93
+ fail('Search failed', detail);
94
+ }
95
+
96
+ const data = await res.json() as SearchResponse;
97
+
98
+ if (!data.ok || !data.skills) {
99
+ fail('Search failed', data.error ?? 'Unexpected response');
100
+ }
101
+
102
+ const { skills, total, offset } = data;
103
+
104
+ if (skills.length === 0) {
105
+ console.log(`\n${BRAND} ${DIM}No skills found.${RESET}`);
106
+ return;
107
+ }
108
+
109
+ const showing = offset > 0
110
+ ? `Showing ${offset + 1}–${offset + skills.length} of ${total}`
111
+ : `${total} skill${total === 1 ? '' : 's'} found`;
112
+
113
+ console.log(`\n${BRAND} ${TEAL}${showing}${RESET}\n`);
114
+
115
+ for (const skill of skills) {
116
+ const id = `${skill.namespace}/${skill.name}`;
117
+ const ver = `v${skill.version}`;
118
+
119
+ // Line 1: name + version
120
+ console.log(` ${BOLD}${id}${RESET} ${DIM}${ver}${RESET}`);
121
+
122
+ // Line 2: description
123
+ if (skill.description) {
124
+ console.log(` ${DIM}${skill.description}${RESET}`);
125
+ }
126
+
127
+ // Line 3: metadata (non-sparse only)
128
+ if (!opts.sparse && (skill.author || skill.audit_status || skill.license)) {
129
+ const parts: string[] = [];
130
+ if (skill.author) {
131
+ const name = skill.author.display_name || skill.author.username;
132
+ parts.push(`by ${name}${skill.author.verified ? ' ✓' : ''}`);
133
+ }
134
+ if (skill.license) parts.push(skill.license);
135
+ if (skill.audit_status && skill.audit_status !== 'none') {
136
+ parts.push(skill.audit_status === 'passed' ? `${GREEN}audited${RESET}` : 'audit failed');
137
+ }
138
+ if (skill.visibility === 'private') parts.push('private');
139
+ if (parts.length > 0) {
140
+ console.log(` ${DIM}${parts.join(' · ')}${RESET}`);
141
+ }
142
+ }
143
+
144
+ console.log(); // blank line between skills
145
+ }
146
+
147
+ // Pagination hint
148
+ if (offset + skills.length < total) {
149
+ const nextOffset = offset + skills.length;
150
+ console.log(`${DIM} Use --offset ${nextOffset} to see more${RESET}\n`);
151
+ }
152
+ }
package/ui.ts CHANGED
@@ -25,6 +25,8 @@ ${DIM}Usage:${RESET}
25
25
  oathbound pull <namespace/skill-name[@version]>
26
26
  oathbound install <namespace/skill-name[@version]>
27
27
  oathbound push [path] [--private] ${DIM}Publish a skill to the registry${RESET}
28
+ oathbound search [query] ${DIM}Search skills in the registry${RESET}
29
+ oathbound list ${DIM}List all public skills${RESET}
28
30
  oathbound login ${DIM}Authenticate with oathbound.ai${RESET}
29
31
  oathbound logout ${DIM}Clear stored credentials${RESET}
30
32
  oathbound whoami ${DIM}Show current user${RESET}