oathbound 0.11.1 → 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 +8 -1
- 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
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';
|
|
@@ -410,6 +411,12 @@ if (subcommand === 'init') {
|
|
|
410
411
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
411
412
|
fail('Push failed', msg);
|
|
412
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
|
+
});
|
|
413
420
|
} else {
|
|
414
421
|
const PULL_ALIASES = new Set(['pull', 'i', 'install']);
|
|
415
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}
|