@promptai.credit/cli 0.2.0 → 0.4.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 +12 -2
  2. package/dist/index.js +469 -77
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,13 +47,16 @@ State lives in `~/.promptai/` (`config.json`, `state.json`, `cli.log`).
47
47
  npm install -g @promptai.credit/cli
48
48
  promptai install claude # Claude Code hooks
49
49
  promptai install cursor # Cursor Agents Window / classic IDE hooks
50
+ ```
51
+
52
+ `promptai install` then asks for your email and payout wallet, and docks ads on the **right** by default (like the IDE panel). Enter accepts a detected email.
50
53
 
51
- # then:
54
+ ```bash
52
55
  promptai status # device, balance, banked ad watches, recent prompts
53
56
  promptai watch # open a rewarded ad now (banks a credit for later)
54
- promptai set wallet 0x... # payout address
55
57
  promptai claim # USDC on Base
56
58
  promptai set ads off # opt out any time
59
+ promptai set side left # or `default` for a normal browser tab
57
60
  ```
58
61
 
59
62
  The default server is `https://api.promptai.credit`; point elsewhere with
@@ -67,6 +70,13 @@ Developing from the repo instead: `pnpm --filter @promptai.credit/cli build`, th
67
70
  - Credits only accrue for turns funded by a **verified** ad watch, enforced
68
71
  server-side (same rules as the Cursor extension: one session funds one prompt,
69
72
  capped at $5).
73
+ - Monthly earn caps: $75/device and $150 cumulative per linked email (UTC
74
+ calendar month). Claims require a linked email that an admin has activated.
75
+ - The account email is auto-detected (Claude Code login in `~/.claude.json`,
76
+ the Cursor account in its state DB, then `git config user.email`) and offered
77
+ as the default during `promptai install`. Auto-detected emails link without
78
+ verification; typing a different address (or `promptai set email`) sends a
79
+ 6-digit code to prove ownership.
70
80
  - Headless environments (CI, ssh without a display) never get browser tabs and
71
81
  simply skip crediting.
72
82
  - Uninstall with `promptai uninstall claude`; other hooks in your settings are
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
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 fs7 from "node:fs";
5
+ import * as path6 from "node:path";
6
6
 
7
7
  // src/claude.ts
8
8
  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";
9
+ import * as fs5 from "node:fs";
10
+ import * as os3 from "node:os";
11
+ import * as path4 from "node:path";
12
12
 
13
13
  // src/api.ts
14
14
  import * as crypto from "node:crypto";
