@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.
@@ -0,0 +1,291 @@
1
+ import * as p from "@clack/prompts";
2
+ import { execSync } from "child_process";
3
+ import {
4
+ readFileSync,
5
+ writeFileSync,
6
+ mkdirSync,
7
+ existsSync,
8
+ } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import { requireAuth } from "../auth-guard.js";
12
+ import { listAgents } from "../api.js";
13
+ import { getSavedKey } from "../config.js";
14
+ import chalk from "chalk";
15
+
16
+ const orange = chalk.hex("#FF6B35");
17
+ const home = homedir();
18
+
19
+ // ── Per-harness env var writers ──
20
+
21
+ const HARNESSES = [
22
+ {
23
+ value: "claude-code",
24
+ label: "Claude Code",
25
+ hint: "~/.claude/settings.json",
26
+ write(apiKey) {
27
+ const settingsPath = join(home, ".claude", "settings.json");
28
+ let settings = {};
29
+ try {
30
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
31
+ } catch {
32
+ // doesn't exist yet
33
+ }
34
+ if (!settings.env) settings.env = {};
35
+ settings.env.CLAWCARD_API_KEY = apiKey;
36
+ mkdirSync(join(home, ".claude"), { recursive: true });
37
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
38
+ return settingsPath;
39
+ },
40
+ },
41
+ {
42
+ value: "cursor",
43
+ label: "Cursor",
44
+ hint: ".cursor/environment.json (project)",
45
+ write(apiKey) {
46
+ const envPath = join(process.cwd(), ".cursor", "environment.json");
47
+ let config = {};
48
+ try {
49
+ config = JSON.parse(readFileSync(envPath, "utf-8"));
50
+ } catch {
51
+ // doesn't exist yet
52
+ }
53
+ if (!config.env) config.env = {};
54
+ config.env.CLAWCARD_API_KEY = apiKey;
55
+ mkdirSync(join(process.cwd(), ".cursor"), { recursive: true });
56
+ writeFileSync(envPath, JSON.stringify(config, null, 2));
57
+ return envPath;
58
+ },
59
+ },
60
+ {
61
+ value: "openclaw",
62
+ label: "OpenClaw",
63
+ hint: "~/.openclaw/.env",
64
+ write(apiKey) {
65
+ const envPath = join(home, ".openclaw", ".env");
66
+ let content = "";
67
+ try {
68
+ content = readFileSync(envPath, "utf-8");
69
+ } catch {
70
+ // doesn't exist
71
+ }
72
+ const regex = /^CLAWCARD_API_KEY=.*$/m;
73
+ if (regex.test(content)) {
74
+ content = content.replace(regex, `CLAWCARD_API_KEY=${apiKey}`);
75
+ } else {
76
+ content = content.trimEnd() + `\nCLAWCARD_API_KEY=${apiKey}\n`;
77
+ }
78
+ mkdirSync(join(home, ".openclaw"), { recursive: true });
79
+ writeFileSync(envPath, content);
80
+ return envPath;
81
+ },
82
+ },
83
+ {
84
+ value: "codex",
85
+ label: "Codex (OpenAI)",
86
+ hint: "shell env",
87
+ write(apiKey) {
88
+ // Codex reads from shell env; write to ~/.clawcard/.env and instruct user
89
+ return writeToClawcardEnv(apiKey);
90
+ },
91
+ },
92
+ {
93
+ value: "cline",
94
+ label: "Cline",
95
+ hint: "shell env",
96
+ write(apiKey) {
97
+ // Cline reads from shell env; write to ~/.clawcard/.env and instruct user
98
+ return writeToClawcardEnv(apiKey);
99
+ },
100
+ },
101
+ {
102
+ value: "other",
103
+ label: "Other / manual",
104
+ hint: "~/.clawcard/.env",
105
+ write(apiKey) {
106
+ return writeToClawcardEnv(apiKey);
107
+ },
108
+ },
109
+ ];
110
+
111
+ function writeToClawcardEnv(apiKey) {
112
+ const envPath = join(home, ".clawcard", ".env");
113
+ mkdirSync(join(home, ".clawcard"), { recursive: true });
114
+ let content = "";
115
+ try {
116
+ content = readFileSync(envPath, "utf-8");
117
+ } catch {
118
+ // doesn't exist
119
+ }
120
+ const regex = /^CLAWCARD_API_KEY=.*$/m;
121
+ if (regex.test(content)) {
122
+ content = content.replace(regex, `CLAWCARD_API_KEY=${apiKey}`);
123
+ } else {
124
+ content = content.trimEnd() + `\nCLAWCARD_API_KEY=${apiKey}\n`;
125
+ }
126
+ writeFileSync(envPath, content);
127
+ return envPath;
128
+ }
129
+
130
+ export async function setupCommand() {
131
+ requireAuth();
132
+
133
+ p.intro(orange.bold("Agent Setup"));
134
+
135
+ // Step 1: Pick or create a key
136
+ const s = p.spinner();
137
+ s.start("Fetching your keys...");
138
+
139
+ let agents;
140
+ try {
141
+ agents = await listAgents();
142
+ s.stop("");
143
+ } catch (err) {
144
+ s.stop("Failed to fetch keys");
145
+ p.log.error(err.message);
146
+ return;
147
+ }
148
+
149
+ if (agents.length === 0) {
150
+ p.log.info("You don't have any keys yet. Let's create one first.");
151
+ const { keysCreateCommand } = await import("./keys.js");
152
+ await keysCreateCommand();
153
+ return setupCommand();
154
+ }
155
+
156
+ const keyChoice = await p.select({
157
+ message: "Which API key should this agent use?",
158
+ options: [
159
+ ...agents.map((a) => ({
160
+ value: a.id,
161
+ label: `${a.name || "unnamed"} (${a.keyPrefix}...)`,
162
+ hint: a.email,
163
+ })),
164
+ { value: "create", label: "Create a new key" },
165
+ ],
166
+ });
167
+
168
+ if (p.isCancel(keyChoice)) return;
169
+
170
+ if (keyChoice === "create") {
171
+ const { keysCreateCommand } = await import("./keys.js");
172
+ await keysCreateCommand();
173
+ return setupCommand();
174
+ }
175
+
176
+ const selectedKey = agents.find((a) => a.id === keyChoice);
177
+
178
+ // Step 2: Get the raw API key
179
+ let apiKey = getSavedKey(keyChoice);
180
+
181
+ if (!apiKey) {
182
+ p.log.warn("Raw API key not found locally (it's only shown at creation).");
183
+ const pastedKey = await p.text({
184
+ message: "Paste the API key for this agent",
185
+ placeholder: "ak_live_...",
186
+ validate: (v) => {
187
+ if (!v?.startsWith("ak_live_")) return "Must start with ak_live_";
188
+ },
189
+ });
190
+ if (p.isCancel(pastedKey)) return;
191
+ apiKey = pastedKey;
192
+ }
193
+
194
+ // Step 3: Which harness?
195
+ const harness = await p.select({
196
+ message: "Which agent harness are you using?",
197
+ options: HARNESSES.map((h) => ({
198
+ value: h.value,
199
+ label: h.label,
200
+ hint: h.hint,
201
+ })),
202
+ });
203
+
204
+ if (p.isCancel(harness)) return;
205
+
206
+ // Step 4: Global or project?
207
+ const scope = await p.select({
208
+ message: "Install skill globally or for this project?",
209
+ options: [
210
+ {
211
+ value: "global",
212
+ label: "Global",
213
+ hint: "available to all your agents everywhere",
214
+ },
215
+ {
216
+ value: "project",
217
+ label: "This project",
218
+ hint: "scoped to current directory",
219
+ },
220
+ ],
221
+ });
222
+
223
+ if (p.isCancel(scope)) return;
224
+
225
+ // Step 5: Install skill via skills.sh
226
+ const s2 = p.spinner();
227
+ s2.start("Installing ClawCard skill...");
228
+
229
+ try {
230
+ const globalFlag = scope === "global" ? " -g" : "";
231
+ execSync(`npx -y skills add clawcard/skill${globalFlag} -y`, {
232
+ stdio: "pipe",
233
+ timeout: 60_000,
234
+ });
235
+ s2.stop("Skill installed!");
236
+ } catch {
237
+ s2.stop("Skill installation failed");
238
+ p.log.warn(
239
+ "Failed to install skill via skills.sh. You can install it manually:"
240
+ );
241
+ p.log.info(
242
+ `Run: npx skills add clawcard/skill${scope === "global" ? " -g" : ""}`
243
+ );
244
+ }
245
+
246
+ // Step 6: Write API key to harness-specific location
247
+ const s3 = p.spinner();
248
+ s3.start("Configuring API key...");
249
+
250
+ const harnessConfig = HARNESSES.find((h) => h.value === harness);
251
+ const writtenPath = harnessConfig.write(apiKey);
252
+
253
+ s3.stop("API key configured!");
254
+
255
+ // Step 7: Harness-specific follow-up instructions
256
+ const needsShellExport = ["codex", "cline", "other"].includes(harness);
257
+
258
+ if (needsShellExport) {
259
+ p.note(
260
+ [
261
+ `Key saved to: ${chalk.dim(writtenPath)}`,
262
+ "",
263
+ `Add this to your shell profile (.zshrc, .bashrc):`,
264
+ "",
265
+ orange(` export CLAWCARD_API_KEY="${apiKey}"`),
266
+ "",
267
+ chalk.dim("Or source the env file:"),
268
+ orange(` source ${writtenPath}`),
269
+ ].join("\n"),
270
+ "Manual step required"
271
+ );
272
+ } else {
273
+ p.log.info(`Key written to ${chalk.dim(writtenPath)}`);
274
+ }
275
+
276
+ // Step 8: Summary
277
+ p.log.success("Your agent is ready to use ClawCard!");
278
+ p.log.info(
279
+ [
280
+ `Key: ${orange(selectedKey.name || "unnamed")} (${selectedKey.keyPrefix}...)`,
281
+ selectedKey.email && `Email: ${selectedKey.email}`,
282
+ selectedKey.phone && `Phone: ${selectedKey.phone}`,
283
+ ]
284
+ .filter(Boolean)
285
+ .join("\n")
286
+ );
287
+
288
+ p.outro(
289
+ chalk.dim('Tell your agent to "use ClawCard to check your identity"')
290
+ );
291
+ }
@@ -0,0 +1,47 @@
1
+ import * as p from "@clack/prompts";
2
+ import open from "open";
3
+ import { startCallbackServer } from "../auth-server.js";
4
+ import { saveConfig, BASE_URL } from "../config.js";
5
+
6
+ export async function signupCommand() {
7
+ const code = await p.text({
8
+ message: "Enter your invite code",
9
+ placeholder: "CLAW-XXXX",
10
+ validate: (val) => {
11
+ if (!/^CLAW-[A-Z0-9]{4}$/i.test(val)) {
12
+ return "Invite code must be in CLAW-XXXX format";
13
+ }
14
+ },
15
+ });
16
+
17
+ if (p.isCancel(code)) {
18
+ p.cancel("Signup cancelled");
19
+ return;
20
+ }
21
+
22
+ const s = p.spinner();
23
+ s.start("Opening browser...");
24
+
25
+ const { promise, port } = startCallbackServer();
26
+ const actualPort = await port;
27
+ const signupUrl = `${BASE_URL}/login?cli=true&port=${actualPort}&signup=true&code=${code}`;
28
+ await open(signupUrl);
29
+
30
+ s.message("Waiting for signup in browser...");
31
+
32
+ try {
33
+ const { token, email } = await promise;
34
+ s.stop("Account created!");
35
+
36
+ saveConfig({
37
+ token,
38
+ email,
39
+ loggedInAt: new Date().toISOString(),
40
+ });
41
+
42
+ p.log.success(`Welcome to ClawCard, ${email}!`);
43
+ } catch (err) {
44
+ s.stop("Signup failed");
45
+ p.log.error(err.message);
46
+ }
47
+ }
@@ -0,0 +1,31 @@
1
+ import * as p from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { requireAuth } from "../auth-guard.js";
4
+ import { getConfig } from "../config.js";
5
+ import { getSubscription } from "../api.js";
6
+
7
+ const orange = chalk.hex("#FF6B35");
8
+
9
+ export async function whoamiCommand() {
10
+ requireAuth();
11
+
12
+ const config = getConfig();
13
+ const s = p.spinner();
14
+ s.start("Fetching account...");
15
+
16
+ try {
17
+ const sub = await getSubscription();
18
+ s.stop("");
19
+
20
+ p.note(
21
+ [
22
+ `Email: ${orange(config.email)}`,
23
+ `Plan: ${sub.subscription?.planName || "Free"}`,
24
+ ].join("\n"),
25
+ "Account"
26
+ );
27
+ } catch {
28
+ s.stop("");
29
+ p.note(`Email: ${orange(config.email)}`, "Account");
30
+ }
31
+ }
package/src/config.js ADDED
@@ -0,0 +1,68 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".clawcard");
6
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
7
+
8
+ export const BASE_URL = "https://www.clawcard.sh";
9
+
10
+ export function getConfig() {
11
+ try {
12
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
13
+ return JSON.parse(raw);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function saveConfig(config) {
20
+ mkdirSync(CONFIG_DIR, { recursive: true });
21
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
22
+ }
23
+
24
+ export function clearConfig() {
25
+ try {
26
+ unlinkSync(CONFIG_PATH);
27
+ } catch {
28
+ // already gone
29
+ }
30
+ }
31
+
32
+ export function isLoggedIn() {
33
+ const config = getConfig();
34
+ if (!config?.token) return false;
35
+ if (config.expiresAt && new Date(config.expiresAt) < new Date()) return false;
36
+ return true;
37
+ }
38
+
39
+ export function getToken() {
40
+ const config = getConfig();
41
+ return config?.token ?? null;
42
+ }
43
+
44
+ // ── Local key storage ──
45
+ // Saves raw API keys locally so `clawcard setup` can reference them later.
46
+ // Keys are stored in ~/.clawcard/keys.json keyed by agent ID.
47
+
48
+ const KEYS_PATH = join(CONFIG_DIR, "keys.json");
49
+
50
+ export function saveKey(agentId, apiKey, meta = {}) {
51
+ mkdirSync(CONFIG_DIR, { recursive: true });
52
+ const keys = getSavedKeys();
53
+ keys[agentId] = { apiKey, ...meta, savedAt: new Date().toISOString() };
54
+ writeFileSync(KEYS_PATH, JSON.stringify(keys, null, 2));
55
+ }
56
+
57
+ export function getSavedKeys() {
58
+ try {
59
+ return JSON.parse(readFileSync(KEYS_PATH, "utf-8"));
60
+ } catch {
61
+ return {};
62
+ }
63
+ }
64
+
65
+ export function getSavedKey(agentId) {
66
+ const keys = getSavedKeys();
67
+ return keys[agentId]?.apiKey ?? null;
68
+ }
package/src/index.js ADDED
@@ -0,0 +1,209 @@
1
+ import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import { showSplash } from "./splash.js";
4
+ import { isLoggedIn } from "./config.js";
5
+
6
+ // Show splash on every invocation
7
+ showSplash();
8
+
9
+ const program = new Command();
10
+ program.name("clawcard").description("The ClawCard CLI").version("1.0.0");
11
+
12
+ // Auth commands
13
+ program
14
+ .command("login")
15
+ .description("Log in via browser")
16
+ .action(async () => {
17
+ const { loginCommand } = await import("./commands/login.js");
18
+ await loginCommand();
19
+ });
20
+
21
+ program
22
+ .command("signup")
23
+ .description("Sign up with invite code")
24
+ .action(async () => {
25
+ const { signupCommand } = await import("./commands/signup.js");
26
+ await signupCommand();
27
+ });
28
+
29
+ program
30
+ .command("logout")
31
+ .description("Clear session")
32
+ .action(async () => {
33
+ const { logoutCommand } = await import("./commands/logout.js");
34
+ await logoutCommand();
35
+ });
36
+
37
+ program
38
+ .command("whoami")
39
+ .description("Show current account")
40
+ .action(async () => {
41
+ const { whoamiCommand } = await import("./commands/whoami.js");
42
+ await whoamiCommand();
43
+ });
44
+
45
+ // Keys
46
+ const keys = program.command("keys").description("Manage API keys");
47
+
48
+ keys.action(async () => {
49
+ const { keysCommand } = await import("./commands/keys.js");
50
+ await keysCommand();
51
+ });
52
+
53
+ keys
54
+ .command("create")
55
+ .description("Create a new API key")
56
+ .action(async () => {
57
+ const { keysCreateCommand } = await import("./commands/keys.js");
58
+ await keysCreateCommand();
59
+ });
60
+
61
+ keys
62
+ .command("list")
63
+ .description("List all keys")
64
+ .action(async () => {
65
+ const { keysListCommand } = await import("./commands/keys.js");
66
+ await keysListCommand();
67
+ });
68
+
69
+ keys
70
+ .command("info [keyId]")
71
+ .description("Show key details")
72
+ .action(async (keyId) => {
73
+ const { keysInfoCommand } = await import("./commands/keys.js");
74
+ await keysInfoCommand(keyId);
75
+ });
76
+
77
+ keys
78
+ .command("revoke")
79
+ .description("Revoke a key")
80
+ .action(async () => {
81
+ const { keysRevokeCommand } = await import("./commands/keys.js");
82
+ await keysRevokeCommand();
83
+ });
84
+
85
+ // Setup
86
+ program
87
+ .command("setup")
88
+ .description("Install ClawCard skill for your agent")
89
+ .action(async () => {
90
+ const { setupCommand } = await import("./commands/setup.js");
91
+ await setupCommand();
92
+ });
93
+
94
+ // Billing
95
+ const billing = program.command("billing").description("Billing dashboard");
96
+
97
+ billing.action(async () => {
98
+ const { billingCommand } = await import("./commands/billing.js");
99
+ await billingCommand();
100
+ });
101
+
102
+ billing
103
+ .command("balance")
104
+ .description("Quick balance check")
105
+ .action(async () => {
106
+ const { billingBalanceCommand } = await import("./commands/billing.js");
107
+ await billingBalanceCommand();
108
+ });
109
+
110
+ billing
111
+ .command("topup")
112
+ .description("Top up balance")
113
+ .action(async () => {
114
+ const { billingTopupCommand } = await import("./commands/billing.js");
115
+ await billingTopupCommand();
116
+ });
117
+
118
+ billing
119
+ .command("upgrade")
120
+ .description("Upgrade subscription")
121
+ .action(async () => {
122
+ const { billingUpgradeCommand } = await import("./commands/billing.js");
123
+ await billingUpgradeCommand();
124
+ });
125
+
126
+ billing
127
+ .command("transactions")
128
+ .description("View transaction history")
129
+ .action(async () => {
130
+ const { billingTransactionsCommand } = await import(
131
+ "./commands/billing.js"
132
+ );
133
+ await billingTransactionsCommand();
134
+ });
135
+
136
+ // Referral
137
+ program
138
+ .command("referral")
139
+ .description("Show referral code")
140
+ .action(async () => {
141
+ const { referralCommand } = await import("./commands/referral.js");
142
+ await referralCommand();
143
+ });
144
+
145
+ // Help
146
+ program
147
+ .command("help")
148
+ .description("Show all commands")
149
+ .action(async () => {
150
+ const { helpCommand } = await import("./commands/help.js");
151
+ helpCommand();
152
+ });
153
+
154
+ // If no subcommand, show interactive menu
155
+ if (process.argv.length <= 2) {
156
+ const loggedIn = isLoggedIn();
157
+
158
+ const options = loggedIn
159
+ ? [
160
+ {
161
+ value: "setup",
162
+ label: "Setup",
163
+ hint: "connect ClawCard to your agent",
164
+ },
165
+ { value: "keys", label: "Keys", hint: "create & manage API keys" },
166
+ { value: "billing", label: "Billing", hint: "check balance & top up" },
167
+ { value: "referral", label: "Referral", hint: "share & earn $10" },
168
+ { value: "whoami", label: "Who am I?", hint: "show current account" },
169
+ { value: "logout", label: "Logout", hint: "clear session" },
170
+ { value: "help", label: "Help", hint: "show all commands" },
171
+ ]
172
+ : [
173
+ { value: "login", label: "Log in", hint: "authenticate via browser" },
174
+ {
175
+ value: "signup",
176
+ label: "Sign up",
177
+ hint: "create account with invite code",
178
+ },
179
+ { value: "help", label: "Help", hint: "show all commands" },
180
+ ];
181
+
182
+ const action = await p.select({
183
+ message: "What would you like to do?",
184
+ options,
185
+ });
186
+
187
+ if (p.isCancel(action)) {
188
+ p.cancel("Goodbye!");
189
+ process.exit(0);
190
+ }
191
+
192
+ const commands = {
193
+ login: () => import("./commands/login.js").then((m) => m.loginCommand()),
194
+ signup: () => import("./commands/signup.js").then((m) => m.signupCommand()),
195
+ logout: () => import("./commands/logout.js").then((m) => m.logoutCommand()),
196
+ whoami: () => import("./commands/whoami.js").then((m) => m.whoamiCommand()),
197
+ keys: () => import("./commands/keys.js").then((m) => m.keysCommand()),
198
+ setup: () => import("./commands/setup.js").then((m) => m.setupCommand()),
199
+ billing: () =>
200
+ import("./commands/billing.js").then((m) => m.billingCommand()),
201
+ referral: () =>
202
+ import("./commands/referral.js").then((m) => m.referralCommand()),
203
+ help: () => import("./commands/help.js").then((m) => m.helpCommand()),
204
+ };
205
+
206
+ await commands[action]?.();
207
+ } else {
208
+ program.parse();
209
+ }
package/src/splash.js ADDED
@@ -0,0 +1,11 @@
1
+ import chalk from "chalk";
2
+
3
+ const o = chalk.hex("#FF6B35");
4
+ const d = chalk.dim;
5
+
6
+ export function showSplash() {
7
+ console.log();
8
+ console.log(` 🦞 ${o.bold("clawcard.sh")}`);
9
+ console.log(` ${d("on-demand credit cards, email and phone for your agents")}`);
10
+ console.log();
11
+ }