oathbound 0.14.0 → 0.15.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/dist/cli.cjs +19114 -0
- package/package.json +6 -28
- package/agent-push.ts +0 -170
- package/agent-search.ts +0 -162
- package/auth.ts +0 -219
- package/bin/cli.cjs +0 -23
- package/cli.ts +0 -617
- package/config.ts +0 -128
- package/content-hash.ts +0 -39
- package/install.cjs +0 -85
- package/push.ts +0 -133
- package/search.ts +0 -159
- package/semver.ts +0 -39
- package/ui.ts +0 -87
- package/update.ts +0 -111
- package/verify.ts +0 -400
package/config.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
writeFileSync, readFileSync, existsSync, mkdirSync,
|
|
3
|
-
} from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export type EnforcementLevel = 'warn' | 'registered' | 'audited';
|
|
7
|
-
|
|
8
|
-
/** Strip // line comments from JSONC, preserving // inside strings. */
|
|
9
|
-
export function stripJsoncComments(text: string): string {
|
|
10
|
-
let result = '';
|
|
11
|
-
let i = 0;
|
|
12
|
-
while (i < text.length) {
|
|
13
|
-
// String literal — copy through, respecting escapes
|
|
14
|
-
if (text[i] === '"') {
|
|
15
|
-
result += '"';
|
|
16
|
-
i++;
|
|
17
|
-
while (i < text.length && text[i] !== '"') {
|
|
18
|
-
if (text[i] === '\\') { result += text[i++]; } // escape char
|
|
19
|
-
if (i < text.length) { result += text[i++]; }
|
|
20
|
-
}
|
|
21
|
-
if (i < text.length) { result += text[i++]; } // closing "
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
// Line comment
|
|
25
|
-
if (text[i] === '/' && text[i + 1] === '/') {
|
|
26
|
-
while (i < text.length && text[i] !== '\n') i++;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
result += text[i++];
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function readOathboundConfig(): { enforcement: EnforcementLevel } | null {
|
|
35
|
-
const configPath = join(process.cwd(), '.oathbound.jsonc');
|
|
36
|
-
if (!existsSync(configPath)) return null;
|
|
37
|
-
try {
|
|
38
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
39
|
-
const parsed = JSON.parse(stripJsoncComments(raw));
|
|
40
|
-
const level = parsed.enforcement;
|
|
41
|
-
if (level === 'warn' || level === 'registered' || level === 'audited') {
|
|
42
|
-
return { enforcement: level };
|
|
43
|
-
}
|
|
44
|
-
return { enforcement: 'warn' };
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function writeOathboundConfig(enforcement: EnforcementLevel): boolean {
|
|
51
|
-
const configPath = join(process.cwd(), '.oathbound.jsonc');
|
|
52
|
-
if (existsSync(configPath)) return false;
|
|
53
|
-
const content = `// Oathbound project configuration
|
|
54
|
-
// Docs: https://oathbound.ai/docs/config
|
|
55
|
-
{
|
|
56
|
-
"$schema": "https://oathbound.ai/schemas/config-v1.json",
|
|
57
|
-
"version": 1,
|
|
58
|
-
"enforcement": "${enforcement}",
|
|
59
|
-
"org": null
|
|
60
|
-
}
|
|
61
|
-
`;
|
|
62
|
-
writeFileSync(configPath, content);
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const SKILL_CHECK = { type: 'command', command: 'npx oathbound verify --check' };
|
|
67
|
-
|
|
68
|
-
const OATHBOUND_HOOKS = {
|
|
69
|
-
SessionStart: [
|
|
70
|
-
{ matcher: '', hooks: [{ type: 'command', command: 'npx oathbound verify' }] },
|
|
71
|
-
],
|
|
72
|
-
PreToolUse: [
|
|
73
|
-
{ matcher: 'Skill', hooks: [SKILL_CHECK] },
|
|
74
|
-
{ matcher: 'Bash', hooks: [SKILL_CHECK] },
|
|
75
|
-
{ matcher: 'Read', hooks: [SKILL_CHECK] },
|
|
76
|
-
{ matcher: 'Glob', hooks: [SKILL_CHECK] },
|
|
77
|
-
{ matcher: 'Grep', hooks: [SKILL_CHECK] },
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function hasOathboundHooks(settings: Record<string, unknown>): boolean {
|
|
82
|
-
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
83
|
-
if (!hooks) return false;
|
|
84
|
-
for (const entries of Object.values(hooks)) {
|
|
85
|
-
if (!Array.isArray(entries)) continue;
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
const e = entry as Record<string, unknown>;
|
|
88
|
-
const innerHooks = e.hooks as Array<Record<string, unknown>> | undefined;
|
|
89
|
-
if (!innerHooks) continue;
|
|
90
|
-
for (const h of innerHooks) {
|
|
91
|
-
if (typeof h.command === 'string' && h.command.startsWith('npx oathbound')) return true;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export type MergeResult = 'created' | 'merged' | 'skipped' | 'malformed';
|
|
99
|
-
|
|
100
|
-
export function mergeClaudeSettings(): MergeResult {
|
|
101
|
-
const claudeDir = join(process.cwd(), '.claude');
|
|
102
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
103
|
-
|
|
104
|
-
if (!existsSync(settingsPath)) {
|
|
105
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
106
|
-
writeFileSync(settingsPath, JSON.stringify({ hooks: OATHBOUND_HOOKS }, null, 2) + '\n');
|
|
107
|
-
return 'created';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let settings: Record<string, unknown>;
|
|
111
|
-
try {
|
|
112
|
-
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
113
|
-
} catch {
|
|
114
|
-
return 'malformed';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (hasOathboundHooks(settings)) return 'skipped';
|
|
118
|
-
|
|
119
|
-
// Merge hooks into existing settings
|
|
120
|
-
const hooks = (settings.hooks ?? {}) as Record<string, unknown[]>;
|
|
121
|
-
for (const [event, entries] of Object.entries(OATHBOUND_HOOKS)) {
|
|
122
|
-
const existing = hooks[event] as unknown[] | undefined;
|
|
123
|
-
hooks[event] = existing ? [...existing, ...entries] : [...entries];
|
|
124
|
-
}
|
|
125
|
-
settings.hooks = hooks;
|
|
126
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
127
|
-
return 'merged';
|
|
128
|
-
}
|
package/content-hash.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join, relative } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const HASH_EXCLUDED = new Set([
|
|
6
|
-
'node_modules',
|
|
7
|
-
'bun.lock',
|
|
8
|
-
'package-lock.json',
|
|
9
|
-
'yarn.lock',
|
|
10
|
-
'.DS_Store',
|
|
11
|
-
]);
|
|
12
|
-
|
|
13
|
-
export function collectFiles(dir: string, base: string = dir): { path: string; content: Buffer }[] {
|
|
14
|
-
const results: { path: string; content: Buffer }[] = [];
|
|
15
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
-
if (HASH_EXCLUDED.has(entry.name)) continue;
|
|
17
|
-
const full = join(dir, entry.name);
|
|
18
|
-
if (entry.isDirectory()) {
|
|
19
|
-
results.push(...collectFiles(full, base));
|
|
20
|
-
} else if (entry.isFile()) {
|
|
21
|
-
results.push({ path: relative(base, full), content: readFileSync(full) });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return results;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function contentHash(files: { path: string; content: Buffer }[]): string {
|
|
28
|
-
const sorted = files.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
29
|
-
const lines = sorted.map((f) => {
|
|
30
|
-
const h = createHash('sha256').update(f.content).digest('hex');
|
|
31
|
-
return `${f.path}\0${h}`;
|
|
32
|
-
});
|
|
33
|
-
return createHash('sha256').update(lines.join('\n')).digest('hex');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function hashSkillDir(skillDir: string): string {
|
|
37
|
-
const files = collectFiles(skillDir);
|
|
38
|
-
return contentHash(files);
|
|
39
|
-
}
|
package/install.cjs
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Postinstall script: downloads the correct platform binary from GitHub Releases.
|
|
4
|
-
// Skips download in CI (binaries don't exist yet during the build job).
|
|
5
|
-
|
|
6
|
-
if (process.env.CI) {
|
|
7
|
-
console.log("oathbound: skipping binary download in CI");
|
|
8
|
-
process.exit(0);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const https = require("https");
|
|
12
|
-
const fs = require("fs");
|
|
13
|
-
const path = require("path");
|
|
14
|
-
|
|
15
|
-
const pkg = require("./package.json");
|
|
16
|
-
const VERSION = pkg.version;
|
|
17
|
-
const REPO = "Joshuatanderson/oath-bound";
|
|
18
|
-
|
|
19
|
-
const PLATFORM_MAP = {
|
|
20
|
-
darwin: "darwin",
|
|
21
|
-
linux: "linux",
|
|
22
|
-
win32: "windows",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const ARCH_MAP = {
|
|
26
|
-
arm64: "arm64",
|
|
27
|
-
x64: "x64",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function getBinaryName() {
|
|
31
|
-
const platform = PLATFORM_MAP[process.platform];
|
|
32
|
-
const arch = ARCH_MAP[process.arch];
|
|
33
|
-
if (!platform || !arch) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`Unsupported platform: ${process.platform}-${process.arch}`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
const ext = process.platform === "win32" ? ".exe" : "";
|
|
39
|
-
return `oathbound-${platform}-${arch}${ext}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function download(url) {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
https
|
|
45
|
-
.get(url, (res) => {
|
|
46
|
-
// Follow redirects (GitHub sends 302 to S3)
|
|
47
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
48
|
-
return download(res.headers.location).then(resolve, reject);
|
|
49
|
-
}
|
|
50
|
-
if (res.statusCode !== 200) {
|
|
51
|
-
return reject(new Error(`Download failed: HTTP ${res.statusCode} for ${url}`));
|
|
52
|
-
}
|
|
53
|
-
const chunks = [];
|
|
54
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
55
|
-
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
56
|
-
res.on("error", reject);
|
|
57
|
-
})
|
|
58
|
-
.on("error", reject);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function main() {
|
|
63
|
-
const binaryName = getBinaryName();
|
|
64
|
-
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`;
|
|
65
|
-
const destDir = path.join(__dirname, "bin");
|
|
66
|
-
const ext = process.platform === "win32" ? ".exe" : "";
|
|
67
|
-
const dest = path.join(destDir, `oathbound${ext}`);
|
|
68
|
-
|
|
69
|
-
console.log(`oathbound: downloading ${binaryName} from v${VERSION} release...`);
|
|
70
|
-
|
|
71
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
72
|
-
|
|
73
|
-
const data = await download(url);
|
|
74
|
-
fs.writeFileSync(dest, data);
|
|
75
|
-
fs.chmodSync(dest, 0o755);
|
|
76
|
-
|
|
77
|
-
console.log(`oathbound: installed to ${dest}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
main().catch((err) => {
|
|
81
|
-
console.error(`oathbound install failed: ${err.message}`);
|
|
82
|
-
console.error("You can download the binary manually from:");
|
|
83
|
-
console.error(` https://github.com/${REPO}/releases/tag/v${VERSION}`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
});
|
package/push.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, basename, resolve } from 'node:path';
|
|
3
|
-
import { intro, outro } from '@clack/prompts';
|
|
4
|
-
import { parse as yamlParse } from 'yaml';
|
|
5
|
-
import { BRAND, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
6
|
-
import { getAccessToken } from './auth';
|
|
7
|
-
import { collectFiles } from './content-hash';
|
|
8
|
-
import { isValidSemver } from './semver';
|
|
9
|
-
|
|
10
|
-
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
11
|
-
|
|
12
|
-
export async function push(pathArg?: string, options?: { private?: boolean }): Promise<void> {
|
|
13
|
-
intro(BRAND);
|
|
14
|
-
|
|
15
|
-
// Resolve skill directory
|
|
16
|
-
const skillDir = resolveSkillDir(pathArg);
|
|
17
|
-
console.log(`${DIM} directory: ${skillDir}${RESET}`);
|
|
18
|
-
|
|
19
|
-
// Read and validate SKILL.md exists
|
|
20
|
-
if (!existsSync(join(skillDir, 'SKILL.md'))) {
|
|
21
|
-
fail('No SKILL.md found', `Expected at ${join(skillDir, 'SKILL.md')}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Collect files
|
|
25
|
-
const rawFiles = collectFiles(skillDir);
|
|
26
|
-
|
|
27
|
-
// Parse SKILL.md frontmatter to extract metadata
|
|
28
|
-
const skillMdFile = rawFiles.find(f => f.path === 'SKILL.md');
|
|
29
|
-
if (!skillMdFile) {
|
|
30
|
-
fail('SKILL.md not found in collected files');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const meta = parseFrontmatter(skillMdFile.content.toString('utf-8'));
|
|
34
|
-
const name = String(meta.name ?? '');
|
|
35
|
-
const description = String(meta.description ?? '');
|
|
36
|
-
const license = String(meta.license ?? '');
|
|
37
|
-
const rawVersion = meta.version != null ? String(meta.version) : null;
|
|
38
|
-
const version = rawVersion && isValidSemver(rawVersion) ? rawVersion : null;
|
|
39
|
-
if (!name) fail('SKILL.md frontmatter missing: name');
|
|
40
|
-
if (!description) fail('SKILL.md frontmatter missing: description');
|
|
41
|
-
if (!license) fail('SKILL.md frontmatter missing: license');
|
|
42
|
-
|
|
43
|
-
const oathboundMeta = (meta.metadata as Record<string, unknown> | undefined)?.oathbound as Record<string, unknown> | undefined;
|
|
44
|
-
const originalAuthor = String(oathboundMeta?.['original-author'] ?? '');
|
|
45
|
-
|
|
46
|
-
// Build files array with root dir prefix (API expects rootDir/path format)
|
|
47
|
-
const files = rawFiles.map(f => ({
|
|
48
|
-
path: `${name}/${f.path}`,
|
|
49
|
-
content: f.content.toString('utf-8'),
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
console.log(`${DIM} name: ${name}${RESET}`);
|
|
53
|
-
console.log(`${DIM} version: ${version ?? 'auto (next)'}${RESET}`);
|
|
54
|
-
console.log(`${DIM} license: ${license}${RESET}`);
|
|
55
|
-
if (originalAuthor) {
|
|
56
|
-
console.log(`${DIM} original author: ${originalAuthor}${RESET}`);
|
|
57
|
-
}
|
|
58
|
-
const visibility = options?.private ? 'private' : 'public';
|
|
59
|
-
if (options?.private) {
|
|
60
|
-
console.log(`${DIM} visibility: ${BOLD}private${RESET}`);
|
|
61
|
-
}
|
|
62
|
-
console.log(`${DIM} ${files.length} file(s)${RESET}`);
|
|
63
|
-
|
|
64
|
-
// Authenticate
|
|
65
|
-
const token = await getAccessToken();
|
|
66
|
-
|
|
67
|
-
// Push to API
|
|
68
|
-
const spin = spinner('Pushing...');
|
|
69
|
-
|
|
70
|
-
const response = await fetch(`${API_BASE}/api/skills`, {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: {
|
|
73
|
-
'Content-Type': 'application/json',
|
|
74
|
-
Authorization: `Bearer ${token}`,
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify({
|
|
77
|
-
name,
|
|
78
|
-
description,
|
|
79
|
-
license,
|
|
80
|
-
version,
|
|
81
|
-
compatibility: String(meta.compatibility ?? '') || null,
|
|
82
|
-
allowedTools: String(meta['allowed-tools'] ?? '') || null,
|
|
83
|
-
originalAuthor: originalAuthor || null,
|
|
84
|
-
visibility,
|
|
85
|
-
files,
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
spin.stop();
|
|
90
|
-
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
const body = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
93
|
-
const details = Array.isArray(body.details)
|
|
94
|
-
? '\n' + body.details.map((d: string) => ` - ${d}`).join('\n')
|
|
95
|
-
: '';
|
|
96
|
-
fail(`Push failed (${response.status})`, `${body.error}${details}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const result = await response.json();
|
|
100
|
-
|
|
101
|
-
outro(`${GREEN}✓ Published ${BOLD}${result.namespace}/${result.name}${RESET}${GREEN} v${result.version}${RESET}`);
|
|
102
|
-
if (result.suiObjectId) {
|
|
103
|
-
console.log(`${DIM} on-chain: ${result.suiObjectId}${RESET}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function resolveSkillDir(pathArg?: string): string {
|
|
108
|
-
if (pathArg) {
|
|
109
|
-
const resolved = resolve(pathArg);
|
|
110
|
-
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
111
|
-
fail('Invalid path', `${resolved} is not a directory`);
|
|
112
|
-
}
|
|
113
|
-
return resolved;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// No path given — check if cwd has a SKILL.md
|
|
117
|
-
if (existsSync(join(process.cwd(), 'SKILL.md'))) {
|
|
118
|
-
return process.cwd();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
fail(
|
|
122
|
-
'No skill directory found',
|
|
123
|
-
'Run from within a skill directory or pass a path: oathbound push ./my-skill',
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Parse YAML frontmatter into a nested object */
|
|
128
|
-
function parseFrontmatter(content: string): Record<string, unknown> {
|
|
129
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
130
|
-
if (!match) return {};
|
|
131
|
-
const parsed = yamlParse(match[1]);
|
|
132
|
-
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : {};
|
|
133
|
-
}
|
package/search.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { BRAND, TEAL, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
2
|
-
|
|
3
|
-
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
4
|
-
|
|
5
|
-
export interface SearchOptions {
|
|
6
|
-
query?: string;
|
|
7
|
-
namespace?: string;
|
|
8
|
-
sparse?: boolean;
|
|
9
|
-
sort?: 'downloads';
|
|
10
|
-
limit?: number;
|
|
11
|
-
offset?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function parseSearchArgs(args: string[]): SearchOptions {
|
|
15
|
-
const opts: SearchOptions = {};
|
|
16
|
-
let i = 0;
|
|
17
|
-
|
|
18
|
-
while (i < args.length) {
|
|
19
|
-
const arg = args[i];
|
|
20
|
-
|
|
21
|
-
if (arg === '--user' || arg === '-u') {
|
|
22
|
-
opts.namespace = args[++i];
|
|
23
|
-
} else if (arg === '--sparse' || arg === '-s') {
|
|
24
|
-
opts.sparse = true;
|
|
25
|
-
} else if (arg === '--sort') {
|
|
26
|
-
const val = args[++i];
|
|
27
|
-
if (val === 'downloads') opts.sort = 'downloads';
|
|
28
|
-
} else if (arg === '--limit') {
|
|
29
|
-
opts.limit = parseInt(args[++i], 10);
|
|
30
|
-
} else if (arg === '--offset') {
|
|
31
|
-
opts.offset = parseInt(args[++i], 10);
|
|
32
|
-
} else if (!arg.startsWith('-')) {
|
|
33
|
-
opts.query = arg;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
i++;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return opts;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface SkillAuthor {
|
|
43
|
-
username: string;
|
|
44
|
-
display_name: string | null;
|
|
45
|
-
verified: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface SkillResult {
|
|
49
|
-
name: string;
|
|
50
|
-
namespace: string;
|
|
51
|
-
description: string;
|
|
52
|
-
version: string;
|
|
53
|
-
license?: string;
|
|
54
|
-
visibility?: string;
|
|
55
|
-
author?: SkillAuthor;
|
|
56
|
-
audit_status?: 'passed' | 'failed' | 'none';
|
|
57
|
-
download_count?: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface SearchResponse {
|
|
61
|
-
ok: boolean;
|
|
62
|
-
skills: SkillResult[];
|
|
63
|
-
total: number;
|
|
64
|
-
limit: number;
|
|
65
|
-
offset: number;
|
|
66
|
-
error?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function search(opts: SearchOptions): Promise<void> {
|
|
70
|
-
const params = new URLSearchParams();
|
|
71
|
-
if (opts.query) params.set('q', opts.query);
|
|
72
|
-
if (opts.namespace) params.set('namespace', opts.namespace);
|
|
73
|
-
if (opts.sparse) params.set('sparse', 'true');
|
|
74
|
-
if (opts.sort) params.set('sort', opts.sort);
|
|
75
|
-
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
76
|
-
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
77
|
-
|
|
78
|
-
const url = `${API_BASE}/api/skills?${params}`;
|
|
79
|
-
|
|
80
|
-
const sp = spinner('Searching...');
|
|
81
|
-
|
|
82
|
-
let res: Response;
|
|
83
|
-
try {
|
|
84
|
-
res = await fetch(url);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
sp.stop();
|
|
87
|
-
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
88
|
-
fail('Search failed', msg);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
sp.stop();
|
|
92
|
-
|
|
93
|
-
if (!res.ok) {
|
|
94
|
-
let detail = `HTTP ${res.status}`;
|
|
95
|
-
try {
|
|
96
|
-
const body = await res.json() as { error?: string };
|
|
97
|
-
if (body.error) detail = body.error;
|
|
98
|
-
} catch { /* ignore parse errors */ }
|
|
99
|
-
fail('Search failed', detail);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const data = await res.json() as SearchResponse;
|
|
103
|
-
|
|
104
|
-
if (!data.ok || !data.skills) {
|
|
105
|
-
fail('Search failed', data.error ?? 'Unexpected response');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const { skills, total, offset } = data;
|
|
109
|
-
|
|
110
|
-
if (skills.length === 0) {
|
|
111
|
-
console.log(`\n${BRAND} ${DIM}No skills found.${RESET}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const showing = offset > 0
|
|
116
|
-
? `Showing ${offset + 1}–${offset + skills.length} of ${total}`
|
|
117
|
-
: `${total} skill${total === 1 ? '' : 's'} found`;
|
|
118
|
-
|
|
119
|
-
console.log(`\n${BRAND} ${TEAL}${showing}${RESET}\n`);
|
|
120
|
-
|
|
121
|
-
for (const skill of skills) {
|
|
122
|
-
const id = `${skill.namespace}/${skill.name}`;
|
|
123
|
-
const ver = `v${skill.version}`;
|
|
124
|
-
|
|
125
|
-
// Line 1: name + version
|
|
126
|
-
console.log(` ${BOLD}${id}${RESET} ${DIM}${ver}${RESET}`);
|
|
127
|
-
|
|
128
|
-
// Line 2: description
|
|
129
|
-
if (skill.description) {
|
|
130
|
-
console.log(` ${DIM}${skill.description}${RESET}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Line 3: metadata (non-sparse only)
|
|
134
|
-
if (!opts.sparse && (skill.author || skill.audit_status || skill.license)) {
|
|
135
|
-
const parts: string[] = [];
|
|
136
|
-
if (skill.author) {
|
|
137
|
-
const name = skill.author.display_name || skill.author.username;
|
|
138
|
-
parts.push(`by ${name}${skill.author.verified ? ' ✓' : ''}`);
|
|
139
|
-
}
|
|
140
|
-
if (skill.license) parts.push(skill.license);
|
|
141
|
-
if (skill.download_count != null) parts.push(`↓ ${skill.download_count}`);
|
|
142
|
-
if (skill.audit_status && skill.audit_status !== 'none') {
|
|
143
|
-
parts.push(skill.audit_status === 'passed' ? `${GREEN}audited${RESET}` : 'audit failed');
|
|
144
|
-
}
|
|
145
|
-
if (skill.visibility === 'private') parts.push('private');
|
|
146
|
-
if (parts.length > 0) {
|
|
147
|
-
console.log(` ${DIM}${parts.join(' · ')}${RESET}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(); // blank line between skills
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Pagination hint
|
|
155
|
-
if (offset + skills.length < total) {
|
|
156
|
-
const nextOffset = offset + skills.length;
|
|
157
|
-
console.log(`${DIM} Use --offset ${nextOffset} to see more${RESET}\n`);
|
|
158
|
-
}
|
|
159
|
-
}
|
package/semver.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// --- ANSI (respect NO_COLOR standard: https://no-color.org) ---
|
|
2
|
-
export const USE_COLOR = process.env.NO_COLOR === undefined && process.stderr.isTTY;
|
|
3
|
-
export const TEAL = USE_COLOR ? '\x1b[38;2;63;168;164m' : ''; // brand teal #3fa8a4
|
|
4
|
-
export const GREEN = USE_COLOR ? '\x1b[32m' : '';
|
|
5
|
-
export const RED = USE_COLOR ? '\x1b[31m' : '';
|
|
6
|
-
export const YELLOW = USE_COLOR ? '\x1b[33m' : '';
|
|
7
|
-
export const DIM = USE_COLOR ? '\x1b[2m' : '';
|
|
8
|
-
export const BOLD = USE_COLOR ? '\x1b[1m' : '';
|
|
9
|
-
export const RESET = USE_COLOR ? '\x1b[0m' : '';
|
|
10
|
-
|
|
11
|
-
const BRAND_MARKS = ['✦', '✧', '✿', '⬡'];
|
|
12
|
-
|
|
13
|
-
export function brand(): string {
|
|
14
|
-
return `${TEAL}${BOLD}⬡ oathbound${RESET}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const BRAND = brand();
|
|
18
|
-
|
|
19
|
-
export function usage(exitCode = 1): never {
|
|
20
|
-
console.log(`
|
|
21
|
-
${BOLD}oathbound${RESET} — install, verify, and publish skills & agents
|
|
22
|
-
|
|
23
|
-
${DIM}Usage:${RESET}
|
|
24
|
-
oathbound init ${DIM}Setup wizard — configure project${RESET}
|
|
25
|
-
oathbound pull <namespace/skill-name[@version]>
|
|
26
|
-
oathbound install <namespace/skill-name[@version]>
|
|
27
|
-
oathbound push [path] [--private] ${DIM}Publish a skill to the registry${RESET}
|
|
28
|
-
oathbound search [query] ${DIM}Search skills in the registry${RESET}
|
|
29
|
-
oathbound list ${DIM}List all public skills${RESET}
|
|
30
|
-
oathbound login ${DIM}Authenticate with oathbound.ai${RESET}
|
|
31
|
-
oathbound logout ${DIM}Clear stored credentials${RESET}
|
|
32
|
-
oathbound whoami ${DIM}Show current user${RESET}
|
|
33
|
-
oathbound verify ${DIM}SessionStart hook — verify all skills${RESET}
|
|
34
|
-
oathbound verify --check ${DIM}PreToolUse hook — check skill integrity${RESET}
|
|
35
|
-
|
|
36
|
-
${DIM}Agents:${RESET}
|
|
37
|
-
oathbound agent push [path] [--private] ${DIM}Publish an agent .md file${RESET}
|
|
38
|
-
oathbound agent pull <namespace/name[@version]>
|
|
39
|
-
oathbound agent search [query] ${DIM}Search agents in the registry${RESET}
|
|
40
|
-
oathbound agent list ${DIM}List all public agents${RESET}
|
|
41
|
-
|
|
42
|
-
${DIM}Options:${RESET}
|
|
43
|
-
--help, -h Show this help message
|
|
44
|
-
--version, -v Show version
|
|
45
|
-
`);
|
|
46
|
-
process.exit(exitCode);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function agentUsage(exitCode = 1): never {
|
|
50
|
-
console.log(`
|
|
51
|
-
${BOLD}oathbound agent${RESET} — manage Claude Code agents
|
|
52
|
-
|
|
53
|
-
${DIM}Usage:${RESET}
|
|
54
|
-
oathbound agent push [path] [--private] ${DIM}Publish an agent .md file to the registry${RESET}
|
|
55
|
-
oathbound agent pull <namespace/name[@version]> ${DIM}Download agent to .claude/agents/${RESET}
|
|
56
|
-
oathbound agent search [query] ${DIM}Search agents in the registry${RESET}
|
|
57
|
-
oathbound agent list ${DIM}List all public agents${RESET}
|
|
58
|
-
|
|
59
|
-
${DIM}Options:${RESET}
|
|
60
|
-
--help, -h Show this help message
|
|
61
|
-
`);
|
|
62
|
-
process.exit(exitCode);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function fail(message: string, detail?: string): never {
|
|
66
|
-
console.log(`\n${BOLD}${RED} ✗ ${message}${RESET}`);
|
|
67
|
-
if (detail) {
|
|
68
|
-
console.log(`${RED} ${detail}${RESET}`);
|
|
69
|
-
}
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function spinner(text: string): { stop: () => void } {
|
|
74
|
-
let i = 0;
|
|
75
|
-
const render = () => `\r${TEAL}${BOLD} ${BRAND_MARKS[i % BRAND_MARKS.length]} ${RESET}${TEAL}${text}${RESET}`;
|
|
76
|
-
process.stdout.write(render());
|
|
77
|
-
const interval = setInterval(() => {
|
|
78
|
-
i++;
|
|
79
|
-
process.stdout.write(render());
|
|
80
|
-
}, 150);
|
|
81
|
-
return {
|
|
82
|
-
stop() {
|
|
83
|
-
clearInterval(interval);
|
|
84
|
-
process.stdout.write(USE_COLOR ? '\r\x1b[2K' : '\n');
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|