@@ -43,6 +43,12 @@ function redeemCredit(serverUrl, params) {
43
43
  function fetchBalance(serverUrl, deviceId) {
44
44
  return request(`${serverUrl}/balance/${deviceId}`);
45
45
  }
46
+ function linkEmail(serverUrl, params) {
47
+ return post(serverUrl, "/devices/link", params);
48
+ }
49
+ function verifyEmailLink(serverUrl, params) {
50
+ return post(serverUrl, "/devices/link/verify", params);
51
+ }
46
52
  function claim(serverUrl, params) {
47
53
  return post(serverUrl, "/claim", {
48
54
  ...params,
@@ -51,21 +57,9 @@ function claim(serverUrl, params) {
51
57
  }
52
58
 
53
59
  // src/browser.ts
54
- import { spawn } from "node:child_process";
55
- function hasDisplay() {
56
- if (process.platform === "darwin" || process.platform === "win32") return true;
57
- return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
58
- }
59
- function openInBrowser(url) {
60
- if (!hasDisplay()) return false;
61
- const [cmd, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
62
- try {
63
- spawn(cmd, args2, { detached: true, stdio: "ignore" }).unref();
64
- return true;
65
- } catch {
66
- return false;
67
- }
68
- }
60
+ import { execFileSync, spawn } from "node:child_process";
61
+ import * as fs2 from "node:fs";
62
+ import * as path2 from "node:path";
69
63
 
70
64
  // src/config.ts
71
65
  import * as crypto2 from "node:crypto";
@@ -94,8 +88,13 @@ function loadConfig() {
94
88
  deviceId: "",
95
89
  serverUrl: "https://api.promptai.credit",
96
90
  wallet: "",
97
- adsOptIn: true
91
+ adsOptIn: true,
92
+ email: "",
93
+ adSide: "right"
98
94
  });
95
+ if (config.adSide !== "right" && config.adSide !== "left" && config.adSide !== "default") {
96
+ config.adSide = "right";
97
+ }
99
98
  if (!config.deviceId) {
100
99
  config.deviceId = crypto2.randomUUID();
101
100
  saveConfig(config);
@@ -139,6 +138,240 @@ function log(message) {
139
138
  }
140
139
  }
141
140
 
141
+ // src/browser.ts
142
+ function hasDisplay() {
143
+ if (process.platform === "darwin" || process.platform === "win32") return true;
144
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
145
+ }
146
+ function screenBounds() {
147
+ try {
148
+ if (process.platform === "darwin") {
149
+ const out = execFileSync(
150
+ "osascript",
151
+ ["-e", 'tell application "Finder" to get bounds of window of desktop'],
152
+ { encoding: "utf8", timeout: 3e3 }
153
+ ).trim();
154
+ const [x1, y1, x2, y2] = out.split(",").map((s) => Number(s.trim()));
155
+ if ([x1, y1, x2, y2].every(Number.isFinite)) {
156
+ return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
157
+ }
158
+ }
159
+ if (process.platform === "linux") {
160
+ const out = execFileSync("xrandr", ["--current"], {
161
+ encoding: "utf8",
162
+ timeout: 3e3
163
+ });
164
+ const m = out.match(/(\d+)x(\d+)\s+\d+\.\d+\*/);
165
+ if (m) return { x: 0, y: 0, width: Number(m[1]), height: Number(m[2]) };
166
+ }
167
+ } catch {
168
+ }
169
+ return null;
170
+ }
171
+ function windowRect(side) {
172
+ const screen = screenBounds() ?? { x: 0, y: 0, width: 1440, height: 900 };
173
+ const w = 500;
174
+ const h = Math.min(800, Math.max(560, screen.height - 80));
175
+ const y = screen.y + 40;
176
+ const x = side === "right" ? screen.x + screen.width - w - 16 : screen.x + 16;
177
+ return { x: Math.round(x), y: Math.round(y), w, h };
178
+ }
179
+ function chromeBin() {
180
+ const candidates = process.platform === "darwin" ? [
181
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
182
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
183
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
184
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
185
+ ] : process.platform === "win32" ? [
186
+ path2.join(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe"),
187
+ path2.join(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
188
+ path2.join(
189
+ process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)",
190
+ "Google",
191
+ "Chrome",
192
+ "Application",
193
+ "chrome.exe"
194
+ ),
195
+ path2.join(process.env.LOCALAPPDATA ?? "", "Microsoft", "Edge", "Application", "msedge.exe")
196
+ ] : ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "microsoft-edge"];
197
+ for (const candidate of candidates) {
198
+ if (!candidate) continue;
199
+ if (candidate.includes("/") || candidate.includes("\\")) {
200
+ if (fs2.existsSync(candidate)) return candidate;
201
+ continue;
202
+ }
203
+ try {
204
+ const resolved = execFileSync("which", [candidate], {
205
+ encoding: "utf8",
206
+ timeout: 2e3
207
+ }).trim();
208
+ if (resolved) return resolved;
209
+ } catch {
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ function openDefault(url) {
215
+ const [cmd, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
216
+ try {
217
+ spawn(cmd, args2, { detached: true, stdio: "ignore" }).unref();
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ function openAndPlaceMac(url, rect) {
224
+ if (!openDefault(url)) return false;
225
+ const script = `
226
+ delay 0.9
227
+ tell application "System Events"
228
+ tell (first application process whose frontmost is true)
229
+ try
230
+ set position of front window to {${rect.x}, ${rect.y}}
231
+ set size of front window to {${rect.w}, ${rect.h}}
232
+ end try
233
+ end tell
234
+ end tell
235
+ `;
236
+ try {
237
+ spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
238
+ } catch {
239
+ }
240
+ return true;
241
+ }
242
+ function openInBrowser(url, side = "right") {
243
+ if (!hasDisplay()) return false;
244
+ if (side === "default") return openDefault(url);
245
+ const rect = windowRect(side);
246
+ const chrome = chromeBin();
247
+ if (chrome) {
248
+ const profile = path2.join(promptaiDir(), "ad-window");
249
+ try {
250
+ spawn(
251
+ chrome,
252
+ [
253
+ `--user-data-dir=${profile}`,
254
+ `--app=${url}`,
255
+ `--window-position=${rect.x},${rect.y}`,
256
+ `--window-size=${rect.w},${rect.h}`,
257
+ "--new-window"
258
+ ],
259
+ { detached: true, stdio: "ignore" }
260
+ ).unref();
261
+ return true;
262
+ } catch {
263
+ }
264
+ }
265
+ if (process.platform === "darwin") return openAndPlaceMac(url, rect);
266
+ return openDefault(url);
267
+ }
268
+
269
+ // src/email.ts
270
+ import { execFileSync as execFileSync2 } from "node:child_process";
271
+ import * as fs3 from "node:fs";
272
+ import * as os2 from "node:os";
273
+ import * as path3 from "node:path";
274
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
275
+ function clean(value) {
276
+ if (typeof value !== "string") return null;
277
+ const email = value.trim().toLowerCase();
278
+ return EMAIL_RE.test(email) ? email : null;
279
+ }
280
+ function fromClaudeJson() {
281
+ try {
282
+ const raw = fs3.readFileSync(path3.join(os2.homedir(), ".claude.json"), "utf8");
283
+ const data = JSON.parse(raw);
284
+ return clean(data.oauthAccount?.emailAddress);
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ function cursorStateDbPath() {
290
+ const home = os2.homedir();
291
+ switch (process.platform) {
292
+ case "darwin":
293
+ return path3.join(
294
+ home,
295
+ "Library",
296
+ "Application Support",
297
+ "Cursor",
298
+ "User",
299
+ "globalStorage",
300
+ "state.vscdb"
301
+ );
302
+ case "win32":
303
+ return path3.join(
304
+ process.env.APPDATA ?? path3.join(home, "AppData", "Roaming"),
305
+ "Cursor",
306
+ "User",
307
+ "globalStorage",
308
+ "state.vscdb"
309
+ );
310
+ default:
311
+ return path3.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
312
+ }
313
+ }
314
+ function fromCursorStateDb() {
315
+ const src = cursorStateDbPath();
316
+ if (!fs3.existsSync(src)) return null;
317
+ let dir = null;
318
+ try {
319
+ dir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "promptai-email-"));
320
+ const db = path3.join(dir, "state.vscdb");
321
+ fs3.copyFileSync(src, db);
322
+ for (const suffix of ["-wal", "-shm"]) {
323
+ if (fs3.existsSync(src + suffix)) fs3.copyFileSync(src + suffix, db + suffix);
324
+ }
325
+ const stdout = execFileSync2(
326
+ "sqlite3",
327
+ [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/cachedEmail'"],
328
+ { encoding: "utf8", timeout: 5e3 }
329
+ ).trim();
330
+ if (stdout.startsWith('"')) {
331
+ try {
332
+ return clean(JSON.parse(stdout));
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ return clean(stdout);
338
+ } catch {
339
+ return null;
340
+ } finally {
341
+ if (dir) fs3.rmSync(dir, { recursive: true, force: true });
342
+ }
343
+ }
344
+ function fromGitConfig() {
345
+ try {
346
+ return clean(
347
+ execFileSync2("git", ["config", "--get", "user.email"], {
348
+ encoding: "utf8",
349
+ timeout: 5e3
350
+ })
351
+ );
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+ function detectEmail() {
357
+ const claude = fromClaudeJson();
358
+ if (claude) return { email: claude, source: "claude-account" };
359
+ const cursor = fromCursorStateDb();
360
+ if (cursor) return { email: cursor, source: "cursor-account" };
361
+ const git = fromGitConfig();
362
+ if (git) return { email: git, source: "git-config" };
363
+ return null;
364
+ }
365
+ function resolveEmail(config) {
366
+ if (config.email) return config.email;
367
+ const detected = detectEmail();
368
+ if (!detected) return "";
369
+ config.email = detected.email;
370
+ saveConfig(config);
371
+ log(`auto-detected email ${detected.email} (${detected.source})`);
372
+ return detected.email;
373
+ }
374
+
142
375
  // src/pricing.ts
143
376
  var TABLE = [
144
377
  { match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
@@ -169,9 +402,9 @@ function costUsd(model, inputTokens, outputTokens) {
169
402
  }
170
403
 
171
404
  // src/transcript.ts
172
- import * as fs2 from "node:fs";
405
+ import * as fs4 from "node:fs";
173
406
  function readTranscriptUsage(transcriptPath, watermark) {
174
- const raw = fs2.readFileSync(transcriptPath, "utf8");
407
+ const raw = fs4.readFileSync(transcriptPath, "utf8");
175
408
  const byMessageId = /* @__PURE__ */ new Map();
176
409
  let parsedCount = 0;
177
410
  for (const line of raw.split("\n")) {
@@ -213,7 +446,7 @@ function readTranscriptUsage(transcriptPath, watermark) {
213
446
  };
214
447
  }
215
448
  function readGlassTranscriptUsage(transcriptPath, watermark) {
216
- const raw = fs2.readFileSync(transcriptPath, "utf8");
449
+ const raw = fs4.readFileSync(transcriptPath, "utf8");
217
450
  const lines = raw.split("\n").filter((l) => l.trim());
218
451
  let inputChars = 0;
219
452
  let outputChars = 0;
@@ -248,13 +481,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
248
481
  var AD_MIN_INTERVAL_MS = 9e4;
249
482
  var SETTLE_RETRIES = [400, 800, 1500];
250
483
  function claudeSettingsPath() {
251
- return path2.join(os2.homedir(), ".claude", "settings.json");
484
+ return path4.join(os3.homedir(), ".claude", "settings.json");
252
485
  }
253
486
  function hookCommand() {
254
- let script = path2.resolve(process.argv[1] ?? "");
487
+ let script = path4.resolve(process.argv[1] ?? "");
255
488
  if (script.endsWith(".ts")) {
256
- const dist = path2.resolve(path2.dirname(script), "..", "dist", "index.js");
257
- if (!fs3.existsSync(dist)) {
489
+ const dist = path4.resolve(path4.dirname(script), "..", "dist", "index.js");
490
+ if (!fs5.existsSync(dist)) {
258
491
  throw new Error(
259
492
  `Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
260
493
  );
@@ -265,13 +498,13 @@ function hookCommand() {
265
498
  }
266
499
  function installClaudeHooks() {
267
500
  const settingsPath = claudeSettingsPath();
268
- fs3.mkdirSync(path2.dirname(settingsPath), { recursive: true });
501
+ fs5.mkdirSync(path4.dirname(settingsPath), { recursive: true });
269
502
  let settings = {};
270
- if (fs3.existsSync(settingsPath)) {
503
+ if (fs5.existsSync(settingsPath)) {
271
504
  try {
272
- settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
505
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
273
506
  } catch {
274
- fs3.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
507
+ fs5.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
275
508
  settings = {};
276
509
  }
277
510
  }
@@ -291,17 +524,17 @@ function installClaudeHooks() {
291
524
  changed = true;
292
525
  }
293
526
  }
294
- if (changed || !fs3.existsSync(settingsPath)) {
295
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
527
+ if (changed || !fs5.existsSync(settingsPath)) {
528
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
296
529
  }
297
530
  return { changed, settingsPath };
298
531
  }
299
532
  function uninstallClaudeHooks() {
300
533
  const settingsPath = claudeSettingsPath();
301
- if (!fs3.existsSync(settingsPath)) return { changed: false };
534
+ if (!fs5.existsSync(settingsPath)) return { changed: false };
302
535
  let settings;
303
536
  try {
304
- settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
537
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
305
538
  } catch {
306
539
  return { changed: false };
307
540
  }
@@ -321,7 +554,7 @@ function uninstallClaudeHooks() {
321
554
  }
322
555
  }
323
556
  if (changed) {
324
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
557
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
325
558
  }
326
559
  return { changed };
327
560
  }
@@ -339,7 +572,7 @@ function handleUserPromptSubmit(payload) {
339
572
  }
340
573
  if (config.adsOptIn && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS) {
341
574
  const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
342
- if (openInBrowser(url)) {
575
+ if (openInBrowser(url, config.adSide)) {
343
576
  state.lastAdOpenedAt = Date.now();
344
577
  log(`[claude] opened ad tab for session=${sessionId}`);
345
578
  } else if (!hasDisplay()) {
@@ -376,7 +609,13 @@ async function handleStop(payload) {
376
609
  const promptId = crypto3.randomUUID();
377
610
  let verified = false;
378
611
  if (config.adsOptIn && cost > 0) {
379
- verified = await redeemAgainstAd(config.serverUrl, config.deviceId, promptId, cost);
612
+ verified = await redeemAgainstAd(
613
+ config.serverUrl,
614
+ config.deviceId,
615
+ promptId,
616
+ cost,
617
+ resolveEmail(config)
618
+ );
380
619
  }
381
620
  state.prompts.unshift({
382
621
  id: promptId,
@@ -394,7 +633,7 @@ async function handleStop(payload) {
394
633
  `[claude] settled session=${sessionId} model=${usage.model} in=${usage.inputTokens} out=${usage.outputTokens} cost=$${cost} verified=${verified}`
395
634
  );
396
635
  }
397
- async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
636
+ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
398
637
  try {
399
638
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
400
639
  if (sessions.length === 0) {
@@ -405,7 +644,8 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
405
644
  sessionId: sessions[0].sessionId,
406
645
  deviceId,
407
646
  promptId,
408
- amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD)
647
+ amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
648
+ email: email || void 0
409
649
  });
410
650
  return true;
411
651
  } catch (err) {
@@ -416,9 +656,9 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
416
656
 
417
657
  // src/cursor.ts
418
658
  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";
659
+ import * as fs6 from "node:fs";
660
+ import * as os4 from "node:os";
661
+ import * as path5 from "node:path";
422
662
  var CURSOR_SOURCE = "cursor-agents";
423
663
  var HOOK_MARKER2 = "promptai";
424
664
  var MAX_CREDIT_PER_AD_USD2 = 5;
@@ -428,17 +668,17 @@ function isCursorPayload(payload) {
428
668
  return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
429
669
  }
430
670
  function cursorHooksJsonPath() {
431
- return path3.join(os3.homedir(), ".cursor", "hooks.json");
671
+ return path5.join(os4.homedir(), ".cursor", "hooks.json");
432
672
  }
433
673
  function installCursorHooks() {
434
674
  const hooksPath = cursorHooksJsonPath();
435
- fs4.mkdirSync(path3.dirname(hooksPath), { recursive: true });
675
+ fs6.mkdirSync(path5.dirname(hooksPath), { recursive: true });
436
676
  let config = {};
437
- if (fs4.existsSync(hooksPath)) {
677
+ if (fs6.existsSync(hooksPath)) {
438
678
  try {
439
- config = JSON.parse(fs4.readFileSync(hooksPath, "utf8"));
679
+ config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
440
680
  } catch {
441
- fs4.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
681
+ fs6.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
442
682
  config = {};
443
683
  }
444
684
  }
@@ -460,17 +700,17 @@ function installCursorHooks() {
460
700
  changed = true;
461
701
  }
462
702
  }
463
- if (changed || !fs4.existsSync(hooksPath)) {
464
- fs4.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
703
+ if (changed || !fs6.existsSync(hooksPath)) {
704
+ fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
465
705
  }
466
706
  return { changed, hooksPath };
467
707
  }
468
708
  function uninstallCursorHooks() {
469
709
  const hooksPath = cursorHooksJsonPath();
470
- if (!fs4.existsSync(hooksPath)) return { changed: false };
710
+ if (!fs6.existsSync(hooksPath)) return { changed: false };
471
711
  let config;
472
712
  try {
473
- config = JSON.parse(fs4.readFileSync(hooksPath, "utf8"));
713
+ config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
474
714
  } catch {
475
715
  return { changed: false };
476
716
  }
@@ -487,14 +727,14 @@ function uninstallCursorHooks() {
487
727
  }
488
728
  }
489
729
  if (changed) {
490
- fs4.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
730
+ fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
491
731
  }
492
732
  return { changed };
493
733
  }
494
734
  function extensionIsActive() {
495
- const infoPath = path3.join(os3.homedir(), ".cursor", "promptai-listener.json");
735
+ const infoPath = path5.join(os4.homedir(), ".cursor", "promptai-listener.json");
496
736
  try {
497
- const info = JSON.parse(fs4.readFileSync(infoPath, "utf8"));
737
+ const info = JSON.parse(fs6.readFileSync(infoPath, "utf8"));
498
738
  if (typeof info.pid !== "number") return false;
499
739
  process.kill(info.pid, 0);
500
740
  return true;
@@ -503,18 +743,18 @@ function extensionIsActive() {
503
743
  }
504
744
  }
505
745
  function acquireOnceLock(key) {
506
- const dir = path3.join(promptaiDir(), "locks");
507
- fs4.mkdirSync(dir, { recursive: true });
746
+ const dir = path5.join(promptaiDir(), "locks");
747
+ fs6.mkdirSync(dir, { recursive: true });
508
748
  try {
509
749
  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 });
750
+ for (const name of fs6.readdirSync(dir)) {
751
+ const p = path5.join(dir, name);
752
+ if (fs6.statSync(p).mtimeMs < cutoff) fs6.rmSync(p, { recursive: true, force: true });
513
753
  }
514
754
  } catch {
515
755
  }
516
756
  try {
517
- fs4.mkdirSync(path3.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
757
+ fs6.mkdirSync(path5.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
518
758
  return true;
519
759
  } catch {
520
760
  return false;
@@ -539,7 +779,7 @@ function handleCursorPromptSubmitted(payload) {
539
779
  const remote = process.env.CURSOR_CODE_REMOTE === "true";
540
780
  if (config.adsOptIn && !extensionIsActive() && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS2) {
541
781
  const url = watchUrl(config.serverUrl, config.deviceId, CURSOR_SOURCE);
542
- if (!remote && openInBrowser(url)) {
782
+ if (!remote && openInBrowser(url, config.adSide)) {
543
783
  state.lastAdOpenedAt = Date.now();
544
784
  log(`[cursor] opened ad tab for conv=${convId}`);
545
785
  } else if (remote || !hasDisplay()) {
@@ -607,7 +847,13 @@ async function handleCursorStop(payload) {
607
847
  const promptId = crypto4.randomUUID();
608
848
  let verified = false;
609
849
  if (config.adsOptIn && cost > 0) {
610
- verified = await redeemAgainstAd2(config.serverUrl, config.deviceId, promptId, cost);
850
+ verified = await redeemAgainstAd2(
851
+ config.serverUrl,
852
+ config.deviceId,
853
+ promptId,
854
+ cost,
855
+ resolveEmail(config)
856
+ );
611
857
  }
612
858
  state.prompts.unshift({
613
859
  id: promptId,
@@ -626,7 +872,7 @@ async function handleCursorStop(payload) {
626
872
  `[cursor] settled conv=${convId} model=${model} in=${inputTokens} out=${outputTokens} cost=$${cost}${estimated ? " (estimated)" : ""} verified=${verified}`
627
873
  );
628
874
  }
629
- async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost) {
875
+ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
630
876
  try {
631
877
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
632
878
  if (sessions.length === 0) {
@@ -637,7 +883,8 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost) {
637
883
  sessionId: sessions[0].sessionId,
638
884
  deviceId,
639
885
  promptId,
640
- amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2)
886
+ amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
887
+ email: email || void 0
641
888
  });
642
889
  return true;
643
890
  } catch (err) {
@@ -646,6 +893,112 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost) {
646
893
  }
647
894
  }
648
895
 
896
+ // src/setup.ts
897
+ import * as readline from "node:readline";
898
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
899
+ var WALLET_RE = /^0x[0-9a-fA-F]{40}$/;
900
+ function promptLine(question) {
901
+ return new Promise((resolve2) => {
902
+ process.stdout.write(question);
903
+ const rl = readline.createInterface({ input: process.stdin });
904
+ rl.once("line", (line) => {
905
+ rl.close();
906
+ resolve2(line);
907
+ });
908
+ });
909
+ }
910
+ function printLinkStatus(claimActivated) {
911
+ console.log(
912
+ claimActivated ? "Email linked. Account is activated for USDC claims." : "Email linked. USDC claims stay blocked until an admin activates this account."
913
+ );
914
+ }
915
+ async function linkEmailInteractive(config, email, source) {
916
+ try {
917
+ const result = await linkEmail(config.serverUrl, {
918
+ deviceId: config.deviceId,
919
+ email,
920
+ source
921
+ });
922
+ if (result.otpRequired) {
923
+ console.log(result.message ?? `Verification code sent to ${email}.`);
924
+ const code = (await promptLine("Enter the 6-digit code: ")).trim();
925
+ if (!/^\d{6}$/.test(code)) {
926
+ console.error("That doesn't look like a 6-digit code. Run: promptai set email " + email);
927
+ return false;
928
+ }
929
+ const verified = await verifyEmailLink(config.serverUrl, {
930
+ deviceId: config.deviceId,
931
+ email,
932
+ code
933
+ });
934
+ printLinkStatus(verified.claimActivated);
935
+ } else {
936
+ printLinkStatus(Boolean(result.claimActivated));
937
+ }
938
+ return true;
939
+ } catch (err) {
940
+ console.error(`Email not linked: ${err instanceof Error ? err.message : String(err)}`);
941
+ return false;
942
+ }
943
+ }
944
+ function parseYes(raw, defaultYes) {
945
+ const v = raw.trim().toLowerCase();
946
+ if (!v) return defaultYes;
947
+ return v === "y" || v === "yes";
948
+ }
949
+ function parseSide(raw) {
950
+ const v = raw.trim().toLowerCase();
951
+ if (v === "left" || v === "l") return "left";
952
+ if (v === "default" || v === "browser" || v === "n" || v === "no") return "default";
953
+ return "right";
954
+ }
955
+ async function onboard(config) {
956
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
957
+ if (!config.adSide) config.adSide = "right";
958
+ saveConfig(config);
959
+ return;
960
+ }
961
+ console.log("");
962
+ console.log("Let's roll you in.");
963
+ console.log("");
964
+ const detected = detectEmail();
965
+ const emailHint = config.email || detected?.email || "";
966
+ const emailRaw = (await promptLine(emailHint ? `Email [${emailHint}]: ` : "Email: ")).trim().toLowerCase();
967
+ const email = emailRaw || emailHint;
968
+ if (email && EMAIL_RE2.test(email)) {
969
+ const source = detected?.email === email ? "auto" : "manual";
970
+ config.email = email;
971
+ const linked = await linkEmailInteractive(config, email, source);
972
+ if (!linked) {
973
+ saveConfig(config);
974
+ }
975
+ } else if (email) {
976
+ console.log("That doesn't look like an email. Set it later: promptai set email you@example.com");
977
+ } else {
978
+ console.log("Skipped email. Set it later: promptai set email you@example.com");
979
+ }
980
+ const walletHint = config.wallet || "";
981
+ const walletRaw = (await promptLine(walletHint ? `Wallet [${walletHint}]: ` : "Wallet (0x\u2026, optional): ")).trim();
982
+ const wallet = walletRaw || walletHint;
983
+ if (wallet) {
984
+ if (WALLET_RE.test(wallet)) {
985
+ config.wallet = wallet;
986
+ console.log("Wallet saved.");
987
+ } else {
988
+ console.log("That doesn't look like a 0x address. Set it later: promptai set wallet 0x...");
989
+ }
990
+ } else {
991
+ console.log("Skipped wallet. Set it later: promptai set wallet 0x...");
992
+ }
993
+ const sideRaw = await promptLine("Show ads on the right while your agent works? [Y/n]: ");
994
+ config.adSide = parseYes(sideRaw, true) ? "right" : parseSide(sideRaw);
995
+ console.log(
996
+ config.adSide === "right" ? "Ads will open on the right." : config.adSide === "left" ? "Ads will open on the left." : "Ads will open in your default browser."
997
+ );
998
+ saveConfig(config);
999
+ console.log("");
1000
+ }
1001
+
649
1002
  // src/index.ts
650
1003
  var HELP = `promptai - ad-subsidized prompt credits for terminal agents
651
1004
 
@@ -657,8 +1010,10 @@ Usage:
657
1010
  promptai watch Open a rewarded ad in the browser now
658
1011
  promptai claim [address] Claim your verified balance as USDC (Base)
659
1012
  promptai set wallet 0x... Set the payout wallet
1013
+ promptai set email <email> Link your email (required for USDC claims; groups devices)
660
1014
  promptai set server <url> Point at a different API server
661
1015
  promptai set ads on|off Toggle the rewarded-ads opt-in
1016
+ promptai set side right|left|default Where the ad window docks (default: right)
662
1017
  promptai hook (internal) invoked by agent hooks, JSON on stdin
663
1018
  `;
664
1019
  function readStdin() {
@@ -677,11 +1032,11 @@ async function cmdHook() {
677
1032
  return;
678
1033
  }
679
1034
  const event = String(payload.hook_event_name ?? "");
680
- if (process.env.PROMPTAI_DUMP === "1" || fs5.existsSync(path4.join(promptaiDir(), "debug"))) {
1035
+ if (process.env.PROMPTAI_DUMP === "1" || fs7.existsSync(path6.join(promptaiDir(), "debug"))) {
681
1036
  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);
1037
+ const dir = path6.join(promptaiDir(), "dump");
1038
+ fs7.mkdirSync(dir, { recursive: true });
1039
+ fs7.writeFileSync(path6.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
685
1040
  } catch {
686
1041
  }
687
1042
  }
@@ -705,10 +1060,13 @@ async function cmdHook() {
705
1060
  async function cmdStatus() {
706
1061
  const config = loadConfig();
707
1062
  const state = loadState();
1063
+ const email = resolveEmail(config);
708
1064
  console.log(`device ${config.deviceId}`);
709
1065
  console.log(`server ${config.serverUrl}`);
710
1066
  console.log(`wallet ${config.wallet || "(not set - promptai set wallet 0x...)"}`);
1067
+ console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
711
1068
  console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
1069
+ console.log(`side ${config.adSide}`);
712
1070
  console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
713
1071
  try {
714
1072
  const [balance, verified] = await Promise.all([
@@ -718,6 +1076,12 @@ async function cmdStatus() {
718
1076
  console.log(
719
1077
  `balance $${balance.balanceUsd.toFixed(4)} (earned $${balance.earnedUsd.toFixed(4)}, claimed $${balance.claimedUsd.toFixed(4)})`
720
1078
  );
1079
+ console.log(
1080
+ `monthly device $${balance.monthEarnedDeviceUsd.toFixed(4)} / $${balance.deviceCapUsd}` + (balance.email ? ` \xB7 email $${balance.monthEarnedEmailUsd.toFixed(4)} / $${balance.emailCapUsd}` : "")
1081
+ );
1082
+ console.log(
1083
+ `claims ${!balance.email ? "blocked (link an email first)" : balance.claimActivated ? "activated" : "blocked (waiting for admin activation)"}`
1084
+ );
721
1085
  console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
722
1086
  } catch (err) {
723
1087
  console.log(`balance unavailable (${String(err)})`);
@@ -736,7 +1100,7 @@ async function cmdStatus() {
736
1100
  async function cmdWatch() {
737
1101
  const config = loadConfig();
738
1102
  const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
739
- if (openInBrowser(url)) {
1103
+ if (openInBrowser(url, config.adSide)) {
740
1104
  console.log(`Opened ${url}`);
741
1105
  console.log("Watch the full ad; the credit banks automatically once the server verifies it.");
742
1106
  } else if (!hasDisplay()) {
@@ -754,6 +1118,13 @@ async function cmdClaim(addressArg) {
754
1118
  process.exitCode = 1;
755
1119
  return;
756
1120
  }
1121
+ const email = resolveEmail(config);
1122
+ if (email) {
1123
+ try {
1124
+ await linkEmail(config.serverUrl, { deviceId: config.deviceId, email, source: "auto" });
1125
+ } catch {
1126
+ }
1127
+ }
757
1128
  const balance = await fetchBalance(config.serverUrl, config.deviceId);
758
1129
  if (balance.balanceUsd < 1e-6) {
759
1130
  console.log("Nothing to claim yet - watch an ad (promptai watch) and run some agent prompts first.");
@@ -768,7 +1139,7 @@ async function cmdClaim(addressArg) {
768
1139
  console.log(`Sent: ${result.txHash}`);
769
1140
  console.log(result.explorerUrl);
770
1141
  }
771
- function cmdSet(key, value) {
1142
+ async function cmdSet(key, value) {
772
1143
  const config = loadConfig();
773
1144
  if (key === "wallet" && value && /^0x[0-9a-fA-F]{40}$/.test(value)) {
774
1145
  config.wallet = value;
@@ -776,21 +1147,40 @@ function cmdSet(key, value) {
776
1147
  config.serverUrl = value.replace(/\/$/, "");
777
1148
  } else if (key === "ads" && (value === "on" || value === "off")) {
778
1149
  config.adsOptIn = value === "on";
1150
+ } else if (key === "side" && (value === "right" || value === "left" || value === "default")) {
1151
+ config.adSide = value;
1152
+ } else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
1153
+ config.email = value.trim().toLowerCase();
779
1154
  } else {
780
- console.error("Usage: promptai set wallet 0x... | set server <url> | set ads on|off");
1155
+ console.error(
1156
+ "Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set side right|left|default | set email you@example.com"
1157
+ );
781
1158
  process.exitCode = 1;
782
1159
  return;
783
1160
  }
1161
+ if (key === "email") {
1162
+ const email = config.email;
1163
+ const ok = await linkEmailInteractive(config, email, "manual");
1164
+ if (!ok) {
1165
+ process.exitCode = 1;
1166
+ return;
1167
+ }
1168
+ saveConfig(config);
1169
+ console.log("email updated.");
1170
+ return;
1171
+ }
784
1172
  saveConfig(config);
785
1173
  console.log(`${key} updated.`);
786
1174
  }
787
- function cmdInstall(agent) {
1175
+ async function cmdInstall(agent) {
788
1176
  if (agent === "claude" || agent === "claude-code") {
789
1177
  const { changed, settingsPath } = installClaudeHooks();
790
1178
  console.log(
791
1179
  changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
792
1180
  );
793
- console.log("Claude Code picks them up on its next session. Try: promptai watch, then run a prompt.");
1181
+ console.log("Claude Code picks them up on its next session.");
1182
+ await onboard(loadConfig());
1183
+ console.log("Try: promptai watch, then run a prompt.");
794
1184
  return;
795
1185
  }
796
1186
  if (agent === "cursor" || agent === "cursor-agents") {
@@ -801,6 +1191,8 @@ function cmdInstall(agent) {
801
1191
  console.log(
802
1192
  "Works in the Agents Window and the classic IDE (the CLI stands down when the promptai extension is running). Cursor hot-reloads hooks.json."
803
1193
  );
1194
+ await onboard(loadConfig());
1195
+ console.log("Try: promptai watch, then run a prompt.");
804
1196
  return;
805
1197
  }
806
1198
  console.error("Supported agents: claude, cursor (Codex CLI coming next). Usage: promptai install <agent>");
@@ -827,7 +1219,7 @@ try {
827
1219
  await cmdHook();
828
1220
  break;
829
1221
  case "install":
830
- cmdInstall(args[0]);
1222
+ await cmdInstall(args[0]);
831
1223
  break;
832
1224
  case "uninstall":
833
1225
  cmdUninstall(args[0]);
@@ -842,7 +1234,7 @@ try {
842
1234
  await cmdClaim(args[0]);
843
1235
  break;
844
1236
  case "set":
845
- cmdSet(args[0], args[1]);
1237
+ await cmdSet(args[0], args[1]);
846
1238
  break;
847
1239
  default:
848
1240
  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.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },