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.
- package/cli.ts +39 -2
- package/package.json +2 -1
- package/search.ts +152 -0
- 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.
|
|
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.
|
|
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}
|