@clawcard/cli 0.1.3 → 1.0.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/bin/clawcard.mjs +2 -0
- package/package.json +13 -5
- package/src/api.js +55 -0
- package/src/auth-guard.js +9 -0
- package/src/auth-server.js +63 -0
- package/src/commands/billing.js +169 -0
- package/src/commands/help.js +49 -0
- package/src/commands/keys.js +230 -0
- package/src/commands/login.js +39 -0
- package/src/commands/logout.js +12 -0
- package/src/commands/referral.js +37 -0
- package/src/commands/setup.js +291 -0
- package/src/commands/signup.js +47 -0
- package/src/commands/whoami.js +31 -0
- package/src/config.js +68 -0
- package/src/index.js +209 -0
- package/src/splash.js +11 -0
- package/bin/setup.mjs +0 -207
package/bin/clawcard.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawcard/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The ClawCard CLI — manage your agent keys, billing, and setup from the terminal",
|
|
5
5
|
"bin": {
|
|
6
|
-
"clawcard": "./bin/
|
|
6
|
+
"clawcard": "./bin/clawcard.mjs"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"files": [
|
|
11
|
-
"bin/"
|
|
12
|
-
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clack/prompts": "^0.10.0",
|
|
16
|
+
"chalk": "^5.4.1",
|
|
17
|
+
"clipboardy": "^4.0.0",
|
|
18
|
+
"commander": "^13.1.0",
|
|
19
|
+
"open": "^10.1.0"
|
|
20
|
+
}
|
|
13
21
|
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getToken, BASE_URL } from "./config.js";
|
|
2
|
+
|
|
3
|
+
class ApiError extends Error {
|
|
4
|
+
constructor(status, body) {
|
|
5
|
+
super(body?.message || body?.error || `API error ${status}`);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.body = body;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { ApiError };
|
|
12
|
+
|
|
13
|
+
async function request(path, options = {}) {
|
|
14
|
+
const token = getToken();
|
|
15
|
+
if (!token) throw new Error("Not authenticated");
|
|
16
|
+
|
|
17
|
+
const url = `${BASE_URL}${path}`;
|
|
18
|
+
const headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Cookie: `better-auth.session_token=${token}`,
|
|
21
|
+
...options.headers,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const res = await fetch(url, { ...options, headers });
|
|
25
|
+
const body = await res.json().catch(() => null);
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new ApiError(res.status, body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Agents / Keys
|
|
35
|
+
export const listAgents = () => request("/api/agents");
|
|
36
|
+
export const createAgent = (data) =>
|
|
37
|
+
request("/api/agents", { method: "POST", body: JSON.stringify(data) });
|
|
38
|
+
export const getAgent = (id) => request(`/api/agents/${id}`);
|
|
39
|
+
export const deleteAgent = (id) =>
|
|
40
|
+
request(`/api/agents/${id}`, { method: "DELETE" });
|
|
41
|
+
|
|
42
|
+
// Billing
|
|
43
|
+
export const getBalance = () => request("/api/billing/balance");
|
|
44
|
+
export const getSubscription = () => request("/api/billing/subscription");
|
|
45
|
+
export const getTransactions = () => request("/api/billing/transactions");
|
|
46
|
+
export const createCheckout = (amountCents) =>
|
|
47
|
+
request("/api/billing/checkout", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({ amountCents }),
|
|
50
|
+
});
|
|
51
|
+
export const createPortal = () =>
|
|
52
|
+
request("/api/billing/portal", { method: "POST" });
|
|
53
|
+
|
|
54
|
+
// User
|
|
55
|
+
export const getMe = () => request("/api/me");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts a temporary localhost server that waits for an auth callback.
|
|
5
|
+
* Returns { promise, port } where:
|
|
6
|
+
* - port: Promise<number> that resolves with the port once the server is listening
|
|
7
|
+
* - promise: Promise<{ token, email }> that resolves when the callback is received
|
|
8
|
+
*/
|
|
9
|
+
export function startCallbackServer({ timeout = 120_000 } = {}) {
|
|
10
|
+
let resolvePort;
|
|
11
|
+
const portPromise = new Promise((r) => {
|
|
12
|
+
resolvePort = r;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
16
|
+
const server = http.createServer((req, res) => {
|
|
17
|
+
const url = new URL(req.url, "http://localhost");
|
|
18
|
+
|
|
19
|
+
if (url.pathname === "/callback") {
|
|
20
|
+
const token = url.searchParams.get("token");
|
|
21
|
+
const email = url.searchParams.get("email");
|
|
22
|
+
const error = url.searchParams.get("error");
|
|
23
|
+
|
|
24
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
25
|
+
res.end(`<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head><title>ClawCard</title></head>
|
|
28
|
+
<body style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:system-ui;background:#1a1a1a;color:#fff;">
|
|
29
|
+
<div style="text-align:center;">
|
|
30
|
+
<h1 style="color:#FF6B35;">🦞 ClawCard</h1>
|
|
31
|
+
<p>${error ? "Something went wrong. Please try again in the terminal." : "You're all set! You can close this window."}</p>
|
|
32
|
+
</div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>`);
|
|
35
|
+
|
|
36
|
+
server.close();
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
reject(new Error(error));
|
|
41
|
+
} else {
|
|
42
|
+
resolve({ token, email });
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
res.writeHead(404);
|
|
46
|
+
res.end();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
server.listen(0, "127.0.0.1", () => {
|
|
51
|
+
resolvePort(server.address().port);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
server.close();
|
|
56
|
+
reject(
|
|
57
|
+
new Error("Auth timed out — no callback received within 2 minutes")
|
|
58
|
+
);
|
|
59
|
+
}, timeout);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { promise: resultPromise, port: portPromise };
|
|
63
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import open from "open";
|
|
4
|
+
import { requireAuth } from "../auth-guard.js";
|
|
5
|
+
import {
|
|
6
|
+
getBalance,
|
|
7
|
+
getSubscription,
|
|
8
|
+
getTransactions,
|
|
9
|
+
createCheckout,
|
|
10
|
+
createPortal,
|
|
11
|
+
} from "../api.js";
|
|
12
|
+
|
|
13
|
+
const orange = chalk.hex("#FF6B35");
|
|
14
|
+
|
|
15
|
+
export async function billingCommand() {
|
|
16
|
+
requireAuth();
|
|
17
|
+
|
|
18
|
+
const s = p.spinner();
|
|
19
|
+
s.start("Loading billing info...");
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const [balance, sub] = await Promise.all([
|
|
23
|
+
getBalance(),
|
|
24
|
+
getSubscription(),
|
|
25
|
+
]);
|
|
26
|
+
s.stop("Billing loaded");
|
|
27
|
+
|
|
28
|
+
const planColors = {
|
|
29
|
+
free: chalk.gray,
|
|
30
|
+
starter: chalk.blue,
|
|
31
|
+
pro: orange,
|
|
32
|
+
};
|
|
33
|
+
const colorPlan = (planColors[sub.subscription?.plan] || chalk.white)(
|
|
34
|
+
sub.subscription?.planName || sub.subscription?.plan || "Free"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const info = [
|
|
38
|
+
`Plan: ${colorPlan}`,
|
|
39
|
+
`Balance: ${chalk.green("$" + (balance.amountCents / 100).toFixed(2))}`,
|
|
40
|
+
`Keys: ${sub.subscription?.currentAgentKeys || 0}/${sub.subscription?.maxAgentKeys || 1}`,
|
|
41
|
+
`Cards: ${sub.subscription?.currentCards || 0}/${sub.subscription?.maxCardsPerMonth || 1} this period`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
p.note(info, "Billing");
|
|
45
|
+
|
|
46
|
+
const action = await p.select({
|
|
47
|
+
message: "What would you like to do?",
|
|
48
|
+
options: [
|
|
49
|
+
{ value: "topup", label: "Top up balance" },
|
|
50
|
+
{ value: "upgrade", label: "Upgrade plan" },
|
|
51
|
+
{ value: "transactions", label: "View transactions" },
|
|
52
|
+
{ value: "back", label: "Back" },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (p.isCancel(action) || action === "back") return;
|
|
57
|
+
|
|
58
|
+
switch (action) {
|
|
59
|
+
case "topup":
|
|
60
|
+
return billingTopupCommand();
|
|
61
|
+
case "upgrade":
|
|
62
|
+
return billingUpgradeCommand();
|
|
63
|
+
case "transactions":
|
|
64
|
+
return billingTransactionsCommand();
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
s.stop("Failed to load billing");
|
|
68
|
+
p.log.error(err.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function billingBalanceCommand() {
|
|
73
|
+
requireAuth();
|
|
74
|
+
|
|
75
|
+
const s = p.spinner();
|
|
76
|
+
s.start("Checking balance...");
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const balance = await getBalance();
|
|
80
|
+
s.stop("");
|
|
81
|
+
p.log.info(
|
|
82
|
+
`Balance: ${chalk.green("$" + (balance.amountCents / 100).toFixed(2))}`
|
|
83
|
+
);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
s.stop("Failed");
|
|
86
|
+
p.log.error(err.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function billingTopupCommand() {
|
|
91
|
+
requireAuth();
|
|
92
|
+
|
|
93
|
+
const amount = await p.select({
|
|
94
|
+
message: "How much would you like to add?",
|
|
95
|
+
options: [
|
|
96
|
+
{ value: 500, label: "$5.00" },
|
|
97
|
+
{ value: 1000, label: "$10.00" },
|
|
98
|
+
{ value: 2500, label: "$25.00" },
|
|
99
|
+
{ value: 5000, label: "$50.00" },
|
|
100
|
+
{ value: 10000, label: "$100.00" },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (p.isCancel(amount)) return;
|
|
105
|
+
|
|
106
|
+
const s = p.spinner();
|
|
107
|
+
s.start("Creating checkout...");
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await createCheckout(amount);
|
|
111
|
+
s.stop("");
|
|
112
|
+
|
|
113
|
+
p.log.info("Opening Stripe checkout in browser...");
|
|
114
|
+
await open(result.url);
|
|
115
|
+
p.log.success("Complete the payment in your browser");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
s.stop("Failed");
|
|
118
|
+
p.log.error(err.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function billingUpgradeCommand() {
|
|
123
|
+
requireAuth();
|
|
124
|
+
|
|
125
|
+
const s = p.spinner();
|
|
126
|
+
s.start("Opening billing portal...");
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await createPortal();
|
|
130
|
+
s.stop("");
|
|
131
|
+
|
|
132
|
+
await open(result.url);
|
|
133
|
+
p.log.success("Manage your subscription in the browser");
|
|
134
|
+
} catch (err) {
|
|
135
|
+
s.stop("Failed");
|
|
136
|
+
p.log.error(err.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function billingTransactionsCommand() {
|
|
141
|
+
requireAuth();
|
|
142
|
+
|
|
143
|
+
const s = p.spinner();
|
|
144
|
+
s.start("Fetching transactions...");
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const txns = await getTransactions();
|
|
148
|
+
s.stop("");
|
|
149
|
+
|
|
150
|
+
if (!txns.length) {
|
|
151
|
+
p.log.info("No transactions yet");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const lines = txns.map((tx) => {
|
|
156
|
+
const amount =
|
|
157
|
+
tx.amountCents >= 0
|
|
158
|
+
? chalk.green(`+$${(tx.amountCents / 100).toFixed(2)}`)
|
|
159
|
+
: chalk.red(`-$${(Math.abs(tx.amountCents) / 100).toFixed(2)}`);
|
|
160
|
+
const date = new Date(tx.createdAt).toLocaleDateString();
|
|
161
|
+
return `${date} ${amount} ${chalk.dim(tx.type)} ${tx.description || ""}`;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
p.note(lines.join("\n"), "Transactions");
|
|
165
|
+
} catch (err) {
|
|
166
|
+
s.stop("Failed");
|
|
167
|
+
p.log.error(err.message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
const orange = chalk.hex("#FF6B35");
|
|
4
|
+
const dim = chalk.dim;
|
|
5
|
+
|
|
6
|
+
const COMMANDS = [
|
|
7
|
+
["", ""],
|
|
8
|
+
[orange.bold("Authentication"), ""],
|
|
9
|
+
[" login", "Log in via browser"],
|
|
10
|
+
[" signup", "Sign up with invite code"],
|
|
11
|
+
[" logout", "Clear session"],
|
|
12
|
+
[" whoami", "Show current account"],
|
|
13
|
+
["", ""],
|
|
14
|
+
[orange.bold("Keys"), ""],
|
|
15
|
+
[" keys", "Interactive key management"],
|
|
16
|
+
[" keys create", "Create a new API key"],
|
|
17
|
+
[" keys list", "List all keys"],
|
|
18
|
+
[" keys info", "Show key details"],
|
|
19
|
+
[" keys revoke", "Revoke a key"],
|
|
20
|
+
["", ""],
|
|
21
|
+
[orange.bold("Setup"), ""],
|
|
22
|
+
[" setup", "Install ClawCard skill for your agent"],
|
|
23
|
+
["", ""],
|
|
24
|
+
[orange.bold("Billing"), ""],
|
|
25
|
+
[" billing", "Interactive billing dashboard"],
|
|
26
|
+
[" billing balance", "Quick balance check"],
|
|
27
|
+
[" billing topup", "Top up balance"],
|
|
28
|
+
[" billing upgrade", "Upgrade subscription"],
|
|
29
|
+
[" billing transactions", "View transaction history"],
|
|
30
|
+
["", ""],
|
|
31
|
+
[orange.bold("Other"), ""],
|
|
32
|
+
[" referral", "Show referral code"],
|
|
33
|
+
[" help", "Show this help"],
|
|
34
|
+
[" --version", "Show version"],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function helpCommand() {
|
|
38
|
+
console.log();
|
|
39
|
+
for (const [cmd, desc] of COMMANDS) {
|
|
40
|
+
if (!cmd && !desc) {
|
|
41
|
+
console.log();
|
|
42
|
+
} else if (!desc) {
|
|
43
|
+
console.log(` ${cmd}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` ${cmd.padEnd(24)}${dim(desc)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { requireAuth } from "../auth-guard.js";
|
|
4
|
+
import { listAgents, createAgent, getAgent, deleteAgent } from "../api.js";
|
|
5
|
+
import { saveKey } from "../config.js";
|
|
6
|
+
|
|
7
|
+
const orange = chalk.hex("#FF6B35");
|
|
8
|
+
|
|
9
|
+
export async function keysCommand() {
|
|
10
|
+
requireAuth();
|
|
11
|
+
|
|
12
|
+
const action = await p.select({
|
|
13
|
+
message: "Key management",
|
|
14
|
+
options: [
|
|
15
|
+
{ value: "create", label: "Create new key" },
|
|
16
|
+
{ value: "list", label: "List keys" },
|
|
17
|
+
{ value: "revoke", label: "Revoke a key" },
|
|
18
|
+
{ value: "back", label: "Back" },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (p.isCancel(action) || action === "back") return;
|
|
23
|
+
|
|
24
|
+
switch (action) {
|
|
25
|
+
case "create":
|
|
26
|
+
return keysCreateCommand();
|
|
27
|
+
case "list":
|
|
28
|
+
return keysListCommand();
|
|
29
|
+
case "revoke":
|
|
30
|
+
return keysRevokeCommand();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function keysCreateCommand() {
|
|
35
|
+
requireAuth();
|
|
36
|
+
|
|
37
|
+
const name = await p.text({
|
|
38
|
+
message: "Key name",
|
|
39
|
+
placeholder: "my-agent",
|
|
40
|
+
validate: (v) => {
|
|
41
|
+
if (!v?.trim()) return "Name is required";
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(name)) return;
|
|
45
|
+
|
|
46
|
+
const spendLimit = await p.text({
|
|
47
|
+
message: "Spend limit (dollars)",
|
|
48
|
+
placeholder: "50",
|
|
49
|
+
validate: (v) => {
|
|
50
|
+
const n = parseFloat(v);
|
|
51
|
+
if (isNaN(n) || n <= 0) return "Enter a valid dollar amount";
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (p.isCancel(spendLimit)) return;
|
|
55
|
+
|
|
56
|
+
const s = p.spinner();
|
|
57
|
+
s.start("Creating key...");
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await createAgent({
|
|
61
|
+
name,
|
|
62
|
+
spendLimitCents: Math.round(parseFloat(spendLimit) * 100),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
s.stop("Key created!");
|
|
66
|
+
|
|
67
|
+
// Save raw key locally so `clawcard setup` can use it later
|
|
68
|
+
saveKey(result.id, result.apiKey, {
|
|
69
|
+
name: result.name,
|
|
70
|
+
keyPrefix: result.keyPrefix,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
p.note(
|
|
74
|
+
`${orange.bold(result.apiKey)}\n\n${chalk.dim("This key will only be shown once. Copy it now.")}`,
|
|
75
|
+
"Your API Key"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const copy = await p.confirm({ message: "Copy to clipboard?" });
|
|
79
|
+
if (copy && !p.isCancel(copy)) {
|
|
80
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
81
|
+
await clipboardy.write(result.apiKey);
|
|
82
|
+
p.log.success("Copied to clipboard");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result.email || result.phone) {
|
|
86
|
+
p.log.info(
|
|
87
|
+
[
|
|
88
|
+
result.email && `Email: ${result.email}`,
|
|
89
|
+
result.phone && `Phone: ${result.phone}`,
|
|
90
|
+
]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join("\n")
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
s.stop("Failed to create key");
|
|
97
|
+
p.log.error(err.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function keysListCommand() {
|
|
102
|
+
requireAuth();
|
|
103
|
+
|
|
104
|
+
const s = p.spinner();
|
|
105
|
+
s.start("Fetching keys...");
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const agents = await listAgents();
|
|
109
|
+
s.stop("Keys loaded");
|
|
110
|
+
|
|
111
|
+
if (!agents.length) {
|
|
112
|
+
p.log.info("No keys yet. Create one with: clawcard keys create");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const lines = agents.map((a) => {
|
|
117
|
+
const status =
|
|
118
|
+
a.status === "active" ? chalk.green("active") : chalk.red(a.status);
|
|
119
|
+
return [
|
|
120
|
+
orange.bold(a.name || "unnamed"),
|
|
121
|
+
chalk.dim(a.keyPrefix + "..."),
|
|
122
|
+
a.email || "-",
|
|
123
|
+
a.phone || "-",
|
|
124
|
+
`$${(a.spendLimitCents / 100).toFixed(2)} limit`,
|
|
125
|
+
status,
|
|
126
|
+
].join(" ");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
p.note(lines.join("\n"), "Your Keys");
|
|
130
|
+
} catch (err) {
|
|
131
|
+
s.stop("Failed to fetch keys");
|
|
132
|
+
p.log.error(err.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function keysInfoCommand(keyIdOrPrefix) {
|
|
137
|
+
requireAuth();
|
|
138
|
+
|
|
139
|
+
const s = p.spinner();
|
|
140
|
+
|
|
141
|
+
if (!keyIdOrPrefix) {
|
|
142
|
+
s.start("Fetching keys...");
|
|
143
|
+
let agents;
|
|
144
|
+
try {
|
|
145
|
+
agents = await listAgents();
|
|
146
|
+
s.stop("");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
s.stop("Failed");
|
|
149
|
+
p.log.error(err.message);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!agents.length) {
|
|
154
|
+
p.log.info("No keys found");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const selected = await p.select({
|
|
159
|
+
message: "Which key?",
|
|
160
|
+
options: agents.map((a) => ({
|
|
161
|
+
value: a.id,
|
|
162
|
+
label: `${a.name || "unnamed"} (${a.keyPrefix}...)`,
|
|
163
|
+
})),
|
|
164
|
+
});
|
|
165
|
+
if (p.isCancel(selected)) return;
|
|
166
|
+
keyIdOrPrefix = selected;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
s.start("Fetching key details...");
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const agent = await getAgent(keyIdOrPrefix);
|
|
173
|
+
s.stop("Key details");
|
|
174
|
+
|
|
175
|
+
const info = [
|
|
176
|
+
`Name: ${orange.bold(agent.name || "unnamed")}`,
|
|
177
|
+
`Key prefix: ${chalk.dim(agent.keyPrefix + "...")}`,
|
|
178
|
+
`Email: ${agent.email || "-"}`,
|
|
179
|
+
`Phone: ${agent.phone || "-"}`,
|
|
180
|
+
`Spend limit: $${(agent.spendLimitCents / 100).toFixed(2)}`,
|
|
181
|
+
`Status: ${agent.status === "active" ? chalk.green("active") : chalk.red(agent.status)}`,
|
|
182
|
+
`Created: ${new Date(agent.createdAt).toLocaleDateString()}`,
|
|
183
|
+
].join("\n");
|
|
184
|
+
|
|
185
|
+
p.note(info, "Key Details");
|
|
186
|
+
} catch (err) {
|
|
187
|
+
s.stop("Failed");
|
|
188
|
+
p.log.error(err.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function keysRevokeCommand() {
|
|
193
|
+
requireAuth();
|
|
194
|
+
|
|
195
|
+
const s = p.spinner();
|
|
196
|
+
s.start("Fetching keys...");
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const agents = await listAgents();
|
|
200
|
+
s.stop("");
|
|
201
|
+
|
|
202
|
+
if (!agents.length) {
|
|
203
|
+
p.log.info("No keys to revoke");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const selected = await p.select({
|
|
208
|
+
message: "Which key do you want to revoke?",
|
|
209
|
+
options: agents.map((a) => ({
|
|
210
|
+
value: a.id,
|
|
211
|
+
label: `${a.name || "unnamed"} (${a.keyPrefix}...)`,
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
if (p.isCancel(selected)) return;
|
|
215
|
+
|
|
216
|
+
const confirm = await p.confirm({
|
|
217
|
+
message: "Are you sure? This cannot be undone.",
|
|
218
|
+
});
|
|
219
|
+
if (p.isCancel(confirm) || !confirm) return;
|
|
220
|
+
|
|
221
|
+
s.start("Revoking key...");
|
|
222
|
+
await deleteAgent(selected);
|
|
223
|
+
s.stop("Key revoked");
|
|
224
|
+
|
|
225
|
+
p.log.success("Key has been revoked");
|
|
226
|
+
} catch (err) {
|
|
227
|
+
s.stop("Failed");
|
|
228
|
+
p.log.error(err.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import open from "open";
|
|
3
|
+
import { startCallbackServer } from "../auth-server.js";
|
|
4
|
+
import { saveConfig, isLoggedIn, getConfig, BASE_URL } from "../config.js";
|
|
5
|
+
|
|
6
|
+
export async function loginCommand() {
|
|
7
|
+
if (isLoggedIn()) {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
p.log.info(`Already logged in as ${config.email}`);
|
|
10
|
+
const cont = await p.confirm({ message: "Log in as a different account?" });
|
|
11
|
+
if (p.isCancel(cont) || !cont) return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const s = p.spinner();
|
|
15
|
+
s.start("Opening browser...");
|
|
16
|
+
|
|
17
|
+
const { promise, port } = startCallbackServer();
|
|
18
|
+
const actualPort = await port;
|
|
19
|
+
const loginUrl = `${BASE_URL}/login?cli=true&port=${actualPort}`;
|
|
20
|
+
await open(loginUrl);
|
|
21
|
+
|
|
22
|
+
s.message("Waiting for login in browser...");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { token, email } = await promise;
|
|
26
|
+
s.stop("Logged in!");
|
|
27
|
+
|
|
28
|
+
saveConfig({
|
|
29
|
+
token,
|
|
30
|
+
email,
|
|
31
|
+
loggedInAt: new Date().toISOString(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
p.log.success(`Logged in as ${email}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
s.stop("Login failed");
|
|
37
|
+
p.log.error(err.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { clearConfig, isLoggedIn } from "../config.js";
|
|
3
|
+
|
|
4
|
+
export async function logoutCommand() {
|
|
5
|
+
if (!isLoggedIn()) {
|
|
6
|
+
p.log.info("Not logged in");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
clearConfig();
|
|
11
|
+
p.log.success("Logged out");
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { requireAuth } from "../auth-guard.js";
|
|
4
|
+
import { getMe } from "../api.js";
|
|
5
|
+
|
|
6
|
+
const orange = chalk.hex("#FF6B35");
|
|
7
|
+
|
|
8
|
+
export async function referralCommand() {
|
|
9
|
+
requireAuth();
|
|
10
|
+
|
|
11
|
+
const s = p.spinner();
|
|
12
|
+
s.start("Fetching referral code...");
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const me = await getMe();
|
|
16
|
+
s.stop("");
|
|
17
|
+
|
|
18
|
+
p.note(
|
|
19
|
+
[
|
|
20
|
+
`Your code: ${orange.bold(me.referralCode || "N/A")}`,
|
|
21
|
+
"",
|
|
22
|
+
chalk.dim("Share this code — you both get $10 credit!"),
|
|
23
|
+
].join("\n"),
|
|
24
|
+
"Referral"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const copy = await p.confirm({ message: "Copy to clipboard?" });
|
|
28
|
+
if (copy && !p.isCancel(copy) && me.referralCode) {
|
|
29
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
30
|
+
await clipboardy.write(me.referralCode);
|
|
31
|
+
p.log.success("Copied to clipboard");
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
s.stop("Failed");
|
|
35
|
+
p.log.error(err.message);
|
|
36
|
+
}
|
|
37
|
+
}
|