@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.
- package/README.md +12 -8
- package/dist/index.js +322 -127
- 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
|
-
|
|
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
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
6-digit code to
|
|
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
|
|
5
|
-
import * as
|
|
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
|
|
9
|
+
import * as fs5 from "node:fs";
|
|
11
10
|
import * as os3 from "node:os";
|
|
12
|
-
import * as
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
185
|
-
process.env.APPDATA ??
|
|
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
|
|
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 (!
|
|
316
|
+
if (!fs3.existsSync(src)) return null;
|
|
198
317
|
let dir = null;
|
|
199
318
|
try {
|
|
200
|
-
dir =
|
|
201
|
-
const db =
|
|
202
|
-
|
|
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 (
|
|
323
|
+
if (fs3.existsSync(src + suffix)) fs3.copyFileSync(src + suffix, db + suffix);
|
|
205
324
|
}
|
|
206
|
-
const stdout =
|
|
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)
|
|
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
|
-
|
|
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
|
|
405
|
+
import * as fs4 from "node:fs";
|
|
287
406
|
function readTranscriptUsage(transcriptPath, watermark) {
|
|
288
|
-
const raw =
|
|
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 =
|
|
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
|
|
484
|
+
return path4.join(os3.homedir(), ".claude", "settings.json");
|
|
366
485
|
}
|
|
367
486
|
function hookCommand() {
|
|
368
|
-
let script =
|
|
487
|
+
let script = path4.resolve(process.argv[1] ?? "");
|
|
369
488
|
if (script.endsWith(".ts")) {
|
|
370
|
-
const dist =
|
|
371
|
-
if (!
|
|
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
|
-
|
|
501
|
+
fs5.mkdirSync(path4.dirname(settingsPath), { recursive: true });
|
|
383
502
|
let settings = {};
|
|
384
|
-
if (
|
|
503
|
+
if (fs5.existsSync(settingsPath)) {
|
|
385
504
|
try {
|
|
386
|
-
settings = JSON.parse(
|
|
505
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
|
|
387
506
|
} catch {
|
|
388
|
-
|
|
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 || !
|
|
409
|
-
|
|
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 (!
|
|
534
|
+
if (!fs5.existsSync(settingsPath)) return { changed: false };
|
|
416
535
|
let settings;
|
|
417
536
|
try {
|
|
418
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
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
|
|
659
|
+
import * as fs6 from "node:fs";
|
|
541
660
|
import * as os4 from "node:os";
|
|
542
|
-
import * as
|
|
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
|
|
671
|
+
return path5.join(os4.homedir(), ".cursor", "hooks.json");
|
|
553
672
|
}
|
|
554
673
|
function installCursorHooks() {
|
|
555
674
|
const hooksPath = cursorHooksJsonPath();
|
|
556
|
-
|
|
675
|
+
fs6.mkdirSync(path5.dirname(hooksPath), { recursive: true });
|
|
557
676
|
let config = {};
|
|
558
|
-
if (
|
|
677
|
+
if (fs6.existsSync(hooksPath)) {
|
|
559
678
|
try {
|
|
560
|
-
config = JSON.parse(
|
|
679
|
+
config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
|
|
561
680
|
} catch {
|
|
562
|
-
|
|
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 || !
|
|
585
|
-
|
|
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 (!
|
|
710
|
+
if (!fs6.existsSync(hooksPath)) return { changed: false };
|
|
592
711
|
let config;
|
|
593
712
|
try {
|
|
594
|
-
config = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
735
|
+
const infoPath = path5.join(os4.homedir(), ".cursor", "promptai-listener.json");
|
|
617
736
|
try {
|
|
618
|
-
const info = JSON.parse(
|
|
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 =
|
|
628
|
-
|
|
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
|
|
632
|
-
const p =
|
|
633
|
-
if (
|
|
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
|
-
|
|
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
|
|
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" ||
|
|
1020
|
+
if (process.env.PROMPTAI_DUMP === "1" || fs7.existsSync(path6.join(promptaiDir(), "debug"))) {
|
|
810
1021
|
try {
|
|
811
|
-
const dir =
|
|
812
|
-
|
|
813
|
-
|
|
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 === "
|
|
922
|
-
config.
|
|
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
|
|
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
|
-
|
|
935
|
-
|
|
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
|
|
969
|
-
|
|
970
|
-
|
|
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
|
|
974
|
-
|
|
975
|
-
|
|
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.
|
|
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;
|