oathbound 0.8.1 → 0.9.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 +25 -11
- package/package.json +1 -1
- package/push.ts +4 -1
- package/ui.ts +2 -2
- package/verify.ts +46 -26
package/cli.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type Mer
|
|
|
25
25
|
export { isNewer } from './update';
|
|
26
26
|
export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult };
|
|
27
27
|
|
|
28
|
-
const VERSION = '0.
|
|
28
|
+
const VERSION = '0.9.0';
|
|
29
29
|
|
|
30
30
|
// --- Supabase ---
|
|
31
31
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
@@ -40,10 +40,19 @@ interface SkillRow {
|
|
|
40
40
|
storage_path: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function parseSkillArg(arg: string): { namespace: string; name: string } | null {
|
|
43
|
+
function parseSkillArg(arg: string): { namespace: string; name: string; version: number | null } | null {
|
|
44
44
|
const slash = arg.indexOf('/');
|
|
45
45
|
if (slash < 1 || slash === arg.length - 1) return null;
|
|
46
|
-
|
|
46
|
+
const afterSlash = arg.slice(slash + 1);
|
|
47
|
+
const atIdx = afterSlash.indexOf('@');
|
|
48
|
+
if (atIdx === -1) {
|
|
49
|
+
return { namespace: arg.slice(0, slash), name: afterSlash, version: null };
|
|
50
|
+
}
|
|
51
|
+
const name = afterSlash.slice(0, atIdx);
|
|
52
|
+
if (!name) return null;
|
|
53
|
+
const vNum = Number(afterSlash.slice(atIdx + 1));
|
|
54
|
+
if (!Number.isInteger(vNum) || vNum < 1) return null;
|
|
55
|
+
return { namespace: arg.slice(0, slash), name, version: vNum };
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
// --- Package manager detection ---
|
|
@@ -223,25 +232,30 @@ async function init(): Promise<void> {
|
|
|
223
232
|
async function pull(skillArg: string): Promise<void> {
|
|
224
233
|
const parsed = parseSkillArg(skillArg);
|
|
225
234
|
if (!parsed) usage();
|
|
226
|
-
const { namespace, name } = parsed;
|
|
235
|
+
const { namespace, name, version } = parsed;
|
|
227
236
|
const fullName = `${namespace}/${name}`;
|
|
228
237
|
|
|
229
|
-
console.log(`\n${BRAND} ${TEAL}↓ Pulling ${fullName}...${RESET}`);
|
|
238
|
+
console.log(`\n${BRAND} ${TEAL}↓ Pulling ${fullName}${version ? `@${version}` : ''}...${RESET}`);
|
|
230
239
|
|
|
231
240
|
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
232
241
|
|
|
233
242
|
// 1. Query for the skill
|
|
234
|
-
|
|
243
|
+
let query = supabase
|
|
235
244
|
.from('skills')
|
|
236
245
|
.select('name, namespace, version, tar_hash, storage_path')
|
|
237
246
|
.eq('namespace', namespace)
|
|
238
|
-
.eq('name', name)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.
|
|
247
|
+
.eq('name', name);
|
|
248
|
+
|
|
249
|
+
if (version !== null) {
|
|
250
|
+
query = query.eq('version', version);
|
|
251
|
+
} else {
|
|
252
|
+
query = query.order('version', { ascending: false }).limit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { data: skill, error } = await query.single<SkillRow>();
|
|
242
256
|
|
|
243
257
|
if (error || !skill) {
|
|
244
|
-
fail(`Skill not found: ${fullName}`);
|
|
258
|
+
fail(`Skill not found: ${fullName}${version ? `@${version}` : ''}`);
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
// 2. Download the tar from storage
|
package/package.json
CHANGED
package/push.ts
CHANGED
|
@@ -33,6 +33,7 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
33
33
|
const name = String(meta.name ?? '');
|
|
34
34
|
const description = String(meta.description ?? '');
|
|
35
35
|
const license = String(meta.license ?? '');
|
|
36
|
+
const version = typeof meta.version === 'number' && Number.isInteger(meta.version) && meta.version > 0 ? meta.version : null;
|
|
36
37
|
if (!name) fail('SKILL.md frontmatter missing: name');
|
|
37
38
|
if (!description) fail('SKILL.md frontmatter missing: description');
|
|
38
39
|
if (!license) fail('SKILL.md frontmatter missing: license');
|
|
@@ -47,6 +48,7 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
47
48
|
}));
|
|
48
49
|
|
|
49
50
|
console.log(`${DIM} name: ${name}${RESET}`);
|
|
51
|
+
console.log(`${DIM} version: ${version ?? 'auto (next)'}${RESET}`);
|
|
50
52
|
console.log(`${DIM} license: ${license}${RESET}`);
|
|
51
53
|
if (originalAuthor) {
|
|
52
54
|
console.log(`${DIM} original author: ${originalAuthor}${RESET}`);
|
|
@@ -69,6 +71,7 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
69
71
|
name,
|
|
70
72
|
description,
|
|
71
73
|
license,
|
|
74
|
+
version,
|
|
72
75
|
compatibility: String(meta.compatibility ?? '') || null,
|
|
73
76
|
allowedTools: String(meta['allowed-tools'] ?? '') || null,
|
|
74
77
|
originalAuthor: originalAuthor || null,
|
|
@@ -88,7 +91,7 @@ export async function push(pathArg?: string): Promise<void> {
|
|
|
88
91
|
|
|
89
92
|
const result = await response.json();
|
|
90
93
|
|
|
91
|
-
outro(`${GREEN}✓ Published ${BOLD}${result.namespace}/${result.name}${RESET}`);
|
|
94
|
+
outro(`${GREEN}✓ Published ${BOLD}${result.namespace}/${result.name}${RESET}${GREEN} v${result.version}${RESET}`);
|
|
92
95
|
if (result.suiObjectId) {
|
|
93
96
|
console.log(`${DIM} on-chain: ${result.suiObjectId}${RESET}`);
|
|
94
97
|
}
|
package/ui.ts
CHANGED
|
@@ -22,8 +22,8 @@ ${BOLD}oathbound${RESET} — install, verify, and publish skills
|
|
|
22
22
|
|
|
23
23
|
${DIM}Usage:${RESET}
|
|
24
24
|
oathbound init ${DIM}Setup wizard — configure project${RESET}
|
|
25
|
-
oathbound pull <namespace/skill-name>
|
|
26
|
-
oathbound install <namespace/skill-name>
|
|
25
|
+
oathbound pull <namespace/skill-name[@version]>
|
|
26
|
+
oathbound install <namespace/skill-name[@version]>
|
|
27
27
|
oathbound push [path] ${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}
|
package/verify.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from 'node:fs';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
+
import { parse as yamlParse } from 'yaml';
|
|
8
9
|
import { BRAND, TEAL, GREEN, RED, YELLOW, DIM, BOLD, RESET } from './ui';
|
|
9
10
|
import { hashSkillDir } from './content-hash';
|
|
10
11
|
import { readOathboundConfig, type EnforcementLevel } from './config';
|
|
@@ -137,6 +138,21 @@ function isExternalSkillAccess(
|
|
|
137
138
|
return false;
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
function parseSkillVersion(skillDir: string): number | null {
|
|
142
|
+
const skillMdPath = join(skillDir, 'SKILL.md');
|
|
143
|
+
if (!existsSync(skillMdPath)) return null;
|
|
144
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
145
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
146
|
+
if (!match) return null;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = yamlParse(match[1]);
|
|
149
|
+
const v = parsed?.version;
|
|
150
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0 ? v : null;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
// --- Verify (SessionStart hook) ---
|
|
141
157
|
export async function verify(supabaseUrl: string, supabaseAnonKey: string): Promise<void> {
|
|
142
158
|
let input: Record<string, unknown>;
|
|
@@ -174,18 +190,20 @@ export async function verify(supabaseUrl: string, supabaseAnonKey: string): Prom
|
|
|
174
190
|
process.exit(0);
|
|
175
191
|
}
|
|
176
192
|
|
|
177
|
-
// Hash each local skill
|
|
178
|
-
const
|
|
193
|
+
// Hash each local skill and parse version from SKILL.md
|
|
194
|
+
const localSkills: Record<string, { hash: string; version: number }> = {};
|
|
179
195
|
for (const dir of skillDirs) {
|
|
180
196
|
const fullPath = join(skillsDir, dir.name);
|
|
181
|
-
|
|
197
|
+
const hash = hashSkillDir(fullPath);
|
|
198
|
+
const version = parseSkillVersion(fullPath) ?? 1; // fallback to v1 for pre-versioning installs
|
|
199
|
+
localSkills[dir.name] = { hash, version };
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
// Read enforcement config
|
|
185
203
|
const config = readOathboundConfig();
|
|
186
204
|
const enforcement: EnforcementLevel = config?.enforcement ?? 'warn';
|
|
187
205
|
|
|
188
|
-
// Fetch registry
|
|
206
|
+
// Fetch registry data from Supabase (all versions)
|
|
189
207
|
// If enforcement=audited, also fetch audit status
|
|
190
208
|
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
191
209
|
const selectFields = enforcement === 'audited'
|
|
@@ -201,19 +219,19 @@ export async function verify(supabaseUrl: string, supabaseAnonKey: string): Prom
|
|
|
201
219
|
process.exit(1);
|
|
202
220
|
}
|
|
203
221
|
|
|
204
|
-
// Build lookup:
|
|
205
|
-
const
|
|
206
|
-
const auditedSkills = new Set<string>(); // skills with at least one passed audit
|
|
222
|
+
// Build lookup: name → version → { hash, audited }
|
|
223
|
+
const registryMap = new Map<string, Map<number, { hash: string; audited: boolean }>>();
|
|
207
224
|
for (const skill of skills ?? []) {
|
|
208
225
|
if (!skill.content_hash) continue;
|
|
209
|
-
if (!
|
|
210
|
-
|
|
226
|
+
if (!registryMap.has(skill.name)) {
|
|
227
|
+
registryMap.set(skill.name, new Map());
|
|
211
228
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
const versionMap = registryMap.get(skill.name)!;
|
|
230
|
+
if (!versionMap.has(skill.version)) {
|
|
231
|
+
const audited = enforcement === 'audited'
|
|
232
|
+
? ((skill as Record<string, unknown>).audits as Array<{ passed: boolean }> | null)?.some(a => a.passed) ?? false
|
|
233
|
+
: false;
|
|
234
|
+
versionMap.set(skill.version, { hash: skill.content_hash, audited });
|
|
217
235
|
}
|
|
218
236
|
}
|
|
219
237
|
|
|
@@ -223,29 +241,31 @@ export async function verify(supabaseUrl: string, supabaseAnonKey: string): Prom
|
|
|
223
241
|
|
|
224
242
|
process.stderr.write(`${BRAND} ${TEAL}verifying skills...${RESET}\n`);
|
|
225
243
|
|
|
226
|
-
for (const [name, localHash] of Object.entries(
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
for (const [name, { hash: localHash, version }] of Object.entries(localSkills)) {
|
|
245
|
+
const versionMap = registryMap.get(name);
|
|
246
|
+
const entry = versionMap?.get(version);
|
|
247
|
+
|
|
248
|
+
if (!entry) {
|
|
249
|
+
process.stderr.write(`${DIM} ${name}@${version}: ${localHash} (not in registry)${RESET}\n`);
|
|
230
250
|
if (enforcement === 'warn') {
|
|
231
251
|
warnings.push({ name, reason: 'not in registry' });
|
|
232
|
-
verified[name] = localHash;
|
|
252
|
+
verified[name] = localHash;
|
|
233
253
|
} else {
|
|
234
254
|
rejected.push({ name, reason: 'not in registry' });
|
|
235
255
|
}
|
|
236
|
-
} else if (localHash !==
|
|
237
|
-
process.stderr.write(`${RED} ${name}: ${localHash} ≠ ${
|
|
256
|
+
} else if (localHash !== entry.hash) {
|
|
257
|
+
process.stderr.write(`${RED} ${name}@${version}: ${localHash} ≠ ${entry.hash}${RESET}\n`);
|
|
238
258
|
if (enforcement === 'warn') {
|
|
239
|
-
warnings.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${
|
|
259
|
+
warnings.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${entry.hash.slice(0, 8)}…)` });
|
|
240
260
|
verified[name] = localHash;
|
|
241
261
|
} else {
|
|
242
|
-
rejected.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${
|
|
262
|
+
rejected.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${entry.hash.slice(0, 8)}…)` });
|
|
243
263
|
}
|
|
244
|
-
} else if (enforcement === 'audited' && !
|
|
245
|
-
process.stderr.write(`${YELLOW} ${name}: ${localHash} (registered but not audited)${RESET}\n`);
|
|
264
|
+
} else if (enforcement === 'audited' && !entry.audited) {
|
|
265
|
+
process.stderr.write(`${YELLOW} ${name}@${version}: ${localHash} (registered but not audited)${RESET}\n`);
|
|
246
266
|
rejected.push({ name, reason: 'no passed audit' });
|
|
247
267
|
} else {
|
|
248
|
-
process.stderr.write(`${GREEN} ${name}: ${localHash} ✓${RESET}\n`);
|
|
268
|
+
process.stderr.write(`${GREEN} ${name}@${version}: ${localHash} ✓${RESET}\n`);
|
|
249
269
|
verified[name] = localHash;
|
|
250
270
|
}
|
|
251
271
|
}
|