@promptai.credit/cli 0.3.0 → 0.4.1

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 -8
  2. package/dist/index.js +322 -127
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,14 +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. Ads dock on the **right** by default. 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 email you@x.com # override the account email (required for claims)
55
- promptai set wallet 0x... # payout address
56
57
  promptai claim # USDC on Base
57
- promptai set ads off # opt out any time
58
+ promptai off # toggle ads off (`promptai on` to turn them back on)
59
+ promptai set side left # or `default` for a normal browser tab
58
60
  ```
59
61
 
60
62
  The default server is `https://api.promptai.credit`; point elsewhere with
@@ -71,10 +73,12 @@ Developing from the repo instead: `pnpm --filter @promptai.credit/cli build`, th
71
73
  - Monthly earn caps: $75/device and $150 cumulative per linked email (UTC
72
74
  calendar month). Claims require a linked email that an admin has activated.
73
75
  - 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.
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.
80
+ - Ads dock on the right by default. Toggle them anytime with `promptai on` /
81
+ `promptai off`.
78
82
  - Headless environments (CI, ssh without a display) never get browser tabs and
79
83
  simply skip crediting.
80
84
  - Uninstall with `promptai uninstall claude`; other hooks in your settings are
package/dist/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as fs6 from "node:fs";
5
- import * as path5 from "node:path";
6
- import * as readline from "node:readline";
4
+ import * as fs7 from "node:fs";
5
+ import * as path6 from "node:path";
7
6
 
8
7
  // src/claude.ts
9
8
  import * as crypto3 from "node:crypto";
10
- import * as fs4 from "node:fs";
9
+ import * as fs5 from "node:fs";
11
10
  import * as os3 from "node:os";
12
- import * as path3 from "node:path";
11
+ import * as path4 from "node:path";
13
12
 
14
13
  // src/api.ts
15
14
  import * as crypto from "node:crypto";
@@ -58,21 +57,9 @@ function claim(serverUrl, params) {
58
57
  }
59
58
 
60
59
  // src/browser.ts
61
- import { spawn } from "node:child_process";
62
- function hasDisplay() {
63
- if (process.platform === "darwin" || process.platform === "win32") return true;
64
- return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
65
- }
66
- function openInBrowser(url) {
67
- if (!hasDisplay()) return false;
68
- const [cmd, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
69
- try {
70
- spawn(cmd, args2, { detached: true, stdio: "ignore" }).unref();
71
- return true;
72
- } catch {
73
- return false;
74
- }
75
- }
60
+ import { execFileSync, spawn } from "node:child_process";
61
+ import * as fs2 from "node:fs";
62
+ import * as path2 from "node:path";
76
63
 
77
64
  // src/config.ts
78
65
  import * as crypto2 from "node:crypto";
@@ -102,8 +89,12 @@ function loadConfig() {
102
89
  serverUrl: "https://api.promptai.credit",
103
90
  wallet: "",
104
91
  adsOptIn: true,
105
- email: ""
92
+ email: "",
93
+ adSide: "right"
106
94
  });
95
+ if (config.adSide !== "right" && config.adSide !== "left" && config.adSide !== "default") {
96
+ config.adSide = "right";
97
+ }
107
98
  if (!config.deviceId) {
108
99
  config.deviceId = crypto2.randomUUID();
109
100
  saveConfig(config);
@@ -147,11 +138,139 @@ function log(message) {
147
138
  }
148
139
  }
149
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
+
150
269
  // src/email.ts
151
- import { execFileSync } from "node:child_process";
152
- import * as fs2 from "node:fs";
270
+ import { execFileSync as execFileSync2 } from "node:child_process";
271
+ import * as fs3 from "node:fs";
153
272
  import * as os2 from "node:os";
