@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.
- package/README.md +8 -0
- package/dist/index.js +252 -56
- 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
|
|
5
|
-
import * as
|
|
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
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
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
|
|
286
|
+
import * as fs3 from "node:fs";
|
|
173
287
|
function readTranscriptUsage(transcriptPath, watermark) {
|
|
174
|
-
const raw =
|
|
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 =
|
|
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
|
|
365
|
+
return path3.join(os3.homedir(), ".claude", "settings.json");
|
|
252
366
|
}
|
|
253
367
|
function hookCommand() {
|
|
254
|
-
let script =
|
|
368
|
+
let script = path3.resolve(process.argv[1] ?? "");
|
|
255
369
|
if (script.endsWith(".ts")) {
|
|
256
|
-
const dist =
|
|
257
|
-
if (!
|
|
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
|
-
|
|
382
|
+
fs4.mkdirSync(path3.dirname(settingsPath), { recursive: true });
|
|
269
383
|
let settings = {};
|
|
270
|
-
if (
|
|
384
|
+
if (fs4.existsSync(settingsPath)) {
|
|
271
385
|
try {
|
|
272
|
-
settings = JSON.parse(
|
|
386
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
|
|
273
387
|
} catch {
|
|
274
|
-
|
|
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 || !
|
|
295
|
-
|
|
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 (!
|
|
415
|
+
if (!fs4.existsSync(settingsPath)) return { changed: false };
|
|
302
416
|
let settings;
|
|
303
417
|
try {
|
|
304
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
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(
|
|
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
|
|
420
|
-
import * as
|
|
421
|
-
import * as
|
|
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
|
|
552
|
+
return path4.join(os4.homedir(), ".cursor", "hooks.json");
|
|
432
553
|
}
|
|
433
554
|
function installCursorHooks() {
|
|
434
555
|
const hooksPath = cursorHooksJsonPath();
|
|
435
|
-
|
|
556
|
+
fs5.mkdirSync(path4.dirname(hooksPath), { recursive: true });
|
|
436
557
|
let config = {};
|
|
437
|
-
if (
|
|
558
|
+
if (fs5.existsSync(hooksPath)) {
|
|
438
559
|
try {
|
|
439
|
-
config = JSON.parse(
|
|
560
|
+
config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
|
|
440
561
|
} catch {
|
|
441
|
-
|
|
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 || !
|
|
464
|
-
|
|
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 (!
|
|
591
|
+
if (!fs5.existsSync(hooksPath)) return { changed: false };
|
|
471
592
|
let config;
|
|
472
593
|
try {
|
|
473
|
-
config = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
616
|
+
const infoPath = path4.join(os4.homedir(), ".cursor", "promptai-listener.json");
|
|
496
617
|
try {
|
|
497
|
-
const info = JSON.parse(
|
|
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 =
|
|
507
|
-
|
|
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
|
|
511
|
-
const p =
|
|
512
|
-
if (
|
|
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
|
-
|
|
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(
|
|
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" ||
|
|
809
|
+
if (process.env.PROMPTAI_DUMP === "1" || fs6.existsSync(path5.join(promptaiDir(), "debug"))) {
|
|
681
810
|
try {
|
|
682
|
-
const dir =
|
|
683
|
-
|
|
684
|
-
|
|
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(
|
|
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);
|