oathbound 0.9.0 → 0.11.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 +35 -20
- package/package.json +2 -1
- package/push.ts +9 -2
- package/semver.ts +39 -0
- package/ui.ts +1 -1
- package/verify.ts +8 -5
package/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type EnforcementLevel, type MergeResult,
|
|
17
17
|
} from './config';
|
|
18
18
|
import { checkForUpdate, isNewer } from './update';
|
|
19
|
+
import { isValidSemver, compareSemver } from './semver';
|
|
19
20
|
import { verify, verifyCheck, findSkillsDir } from './verify';
|
|
20
21
|
import { login, logout, whoami } from './auth';
|
|
21
22
|
import { push } from './push';
|
|
@@ -25,7 +26,7 @@ export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type Mer
|
|
|
25
26
|
export { isNewer } from './update';
|
|
26
27
|
export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult };
|
|
27
28
|
|
|
28
|
-
const VERSION = '0.
|
|
29
|
+
const VERSION = '0.11.0';
|
|
29
30
|
|
|
30
31
|
// --- Supabase ---
|
|
31
32
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
@@ -35,12 +36,12 @@ const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
|
|
|
35
36
|
interface SkillRow {
|
|
36
37
|
name: string;
|
|
37
38
|
namespace: string;
|
|
38
|
-
version:
|
|
39
|
+
version: string;
|
|
39
40
|
tar_hash: string;
|
|
40
41
|
storage_path: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function parseSkillArg(arg: string): { namespace: string; name: string; version:
|
|
44
|
+
function parseSkillArg(arg: string): { namespace: string; name: string; version: string | null } | null {
|
|
44
45
|
const slash = arg.indexOf('/');
|
|
45
46
|
if (slash < 1 || slash === arg.length - 1) return null;
|
|
46
47
|
const afterSlash = arg.slice(slash + 1);
|
|
@@ -50,9 +51,9 @@ function parseSkillArg(arg: string): { namespace: string; name: string; version:
|
|
|
50
51
|
}
|
|
51
52
|
const name = afterSlash.slice(0, atIdx);
|
|
52
53
|
if (!name) return null;
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
-
return { namespace: arg.slice(0, slash), name, version:
|
|
54
|
+
const vStr = afterSlash.slice(atIdx + 1);
|
|
55
|
+
if (!isValidSemver(vStr)) return null;
|
|
56
|
+
return { namespace: arg.slice(0, slash), name, version: vStr };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// --- Package manager detection ---
|
|
@@ -240,22 +241,33 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
240
241
|
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
241
242
|
|
|
242
243
|
// 1. Query for the skill
|
|
243
|
-
let
|
|
244
|
-
.from('skills')
|
|
245
|
-
.select('name, namespace, version, tar_hash, storage_path')
|
|
246
|
-
.eq('namespace', namespace)
|
|
247
|
-
.eq('name', name);
|
|
244
|
+
let skill: SkillRow;
|
|
248
245
|
|
|
249
246
|
if (version !== null) {
|
|
250
|
-
|
|
247
|
+
const { data, error } = await supabase
|
|
248
|
+
.from('skills')
|
|
249
|
+
.select('name, namespace, version, tar_hash, storage_path')
|
|
250
|
+
.eq('namespace', namespace)
|
|
251
|
+
.eq('name', name)
|
|
252
|
+
.eq('version', version)
|
|
253
|
+
.single<SkillRow>();
|
|
254
|
+
|
|
255
|
+
if (error || !data) {
|
|
256
|
+
fail(`Skill not found: ${fullName}@${version}`);
|
|
257
|
+
}
|
|
258
|
+
skill = data;
|
|
251
259
|
} else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
// Fetch all versions, pick highest via semver comparison
|
|
261
|
+
const { data, error } = await supabase
|
|
262
|
+
.from('skills')
|
|
263
|
+
.select('name, namespace, version, tar_hash, storage_path')
|
|
264
|
+
.eq('namespace', namespace)
|
|
265
|
+
.eq('name', name);
|
|
266
|
+
|
|
267
|
+
if (error || !data || data.length === 0) {
|
|
268
|
+
fail(`Skill not found: ${fullName}`);
|
|
269
|
+
}
|
|
270
|
+
skill = (data as SkillRow[]).sort((a, b) => compareSemver(a.version, b.version)).at(-1)!;
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
// 2. Download the tar from storage
|
|
@@ -361,7 +373,10 @@ if (subcommand === 'init') {
|
|
|
361
373
|
fail('Failed', msg);
|
|
362
374
|
});
|
|
363
375
|
} else if (subcommand === 'push') {
|
|
364
|
-
|
|
376
|
+
const pushArgs = args.slice(1);
|
|
377
|
+
const isPrivate = pushArgs.includes('--private');
|
|
378
|
+
const pushPath = pushArgs.find(a => !a.startsWith('--'));
|
|
379
|
+
push(pushPath, { private: isPrivate }).catch((err: unknown) => {
|
|
365
380
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
366
381
|
fail('Push failed', msg);
|
|
367
382
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
+
"semver.ts",
|
|
34
35
|
"bin/cli.cjs",
|
|
35
36
|
"install.cjs"
|
|
36
37
|
],
|
package/push.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { parse as yamlParse } from 'yaml';
|
|
|
5
5
|
import { BRAND, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
6
6
|
import { getAccessToken } from './auth';
|
|
7
7
|
import { collectFiles } from './content-hash';
|
|
8
|
+
import { isValidSemver } from './semver';
|
|
8
9
|
|
|
9
10
|
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
10
11
|
|
|
11
|
-
export async function push(pathArg?: string): Promise<void> {
|
|
12
|
+
export async function push(pathArg?: string, options?: { private?: boolean }): Promise<void> {
|
|
12
13
|
intro(BRAND);
|
|
13
14
|
|
|
14
15
|
// Resolve skill directory
|
|
@@ -33,7 +34,8 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
33
34
|
const name = String(meta.name ?? '');
|
|
34
35
|
const description = String(meta.description ?? '');
|
|
35
36
|
const license = String(meta.license ?? '');
|
|
36
|
-
const
|
|
37
|
+
const rawVersion = meta.version != null ? String(meta.version) : null;
|
|
38
|
+
const version = rawVersion && isValidSemver(rawVersion) ? rawVersion : null;
|
|
37
39
|
if (!name) fail('SKILL.md frontmatter missing: name');
|
|
38
40
|
if (!description) fail('SKILL.md frontmatter missing: description');
|
|
39
41
|
if (!license) fail('SKILL.md frontmatter missing: license');
|
|
@@ -53,6 +55,10 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
53
55
|
if (originalAuthor) {
|
|
54
56
|
console.log(`${DIM} original author: ${originalAuthor}${RESET}`);
|
|
55
57
|
}
|
|
58
|
+
const visibility = options?.private ? 'private' : 'public';
|
|
59
|
+
if (options?.private) {
|
|
60
|
+
console.log(`${DIM} visibility: ${BOLD}private${RESET}`);
|
|
61
|
+
}
|
|
56
62
|
console.log(`${DIM} ${files.length} file(s)${RESET}`);
|
|
57
63
|
|
|
58
64
|
// Authenticate
|
|
@@ -75,6 +81,7 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
75
81
|
compatibility: String(meta.compatibility ?? '') || null,
|
|
76
82
|
allowedTools: String(meta['allowed-tools'] ?? '') || null,
|
|
77
83
|
originalAuthor: originalAuthor || null,
|
|
84
|
+
visibility,
|
|
78
85
|
files,
|
|
79
86
|
}),
|
|
80
87
|
});
|
package/semver.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
2
|
+
|
|
3
|
+
export interface SemverParts {
|
|
4
|
+
major: number;
|
|
5
|
+
minor: number;
|
|
6
|
+
patch: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Parse a semver string into parts. Returns null if invalid. */
|
|
10
|
+
export function parseSemver(version: string): SemverParts | null {
|
|
11
|
+
if (!SEMVER_RE.test(version)) return null;
|
|
12
|
+
const [major, minor, patch] = version.split(".").map(Number);
|
|
13
|
+
return { major, minor, patch };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Validate a semver string (MAJOR.MINOR.PATCH). */
|
|
17
|
+
export function isValidSemver(version: string): boolean {
|
|
18
|
+
return SEMVER_RE.test(version);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compare two semver strings.
|
|
23
|
+
* Returns negative if a < b, 0 if equal, positive if a > b.
|
|
24
|
+
*/
|
|
25
|
+
export function compareSemver(a: string, b: string): number {
|
|
26
|
+
const pa = parseSemver(a);
|
|
27
|
+
const pb = parseSemver(b);
|
|
28
|
+
if (!pa || !pb) throw new Error(`Invalid semver: ${!pa ? a : b}`);
|
|
29
|
+
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
30
|
+
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
31
|
+
return pa.patch - pb.patch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Bump the patch version: "1.2.3" -> "1.2.4" */
|
|
35
|
+
export function bumpPatch(version: string): string {
|
|
36
|
+
const parts = parseSemver(version);
|
|
37
|
+
if (!parts) throw new Error(`Invalid semver: ${version}`);
|
|
38
|
+
return `${parts.major}.${parts.minor}.${parts.patch + 1}`;
|
|
39
|
+
}
|
package/ui.ts
CHANGED
|
@@ -24,7 +24,7 @@ ${DIM}Usage:${RESET}
|
|
|
24
24
|
oathbound init ${DIM}Setup wizard — configure project${RESET}
|
|
25
25
|
oathbound pull <namespace/skill-name[@version]>
|
|
26
26
|
oathbound install <namespace/skill-name[@version]>
|
|
27
|
-
oathbound push [path]
|
|
27
|
+
oathbound push [path] [--private] ${DIM}Publish a skill to the registry${RESET}
|
|
28
28
|
oathbound login ${DIM}Authenticate with oathbound.ai${RESET}
|
|
29
29
|
oathbound logout ${DIM}Clear stored credentials${RESET}
|
|
30
30
|
oathbound whoami ${DIM}Show current user${RESET}
|
package/verify.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { parse as yamlParse } from 'yaml';
|
|
|
9
9
|
import { BRAND, TEAL, GREEN, RED, YELLOW, DIM, BOLD, RESET } from './ui';
|
|
10
10
|
import { hashSkillDir } from './content-hash';
|
|
11
11
|
import { readOathboundConfig, type EnforcementLevel } from './config';
|
|
12
|
+
import { isValidSemver } from './semver';
|
|
12
13
|
|
|
13
14
|
// --- Session state file ---
|
|
14
15
|
interface SessionState {
|
|
@@ -138,7 +139,7 @@ function isExternalSkillAccess(
|
|
|
138
139
|
return false;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
function parseSkillVersion(skillDir: string):
|
|
142
|
+
function parseSkillVersion(skillDir: string): string | null {
|
|
142
143
|
const skillMdPath = join(skillDir, 'SKILL.md');
|
|
143
144
|
if (!existsSync(skillMdPath)) return null;
|
|
144
145
|
const content = readFileSync(skillMdPath, 'utf-8');
|
|
@@ -147,7 +148,9 @@ function parseSkillVersion(skillDir: string): number | null {
|
|
|
147
148
|
try {
|
|
148
149
|
const parsed = yamlParse(match[1]);
|
|
149
150
|
const v = parsed?.version;
|
|
150
|
-
|
|
151
|
+
if (v == null) return null;
|
|
152
|
+
const vStr = String(v);
|
|
153
|
+
return isValidSemver(vStr) ? vStr : null;
|
|
151
154
|
} catch {
|
|
152
155
|
return null;
|
|
153
156
|
}
|
|
@@ -191,11 +194,11 @@ export async function verify(supabaseUrl: string, supabaseAnonKey: string): Prom
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
// Hash each local skill and parse version from SKILL.md
|
|
194
|
-
const localSkills: Record<string, { hash: string; version:
|
|
197
|
+
const localSkills: Record<string, { hash: string; version: string }> = {};
|
|
195
198
|
for (const dir of skillDirs) {
|
|
196
199
|
const fullPath = join(skillsDir, dir.name);
|
|
197
200
|
const hash = hashSkillDir(fullPath);
|
|
198
|
-
const version = parseSkillVersion(fullPath) ?? 1; // fallback
|
|
201
|
+
const version = parseSkillVersion(fullPath) ?? "1.0.0"; // fallback for pre-semver installs
|
|
199
202
|
localSkills[dir.name] = { hash, version };
|
|
200
203
|
}
|
|
201
204
|
|
|
@@ -220,7 +223,7 @@ export async function verify(supabaseUrl: string, supabaseAnonKey: string): Prom
|
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
// Build lookup: name → version → { hash, audited }
|
|
223
|
-
const registryMap = new Map<string, Map<
|
|
226
|
+
const registryMap = new Map<string, Map<string, { hash: string; audited: boolean }>>();
|
|
224
227
|
for (const skill of skills ?? []) {
|
|
225
228
|
if (!skill.content_hash) continue;
|
|
226
229
|
if (!registryMap.has(skill.name)) {
|