154
- import * as path2 from "node:path";
273
+ import * as path3 from "node:path";
155
274
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
156
275
  function clean(value) {
157
276
  if (typeof value !== "string") return null;
@@ -160,7 +279,7 @@ function clean(value) {
160
279
  }
161
280
  function fromClaudeJson() {
162
281
  try {
163
- const raw = fs2.readFileSync(path2.join(os2.homedir(), ".claude.json"), "utf8");
282
+ const raw = fs3.readFileSync(path3.join(os2.homedir(), ".claude.json"), "utf8");
164
283
  const data = JSON.parse(raw);
165
284
  return clean(data.oauthAccount?.emailAddress);
166
285
  } catch {
@@ -171,7 +290,7 @@ function cursorStateDbPath() {
171
290
  const home = os2.homedir();
172
291
  switch (process.platform) {
173
292
  case "darwin":
174
- return path2.join(
293
+ return path3.join(
175
294
  home,
176
295
  "Library",
177
296
  "Application Support",
@@ -181,29 +300,29 @@ function cursorStateDbPath() {
181
300
  "state.vscdb"
182
301
  );
183
302
  case "win32":
184
- return path2.join(
185
- process.env.APPDATA ?? path2.join(home, "AppData", "Roaming"),
303
+ return path3.join(
304
+ process.env.APPDATA ?? path3.join(home, "AppData", "Roaming"),
186
305
  "Cursor",
187
306
  "User",
188
307
  "globalStorage",
189
308
  "state.vscdb"
190
309
  );
191
310
  default:
192
- return path2.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
311
+ return path3.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
193
312
  }
194
313
  }
195
314
  function fromCursorStateDb() {
196
315
  const src = cursorStateDbPath();
197
- if (!fs2.existsSync(src)) return null;
316
+ if (!fs3.existsSync(src)) return null;
198
317
  let dir = null;
199
318
  try {
200
- dir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "promptai-email-"));
201
- const db = path2.join(dir, "state.vscdb");
202
- fs2.copyFileSync(src, db);
319
+ dir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "promptai-email-"));
320
+ const db = path3.join(dir, "state.vscdb");
321
+ fs3.copyFileSync(src, db);
203
322
  for (const suffix of ["-wal", "-shm"]) {
204
- if (fs2.existsSync(src + suffix)) fs2.copyFileSync(src + suffix, db + suffix);
323
+ if (fs3.existsSync(src + suffix)) fs3.copyFileSync(src + suffix, db + suffix);
205
324
  }
206
- const stdout = execFileSync(
325
+ const stdout = execFileSync2(
207
326
  "sqlite3",
208
327
  [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/cachedEmail'"],
209
328
  { encoding: "utf8", timeout: 5e3 }
@@ -219,13 +338,13 @@ function fromCursorStateDb() {
219
338
  } catch {
220
339
  return null;
221
340
  } finally {
222
- if (dir) fs2.rmSync(dir, { recursive: true, force: true });
341
+ if (dir) fs3.rmSync(dir, { recursive: true, force: true });
223
342
  }
224
343
  }
225
344
  function fromGitConfig() {
226
345
  try {
227
346
  return clean(
228
- execFileSync("git", ["config", "--get", "user.email"], {
347
+ execFileSync2("git", ["config", "--get", "user.email"], {
229
348
  encoding: "utf8",
230
349
  timeout: 5e3
231
350
  })
@@ -283,9 +402,9 @@ function costUsd(model, inputTokens, outputTokens) {
283
402
  }
284
403
 
285
404
  // src/transcript.ts
286
- import * as fs3 from "node:fs";
405
+ import * as fs4 from "node:fs";
287
406
  function readTranscriptUsage(transcriptPath, watermark) {
288
- const raw = fs3.readFileSync(transcriptPath, "utf8");
407
+ const raw = fs4.readFileSync(transcriptPath, "utf8");
289
408
  const byMessageId = /* @__PURE__ */ new Map();
290
409
  let parsedCount = 0;
291
410
  for (const line of raw.split("\n")) {
@@ -327,7 +446,7 @@ function readTranscriptUsage(transcriptPath, watermark) {
327
446
  };
328
447
  }
329
448
  function readGlassTranscriptUsage(transcriptPath, watermark) {
330
- const raw = fs3.readFileSync(transcriptPath, "utf8");
449
+ const raw = fs4.readFileSync(transcriptPath, "utf8");
331
450
  const lines = raw.split("\n").filter((l) => l.trim());
332
451
  let inputChars = 0;
333
452
  let outputChars = 0;
@@ -362,13 +481,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
362
481
  var AD_MIN_INTERVAL_MS = 9e4;
363
482
  var SETTLE_RETRIES = [400, 800, 1500];
364
483
  function claudeSettingsPath() {
365
- return path3.join(os3.homedir(), ".claude", "settings.json");
484
+ return path4.join(os3.homedir(), ".claude", "settings.json");
366
485
  }
367
486
  function hookCommand() {
368
- let script = path3.resolve(process.argv[1] ?? "");
487
+ let script = path4.resolve(process.argv[1] ?? "");
369
488
  if (script.endsWith(".ts")) {
370
- const dist = path3.resolve(path3.dirname(script), "..", "dist", "index.js");
371
- if (!fs4.existsSync(dist)) {
489
+ const dist = path4.resolve(path4.dirname(script), "..", "dist", "index.js");
490
+ if (!fs5.existsSync(dist)) {
372
491
  throw new Error(
373
492
  `Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
374
493
  );
@@ -379,13 +498,13 @@ function hookCommand() {
379
498
  }
380
499
  function installClaudeHooks() {
381
500
  const settingsPath = claudeSettingsPath();
382
- fs4.mkdirSync(path3.dirname(settingsPath), { recursive: true });
501
+ fs5.mkdirSync(path4.dirname(settingsPath), { recursive: true });
383
502
  let settings = {};
384
- if (fs4.existsSync(settingsPath)) {
503
+ if (fs5.existsSync(settingsPath)) {
385
504
  try {
386
- settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
505
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
387
506
  } catch {
388
- fs4.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
507
+ fs5.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
389
508
  settings = {};
390
509
  }
391
510
  }
@@ -405,17 +524,17 @@ function installClaudeHooks() {
405
524
  changed = true;
406
525
  }
407
526
  }
408
- if (changed || !fs4.existsSync(settingsPath)) {
409
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
527
+ if (changed || !fs5.existsSync(settingsPath)) {
528
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
410
529
  }
411
530
  return { changed, settingsPath };
412
531
  }
413
532
  function uninstallClaudeHooks() {
414
533
  const settingsPath = claudeSettingsPath();
415
- if (!fs4.existsSync(settingsPath)) return { changed: false };
534
+ if (!fs5.existsSync(settingsPath)) return { changed: false };
416
535
  let settings;
417
536
  try {
418
- settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
537
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
419
538
  } catch {
420
539
  return { changed: false };
421
540
  }
@@ -435,7 +554,7 @@ function uninstallClaudeHooks() {
435
554
  }
436
555
  }
437
556
  if (changed) {
438
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
557
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
439
558
  }
440
559
  return { changed };
441
560
  }
@@ -453,7 +572,7 @@ function handleUserPromptSubmit(payload) {
453
572
  }
454
573
  if (config.adsOptIn && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS) {
455
574
  const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
456
- if (openInBrowser(url)) {
575
+ if (openInBrowser(url, config.adSide)) {
457
576
  state.lastAdOpenedAt = Date.now();
458
577
  log(`[claude] opened ad tab for session=${sessionId}`);
459
578
  } else if (!hasDisplay()) {
@@ -537,9 +656,9 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
537
656
 
538
657
  // src/cursor.ts
539
658
  import * as crypto4 from "node:crypto";
540
- import * as fs5 from "node:fs";
659
+ import * as fs6 from "node:fs";
541
660
  import * as os4 from "node:os";
542
- import * as path4 from "node:path";
661
+ import * as path5 from "node:path";
543
662
  var CURSOR_SOURCE = "cursor-agents";
544
663
  var HOOK_MARKER2 = "promptai";
545
664
  var MAX_CREDIT_PER_AD_USD2 = 5;
@@ -549,17 +668,17 @@ function isCursorPayload(payload) {
549
668
  return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
550
669
  }
551
670
  function cursorHooksJsonPath() {
552
- return path4.join(os4.homedir(), ".cursor", "hooks.json");
671
+ return path5.join(os4.homedir(), ".cursor", "hooks.json");
553
672
  }
554
673
  function installCursorHooks() {
555
674
  const hooksPath = cursorHooksJsonPath();
556
- fs5.mkdirSync(path4.dirname(hooksPath), { recursive: true });
675
+ fs6.mkdirSync(path5.dirname(hooksPath), { recursive: true });
557
676
  let config = {};
558
- if (fs5.existsSync(hooksPath)) {
677
+ if (fs6.existsSync(hooksPath)) {
559
678
  try {
560
- config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
679
+ config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
561
680
  } catch {
562
- fs5.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
681
+ fs6.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
563
682
  config = {};
564
683
  }
565
684
  }
@@ -581,17 +700,17 @@ function installCursorHooks() {
581
700
  changed = true;
582
701
  }
583
702
  }
584
- if (changed || !fs5.existsSync(hooksPath)) {
585
- fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
703
+ if (changed || !fs6.existsSync(hooksPath)) {
704
+ fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
586
705
  }
587
706
  return { changed, hooksPath };
588
707
  }
589
708
  function uninstallCursorHooks() {
590
709
  const hooksPath = cursorHooksJsonPath();
591
- if (!fs5.existsSync(hooksPath)) return { changed: false };
710
+ if (!fs6.existsSync(hooksPath)) return { changed: false };
592
711
  let config;
593
712
  try {
594
- config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
713
+ config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
595
714
  } catch {
596
715
  return { changed: false };
597
716
  }
@@ -608,14 +727,14 @@ function uninstallCursorHooks() {
608
727
  }
609
728
  }
610
729
  if (changed) {
611
- fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
730
+ fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
612
731
  }
613
732
  return { changed };
614
733
  }
615
734
  function extensionIsActive() {
616
- const infoPath = path4.join(os4.homedir(), ".cursor", "promptai-listener.json");
735
+ const infoPath = path5.join(os4.homedir(), ".cursor", "promptai-listener.json");
617
736
  try {
618
- const info = JSON.parse(fs5.readFileSync(infoPath, "utf8"));
737
+ const info = JSON.parse(fs6.readFileSync(infoPath, "utf8"));
619
738
  if (typeof info.pid !== "number") return false;
620
739
  process.kill(info.pid, 0);
621
740
  return true;
@@ -624,18 +743,18 @@ function extensionIsActive() {
624
743
  }
625
744
  }
626
745
  function acquireOnceLock(key) {
627
- const dir = path4.join(promptaiDir(), "locks");
628
- fs5.mkdirSync(dir, { recursive: true });
746
+ const dir = path5.join(promptaiDir(), "locks");
747
+ fs6.mkdirSync(dir, { recursive: true });
629
748
  try {
630
749
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
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 });
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 });
634
753
  }
635
754
  } catch {
636
755
  }
637
756
  try {
638
- fs5.mkdirSync(path4.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
757
+ fs6.mkdirSync(path5.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
639
758
  return true;
640
759
  } catch {
641
760
  return false;
@@ -660,7 +779,7 @@ function handleCursorPromptSubmitted(payload) {
660
779
  const remote = process.env.CURSOR_CODE_REMOTE === "true";
661
780
  if (config.adsOptIn && !extensionIsActive() && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS2) {
662
781
  const url = watchUrl(config.serverUrl, config.deviceId, CURSOR_SOURCE);
663
- if (!remote && openInBrowser(url)) {
782
+ if (!remote && openInBrowser(url, config.adSide)) {
664
783
  state.lastAdOpenedAt = Date.now();
665
784
  log(`[cursor] opened ad tab for conv=${convId}`);
666
785
  } else if (remote || !hasDisplay()) {
@@ -774,6 +893,97 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
774
893
  }
775
894
  }
776
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
+ async function onboard(config) {
945
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
946
+ if (!config.adSide) config.adSide = "right";
947
+ saveConfig(config);
948
+ return;
949
+ }
950
+ console.log("");
951
+ console.log("Let's roll you in.");
952
+ console.log("");
953
+ const detected = detectEmail();
954
+ const emailHint = config.email || detected?.email || "";
955
+ const emailRaw = (await promptLine(emailHint ? `Email [${emailHint}]: ` : "Email: ")).trim().toLowerCase();
956
+ const email = emailRaw || emailHint;
957
+ if (email && EMAIL_RE2.test(email)) {
958
+ const source = detected?.email === email ? "auto" : "manual";
959
+ config.email = email;
960
+ const linked = await linkEmailInteractive(config, email, source);
961
+ if (!linked) {
962
+ saveConfig(config);
963
+ }
964
+ } else if (email) {
965
+ console.log("That doesn't look like an email. Set it later: promptai set email you@example.com");
966
+ } else {
967
+ console.log("Skipped email. Set it later: promptai set email you@example.com");
968
+ }
969
+ const walletHint = config.wallet || "";
970
+ const walletRaw = (await promptLine(walletHint ? `Wallet [${walletHint}]: ` : "Wallet (0x\u2026, optional): ")).trim();
971
+ const wallet = walletRaw || walletHint;
972
+ if (wallet) {
973
+ if (WALLET_RE.test(wallet)) {
974
+ config.wallet = wallet;
975
+ console.log("Wallet saved.");
976
+ } else {
977
+ console.log("That doesn't look like a 0x address. Set it later: promptai set wallet 0x...");
978
+ }
979
+ } else {
980
+ console.log("Skipped wallet. Set it later: promptai set wallet 0x...");
981
+ }
982
+ config.adSide = config.adSide || "right";
983
+ saveConfig(config);
984
+ console.log("");
985
+ }
986
+
777
987
  // src/index.ts
778
988
  var HELP = `promptai - ad-subsidized prompt credits for terminal agents
779
989
 
@@ -787,7 +997,8 @@ Usage:
787
997
  promptai set wallet 0x... Set the payout wallet
788
998
  promptai set email <email> Link your email (required for USDC claims; groups devices)
789
999
  promptai set server <url> Point at a different API server
790
- promptai set ads on|off Toggle the rewarded-ads opt-in
1000
+ promptai on | off Toggle rewarded ads
1001
+ promptai set side right|left|default Where the ad window docks (default: right)
791
1002
  promptai hook (internal) invoked by agent hooks, JSON on stdin
792
1003
  `;
793
1004
  function readStdin() {
@@ -806,11 +1017,11 @@ async function cmdHook() {
806
1017
  return;
807
1018
  }
808
1019
  const event = String(payload.hook_event_name ?? "");
809
- if (process.env.PROMPTAI_DUMP === "1" || fs6.existsSync(path5.join(promptaiDir(), "debug"))) {
1020
+ if (process.env.PROMPTAI_DUMP === "1" || fs7.existsSync(path6.join(promptaiDir(), "debug"))) {
810
1021
  try {
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);
1022
+ const dir = path6.join(promptaiDir(), "dump");
1023
+ fs7.mkdirSync(dir, { recursive: true });
1024
+ fs7.writeFileSync(path6.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
814
1025
  } catch {
815
1026
  }
816
1027
  }
@@ -840,6 +1051,7 @@ async function cmdStatus() {
840
1051
  console.log(`wallet ${config.wallet || "(not set - promptai set wallet 0x...)"}`);
841
1052
  console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
842
1053
  console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
1054
+ console.log(`side ${config.adSide}`);
843
1055
  console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
844
1056
  try {
845
1057
  const [balance, verified] = await Promise.all([
@@ -873,7 +1085,7 @@ async function cmdStatus() {
873
1085
  async function cmdWatch() {
874
1086
  const config = loadConfig();
875
1087
  const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
876
- if (openInBrowser(url)) {
1088
+ if (openInBrowser(url, config.adSide)) {
877
1089
  console.log(`Opened ${url}`);
878
1090
  console.log("Watch the full ad; the credit banks automatically once the server verifies it.");
879
1091
  } else if (!hasDisplay()) {
@@ -918,75 +1130,50 @@ async function cmdSet(key, value) {
918
1130
  config.wallet = value;
919
1131
  } else if (key === "server" && value && /^https?:\/\//.test(value)) {
920
1132
  config.serverUrl = value.replace(/\/$/, "");
921
- } else if (key === "ads" && (value === "on" || value === "off")) {
922
- config.adsOptIn = value === "on";
1133
+ } else if (key === "side" && (value === "right" || value === "left" || value === "default")) {
1134
+ config.adSide = value;
923
1135
  } else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
924
1136
  config.email = value.trim().toLowerCase();
925
1137
  } else {
926
1138
  console.error(
927
- "Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set email you@example.com"
1139
+ "Usage: promptai set wallet 0x... | set server <url> | set side right|left|default | set email you@example.com"
928
1140
  );
929
1141
  process.exitCode = 1;
930
1142
  return;
931
1143
  }
932
1144
  if (key === "email") {
933
1145
  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)}`);
1146
+ const ok = await linkEmailInteractive(config, email, "manual");
1147
+ if (!ok) {
961
1148
  process.exitCode = 1;
1149
+ return;
962
1150
  }
1151
+ saveConfig(config);
1152
+ console.log("email updated.");
963
1153
  return;
964
1154
  }
965
1155
  saveConfig(config);
966
1156
  console.log(`${key} updated.`);
967
1157
  }
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
- );
1158
+ function cmdAds(on) {
1159
+ const config = loadConfig();
1160
+ config.adsOptIn = on;
1161
+ saveConfig(config);
1162
+ console.log(on ? "Ads on." : "Ads off.");
972
1163
  }
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
- });
1164
+ function printInstallNext() {
1165
+ console.log("You can toggle ads with promptai on / promptai off.");
1166
+ console.log("Try: promptai watch, then run a prompt.");
982
1167
  }
983
- function cmdInstall(agent) {
1168
+ async function cmdInstall(agent) {
984
1169
  if (agent === "claude" || agent === "claude-code") {
985
1170
  const { changed, settingsPath } = installClaudeHooks();
986
1171
  console.log(
987
1172
  changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
988
1173
  );
989
- console.log("Claude Code picks them up on its next session. Try: promptai watch, then run a prompt.");
1174
+ console.log("Claude Code picks them up on its next session.");
1175
+ await onboard(loadConfig());
1176
+ printInstallNext();
990
1177
  return;
991
1178
  }
992
1179
  if (agent === "cursor" || agent === "cursor-agents") {
@@ -997,6 +1184,8 @@ function cmdInstall(agent) {
997
1184
  console.log(
998
1185
  "Works in the Agents Window and the classic IDE (the CLI stands down when the promptai extension is running). Cursor hot-reloads hooks.json."
999
1186
  );
1187
+ await onboard(loadConfig());
1188
+ printInstallNext();
1000
1189
  return;
1001
1190
  }
1002
1191
  console.error("Supported agents: claude, cursor (Codex CLI coming next). Usage: promptai install <agent>");
@@ -1023,7 +1212,7 @@ try {
1023
1212
  await cmdHook();
1024
1213
  break;
1025
1214
  case "install":
1026
- cmdInstall(args[0]);
1215
+ await cmdInstall(args[0]);
1027
1216
  break;
1028
1217
  case "uninstall":
1029
1218
  cmdUninstall(args[0]);
@@ -1040,6 +1229,12 @@ try {
1040
1229
  case "set":
1041
1230
  await cmdSet(args[0], args[1]);
1042
1231
  break;
1232
+ case "on":
1233
+ cmdAds(true);
1234
+ break;
1235
+ case "off":
1236
+ cmdAds(false);
1237
+ break;
1043
1238
  default:
1044
1239
  console.log(HELP);
1045
1240
  if (command && command !== "help" && command !== "--help") process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptai.credit/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },