@promptai.credit/cli 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +252 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,7 @@ promptai install cursor # Cursor Agents Window / classic IDE hooks
51
51
  # then:
52
52
  promptai status # device, balance, banked ad watches, recent prompts
53
53
  promptai watch # open a rewarded ad now (banks a credit for later)
54
+ promptai set email you@x.com # override the account email (required for claims)
54
55
  promptai set wallet 0x... # payout address
55
56
  promptai claim # USDC on Base
56
57
  promptai set ads off # opt out any time
@@ -67,6 +68,13 @@ Developing from the repo instead: `pnpm --filter @promptai.credit/cli build`, th
67
68
  - Credits only accrue for turns funded by a **verified** ad watch, enforced
68
69
  server-side (same rules as the Cursor extension: one session funds one prompt,
69
70
  capped at $5).
71
+ - Monthly earn caps: $75/device and $150 cumulative per linked email (UTC
72
+ calendar month). Claims require a linked email that an admin has activated.
73
+ - The account email is auto-detected (Claude Code login in `~/.claude.json`,
74
+ the Cursor account in its state DB, then `git config user.email`) and linked
75
+ on the next credited prompt. Auto-detected emails link without verification;
76
+ `promptai set email` (manual entry or changing a linked email) sends a
77
+ 6-digit code to the address, which the command prompts for.
70
78
  - Headless environments (CI, ssh without a display) never get browser tabs and
71
79
  simply skip crediting.
72
80
  - Uninstall with `promptai uninstall claude`; other hooks in your settings are
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as fs5 from "node:fs";
5
- import * as path4 from "node:path";
4
+ import * as fs6 from "node:fs";
5
+ import * as path5 from "node:path";
6
+ import * as readline from "node:readline";
6
7
 
7
8
  // src/claude.ts
8
9
  import * as crypto3 from "node:crypto";
9
- import * as fs3 from "node:fs";
10
- import * as os2 from "node:os";
11
- import * as path2 from "node:path";
10
+ import * as fs4 from "node:fs";
11
+ import * as os3 from "node:os";
12
+ import * as path3 from "node:path";
12
13
 
13
14
  // src/api.ts
14
15
  import * as crypto from "node:crypto";
