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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/cli.ts +53 -17
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -80,4 +80,4 @@ The content hash algorithm is deterministic: files are sorted lexicographically
80
80
 
81
81
  ## License
82
82
 
83
- Apache 2.0
83
+ Business Source License 1.1 (BUSL-1.1). The Change Date is 2028-03-04, after which the code is available under Apache 2.0.
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.0';
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 TEAL = '\x1b[38;2;63;168;164m'; // brand teal #3fa8a4
18
- const GREEN = '\x1b[32m';
19
- const RED = '\x1b[31m';
20
- const DIM = '\x1b[2m';
21
- const BOLD = '\x1b[1m';
22
- const RESET = '\x1b[0m';
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
- const input = JSON.parse(await readStdin());
184
- const sessionId: string = input.session_id;
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
- const input = JSON.parse(await readStdin());
277
- const sessionId: string = input.session_id;
278
- const skillName: string | undefined = input.tool_input?.skill;
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
- const state: SessionState = JSON.parse(readFileSync(stateFile, 'utf-8'));
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 = `${name}.tar.gz`;
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
- const skillsDir = findSkillsDir();
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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Install verified Claude Code skills from the Oath Bound registry",
5
- "license": "Apache-2.0",
5
+ "license": "BUSL-1.1",
6
6
  "author": "Josh Anderson",
7
7
  "homepage": "https://oathbound.ai",
8
8
  "repository": {