oathbound 0.1.2 → 0.2.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 +343 -10
- package/package.json +2 -1
package/cli.ts
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
import { createClient } from '@supabase/supabase-js';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
writeFileSync, readFileSync, unlinkSync, existsSync,
|
|
8
|
+
readdirSync, statSync, mkdirSync, renameSync, chmodSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join, relative, dirname } from 'node:path';
|
|
11
|
+
import { tmpdir, homedir, platform } from 'node:os';
|
|
12
|
+
import { intro, outro, select, cancel, isCancel } from '@clack/prompts';
|
|
9
13
|
|
|
10
|
-
const VERSION = '0.
|
|
14
|
+
const VERSION = '0.2.0';
|
|
11
15
|
|
|
12
16
|
// --- Supabase ---
|
|
13
17
|
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
@@ -18,6 +22,7 @@ const USE_COLOR = process.env.NO_COLOR === undefined && process.stderr.isTTY;
|
|
|
18
22
|
const TEAL = USE_COLOR ? '\x1b[38;2;63;168;164m' : ''; // brand teal #3fa8a4
|
|
19
23
|
const GREEN = USE_COLOR ? '\x1b[32m' : '';
|
|
20
24
|
const RED = USE_COLOR ? '\x1b[31m' : '';
|
|
25
|
+
const YELLOW = USE_COLOR ? '\x1b[33m' : '';
|
|
21
26
|
const DIM = USE_COLOR ? '\x1b[2m' : '';
|
|
22
27
|
const BOLD = USE_COLOR ? '\x1b[1m' : '';
|
|
23
28
|
const RESET = USE_COLOR ? '\x1b[0m' : '';
|
|
@@ -37,6 +42,7 @@ function usage(exitCode = 1): never {
|
|
|
37
42
|
${BOLD}oathbound${RESET} — install and verify skills
|
|
38
43
|
|
|
39
44
|
${DIM}Usage:${RESET}
|
|
45
|
+
oathbound init ${DIM}Setup wizard — configure project${RESET}
|
|
40
46
|
oathbound pull <namespace/skill-name>
|
|
41
47
|
oathbound install <namespace/skill-name>
|
|
42
48
|
oathbound verify ${DIM}SessionStart hook — verify all skills${RESET}
|
|
@@ -160,6 +166,278 @@ function hashSkillDir(skillDir: string): string {
|
|
|
160
166
|
return contentHash(files);
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
// --- JSONC / Config helpers ---
|
|
170
|
+
type EnforcementLevel = 'warn' | 'registered' | 'audited';
|
|
171
|
+
|
|
172
|
+
/** Strip // line comments from JSONC, preserving // inside strings. */
|
|
173
|
+
export function stripJsoncComments(text: string): string {
|
|
174
|
+
let result = '';
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < text.length) {
|
|
177
|
+
// String literal — copy through, respecting escapes
|
|
178
|
+
if (text[i] === '"') {
|
|
179
|
+
result += '"';
|
|
180
|
+
i++;
|
|
181
|
+
while (i < text.length && text[i] !== '"') {
|
|
182
|
+
if (text[i] === '\\') { result += text[i++]; } // escape char
|
|
183
|
+
if (i < text.length) { result += text[i++]; }
|
|
184
|
+
}
|
|
185
|
+
if (i < text.length) { result += text[i++]; } // closing "
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Line comment
|
|
189
|
+
if (text[i] === '/' && text[i + 1] === '/') {
|
|
190
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
result += text[i++];
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function readOathboundConfig(): { enforcement: EnforcementLevel } | null {
|
|
199
|
+
const configPath = join(process.cwd(), '.oathbound.jsonc');
|
|
200
|
+
if (!existsSync(configPath)) return null;
|
|
201
|
+
try {
|
|
202
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
203
|
+
const parsed = JSON.parse(stripJsoncComments(raw));
|
|
204
|
+
const level = parsed.enforcement;
|
|
205
|
+
if (level === 'warn' || level === 'registered' || level === 'audited') {
|
|
206
|
+
return { enforcement: level };
|
|
207
|
+
}
|
|
208
|
+
return { enforcement: 'warn' };
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Auto-update helpers ---
|
|
215
|
+
export function isNewer(remote: string, local: string): boolean {
|
|
216
|
+
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
|
|
217
|
+
const [rMaj, rMin, rPat] = parse(remote);
|
|
218
|
+
const [lMaj, lMin, lPat] = parse(local);
|
|
219
|
+
if (rMaj !== lMaj) return rMaj > lMaj;
|
|
220
|
+
if (rMin !== lMin) return rMin > lMin;
|
|
221
|
+
return rPat > lPat;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getCacheDir(): string {
|
|
225
|
+
if (platform() === 'darwin') {
|
|
226
|
+
return join(homedir(), 'Library', 'Caches', 'oathbound');
|
|
227
|
+
}
|
|
228
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache'), 'oathbound');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getPlatformBinaryName(): string {
|
|
232
|
+
const os = platform() === 'darwin' ? 'macos' : 'linux';
|
|
233
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
234
|
+
return `oathbound-${os}-${arch}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function printUpdateBox(current: string, latest: string): void {
|
|
238
|
+
const line = `Update available: ${current} → ${latest}`;
|
|
239
|
+
const install = 'Run: npm install -g oathbound';
|
|
240
|
+
const width = Math.max(line.length, install.length) + 2;
|
|
241
|
+
const pad = (s: string) => s + ' '.repeat(width - s.length);
|
|
242
|
+
process.stderr.write(`\n${TEAL}┌${'─'.repeat(width)}┐${RESET}\n`);
|
|
243
|
+
process.stderr.write(`${TEAL}│${RESET} ${pad(line)}${TEAL}│${RESET}\n`);
|
|
244
|
+
process.stderr.write(`${TEAL}│${RESET} ${pad(install)}${TEAL}│${RESET}\n`);
|
|
245
|
+
process.stderr.write(`${TEAL}└${'─'.repeat(width)}┘${RESET}\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function checkForUpdate(): Promise<void> {
|
|
249
|
+
const cacheDir = getCacheDir();
|
|
250
|
+
const cacheFile = join(cacheDir, 'update-check.json');
|
|
251
|
+
|
|
252
|
+
// Check cache freshness (24h)
|
|
253
|
+
if (existsSync(cacheFile)) {
|
|
254
|
+
try {
|
|
255
|
+
const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
256
|
+
if (Date.now() - cache.checkedAt < 86_400_000) {
|
|
257
|
+
if (cache.latestVersion && isNewer(cache.latestVersion, VERSION)) {
|
|
258
|
+
printUpdateBox(VERSION, cache.latestVersion);
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
} catch { /* stale cache, re-check */ }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fetch latest version from npm
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
268
|
+
try {
|
|
269
|
+
const resp = await fetch(
|
|
270
|
+
'https://registry.npmjs.org/oathbound?fields=dist-tags',
|
|
271
|
+
{ signal: controller.signal },
|
|
272
|
+
);
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
if (!resp.ok) return;
|
|
275
|
+
const data = await resp.json() as { 'dist-tags'?: { latest?: string } };
|
|
276
|
+
const latest = data['dist-tags']?.latest;
|
|
277
|
+
if (!latest) return;
|
|
278
|
+
|
|
279
|
+
// Write cache
|
|
280
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
281
|
+
writeFileSync(cacheFile, JSON.stringify({ checkedAt: Date.now(), latestVersion: latest }));
|
|
282
|
+
|
|
283
|
+
if (!isNewer(latest, VERSION)) return;
|
|
284
|
+
|
|
285
|
+
// Try auto-update the binary
|
|
286
|
+
const binaryPath = process.argv[0];
|
|
287
|
+
if (!binaryPath || binaryPath.includes('bun') || binaryPath.includes('node')) {
|
|
288
|
+
// Running via bun/node, not compiled binary — just print box
|
|
289
|
+
printUpdateBox(VERSION, latest);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const binaryName = getPlatformBinaryName();
|
|
294
|
+
const url = `https://github.com/Joshuatanderson/oath-bound/releases/download/v${latest}/${binaryName}`;
|
|
295
|
+
const dlController = new AbortController();
|
|
296
|
+
const dlTimeout = setTimeout(() => dlController.abort(), 30_000);
|
|
297
|
+
const dlResp = await fetch(url, { signal: dlController.signal, redirect: 'follow' });
|
|
298
|
+
clearTimeout(dlTimeout);
|
|
299
|
+
|
|
300
|
+
if (!dlResp.ok || !dlResp.body) {
|
|
301
|
+
printUpdateBox(VERSION, latest);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const bytes = Buffer.from(await dlResp.arrayBuffer());
|
|
306
|
+
const tmpPath = `${binaryPath}.update-${Date.now()}`;
|
|
307
|
+
writeFileSync(tmpPath, bytes);
|
|
308
|
+
chmodSync(tmpPath, 0o755);
|
|
309
|
+
renameSync(tmpPath, binaryPath);
|
|
310
|
+
process.stderr.write(`${TEAL} ✓ Updated oathbound ${VERSION} → ${latest}${RESET}\n`);
|
|
311
|
+
} catch {
|
|
312
|
+
// Network error or permission issue — silently ignore
|
|
313
|
+
// The next run will retry
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Init helpers ---
|
|
318
|
+
export function writeOathboundConfig(enforcement: EnforcementLevel): boolean {
|
|
319
|
+
const configPath = join(process.cwd(), '.oathbound.jsonc');
|
|
320
|
+
if (existsSync(configPath)) return false;
|
|
321
|
+
const content = `// Oathbound project configuration
|
|
322
|
+
// Docs: https://oathbound.ai/docs/config
|
|
323
|
+
{
|
|
324
|
+
"$schema": "https://oathbound.ai/schemas/config-v1.json",
|
|
325
|
+
"version": 1,
|
|
326
|
+
"enforcement": "${enforcement}",
|
|
327
|
+
"org": null
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
writeFileSync(configPath, content);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const OATHBOUND_HOOKS = {
|
|
335
|
+
SessionStart: [
|
|
336
|
+
{ matcher: '', hooks: [{ type: 'command', command: 'oathbound verify' }] },
|
|
337
|
+
],
|
|
338
|
+
PreToolUse: [
|
|
339
|
+
{ matcher: 'Skill', hooks: [{ type: 'command', command: 'oathbound verify --check' }] },
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
function hasOathboundHooks(settings: Record<string, unknown>): boolean {
|
|
344
|
+
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
345
|
+
if (!hooks) return false;
|
|
346
|
+
for (const entries of Object.values(hooks)) {
|
|
347
|
+
if (!Array.isArray(entries)) continue;
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
const e = entry as Record<string, unknown>;
|
|
350
|
+
const innerHooks = e.hooks as Array<Record<string, unknown>> | undefined;
|
|
351
|
+
if (!innerHooks) continue;
|
|
352
|
+
for (const h of innerHooks) {
|
|
353
|
+
if (typeof h.command === 'string' && h.command.startsWith('oathbound')) return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export type MergeResult = 'created' | 'merged' | 'skipped' | 'malformed';
|
|
361
|
+
|
|
362
|
+
export function mergeClaudeSettings(): MergeResult {
|
|
363
|
+
const claudeDir = join(process.cwd(), '.claude');
|
|
364
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
365
|
+
|
|
366
|
+
if (!existsSync(settingsPath)) {
|
|
367
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
368
|
+
writeFileSync(settingsPath, JSON.stringify({ hooks: OATHBOUND_HOOKS }, null, 2) + '\n');
|
|
369
|
+
return 'created';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let settings: Record<string, unknown>;
|
|
373
|
+
try {
|
|
374
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
375
|
+
} catch {
|
|
376
|
+
return 'malformed';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (hasOathboundHooks(settings)) return 'skipped';
|
|
380
|
+
|
|
381
|
+
// Merge hooks into existing settings
|
|
382
|
+
const hooks = (settings.hooks ?? {}) as Record<string, unknown[]>;
|
|
383
|
+
for (const [event, entries] of Object.entries(OATHBOUND_HOOKS)) {
|
|
384
|
+
const existing = hooks[event] as unknown[] | undefined;
|
|
385
|
+
hooks[event] = existing ? [...existing, ...entries] : [...entries];
|
|
386
|
+
}
|
|
387
|
+
settings.hooks = hooks;
|
|
388
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
389
|
+
return 'merged';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// --- Init command ---
|
|
393
|
+
async function init(): Promise<void> {
|
|
394
|
+
intro(`${TEAL}${BOLD} oathbound init ${RESET}`);
|
|
395
|
+
|
|
396
|
+
const enforcement = await select({
|
|
397
|
+
message: 'Choose an enforcement level:',
|
|
398
|
+
options: [
|
|
399
|
+
{ value: 'warn', label: 'Warn', hint: 'Report unverified skills but allow them' },
|
|
400
|
+
{ value: 'registered', label: 'Registered', hint: 'Block unregistered skills' },
|
|
401
|
+
{ value: 'audited', label: 'Audited', hint: 'Block skills without a passed audit' },
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (isCancel(enforcement)) {
|
|
406
|
+
cancel('Setup cancelled.');
|
|
407
|
+
process.exit(0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const level = enforcement as EnforcementLevel;
|
|
411
|
+
|
|
412
|
+
// Write .oathbound.jsonc
|
|
413
|
+
const configWritten = writeOathboundConfig(level);
|
|
414
|
+
if (configWritten) {
|
|
415
|
+
process.stderr.write(`${GREEN} ✓ Created .oathbound.jsonc${RESET}\n`);
|
|
416
|
+
} else {
|
|
417
|
+
process.stderr.write(`${DIM} .oathbound.jsonc already exists — skipped${RESET}\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Merge hooks into .claude/settings.json
|
|
421
|
+
const mergeResult = mergeClaudeSettings();
|
|
422
|
+
switch (mergeResult) {
|
|
423
|
+
case 'created':
|
|
424
|
+
process.stderr.write(`${GREEN} ✓ Created .claude/settings.json with hooks${RESET}\n`);
|
|
425
|
+
break;
|
|
426
|
+
case 'merged':
|
|
427
|
+
process.stderr.write(`${GREEN} ✓ Added hooks to .claude/settings.json${RESET}\n`);
|
|
428
|
+
break;
|
|
429
|
+
case 'skipped':
|
|
430
|
+
process.stderr.write(`${DIM} .claude/settings.json already has oathbound hooks — skipped${RESET}\n`);
|
|
431
|
+
break;
|
|
432
|
+
case 'malformed':
|
|
433
|
+
process.stderr.write(`${RED} ✗ .claude/settings.json is malformed JSON — skipped${RESET}\n`);
|
|
434
|
+
process.stderr.write(`${RED} Please fix the file manually and re-run oathbound init${RESET}\n`);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
outro(`${TEAL}${BOLD} oathbound configured (${level}) ${RESET}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
163
441
|
// --- Session state file ---
|
|
164
442
|
interface SessionState {
|
|
165
443
|
verified: Record<string, string>; // skill name → content_hash
|
|
@@ -223,11 +501,19 @@ async function verify(): Promise<void> {
|
|
|
223
501
|
localHashes[dir.name] = hashSkillDir(fullPath);
|
|
224
502
|
}
|
|
225
503
|
|
|
504
|
+
// Read enforcement config
|
|
505
|
+
const config = readOathboundConfig();
|
|
506
|
+
const enforcement: EnforcementLevel = config?.enforcement ?? 'warn';
|
|
507
|
+
|
|
226
508
|
// Fetch registry hashes from Supabase (latest version per skill name)
|
|
509
|
+
// If enforcement=audited, also fetch audit status
|
|
227
510
|
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
511
|
+
const selectFields = enforcement === 'audited'
|
|
512
|
+
? 'name, namespace, content_hash, version, audits(passed)'
|
|
513
|
+
: 'name, namespace, content_hash, version';
|
|
228
514
|
const { data: skills, error } = await supabase
|
|
229
515
|
.from('skills')
|
|
230
|
-
.select(
|
|
516
|
+
.select(selectFields)
|
|
231
517
|
.order('version', { ascending: false });
|
|
232
518
|
|
|
233
519
|
if (error) {
|
|
@@ -237,24 +523,45 @@ async function verify(): Promise<void> {
|
|
|
237
523
|
|
|
238
524
|
// Build lookup: skill name → latest content_hash (dedupe by taking first per name)
|
|
239
525
|
const registryHashes = new Map<string, string>();
|
|
526
|
+
const auditedSkills = new Set<string>(); // skills with at least one passed audit
|
|
240
527
|
for (const skill of skills ?? []) {
|
|
241
528
|
if (!skill.content_hash) continue;
|
|
242
529
|
if (!registryHashes.has(skill.name)) {
|
|
243
530
|
registryHashes.set(skill.name, skill.content_hash);
|
|
244
531
|
}
|
|
532
|
+
if (enforcement === 'audited') {
|
|
533
|
+
const audits = (skill as Record<string, unknown>).audits as Array<{ passed: boolean }> | null;
|
|
534
|
+
if (audits?.some((a) => a.passed)) {
|
|
535
|
+
auditedSkills.add(skill.name);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
245
538
|
}
|
|
246
539
|
|
|
247
540
|
const verified: Record<string, string> = {};
|
|
248
541
|
const rejected: { name: string; reason: string }[] = [];
|
|
542
|
+
const warnings: { name: string; reason: string }[] = [];
|
|
249
543
|
|
|
250
544
|
for (const [name, localHash] of Object.entries(localHashes)) {
|
|
251
545
|
const registryHash = registryHashes.get(name);
|
|
252
546
|
if (!registryHash) {
|
|
253
547
|
process.stderr.write(`${DIM} ${name}: ${localHash} (not in registry)${RESET}\n`);
|
|
254
|
-
|
|
548
|
+
if (enforcement === 'warn') {
|
|
549
|
+
warnings.push({ name, reason: 'not in registry' });
|
|
550
|
+
verified[name] = localHash; // allow in warn mode
|
|
551
|
+
} else {
|
|
552
|
+
rejected.push({ name, reason: 'not in registry' });
|
|
553
|
+
}
|
|
255
554
|
} else if (localHash !== registryHash) {
|
|
256
555
|
process.stderr.write(`${RED} ${name}: ${localHash} ≠ ${registryHash}${RESET}\n`);
|
|
257
|
-
|
|
556
|
+
if (enforcement === 'warn') {
|
|
557
|
+
warnings.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${registryHash.slice(0, 8)}…)` });
|
|
558
|
+
verified[name] = localHash;
|
|
559
|
+
} else {
|
|
560
|
+
rejected.push({ name, reason: `content hash mismatch (local: ${localHash.slice(0, 8)}…, registry: ${registryHash.slice(0, 8)}…)` });
|
|
561
|
+
}
|
|
562
|
+
} else if (enforcement === 'audited' && !auditedSkills.has(name)) {
|
|
563
|
+
process.stderr.write(`${YELLOW} ${name}: ${localHash} (registered but not audited)${RESET}\n`);
|
|
564
|
+
rejected.push({ name, reason: 'no passed audit' });
|
|
258
565
|
} else {
|
|
259
566
|
process.stderr.write(`${GREEN} ${name}: ${localHash} ✓${RESET}\n`);
|
|
260
567
|
verified[name] = localHash;
|
|
@@ -265,7 +572,7 @@ async function verify(): Promise<void> {
|
|
|
265
572
|
const state: SessionState = { verified, rejected, ok };
|
|
266
573
|
writeFileSync(sessionStatePath(sessionId), JSON.stringify(state));
|
|
267
574
|
|
|
268
|
-
if (ok) {
|
|
575
|
+
if (ok && warnings.length === 0) {
|
|
269
576
|
const names = Object.keys(verified).join(', ');
|
|
270
577
|
console.log(JSON.stringify({
|
|
271
578
|
hookSpecificOutput: {
|
|
@@ -274,9 +581,21 @@ async function verify(): Promise<void> {
|
|
|
274
581
|
},
|
|
275
582
|
}));
|
|
276
583
|
process.exit(0);
|
|
584
|
+
} else if (ok && warnings.length > 0) {
|
|
585
|
+
// Warn mode — all skills allowed but with warnings
|
|
586
|
+
const warnLines = warnings.map((w) => ` ⚠ ${w.name}: ${w.reason}`).join('\n');
|
|
587
|
+
const names = Object.keys(verified).join(', ');
|
|
588
|
+
process.stderr.write(`${YELLOW}Oathbound warnings (enforcement: warn):\n${warnLines}${RESET}\n`);
|
|
589
|
+
console.log(JSON.stringify({
|
|
590
|
+
hookSpecificOutput: {
|
|
591
|
+
hookEventName: 'SessionStart',
|
|
592
|
+
additionalContext: `Oathbound (warn mode): ${Object.keys(verified).length} skill(s) allowed [${names}]. Warnings:\n${warnLines}`,
|
|
593
|
+
},
|
|
594
|
+
}));
|
|
595
|
+
process.exit(0);
|
|
277
596
|
} else {
|
|
278
597
|
const lines = rejected.map((r) => ` - ${r.name}: ${r.reason}`);
|
|
279
|
-
process.stderr.write(`Oathbound: skill verification failed
|
|
598
|
+
process.stderr.write(`Oathbound: skill verification failed! (enforcement: ${enforcement})\n${lines.join('\n')}\nDo NOT use unverified skills.\n`);
|
|
280
599
|
process.exit(2);
|
|
281
600
|
}
|
|
282
601
|
}
|
|
@@ -439,6 +758,9 @@ async function pull(skillArg: string): Promise<void> {
|
|
|
439
758
|
}
|
|
440
759
|
|
|
441
760
|
// --- Entry ---
|
|
761
|
+
if (!import.meta.main) {
|
|
762
|
+
// Module imported for testing — skip CLI entry
|
|
763
|
+
} else {
|
|
442
764
|
const args = Bun.argv.slice(2);
|
|
443
765
|
const subcommand = args[0];
|
|
444
766
|
|
|
@@ -451,7 +773,17 @@ if (subcommand === '--version' || subcommand === '-v') {
|
|
|
451
773
|
process.exit(0);
|
|
452
774
|
}
|
|
453
775
|
|
|
454
|
-
|
|
776
|
+
// Fire-and-forget auto-update on every command except verify (hooks must be fast)
|
|
777
|
+
if (subcommand !== 'verify') {
|
|
778
|
+
checkForUpdate().catch(() => {});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (subcommand === 'init') {
|
|
782
|
+
init().catch((err: unknown) => {
|
|
783
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
784
|
+
fail('Init failed', msg);
|
|
785
|
+
});
|
|
786
|
+
} else if (subcommand === 'verify') {
|
|
455
787
|
const isCheck = args.includes('--check');
|
|
456
788
|
const run = isCheck ? verifyCheck : verify;
|
|
457
789
|
run().catch((err: unknown) => {
|
|
@@ -472,3 +804,4 @@ if (subcommand === 'verify') {
|
|
|
472
804
|
fail('Unexpected error', msg);
|
|
473
805
|
});
|
|
474
806
|
}
|
|
807
|
+
} // end if (import.meta.main)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"postinstall": "node install.cjs"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@clack/prompts": "^1.1.0",
|
|
47
48
|
"@supabase/supabase-js": "^2"
|
|
48
49
|
}
|
|
49
50
|
}
|