@promptai.credit/cli 0.3.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.
- package/README.md +9 -7
- package/dist/index.js +322 -126
- 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, and docks ads on the **right** by default (like the IDE panel). 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
58
|
promptai set ads off # opt out any time
|
|
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,10 @@ 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.
|
|
78
80
|
- Headless environments (CI, ssh without a display) never get browser tabs and
|
|
79
81
|
simply skip crediting.
|
|
80
82
|
- 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,112 @@ 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
|
+
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
|
+
|
|
777
1002
|
// src/index.ts
|
|
778
1003
|
var HELP = `promptai - ad-subsidized prompt credits for terminal agents
|
|
779
1004
|
|
|
@@ -788,6 +1013,7 @@ Usage:
|
|
|
788
1013
|
promptai set email <email> Link your email (required for USDC claims; groups devices)
|
|
789
1014
|
promptai set server <url> Point at a different API server
|
|
790
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)
|
|
791
1017
|
promptai hook (internal) invoked by agent hooks, JSON on stdin
|
|
792
1018
|
`;
|
|
793
1019
|
function readStdin() {
|
|
@@ -806,11 +1032,11 @@ async function cmdHook() {
|
|
|
806
1032
|
return;
|
|
807
1033
|
}
|
|
808
1034
|
const event = String(payload.hook_event_name ?? "");
|
|
809
|
-
if (process.env.PROMPTAI_DUMP === "1" ||
|
|
1035
|
+
if (process.env.PROMPTAI_DUMP === "1" || fs7.existsSync(path6.join(promptaiDir(), "debug"))) {
|
|
810
1036
|
try {
|
|
811
|
-
const dir =
|
|
812
|
-
|
|
813
|
-
|
|
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);
|
|
814
1040
|
} catch {
|
|
815
1041
|
}
|
|
816
1042
|
}
|
|
@@ -840,6 +1066,7 @@ async function cmdStatus() {
|
|
|
840
1066
|
console.log(`wallet ${config.wallet || "(not set - promptai set wallet 0x...)"}`);
|
|
841
1067
|
console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
|
|
842
1068
|
console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
|
|
1069
|
+
console.log(`side ${config.adSide}`);
|
|
843
1070
|
console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
|
|
844
1071
|
try {
|
|
845
1072
|
const [balance, verified] = await Promise.all([
|
|
@@ -873,7 +1100,7 @@ async function cmdStatus() {
|
|
|
873
1100
|
async function cmdWatch() {
|
|
874
1101
|
const config = loadConfig();
|
|
875
1102
|
const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
|
|
876
|
-
if (openInBrowser(url)) {
|
|
1103
|
+
if (openInBrowser(url, config.adSide)) {
|
|
877
1104
|
console.log(`Opened ${url}`);
|
|
878
1105
|
console.log("Watch the full ad; the credit banks automatically once the server verifies it.");
|
|
879
1106
|
} else if (!hasDisplay()) {
|
|
@@ -920,73 +1147,40 @@ async function cmdSet(key, value) {
|
|
|
920
1147
|
config.serverUrl = value.replace(/\/$/, "");
|
|
921
1148
|
} else if (key === "ads" && (value === "on" || value === "off")) {
|
|
922
1149
|
config.adsOptIn = value === "on";
|
|
1150
|
+
} else if (key === "side" && (value === "right" || value === "left" || value === "default")) {
|
|
1151
|
+
config.adSide = value;
|
|
923
1152
|
} else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
|
|
924
1153
|
config.email = value.trim().toLowerCase();
|
|
925
1154
|
} else {
|
|
926
1155
|
console.error(
|
|
927
|
-
"Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set email you@example.com"
|
|
1156
|
+
"Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set side right|left|default | set email you@example.com"
|
|
928
1157
|
);
|
|
929
1158
|
process.exitCode = 1;
|
|
930
1159
|
return;
|
|
931
1160
|
}
|
|
932
1161
|
if (key === "email") {
|
|
933
1162
|
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)}`);
|
|
1163
|
+
const ok = await linkEmailInteractive(config, email, "manual");
|
|
1164
|
+
if (!ok) {
|
|
961
1165
|
process.exitCode = 1;
|
|
1166
|
+
return;
|
|
962
1167
|
}
|
|
1168
|
+
saveConfig(config);
|
|
1169
|
+
console.log("email updated.");
|
|
963
1170
|
return;
|
|
964
1171
|
}
|
|
965
1172
|
saveConfig(config);
|
|
966
1173
|
console.log(`${key} updated.`);
|
|
967
1174
|
}
|
|
968
|
-
function
|
|
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
|
-
}
|
|
983
|
-
function cmdInstall(agent) {
|
|
1175
|
+
async function cmdInstall(agent) {
|
|
984
1176
|
if (agent === "claude" || agent === "claude-code") {
|
|
985
1177
|
const { changed, settingsPath } = installClaudeHooks();
|
|
986
1178
|
console.log(
|
|
987
1179
|
changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
|
|
988
1180
|
);
|
|
989
|
-
console.log("Claude Code picks them up on its next session.
|
|
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.");
|
|
990
1184
|
return;
|
|
991
1185
|
}
|
|
992
1186
|
if (agent === "cursor" || agent === "cursor-agents") {
|
|
@@ -997,6 +1191,8 @@ function cmdInstall(agent) {
|
|
|
997
1191
|
console.log(
|
|
998
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."
|
|
999
1193
|
);
|
|
1194
|
+
await onboard(loadConfig());
|
|
1195
|
+
console.log("Try: promptai watch, then run a prompt.");
|
|
1000
1196
|
return;
|
|
1001
1197
|
}
|
|
1002
1198
|
console.error("Supported agents: claude, cursor (Codex CLI coming next). Usage: promptai install <agent>");
|
|
@@ -1023,7 +1219,7 @@ try {
|
|
|
1023
1219
|
await cmdHook();
|
|
1024
1220
|
break;
|
|
1025
1221
|
case "install":
|
|
1026
|
-
cmdInstall(args[0]);
|
|
1222
|
+
await cmdInstall(args[0]);
|
|
1027
1223
|
break;
|
|
1028
1224
|
case "uninstall":
|
|
1029
1225
|
cmdUninstall(args[0]);
|