@@ -43,6 +44,12 @@ function redeemCredit(serverUrl, params) {
43
44
  function fetchBalance(serverUrl, deviceId) {
44
45
  return request(`${serverUrl}/balance/${deviceId}`);
45
46
  }
47
+ function linkEmail(serverUrl, params) {
48
+ return post(serverUrl, "/devices/link", params);
49
+ }
50
+ function verifyEmailLink(serverUrl, params) {
51
+ return post(serverUrl, "/devices/link/verify", params);
52
+ }
46
53
  function claim(serverUrl, params) {
47
54
  return post(serverUrl, "/claim", {
48
55
  ...params,
@@ -94,7 +101,8 @@ function loadConfig() {
94
101
  deviceId: "",
95
102
  serverUrl: "https://api.promptai.credit",
96
103
  wallet: "",
97
- adsOptIn: true
104
+ adsOptIn: true,
105
+ email: ""
98
106
  });
99
107
  if (!config.deviceId) {
100
108
  config.deviceId = crypto2.randomUUID();
@@ -139,6 +147,112 @@ function log(message) {
139
147
  }
140
148
  }
141
149
 
150
+ // src/email.ts
151
+ import { execFileSync } from "node:child_process";
152
+ import * as fs2 from "node:fs";
153
+ import * as os2 from "node:os";
154
+ import * as path2 from "node:path";
155
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
156
+ function clean(value) {
157
+ if (typeof value !== "string") return null;
158
+ const email = value.trim().toLowerCase();
159
+ return EMAIL_RE.test(email) ? email : null;
160
+ }
161
+ function fromClaudeJson() {
162
+ try {
163
+ const raw = fs2.readFileSync(path2.join(os2.homedir(), ".claude.json"), "utf8");
164
+ const data = JSON.parse(raw);
165
+ return clean(data.oauthAccount?.emailAddress);
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ function cursorStateDbPath() {
171
+ const home = os2.homedir();
172
+ switch (process.platform) {
173
+ case "darwin":
174
+ return path2.join(
175
+ home,
176
+ "Library",
177
+ "Application Support",
178
+ "Cursor",
179
+ "User",
180
+ "globalStorage",
181
+ "state.vscdb"
182
+ );
183
+ case "win32":
184
+ return path2.join(
185
+ process.env.APPDATA ?? path2.join(home, "AppData", "Roaming"),
186
+ "Cursor",
187
+ "User",
188
+ "globalStorage",
189
+ "state.vscdb"
190
+ );
191
+ default:
192
+ return path2.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
193
+ }
194
+ }
195
+ function fromCursorStateDb() {
196
+ const src = cursorStateDbPath();
197
+ if (!fs2.existsSync(src)) return null;
198
+ let dir = null;
199
+ try {
200
+ dir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "promptai-email-"));
201
+ const db = path2.join(dir, "state.vscdb");
202
+ fs2.copyFileSync(src, db);
203
+ for (const suffix of ["-wal", "-shm"]) {
204
+ if (fs2.existsSync(src + suffix)) fs2.copyFileSync(src + suffix, db + suffix);
205
+ }
206
+ const stdout = execFileSync(
207
+ "sqlite3",
208
+ [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/cachedEmail'"],
209
+ { encoding: "utf8", timeout: 5e3 }
210
+ ).trim();
211
+ if (stdout.startsWith('"')) {
212
+ try {
213
+ return clean(JSON.parse(stdout));
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ return clean(stdout);
219
+ } catch {
220
+ return null;
221
+ } finally {
222
+ if (dir) fs2.rmSync(dir, { recursive: true, force: true });
223
+ }
224
+ }
225
+ function fromGitConfig() {
226
+ try {
227
+ return clean(
228
+ execFileSync("git", ["config", "--get", "user.email"], {
229
+ encoding: "utf8",
230
+ timeout: 5e3
231
+ })
232
+ );
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ function detectEmail() {
238
+ const claude = fromClaudeJson();
239
+ if (claude) return { email: claude, source: "claude-account" };
240
+ const cursor = fromCursorStateDb();
241
+ if (cursor) return { email: cursor, source: "cursor-account" };
242
+ const git = fromGitConfig();
243
+ if (git) return { email: git, source: "git-config" };
244
+ return null;
245
+ }
246
+ function resolveEmail(config) {
247
+ if (config.email) return config.email;
248
+ const detected = detectEmail();
249
+ if (!detected) return "";
250
+ config.email = detected.email;
251
+ saveConfig(config);
252
+ log(`auto-detected email ${detected.email} (${detected.source})`);
253
+ return detected.email;
254
+ }
255
+
142
256
  // src/pricing.ts
143
257
  var TABLE = [
144
258
  { match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
@@ -169,9 +283,9 @@ function costUsd(model, inputTokens, outputTokens) {
169
283
  }
170
284
 
171
285
  // src/transcript.ts
172
- import * as fs2 from "node:fs";
286
+ import * as fs3 from "node:fs";
173
287
  function readTranscriptUsage(transcriptPath, watermark) {
174
- const raw = fs2.readFileSync(transcriptPath, "utf8");
288
+ const raw = fs3.readFileSync(transcriptPath, "utf8");
175
289
  const byMessageId = /* @__PURE__ */ new Map();
176
290
  let parsedCount = 0;
177
291
  for (const line of raw.split("\n")) {
@@ -213,7 +327,7 @@ function readTranscriptUsage(transcriptPath, watermark) {
213
327
  };
214
328
  }
215
329
  function readGlassTranscriptUsage(transcriptPath, watermark) {
216
- const raw = fs2.readFileSync(transcriptPath, "utf8");
330
+ const raw = fs3.readFileSync(transcriptPath, "utf8");
217
331
  const lines = raw.split("\n").filter((l) => l.trim());
218
332
  let inputChars = 0;
219
333
  let outputChars = 0;
@@ -248,13 +362,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
248
362
  var AD_MIN_INTERVAL_MS = 9e4;
249
363
  var SETTLE_RETRIES = [400, 800, 1500];
250
364
  function claudeSettingsPath() {
251
- return path2.join(os2.homedir(), ".claude", "settings.json");
365
+ return path3.join(os3.homedir(), ".claude", "settings.json");
252
366
  }
253
367
  function hookCommand() {
254
- let script = path2.resolve(process.argv[1] ?? "");
368
+ let script = path3.resolve(process.argv[1] ?? "");
255
369
  if (script.endsWith(".ts")) {
256
- const dist = path2.resolve(path2.dirname(script), "..", "dist", "index.js");
257
- if (!fs3.existsSync(dist)) {
370
+ const dist = path3.resolve(path3.dirname(script), "..", "dist", "index.js");
371
+ if (!fs4.existsSync(dist)) {
258
372
  throw new Error(
259
373
  `Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
260
374
  );
@@ -265,13 +379,13 @@ function hookCommand() {
265
379
  }
266
380
  function installClaudeHooks() {
267
381
  const settingsPath = claudeSettingsPath();
268
- fs3.mkdirSync(path2.dirname(settingsPath), { recursive: true });
382
+ fs4.mkdirSync(path3.dirname(settingsPath), { recursive: true });
269
383
  let settings = {};
270
- if (fs3.existsSync(settingsPath)) {
384
+ if (fs4.existsSync(settingsPath)) {
271
385
  try {
272
- settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
386
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
273
387
  } catch {
274
- fs3.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
388
+ fs4.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
275
389
  settings = {};
276
390
  }
277
391
  }
@@ -291,17 +405,17 @@ function installClaudeHooks() {
291
405
  changed = true;
292
406
  }
293
407
  }
294
- if (changed || !fs3.existsSync(settingsPath)) {
295
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
408
+ if (changed || !fs4.existsSync(settingsPath)) {
409
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
296
410
  }
297
411
  return { changed, settingsPath };
298
412
  }
299
413
  function uninstallClaudeHooks() {
300
414
  const settingsPath = claudeSettingsPath();
301
- if (!fs3.existsSync(settingsPath)) return { changed: false };
415
+ if (!fs4.existsSync(settingsPath)) return { changed: false };
302
416
  let settings;
303
417
  try {
304
- settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
418
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
305
419
  } catch {
306
420
  return { changed: false };
307
421
  }
@@ -321,7 +435,7 @@ function uninstallClaudeHooks() {
321
435
  }
322
436
  }
323
437
  if (changed) {
324
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
438
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
325
439
  }
326
440
  return { changed };
327
441
  }
@@ -376,7 +490,13 @@ async function handleStop(payload) {
376
490
  const promptId = crypto3.randomUUID();
377
491
  let verified = false;
378
492
  if (config.adsOptIn && cost > 0) {
379
- verified = await redeemAgainstAd(config.serverUrl, config.deviceId, promptId, cost);
493
+ verified = await redeemAgainstAd(
494
+ config.serverUrl,
495
+ config.deviceId,
496
+ promptId,
497
+ cost,
498
+ resolveEmail(config)
499
+ );
380
500
  }
381
501
  state.prompts.unshift({
382
502
  id: promptId,
@@ -394,7 +514,7 @@ async function handleStop(payload) {
394
514
  `[claude] settled session=${sessionId} model=${usage.model} in=${usage.inputTokens} out=${usage.outputTokens} cost=$${cost} verified=${verified}`
395
515
  );
396
516
  }
397
- async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
517
+ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
398
518
  try {
399
519
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
400
520
  if (sessions.length === 0) {
@@ -405,7 +525,8 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
405
525
  sessionId: sessions[0].sessionId,
406
526
  deviceId,
407
527
  promptId,
408
- amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD)
528
+ amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
529
+ email: email || void 0
409
530
  });
410
531
  return true;
411
532
  } catch (err) {
@@ -416,9 +537,9 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
416
537
 
417
538
  // src/cursor.ts
418
539
  import * as crypto4 from "node:crypto";
419
- import * as fs4 from "node:fs";
420
- import * as os3 from "node:os";
421
- import * as path3 from "node:path";
540
+ import * as fs5 from "node:fs";
541
+ import * as os4 from "node:os";
542
+ import * as path4 from "node:path";
422
543
  var CURSOR_SOURCE = "cursor-agents";
423
544
  var HOOK_MARKER2 = "promptai";
424
545
  var MAX_CREDIT_PER_AD_USD2 = 5;
@@ -428,17 +549,17 @@ function isCursorPayload(payload) {
428
549
  return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
429
550
  }
430
551
  function cursorHooksJsonPath() {
431
- return path3.join(os3.homedir(), ".cursor", "hooks.json");
552
+ return path4.join(os4.homedir(), ".cursor", "hooks.json");
432
553
  }
433
554
  function installCursorHooks() {
434
555
  const hooksPath = cursorHooksJsonPath();
435
- fs4.mkdirSync(path3.dirname(hooksPath), { recursive: true });
556
+ fs5.mkdirSync(path4.dirname(hooksPath), { recursive: true });
436
557
  let config = {};
437
- if (fs4.existsSync(hooksPath)) {
558
+ if (fs5.existsSync(hooksPath)) {
438
559
  try {
439
- config = JSON.parse(fs4.readFileSync(hooksPath, "utf8"));
560
+ config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
440
561
  } catch {
441
- fs4.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
562
+ fs5.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
442
563
  config = {};
443
564
  }
444
565
  }
@@ -460,17 +581,17 @@ function installCursorHooks() {
460
581
  changed = true;
461
582
  }
462
583
  }
463
- if (changed || !fs4.existsSync(hooksPath)) {
464
- fs4.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
584
+ if (changed || !fs5.existsSync(hooksPath)) {
585
+ fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
465
586
  }
466
587
  return { changed, hooksPath };
467
588
  }
468
589
  function uninstallCursorHooks() {
469
590
  const hooksPath = cursorHooksJsonPath();
470
- if (!fs4.existsSync(hooksPath)) return { changed: false };
591
+ if (!fs5.existsSync(hooksPath)) return { changed: false };
471
592
  let config;
472
593
  try {
473
- config = JSON.parse(fs4.readFileSync(hooksPath, "utf8"));
594
+ config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
474
595
  } catch {
475
596
  return { changed: false };
476
597
  }
@@ -487,14 +608,14 @@ function uninstallCursorHooks() {
487
608
  }
488
609
  }
489
610
  if (changed) {
490
- fs4.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
611
+ fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
491
612
  }
492
613
  return { changed };
493
614
  }
494
615
  function extensionIsActive() {
495
- const infoPath = path3.join(os3.homedir(), ".cursor", "promptai-listener.json");
616
+ const infoPath = path4.join(os4.homedir(), ".cursor", "promptai-listener.json");
496
617
  try {
497
- const info = JSON.parse(fs4.readFileSync(infoPath, "utf8"));
618
+ const info = JSON.parse(fs5.readFileSync(infoPath, "utf8"));
498
619
  if (typeof info.pid !== "number") return false;
499
620
  process.kill(info.pid, 0);
500
621
  return true;
@@ -503,18 +624,18 @@ function extensionIsActive() {
503
624
  }
504
625
  }
505
626
  function acquireOnceLock(key) {
506
- const dir = path3.join(promptaiDir(), "locks");
507
- fs4.mkdirSync(dir, { recursive: true });
627
+ const dir = path4.join(promptaiDir(), "locks");
628
+ fs5.mkdirSync(dir, { recursive: true });
508
629
  try {
509
630
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
510
- for (const name of fs4.readdirSync(dir)) {
511
- const p = path3.join(dir, name);
512
- if (fs4.statSync(p).mtimeMs < cutoff) fs4.rmSync(p, { recursive: true, force: true });
631
+ for (const name of fs5.readdirSync(dir)) {
632
+ const p = path4.join(dir, name);
633
+ if (fs5.statSync(p).mtimeMs < cutoff) fs5.rmSync(p, { recursive: true, force: true });
513
634
  }
514
635
  } catch {
515
636
  }
516
637
  try {
517
- fs4.mkdirSync(path3.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
638
+ fs5.mkdirSync(path4.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
518
639
  return true;
519
640
  } catch {
520
641
  return false;
@@ -607,7 +728,13 @@ async function handleCursorStop(payload) {
607
728
  const promptId = crypto4.randomUUID();
608
729
  let verified = false;
609
730
  if (config.adsOptIn && cost > 0) {
610
- verified = await redeemAgainstAd2(config.serverUrl, config.deviceId, promptId, cost);
731
+ verified = await redeemAgainstAd2(
732
+ config.serverUrl,
733
+ config.deviceId,
734
+ promptId,
735
+ cost,
736
+ resolveEmail(config)
737
+ );
611
738
  }
612
739
  state.prompts.unshift({
613
740
  id: promptId,
@@ -626,7 +753,7 @@ async function handleCursorStop(payload) {
626
753
  `[cursor] settled conv=${convId} model=${model} in=${inputTokens} out=${outputTokens} cost=$${cost}${estimated ? " (estimated)" : ""} verified=${verified}`
627
754
  );
628
755
  }
629
- async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost) {
756
+ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
630
757
  try {
631
758
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
632
759
  if (sessions.length === 0) {
@@ -637,7 +764,8 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost) {
637
764
  sessionId: sessions[0].sessionId,
638
765
  deviceId,
639
766
  promptId,
640
- amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2)
767
+ amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
768
+ email: email || void 0
641
769
  });
642
770
  return true;
643
771
  } catch (err) {
@@ -657,6 +785,7 @@ Usage:
657
785
  promptai watch Open a rewarded ad in the browser now
658
786
  promptai claim [address] Claim your verified balance as USDC (Base)
659
787
  promptai set wallet 0x... Set the payout wallet
788
+ promptai set email <email> Link your email (required for USDC claims; groups devices)
660
789
  promptai set server <url> Point at a different API server
661
790
  promptai set ads on|off Toggle the rewarded-ads opt-in
662
791
  promptai hook (internal) invoked by agent hooks, JSON on stdin
@@ -677,11 +806,11 @@ async function cmdHook() {
677
806
  return;
678
807
  }
679
808
  const event = String(payload.hook_event_name ?? "");
680
- if (process.env.PROMPTAI_DUMP === "1" || fs5.existsSync(path4.join(promptaiDir(), "debug"))) {
809
+ if (process.env.PROMPTAI_DUMP === "1" || fs6.existsSync(path5.join(promptaiDir(), "debug"))) {
681
810
  try {
682
- const dir = path4.join(promptaiDir(), "dump");
683
- fs5.mkdirSync(dir, { recursive: true });
684
- fs5.writeFileSync(path4.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
811
+ const dir = path5.join(promptaiDir(), "dump");
812
+ fs6.mkdirSync(dir, { recursive: true });
813
+ fs6.writeFileSync(path5.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
685
814
  } catch {
686
815
  }
687
816
  }
@@ -705,9 +834,11 @@ async function cmdHook() {
705
834
  async function cmdStatus() {
706
835
  const config = loadConfig();
707
836
  const state = loadState();
837
+ const email = resolveEmail(config);
708
838
  console.log(`device ${config.deviceId}`);
709
839
  console.log(`server ${config.serverUrl}`);
710
840
  console.log(`wallet ${config.wallet || "(not set - promptai set wallet 0x...)"}`);
841
+ console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
711
842
  console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
712
843
  console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
713
844
  try {
@@ -718,6 +849,12 @@ async function cmdStatus() {
718
849
  console.log(
719
850
  `balance $${balance.balanceUsd.toFixed(4)} (earned $${balance.earnedUsd.toFixed(4)}, claimed $${balance.claimedUsd.toFixed(4)})`
720
851
  );
852
+ console.log(
853
+ `monthly device $${balance.monthEarnedDeviceUsd.toFixed(4)} / $${balance.deviceCapUsd}` + (balance.email ? ` \xB7 email $${balance.monthEarnedEmailUsd.toFixed(4)} / $${balance.emailCapUsd}` : "")
854
+ );
855
+ console.log(
856
+ `claims ${!balance.email ? "blocked (link an email first)" : balance.claimActivated ? "activated" : "blocked (waiting for admin activation)"}`
857
+ );
721
858
  console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
722
859
  } catch (err) {
723
860
  console.log(`balance unavailable (${String(err)})`);
@@ -754,6 +891,13 @@ async function cmdClaim(addressArg) {
754
891
  process.exitCode = 1;
755
892
  return;
756
893
  }
894
+ const email = resolveEmail(config);
895
+ if (email) {
896
+ try {
897
+ await linkEmail(config.serverUrl, { deviceId: config.deviceId, email, source: "auto" });
898
+ } catch {
899
+ }
900
+ }
757
901
  const balance = await fetchBalance(config.serverUrl, config.deviceId);
758
902
  if (balance.balanceUsd < 1e-6) {
759
903
  console.log("Nothing to claim yet - watch an ad (promptai watch) and run some agent prompts first.");
@@ -768,7 +912,7 @@ async function cmdClaim(addressArg) {
768
912
  console.log(`Sent: ${result.txHash}`);
769
913
  console.log(result.explorerUrl);
770
914
  }
771
- function cmdSet(key, value) {
915
+ async function cmdSet(key, value) {
772
916
  const config = loadConfig();
773
917
  if (key === "wallet" && value && /^0x[0-9a-fA-F]{40}$/.test(value)) {
774
918
  config.wallet = value;
@@ -776,14 +920,66 @@ function cmdSet(key, value) {
776
920
  config.serverUrl = value.replace(/\/$/, "");
777
921
  } else if (key === "ads" && (value === "on" || value === "off")) {
778
922
  config.adsOptIn = value === "on";
923
+ } else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
924
+ config.email = value.trim().toLowerCase();
779
925
  } else {
780
- console.error("Usage: promptai set wallet 0x... | set server <url> | set ads on|off");
926
+ console.error(
927
+ "Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set email you@example.com"
928
+ );
781
929
  process.exitCode = 1;
782
930
  return;
783
931
  }
932
+ if (key === "email") {
933
+ const email = config.email;
934
+ try {
935
+ const result = await linkEmail(config.serverUrl, {
936
+ deviceId: config.deviceId,
937
+ email,
938
+ source: "manual"
939
+ });
940
+ if (result.otpRequired) {
941
+ console.log(result.message ?? `Verification code sent to ${email}.`);
942
+ const code = (await promptLine("Enter the 6-digit code: ")).trim();
943
+ if (!/^\d{6}$/.test(code)) {
944
+ console.error("That doesn't look like a 6-digit code. Run the command again.");
945
+ process.exitCode = 1;
946
+ return;
947
+ }
948
+ const verified = await verifyEmailLink(config.serverUrl, {
949
+ deviceId: config.deviceId,
950
+ email,
951
+ code
952
+ });
953
+ printLinkStatus(verified.claimActivated);
954
+ } else {
955
+ printLinkStatus(Boolean(result.claimActivated));
956
+ }
957
+ saveConfig(config);
958
+ console.log("email updated.");
959
+ } catch (err) {
960
+ console.error(`Email not linked: ${err instanceof Error ? err.message : String(err)}`);
961
+ process.exitCode = 1;
962
+ }
963
+ return;
964
+ }
784
965
  saveConfig(config);
785
966
  console.log(`${key} updated.`);
786
967
  }
968
+ function printLinkStatus(claimActivated) {
969
+ console.log(
970
+ claimActivated ? "Email linked. Account is activated for USDC claims." : "Email linked. USDC claims stay blocked until an admin activates this account."
971
+ );
972
+ }
973
+ function promptLine(question) {
974
+ return new Promise((resolve2) => {
975
+ process.stdout.write(question);
976
+ const rl = readline.createInterface({ input: process.stdin });
977
+ rl.once("line", (line) => {
978
+ rl.close();
979
+ resolve2(line);
980
+ });
981
+ });
982
+ }
787
983
  function cmdInstall(agent) {
788
984
  if (agent === "claude" || agent === "claude-code") {
789
985
  const { changed, settingsPath } = installClaudeHooks();
@@ -842,7 +1038,7 @@ try {
842
1038
  await cmdClaim(args[0]);
843
1039
  break;
844
1040
  case "set":
845
- cmdSet(args[0], args[1]);
1041
+ await cmdSet(args[0], args[1]);
846
1042
  break;
847
1043
  default:
848
1044
  console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptai.credit/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },