oathbound 0.1.0 → 0.1.2
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/README.md +1 -1
- package/cli.ts +53 -17
- package/package.json +2 -2
package/README.md
CHANGED
package/cli.ts
CHANGED
|
@@ -7,19 +7,20 @@ import { writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync, statS
|
|
|
7
7
|
import { join, relative } from 'node:path';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
|
|
10
|
-
const VERSION = '0.1.
|
|
10
|
+
const VERSION = '0.1.2';
|
|
11
11
|
|
|
12
12
|
// --- Supabase ---
|
|
13
13
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
14
14
|
const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
|
|
15
15
|
|
|
16
|
-
// --- ANSI ---
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
16
|
+
// --- ANSI (respect NO_COLOR standard: https://no-color.org) ---
|
|
17
|
+
const USE_COLOR = process.env.NO_COLOR === undefined && process.stderr.isTTY;
|
|
18
|
+
const TEAL = USE_COLOR ? '\x1b[38;2;63;168;164m' : ''; // brand teal #3fa8a4
|
|
19
|
+
const GREEN = USE_COLOR ? '\x1b[32m' : '';
|
|
20
|
+
const RED = USE_COLOR ? '\x1b[31m' : '';
|
|
21
|
+
const DIM = USE_COLOR ? '\x1b[2m' : '';
|
|
22
|
+
const BOLD = USE_COLOR ? '\x1b[1m' : '';
|
|
23
|
+
const RESET = USE_COLOR ? '\x1b[0m' : '';
|
|
23
24
|
|
|
24
25
|
// --- Types ---
|
|
25
26
|
interface SkillRow {
|
|
@@ -67,7 +68,7 @@ function spinner(text: string): { stop: () => void } {
|
|
|
67
68
|
return {
|
|
68
69
|
stop() {
|
|
69
70
|
clearInterval(interval);
|
|
70
|
-
process.stdout.write('\r\x1b[2K');
|
|
71
|
+
process.stdout.write(USE_COLOR ? '\r\x1b[2K' : '\n');
|
|
71
72
|
},
|
|
72
73
|
};
|
|
73
74
|
}
|
|
@@ -180,8 +181,14 @@ async function readStdin(): Promise<string> {
|
|
|
180
181
|
|
|
181
182
|
// --- Verify (SessionStart hook) ---
|
|
182
183
|
async function verify(): Promise<void> {
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
let input: Record<string, unknown>;
|
|
185
|
+
try {
|
|
186
|
+
input = JSON.parse(await readStdin());
|
|
187
|
+
} catch {
|
|
188
|
+
process.stderr.write('oathbound verify: invalid JSON on stdin\n');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const sessionId: string = input.session_id as string;
|
|
185
192
|
if (!sessionId) {
|
|
186
193
|
process.stderr.write('oathbound verify: no session_id in stdin\n');
|
|
187
194
|
process.exit(1);
|
|
@@ -243,10 +250,13 @@ async function verify(): Promise<void> {
|
|
|
243
250
|
for (const [name, localHash] of Object.entries(localHashes)) {
|
|
244
251
|
const registryHash = registryHashes.get(name);
|
|
245
252
|
if (!registryHash) {
|
|
253
|
+
process.stderr.write(`${DIM} ${name}: ${localHash} (not in registry)${RESET}\n`);
|
|
246
254
|
rejected.push({ name, reason: 'not in registry' });
|
|
247
255
|
} else if (localHash !== registryHash) {
|
|
256
|
+
process.stderr.write(`${RED} ${name}: ${localHash} ≠ ${registryHash}${RESET}\n`);
|
|
248
257
|
rejected.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${registryHash.slice(0, 8)}…)` });
|
|
249
258
|
} else {
|
|
259
|
+
process.stderr.write(`${GREEN} ${name}: ${localHash} ✓${RESET}\n`);
|
|
250
260
|
verified[name] = localHash;
|
|
251
261
|
}
|
|
252
262
|
}
|
|
@@ -273,9 +283,15 @@ async function verify(): Promise<void> {
|
|
|
273
283
|
|
|
274
284
|
// --- Verify --check (PreToolUse hook) ---
|
|
275
285
|
async function verifyCheck(): Promise<void> {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
let input: Record<string, unknown>;
|
|
287
|
+
try {
|
|
288
|
+
input = JSON.parse(await readStdin());
|
|
289
|
+
} catch {
|
|
290
|
+
process.stderr.write('oathbound verify --check: invalid JSON on stdin\n');
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const sessionId: string = input.session_id as string;
|
|
294
|
+
const skillName: string | undefined = (input.tool_input as Record<string, unknown> | undefined)?.skill as string | undefined;
|
|
279
295
|
|
|
280
296
|
if (!sessionId || !skillName) {
|
|
281
297
|
// Can't verify — allow through (non-skill invocation or missing context)
|
|
@@ -288,7 +304,13 @@ async function verifyCheck(): Promise<void> {
|
|
|
288
304
|
process.exit(0);
|
|
289
305
|
}
|
|
290
306
|
|
|
291
|
-
|
|
307
|
+
let state: SessionState;
|
|
308
|
+
try {
|
|
309
|
+
state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
310
|
+
} catch {
|
|
311
|
+
process.stderr.write('oathbound verify --check: corrupt session state file\n');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
292
314
|
|
|
293
315
|
// Extract just the skill name (strip namespace/ prefix if present)
|
|
294
316
|
const baseName = skillName.includes(':') ? skillName.split(':').pop()! : skillName;
|
|
@@ -312,6 +334,7 @@ async function verifyCheck(): Promise<void> {
|
|
|
312
334
|
const sessionHash = state.verified[baseName];
|
|
313
335
|
|
|
314
336
|
if (!sessionHash) {
|
|
337
|
+
process.stderr.write(`${RED} ${baseName}: ${currentHash} (not verified at session start)${RESET}\n`);
|
|
315
338
|
console.log(JSON.stringify({
|
|
316
339
|
hookSpecificOutput: {
|
|
317
340
|
hookEventName: 'PreToolUse',
|
|
@@ -323,6 +346,7 @@ async function verifyCheck(): Promise<void> {
|
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
if (currentHash !== sessionHash) {
|
|
349
|
+
process.stderr.write(`${RED} ${baseName}: ${currentHash} ≠ ${sessionHash} (tampered)${RESET}\n`);
|
|
326
350
|
console.log(JSON.stringify({
|
|
327
351
|
hookSpecificOutput: {
|
|
328
352
|
hookEventName: 'PreToolUse',
|
|
@@ -333,6 +357,8 @@ async function verifyCheck(): Promise<void> {
|
|
|
333
357
|
process.exit(0);
|
|
334
358
|
}
|
|
335
359
|
|
|
360
|
+
process.stderr.write(`${GREEN} ${baseName}: ${currentHash} ✓${RESET}\n`);
|
|
361
|
+
|
|
336
362
|
// Hash matches — allow
|
|
337
363
|
process.exit(0);
|
|
338
364
|
}
|
|
@@ -373,19 +399,29 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
373
399
|
}
|
|
374
400
|
|
|
375
401
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
376
|
-
const tarFile =
|
|
402
|
+
const tarFile = join(tmpdir(), `oathbound-${name}-${Date.now()}.tar.gz`);
|
|
377
403
|
|
|
378
404
|
// 3. Hash and verify
|
|
379
405
|
const verify = spinner('Verifying...');
|
|
380
406
|
const hash = createHash('sha256').update(buffer).digest('hex');
|
|
381
407
|
verify.stop();
|
|
382
408
|
|
|
409
|
+
console.log(`${DIM} tar hash: ${hash}${RESET}`);
|
|
410
|
+
|
|
383
411
|
if (hash !== skill.tar_hash) {
|
|
412
|
+
console.log(`${RED} expected: ${skill.tar_hash}${RESET}`);
|
|
384
413
|
fail('Verification failed', `Downloaded file does not match expected hash for ${fullName}`);
|
|
385
414
|
}
|
|
386
415
|
|
|
387
416
|
// 4. Find target directory and extract
|
|
388
|
-
|
|
417
|
+
let skillsDir = findSkillsDir();
|
|
418
|
+
if (!skillsDir.endsWith('.claude/skills') && !skillsDir.includes('.claude/skills')) {
|
|
419
|
+
// findSkillsDir() fell back to cwd — create .claude/skills instead of extracting into project root
|
|
420
|
+
skillsDir = join(process.cwd(), '.claude', 'skills');
|
|
421
|
+
const { mkdirSync } = await import('node:fs');
|
|
422
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
423
|
+
console.log(`${DIM} Created ${skillsDir}${RESET}`);
|
|
424
|
+
}
|
|
389
425
|
writeFileSync(tarFile, buffer);
|
|
390
426
|
try {
|
|
391
427
|
execFileSync('tar', ['-xf', tarFile, '-C', skillsDir], { stdio: 'pipe' });
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Install verified Claude Code skills from the Oath Bound registry",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Josh Anderson",
|
|
7
7
|
"homepage": "https://oathbound.ai",
|
|
8
8
|
"repository": {
|