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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oathbound",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Install verified Claude Code skills and agents from the Oath Bound registry",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Josh Anderson",
|
|
@@ -18,43 +18,21 @@
|
|
|
18
18
|
"security",
|
|
19
19
|
"ai-tools"
|
|
20
20
|
],
|
|
21
|
-
"type": "module",
|
|
22
21
|
"bin": {
|
|
23
|
-
"oathbound": "./
|
|
22
|
+
"oathbound": "./dist/cli.cjs"
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
26
|
-
"cli.
|
|
27
|
-
"ui.ts",
|
|
28
|
-
"config.ts",
|
|
29
|
-
"update.ts",
|
|
30
|
-
"verify.ts",
|
|
31
|
-
"content-hash.ts",
|
|
32
|
-
"auth.ts",
|
|
33
|
-
"push.ts",
|
|
34
|
-
"search.ts",
|
|
35
|
-
"agent-push.ts",
|
|
36
|
-
"agent-search.ts",
|
|
37
|
-
"semver.ts",
|
|
38
|
-
"bin/cli.cjs",
|
|
39
|
-
"install.cjs"
|
|
25
|
+
"dist/cli.cjs"
|
|
40
26
|
],
|
|
41
27
|
"publishConfig": {
|
|
42
28
|
"access": "public",
|
|
43
29
|
"provenance": true
|
|
44
30
|
},
|
|
45
31
|
"scripts": {
|
|
46
|
-
"build": "bun build
|
|
47
|
-
"
|
|
48
|
-
"build:macos-x64": "bun build ./cli.ts --compile --target=bun-darwin-x64 --outfile dist/oathbound-macos-x64",
|
|
49
|
-
"build:linux-x64": "bun build ./cli.ts --compile --target=bun-linux-x64 --outfile dist/oathbound-linux-x64",
|
|
50
|
-
"build:linux-arm64": "bun build ./cli.ts --compile --target=bun-linux-arm64 --outfile dist/oathbound-linux-arm64",
|
|
51
|
-
"build:windows-x64": "bun build ./cli.ts --compile --target=bun-windows-x64 --outfile dist/oathbound-windows-x64",
|
|
52
|
-
"build:windows-arm64": "bun build ./cli.ts --compile --target=bun-windows-arm64 --outfile dist/oathbound-windows-arm64",
|
|
53
|
-
"build:all": "bun run build:macos-arm64 && bun run build:macos-x64 && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:windows-x64 && bun run build:windows-arm64",
|
|
54
|
-
"test": "bun test",
|
|
55
|
-
"postinstall": "node install.cjs"
|
|
32
|
+
"build": "bun build cli.ts --outfile=dist/cli.cjs --target=node --format=cjs && node -e \"const f='dist/cli.cjs';let s=require('fs').readFileSync(f,'utf8');s=s.replace('require.main == require.module','require.main === module');require('fs').writeFileSync(f,s)\" && printf '%s\\n' '#!/usr/bin/env node' | cat - dist/cli.cjs > dist/tmp && mv dist/tmp dist/cli.cjs",
|
|
33
|
+
"test": "bun test"
|
|
56
34
|
},
|
|
57
|
-
"
|
|
35
|
+
"devDependencies": {
|
|
58
36
|
"@clack/prompts": "^1.1.0",
|
|
59
37
|
"@supabase/supabase-js": "^2",
|
|
60
38
|
"yaml": "^2.8.2"
|
package/agent-push.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, 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 { isValidSemver } from './semver';
|
|
8
|
-
|
|
9
|
-
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
10
|
-
|
|
11
|
-
/** Parse YAML frontmatter from an agent .md file. */
|
|
12
|
-
function parseAgentFrontmatter(content: string): {
|
|
13
|
-
meta: Record<string, unknown>;
|
|
14
|
-
body: string;
|
|
15
|
-
} {
|
|
16
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
17
|
-
if (!match) return { meta: {}, body: content };
|
|
18
|
-
const parsed = yamlParse(match[1]);
|
|
19
|
-
const meta: Record<string, unknown> =
|
|
20
|
-
parsed && typeof parsed === 'object' ? parsed : {};
|
|
21
|
-
return { meta, body: match[2] };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Resolve agent .md file path from user argument or auto-detect. */
|
|
25
|
-
function resolveAgentFile(pathArg?: string): string {
|
|
26
|
-
if (pathArg) {
|
|
27
|
-
const resolved = resolve(pathArg);
|
|
28
|
-
if (!existsSync(resolved)) {
|
|
29
|
-
fail('File not found', resolved);
|
|
30
|
-
}
|
|
31
|
-
if (statSync(resolved).isDirectory()) {
|
|
32
|
-
// Look for a single .md file with agent frontmatter in the directory
|
|
33
|
-
return findAgentInDir(resolved);
|
|
34
|
-
}
|
|
35
|
-
if (!resolved.endsWith('.md')) {
|
|
36
|
-
fail('Invalid file', 'Agent files must be .md files');
|
|
37
|
-
}
|
|
38
|
-
return resolved;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// No path — look in cwd for a single .md with agent frontmatter
|
|
42
|
-
return findAgentInDir(process.cwd());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Find a single agent .md file in a directory. */
|
|
46
|
-
function findAgentInDir(dir: string): string {
|
|
47
|
-
const mdFiles = readdirSync(dir)
|
|
48
|
-
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
49
|
-
.map(f => join(dir, f));
|
|
50
|
-
|
|
51
|
-
const agents: string[] = [];
|
|
52
|
-
for (const file of mdFiles) {
|
|
53
|
-
try {
|
|
54
|
-
const content = readFileSync(file, 'utf-8');
|
|
55
|
-
const { meta, body } = parseAgentFrontmatter(content);
|
|
56
|
-
if (meta.name && meta.description && body.trim()) {
|
|
57
|
-
agents.push(file);
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
// Skip unreadable files
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (agents.length === 0) {
|
|
65
|
-
fail(
|
|
66
|
-
'No agent file found',
|
|
67
|
-
'Run from a directory with an agent .md file, or pass a path: oathbound agent push ./my-agent.md',
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (agents.length > 1) {
|
|
72
|
-
fail(
|
|
73
|
-
'Multiple agent files found',
|
|
74
|
-
`Found ${agents.length} .md files with agent frontmatter. Specify which one: oathbound agent push ./file.md`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return agents[0];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function agentPush(pathArg?: string, options?: { private?: boolean }): Promise<void> {
|
|
82
|
-
intro(BRAND);
|
|
83
|
-
|
|
84
|
-
const agentFile = resolveAgentFile(pathArg);
|
|
85
|
-
const content = readFileSync(agentFile, 'utf-8');
|
|
86
|
-
const { meta, body } = parseAgentFrontmatter(content);
|
|
87
|
-
|
|
88
|
-
// Validate required fields
|
|
89
|
-
const name = String(meta.name ?? '');
|
|
90
|
-
const description = String(meta.description ?? '');
|
|
91
|
-
const license = String(meta.license ?? '');
|
|
92
|
-
if (!name) fail('Frontmatter missing: name');
|
|
93
|
-
if (!description) fail('Frontmatter missing: description');
|
|
94
|
-
if (!license) fail('Frontmatter missing: license');
|
|
95
|
-
if (!body.trim()) fail('No system prompt (markdown body) after frontmatter');
|
|
96
|
-
|
|
97
|
-
const rawVersion = meta.version != null ? String(meta.version) : null;
|
|
98
|
-
const version = rawVersion && isValidSemver(rawVersion) ? rawVersion : null;
|
|
99
|
-
|
|
100
|
-
console.log(`${DIM} file: ${agentFile}${RESET}`);
|
|
101
|
-
console.log(`${DIM} name: ${name}${RESET}`);
|
|
102
|
-
console.log(`${DIM} version: ${version ?? 'auto (next)'}${RESET}`);
|
|
103
|
-
console.log(`${DIM} license: ${license}${RESET}`);
|
|
104
|
-
if (meta.model) console.log(`${DIM} model: ${meta.model}${RESET}`);
|
|
105
|
-
if (meta.permissionMode) console.log(`${DIM} permissionMode: ${meta.permissionMode}${RESET}`);
|
|
106
|
-
|
|
107
|
-
const visibility = options?.private ? 'private' : 'public';
|
|
108
|
-
if (options?.private) {
|
|
109
|
-
console.log(`${DIM} visibility: ${BOLD}private${RESET}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Authenticate
|
|
113
|
-
const token = await getAccessToken();
|
|
114
|
-
|
|
115
|
-
// Build request body
|
|
116
|
-
const requestBody: Record<string, unknown> = {
|
|
117
|
-
name,
|
|
118
|
-
description,
|
|
119
|
-
license,
|
|
120
|
-
version,
|
|
121
|
-
systemPrompt: body,
|
|
122
|
-
tools: meta.tools != null ? String(meta.tools) : null,
|
|
123
|
-
disallowedTools: meta.disallowedTools != null ? String(meta.disallowedTools) : null,
|
|
124
|
-
model: meta.model != null ? String(meta.model) : null,
|
|
125
|
-
permissionMode: meta.permissionMode != null ? String(meta.permissionMode) : null,
|
|
126
|
-
maxTurns: meta.maxTurns != null ? Number(meta.maxTurns) : null,
|
|
127
|
-
memoryScope: meta.memory != null ? String(meta.memory) : null,
|
|
128
|
-
background: meta.background != null ? Boolean(meta.background) : null,
|
|
129
|
-
effort: meta.effort != null ? String(meta.effort) : null,
|
|
130
|
-
isolation: meta.isolation != null ? String(meta.isolation) : null,
|
|
131
|
-
config: {
|
|
132
|
-
hooks: meta.hooks ?? null,
|
|
133
|
-
mcpServers: meta.mcpServers ?? null,
|
|
134
|
-
skillsRefs: Array.isArray(meta.skills) ? meta.skills : null,
|
|
135
|
-
initialPrompt: meta.initialPrompt != null ? String(meta.initialPrompt) : null,
|
|
136
|
-
},
|
|
137
|
-
compatibility: meta.compatibility != null ? String(meta.compatibility) : null,
|
|
138
|
-
originalAuthor: meta['original-author'] != null ? String(meta['original-author']) : null,
|
|
139
|
-
visibility,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Push to API
|
|
143
|
-
const spin = spinner('Pushing...');
|
|
144
|
-
|
|
145
|
-
const response = await fetch(`${API_BASE}/api/agents`, {
|
|
146
|
-
method: 'POST',
|
|
147
|
-
headers: {
|
|
148
|
-
'Content-Type': 'application/json',
|
|
149
|
-
Authorization: `Bearer ${token}`,
|
|
150
|
-
},
|
|
151
|
-
body: JSON.stringify(requestBody),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
spin.stop();
|
|
155
|
-
|
|
156
|
-
if (!response.ok) {
|
|
157
|
-
const resBody = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
158
|
-
const details = Array.isArray(resBody.details)
|
|
159
|
-
? '\n' + resBody.details.map((d: string) => ` - ${d}`).join('\n')
|
|
160
|
-
: '';
|
|
161
|
-
fail(`Push failed (${response.status})`, `${resBody.error}${details}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const result = await response.json();
|
|
165
|
-
|
|
166
|
-
outro(`${GREEN}✓ Published ${BOLD}${result.namespace}/${result.name}${RESET}${GREEN} v${result.version}${RESET}`);
|
|
167
|
-
if (result.suiObjectId) {
|
|
168
|
-
console.log(`${DIM} on-chain: ${result.suiObjectId}${RESET}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
package/agent-search.ts
DELETED
|
@@ -1,162 +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 AgentSearchOptions {
|
|
6
|
-
query?: string;
|
|
7
|
-
namespace?: string;
|
|
8
|
-
sparse?: boolean;
|
|
9
|
-
sort?: 'downloads';
|
|
10
|
-
limit?: number;
|
|
11
|
-
offset?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function parseAgentSearchArgs(args: string[]): AgentSearchOptions {
|
|
15
|
-
const opts: AgentSearchOptions = {};
|
|
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 AgentAuthor {
|
|
43
|
-
username: string;
|
|
44
|
-
display_name: string | null;
|
|
45
|
-
verified: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface AgentResult {
|
|
49
|
-
name: string;
|
|
50
|
-
namespace: string;
|
|
51
|
-
description: string;
|
|
52
|
-
version: string;
|
|
53
|
-
license?: string;
|
|
54
|
-
visibility?: string;
|
|
55
|
-
model?: string | null;
|
|
56
|
-
tools?: string | null;
|
|
57
|
-
permission_mode?: string | null;
|
|
58
|
-
effort?: string | null;
|
|
59
|
-
author?: AgentAuthor;
|
|
60
|
-
download_count?: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface AgentSearchResponse {
|
|
64
|
-
ok: boolean;
|
|
65
|
-
agents: AgentResult[];
|
|
66
|
-
total: number;
|
|
67
|
-
limit: number;
|
|
68
|
-
offset: number;
|
|
69
|
-
error?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function agentSearch(opts: AgentSearchOptions): Promise<void> {
|
|
73
|
-
const params = new URLSearchParams();
|
|
74
|
-
if (opts.query) params.set('q', opts.query);
|
|
75
|
-
if (opts.namespace) params.set('namespace', opts.namespace);
|
|
76
|
-
if (opts.sparse) params.set('sparse', 'true');
|
|
77
|
-
if (opts.sort) params.set('sort', opts.sort);
|
|
78
|
-
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
79
|
-
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
80
|
-
|
|
81
|
-
const url = `${API_BASE}/api/agents?${params}`;
|
|
82
|
-
|
|
83
|
-
const sp = spinner('Searching agents...');
|
|
84
|
-
|
|
85
|
-
let res: Response;
|
|
86
|
-
try {
|
|
87
|
-
res = await fetch(url);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
sp.stop();
|
|
90
|
-
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
91
|
-
fail('Search failed', msg);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
sp.stop();
|
|
95
|
-
|
|
96
|
-
if (!res.ok) {
|
|
97
|
-
let detail = `HTTP ${res.status}`;
|
|
98
|
-
try {
|
|
99
|
-
const body = await res.json() as { error?: string };
|
|
100
|
-
if (body.error) detail = body.error;
|
|
101
|
-
} catch { /* ignore parse errors */ }
|
|
102
|
-
fail('Search failed', detail);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const data = await res.json() as AgentSearchResponse;
|
|
106
|
-
|
|
107
|
-
if (!data.ok || !data.agents) {
|
|
108
|
-
fail('Search failed', data.error ?? 'Unexpected response');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const { agents, total, offset } = data;
|
|
112
|
-
|
|
113
|
-
if (agents.length === 0) {
|
|
114
|
-
console.log(`\n${BRAND} ${DIM}No agents found.${RESET}`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const showing = offset > 0
|
|
119
|
-
? `Showing ${offset + 1}–${offset + agents.length} of ${total}`
|
|
120
|
-
: `${total} agent${total === 1 ? '' : 's'} found`;
|
|
121
|
-
|
|
122
|
-
console.log(`\n${BRAND} ${TEAL}${showing}${RESET}\n`);
|
|
123
|
-
|
|
124
|
-
for (const agent of agents) {
|
|
125
|
-
const id = `${agent.namespace}/${agent.name}`;
|
|
126
|
-
const ver = `v${agent.version}`;
|
|
127
|
-
|
|
128
|
-
// Line 1: name + version
|
|
129
|
-
console.log(` ${BOLD}${id}${RESET} ${DIM}${ver}${RESET}`);
|
|
130
|
-
|
|
131
|
-
// Line 2: description
|
|
132
|
-
if (agent.description) {
|
|
133
|
-
console.log(` ${DIM}${agent.description}${RESET}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Line 3: agent-specific metadata (non-sparse only)
|
|
137
|
-
if (!opts.sparse) {
|
|
138
|
-
const parts: string[] = [];
|
|
139
|
-
if (agent.author) {
|
|
140
|
-
const name = agent.author.display_name || agent.author.username;
|
|
141
|
-
parts.push(`by ${name}${agent.author.verified ? ' ✓' : ''}`);
|
|
142
|
-
}
|
|
143
|
-
if (agent.license) parts.push(agent.license);
|
|
144
|
-
if (agent.download_count != null) parts.push(`↓ ${agent.download_count}`);
|
|
145
|
-
if (agent.model) parts.push(`model: ${agent.model}`);
|
|
146
|
-
if (agent.permission_mode) parts.push(`mode: ${agent.permission_mode}`);
|
|
147
|
-
if (agent.effort) parts.push(`effort: ${agent.effort}`);
|
|
148
|
-
if (agent.visibility === 'private') parts.push('private');
|
|
149
|
-
if (parts.length > 0) {
|
|
150
|
-
console.log(` ${DIM}${parts.join(' · ')}${RESET}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
console.log(); // blank line between agents
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Pagination hint
|
|
158
|
-
if (offset + agents.length < total) {
|
|
159
|
-
const nextOffset = offset + agents.length;
|
|
160
|
-
console.log(`${DIM} Use --offset ${nextOffset} to see more${RESET}\n`);
|
|
161
|
-
}
|
|
162
|
-
}
|
package/auth.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { createClient } from '@supabase/supabase-js';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
4
|
-
mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync,
|
|
5
|
-
} from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
|
-
import { intro, outro } from '@clack/prompts';
|
|
9
|
-
import { BRAND, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
|
|
10
|
-
|
|
11
|
-
const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
|
|
12
|
-
const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
|
|
13
|
-
const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
|
|
14
|
-
|
|
15
|
-
const AUTH_DIR = join(homedir(), '.oathbound');
|
|
16
|
-
const AUTH_FILE = join(AUTH_DIR, 'auth.json');
|
|
17
|
-
|
|
18
|
-
interface StoredSession {
|
|
19
|
-
access_token: string;
|
|
20
|
-
refresh_token: string;
|
|
21
|
-
expires_at: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function saveSession(session: StoredSession): void {
|
|
25
|
-
mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
26
|
-
writeFileSync(AUTH_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function loadSession(): StoredSession | null {
|
|
30
|
-
if (!existsSync(AUTH_FILE)) return null;
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function clearSession(): void {
|
|
39
|
-
if (existsSync(AUTH_FILE)) unlinkSync(AUTH_FILE);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function openBrowser(url: string): void {
|
|
43
|
-
const cmd = process.platform === 'darwin' ? 'open'
|
|
44
|
-
: process.platform === 'win32' ? 'cmd'
|
|
45
|
-
: 'xdg-open';
|
|
46
|
-
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
47
|
-
try {
|
|
48
|
-
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
49
|
-
} catch {
|
|
50
|
-
// URL is already printed — user can open manually
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
55
|
-
<html><head><title>Oathbound CLI</title></head>
|
|
56
|
-
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5">
|
|
57
|
-
<div style="text-align:center">
|
|
58
|
-
<h1 style="color:#3fa8a4">✓ Logged in</h1>
|
|
59
|
-
<p>You can close this tab and return to your terminal.</p>
|
|
60
|
-
</div></body></html>`;
|
|
61
|
-
|
|
62
|
-
const ERROR_HTML = `<!DOCTYPE html>
|
|
63
|
-
<html><head><title>Oathbound CLI</title></head>
|
|
64
|
-
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5">
|
|
65
|
-
<div style="text-align:center">
|
|
66
|
-
<h1 style="color:#ef4444">Login failed</h1>
|
|
67
|
-
<p>Missing session tokens. Please try again.</p>
|
|
68
|
-
</div></body></html>`;
|
|
69
|
-
|
|
70
|
-
export async function login(): Promise<void> {
|
|
71
|
-
intro(BRAND);
|
|
72
|
-
|
|
73
|
-
let resolveSession: (s: StoredSession) => void;
|
|
74
|
-
let rejectSession: (e: Error) => void;
|
|
75
|
-
const sessionPromise = new Promise<StoredSession>((res, rej) => {
|
|
76
|
-
resolveSession = res;
|
|
77
|
-
rejectSession = rej;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const server = Bun.serve({
|
|
81
|
-
port: 0,
|
|
82
|
-
fetch(req) {
|
|
83
|
-
const url = new URL(req.url);
|
|
84
|
-
if (url.pathname !== '/callback') {
|
|
85
|
-
return new Response('Not found', { status: 404 });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const accessToken = url.searchParams.get('access_token');
|
|
89
|
-
const refreshToken = url.searchParams.get('refresh_token');
|
|
90
|
-
const expiresAt = url.searchParams.get('expires_at');
|
|
91
|
-
|
|
92
|
-
if (!accessToken || !refreshToken || !expiresAt) {
|
|
93
|
-
rejectSession!(new Error('Missing session tokens from callback'));
|
|
94
|
-
setTimeout(() => server.stop(), 500);
|
|
95
|
-
return new Response(ERROR_HTML, { headers: { 'Content-Type': 'text/html' } });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
resolveSession!({
|
|
99
|
-
access_token: accessToken,
|
|
100
|
-
refresh_token: refreshToken,
|
|
101
|
-
expires_at: Number(expiresAt),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
setTimeout(() => server.stop(), 500);
|
|
105
|
-
return new Response(SUCCESS_HTML, { headers: { 'Content-Type': 'text/html' } });
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const port = server.port;
|
|
110
|
-
const loginUrl = `${API_BASE}/cli-login?port=${port}`;
|
|
111
|
-
|
|
112
|
-
console.log(`${DIM} Opening browser...${RESET}`);
|
|
113
|
-
console.log(`${DIM} If it doesn't open, visit:${RESET}`);
|
|
114
|
-
console.log(`${DIM} ${loginUrl}${RESET}\n`);
|
|
115
|
-
|
|
116
|
-
openBrowser(loginUrl);
|
|
117
|
-
|
|
118
|
-
const spin = spinner('Waiting for login...');
|
|
119
|
-
|
|
120
|
-
const timeout = new Promise<never>((_, rej) =>
|
|
121
|
-
setTimeout(() => rej(new Error('Login timed out (2 minutes). Please try again.')), 120_000),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
let session: StoredSession;
|
|
125
|
-
try {
|
|
126
|
-
session = await Promise.race([sessionPromise, timeout]);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
spin.stop();
|
|
129
|
-
server.stop();
|
|
130
|
-
fail('Login failed', err instanceof Error ? err.message : 'Unknown error');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
spin.stop();
|
|
134
|
-
saveSession(session);
|
|
135
|
-
|
|
136
|
-
// Get username for display
|
|
137
|
-
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
138
|
-
global: { headers: { Authorization: `Bearer ${session.access_token}` } },
|
|
139
|
-
auth: { autoRefreshToken: false, persistSession: false },
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const { data: { user } } = await supabase.auth.getUser();
|
|
143
|
-
let displayName = user?.email ?? 'unknown';
|
|
144
|
-
if (user) {
|
|
145
|
-
const { data: userRecord } = await supabase
|
|
146
|
-
.from('users')
|
|
147
|
-
.select('username')
|
|
148
|
-
.eq('user_id', user.id)
|
|
149
|
-
.single();
|
|
150
|
-
if (userRecord?.username) displayName = userRecord.username;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
outro(`Logged in as ${BOLD}${displayName}${RESET}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function logout(): Promise<void> {
|
|
157
|
-
clearSession();
|
|
158
|
-
console.log(`\n${BRAND} ${GREEN}✓ Logged out${RESET}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function getAccessToken(): Promise<string> {
|
|
162
|
-
const session = loadSession();
|
|
163
|
-
if (!session) {
|
|
164
|
-
fail('Not logged in', 'Run: oathbound login');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Token still valid (with 60s buffer) — use it directly
|
|
168
|
-
const now = Math.floor(Date.now() / 1000);
|
|
169
|
-
if (session.expires_at > now + 60) {
|
|
170
|
-
return session.access_token;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Token expired or expiring soon — refresh
|
|
174
|
-
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
175
|
-
auth: { autoRefreshToken: false, persistSession: false },
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const { data, error } = await supabase.auth.setSession({
|
|
179
|
-
access_token: session.access_token,
|
|
180
|
-
refresh_token: session.refresh_token,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (error || !data.session) {
|
|
184
|
-
clearSession();
|
|
185
|
-
fail('Session expired', 'Run: oathbound login');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
saveSession({
|
|
189
|
-
access_token: data.session.access_token,
|
|
190
|
-
refresh_token: data.session.refresh_token,
|
|
191
|
-
expires_at: data.session.expires_at!,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
return data.session.access_token;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export async function whoami(): Promise<void> {
|
|
198
|
-
const token = await getAccessToken();
|
|
199
|
-
|
|
200
|
-
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
201
|
-
global: { headers: { Authorization: `Bearer ${token}` } },
|
|
202
|
-
auth: { autoRefreshToken: false, persistSession: false },
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const { data: { user }, error } = await supabase.auth.getUser();
|
|
206
|
-
if (error || !user) {
|
|
207
|
-
fail('Failed to get user', error?.message ?? 'Unknown error');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const { data: userRecord } = await supabase
|
|
211
|
-
.from('users')
|
|
212
|
-
.select('username')
|
|
213
|
-
.eq('user_id', user.id)
|
|
214
|
-
.single();
|
|
215
|
-
|
|
216
|
-
console.log(`\n${BRAND}`);
|
|
217
|
-
console.log(` ${BOLD}Username:${RESET} ${userRecord?.username ?? 'not set'}`);
|
|
218
|
-
console.log(` ${DIM}Email:${RESET} ${user.email ?? 'unknown'}`);
|
|
219
|
-
}
|
package/bin/cli.cjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Thin wrapper that spawns the platform binary downloaded by postinstall.
|
|
4
|
-
|
|
5
|
-
const { execFileSync } = require("child_process");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
|
|
9
|
-
const ext = process.platform === "win32" ? ".exe" : "";
|
|
10
|
-
const binary = path.join(__dirname, `oathbound${ext}`);
|
|
11
|
-
|
|
12
|
-
if (!fs.existsSync(binary)) {
|
|
13
|
-
console.error(
|
|
14
|
-
"oathbound binary not found. Run `npm rebuild oathbound` or reinstall the package."
|
|
15
|
-
);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
21
|
-
} catch (err) {
|
|
22
|
-
process.exit(err.status ?? 1);
|
|
23
|
-
}
|