@promptai.credit/cli 0.1.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 +36 -4
- package/dist/index.js +536 -43
- package/package.json +1 -6
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# promptai CLI
|
|
2
2
|
|
|
3
3
|
Brings the earn loop (prompt → opt-in ad during the wait → server-side verification →
|
|
4
|
-
credit → USDC claim) to
|
|
4
|
+
credit → USDC claim) to agents beyond the classic Cursor IDE. Claude Code and the
|
|
5
|
+
Cursor Agents Window are supported today; Codex CLI is next.
|
|
5
6
|
|
|
6
7
|
## How it works
|
|
7
8
|
|
|
@@ -14,7 +15,29 @@ credit → USDC claim) to terminal agents. Claude Code is supported today; Codex
|
|
|
14
15
|
server-verified ad session against it via the API.
|
|
15
16
|
2. The `/watch` page creates the ad session server-side on load, so SSV wall-clock
|
|
16
17
|
timing is enforced by the server; the browser countdown is cosmetic.
|
|
17
|
-
3. `promptai claim` pays out your verified balance as USDC on Base
|
|
18
|
+
3. `promptai claim` pays out your verified balance as USDC on Base.
|
|
19
|
+
|
|
20
|
+
## Cursor Agents Window (Glass)
|
|
21
|
+
|
|
22
|
+
The Agents Window doesn't run VS Code extensions, so the promptai extension can't
|
|
23
|
+
cover it. `promptai install cursor` wires `beforeSubmitPrompt` + `stop` into
|
|
24
|
+
`~/.cursor/hooks.json` (core Cursor hooks fire in both the Agents Window and the
|
|
25
|
+
classic IDE):
|
|
26
|
+
|
|
27
|
+
- Hook payloads are routed by shape (`cursor_version`/`conversation_id`), so events
|
|
28
|
+
reach the right adapter even if Cursor loads our Claude-format hooks through its
|
|
29
|
+
third-party mapping.
|
|
30
|
+
- Cursor 3.7+ reports real token usage on the `stop` payload (`input_tokens`,
|
|
31
|
+
`output_tokens`, cache splits); cost is priced from that, with cached reads/writes
|
|
32
|
+
discounted. On older versions the cost falls back to a transcript character-volume
|
|
33
|
+
**estimate** (Glass transcripts carry no token counts and redact assistant prose).
|
|
34
|
+
- When the classic IDE's extension host is running it already credits prompts, so
|
|
35
|
+
the CLI detects its listener file (live pid) and stands down - no double credit.
|
|
36
|
+
- Duplicate stop events for the same generation are collapsed with an atomic
|
|
37
|
+
once-lock in `~/.promptai/locks/`.
|
|
38
|
+
- On remote/SSH workspaces (`CURSOR_CODE_REMOTE`), the remote host has no display:
|
|
39
|
+
no ad tabs open there. Bank credits with `promptai watch` from any browser
|
|
40
|
+
(`/watch?device=<your device id>`) instead.
|
|
18
41
|
|
|
19
42
|
State lives in `~/.promptai/` (`config.json`, `state.json`, `cli.log`).
|
|
20
43
|
|
|
@@ -22,13 +45,15 @@ State lives in `~/.promptai/` (`config.json`, `state.json`, `cli.log`).
|
|
|
22
45
|
|
|
23
46
|
```bash
|
|
24
47
|
npm install -g @promptai.credit/cli
|
|
25
|
-
promptai install claude
|
|
48
|
+
promptai install claude # Claude Code hooks
|
|
49
|
+
promptai install cursor # Cursor Agents Window / classic IDE hooks
|
|
26
50
|
|
|
27
51
|
# then:
|
|
28
52
|
promptai status # device, balance, banked ad watches, recent prompts
|
|
29
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)
|
|
30
55
|
promptai set wallet 0x... # payout address
|
|
31
|
-
promptai claim # USDC on Base
|
|
56
|
+
promptai claim # USDC on Base
|
|
32
57
|
promptai set ads off # opt out any time
|
|
33
58
|
```
|
|
34
59
|
|
|
@@ -43,6 +68,13 @@ Developing from the repo instead: `pnpm --filter @promptai.credit/cli build`, th
|
|
|
43
68
|
- Credits only accrue for turns funded by a **verified** ad watch, enforced
|
|
44
69
|
server-side (same rules as the Cursor extension: one session funds one prompt,
|
|
45
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.
|
|
46
78
|
- Headless environments (CI, ssh without a display) never get browser tabs and
|
|
47
79
|
simply skip crediting.
|
|
48
80
|
- Uninstall with `promptai uninstall claude`; other hooks in your settings are
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as fs6 from "node:fs";
|
|
5
|
+
import * as path5 from "node:path";
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
|
|
3
8
|
// src/claude.ts
|
|
4
9
|
import * as crypto3 from "node:crypto";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
10
|
+
import * as fs4 from "node:fs";
|
|
11
|
+
import * as os3 from "node:os";
|
|
12
|
+
import * as path3 from "node:path";
|
|
8
13
|
|
|
9
14
|
// src/api.ts
|
|
10
15
|
import * as crypto from "node:crypto";
|
|
@@ -39,6 +44,12 @@ function redeemCredit(serverUrl, params) {
|
|
|
39
44
|
function fetchBalance(serverUrl, deviceId) {
|
|
40
45
|
return request(`${serverUrl}/balance/${deviceId}`);
|
|
41
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
|
+
}
|
|
42
53
|
function claim(serverUrl, params) {
|
|
43
54
|
return post(serverUrl, "/claim", {
|
|
44
55
|
...params,
|
|
@@ -90,7 +101,8 @@ function loadConfig() {
|
|
|
90
101
|
deviceId: "",
|
|
91
102
|
serverUrl: "https://api.promptai.credit",
|
|
92
103
|
wallet: "",
|
|
93
|
-
adsOptIn: true
|
|
104
|
+
adsOptIn: true,
|
|
105
|
+
email: ""
|
|
94
106
|
});
|
|
95
107
|
if (!config.deviceId) {
|
|
96
108
|
config.deviceId = crypto2.randomUUID();
|
|
@@ -135,6 +147,112 @@ function log(message) {
|
|
|
135
147
|
}
|
|
136
148
|
}
|
|
137
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
|
+
|
|
138
256
|
// src/pricing.ts
|
|
139
257
|
var TABLE = [
|
|
140
258
|
{ match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
|
|
@@ -165,9 +283,9 @@ function costUsd(model, inputTokens, outputTokens) {
|
|
|
165
283
|
}
|
|
166
284
|
|
|
167
285
|
// src/transcript.ts
|
|
168
|
-
import * as
|
|
286
|
+
import * as fs3 from "node:fs";
|
|
169
287
|
function readTranscriptUsage(transcriptPath, watermark) {
|
|
170
|
-
const raw =
|
|
288
|
+
const raw = fs3.readFileSync(transcriptPath, "utf8");
|
|
171
289
|
const byMessageId = /* @__PURE__ */ new Map();
|
|
172
290
|
let parsedCount = 0;
|
|
173
291
|
for (const line of raw.split("\n")) {
|
|
@@ -208,6 +326,34 @@ function readTranscriptUsage(transcriptPath, watermark) {
|
|
|
208
326
|
seenCount: entries.length
|
|
209
327
|
};
|
|
210
328
|
}
|
|
329
|
+
function readGlassTranscriptUsage(transcriptPath, watermark) {
|
|
330
|
+
const raw = fs3.readFileSync(transcriptPath, "utf8");
|
|
331
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
332
|
+
let inputChars = 0;
|
|
333
|
+
let outputChars = 0;
|
|
334
|
+
let newEntries = 0;
|
|
335
|
+
for (const line of lines.slice(watermark.seenCount)) {
|
|
336
|
+
let entry;
|
|
337
|
+
try {
|
|
338
|
+
entry = JSON.parse(line);
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const chars = JSON.stringify(entry.message?.content ?? "").length;
|
|
343
|
+
if (entry.role === "assistant") {
|
|
344
|
+
outputChars += chars;
|
|
345
|
+
newEntries += 1;
|
|
346
|
+
} else {
|
|
347
|
+
inputChars += chars;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
inputTokens: Math.round(inputChars / 4),
|
|
352
|
+
outputTokens: Math.round(outputChars / 4),
|
|
353
|
+
newEntries,
|
|
354
|
+
seenCount: lines.length
|
|
355
|
+
};
|
|
356
|
+
}
|
|
211
357
|
|
|
212
358
|
// src/claude.ts
|
|
213
359
|
var SOURCE = "claude-code";
|
|
@@ -216,13 +362,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
|
|
|
216
362
|
var AD_MIN_INTERVAL_MS = 9e4;
|
|
217
363
|
var SETTLE_RETRIES = [400, 800, 1500];
|
|
218
364
|
function claudeSettingsPath() {
|
|
219
|
-
return
|
|
365
|
+
return path3.join(os3.homedir(), ".claude", "settings.json");
|
|
220
366
|
}
|
|
221
367
|
function hookCommand() {
|
|
222
|
-
let script =
|
|
368
|
+
let script = path3.resolve(process.argv[1] ?? "");
|
|
223
369
|
if (script.endsWith(".ts")) {
|
|
224
|
-
const dist =
|
|
225
|
-
if (!
|
|
370
|
+
const dist = path3.resolve(path3.dirname(script), "..", "dist", "index.js");
|
|
371
|
+
if (!fs4.existsSync(dist)) {
|
|
226
372
|
throw new Error(
|
|
227
373
|
`Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
|
|
228
374
|
);
|
|
@@ -233,13 +379,13 @@ function hookCommand() {
|
|
|
233
379
|
}
|
|
234
380
|
function installClaudeHooks() {
|
|
235
381
|
const settingsPath = claudeSettingsPath();
|
|
236
|
-
|
|
382
|
+
fs4.mkdirSync(path3.dirname(settingsPath), { recursive: true });
|
|
237
383
|
let settings = {};
|
|
238
|
-
if (
|
|
384
|
+
if (fs4.existsSync(settingsPath)) {
|
|
239
385
|
try {
|
|
240
|
-
settings = JSON.parse(
|
|
386
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
|
|
241
387
|
} catch {
|
|
242
|
-
|
|
388
|
+
fs4.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
|
|
243
389
|
settings = {};
|
|
244
390
|
}
|
|
245
391
|
}
|
|
@@ -259,17 +405,17 @@ function installClaudeHooks() {
|
|
|
259
405
|
changed = true;
|
|
260
406
|
}
|
|
261
407
|
}
|
|
262
|
-
if (changed || !
|
|
263
|
-
|
|
408
|
+
if (changed || !fs4.existsSync(settingsPath)) {
|
|
409
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
264
410
|
}
|
|
265
411
|
return { changed, settingsPath };
|
|
266
412
|
}
|
|
267
413
|
function uninstallClaudeHooks() {
|
|
268
414
|
const settingsPath = claudeSettingsPath();
|
|
269
|
-
if (!
|
|
415
|
+
if (!fs4.existsSync(settingsPath)) return { changed: false };
|
|
270
416
|
let settings;
|
|
271
417
|
try {
|
|
272
|
-
settings = JSON.parse(
|
|
418
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
|
|
273
419
|
} catch {
|
|
274
420
|
return { changed: false };
|
|
275
421
|
}
|
|
@@ -289,7 +435,7 @@ function uninstallClaudeHooks() {
|
|
|
289
435
|
}
|
|
290
436
|
}
|
|
291
437
|
if (changed) {
|
|
292
|
-
|
|
438
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
293
439
|
}
|
|
294
440
|
return { changed };
|
|
295
441
|
}
|
|
@@ -344,7 +490,13 @@ async function handleStop(payload) {
|
|
|
344
490
|
const promptId = crypto3.randomUUID();
|
|
345
491
|
let verified = false;
|
|
346
492
|
if (config.adsOptIn && cost > 0) {
|
|
347
|
-
verified = await redeemAgainstAd(
|
|
493
|
+
verified = await redeemAgainstAd(
|
|
494
|
+
config.serverUrl,
|
|
495
|
+
config.deviceId,
|
|
496
|
+
promptId,
|
|
497
|
+
cost,
|
|
498
|
+
resolveEmail(config)
|
|
499
|
+
);
|
|
348
500
|
}
|
|
349
501
|
state.prompts.unshift({
|
|
350
502
|
id: promptId,
|
|
@@ -362,7 +514,7 @@ async function handleStop(payload) {
|
|
|
362
514
|
`[claude] settled session=${sessionId} model=${usage.model} in=${usage.inputTokens} out=${usage.outputTokens} cost=$${cost} verified=${verified}`
|
|
363
515
|
);
|
|
364
516
|
}
|
|
365
|
-
async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
|
|
517
|
+
async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
|
|
366
518
|
try {
|
|
367
519
|
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
368
520
|
if (sessions.length === 0) {
|
|
@@ -373,7 +525,8 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
|
|
|
373
525
|
sessionId: sessions[0].sessionId,
|
|
374
526
|
deviceId,
|
|
375
527
|
promptId,
|
|
376
|
-
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD)
|
|
528
|
+
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
|
|
529
|
+
email: email || void 0
|
|
377
530
|
});
|
|
378
531
|
return true;
|
|
379
532
|
} catch (err) {
|
|
@@ -382,16 +535,257 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
|
|
|
382
535
|
}
|
|
383
536
|
}
|
|
384
537
|
|
|
538
|
+
// src/cursor.ts
|
|
539
|
+
import * as crypto4 from "node:crypto";
|
|
540
|
+
import * as fs5 from "node:fs";
|
|
541
|
+
import * as os4 from "node:os";
|
|
542
|
+
import * as path4 from "node:path";
|
|
543
|
+
var CURSOR_SOURCE = "cursor-agents";
|
|
544
|
+
var HOOK_MARKER2 = "promptai";
|
|
545
|
+
var MAX_CREDIT_PER_AD_USD2 = 5;
|
|
546
|
+
var AD_MIN_INTERVAL_MS2 = 9e4;
|
|
547
|
+
var SETTLE_RETRIES2 = [400, 800, 1500];
|
|
548
|
+
function isCursorPayload(payload) {
|
|
549
|
+
return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
|
|
550
|
+
}
|
|
551
|
+
function cursorHooksJsonPath() {
|
|
552
|
+
return path4.join(os4.homedir(), ".cursor", "hooks.json");
|
|
553
|
+
}
|
|
554
|
+
function installCursorHooks() {
|
|
555
|
+
const hooksPath = cursorHooksJsonPath();
|
|
556
|
+
fs5.mkdirSync(path4.dirname(hooksPath), { recursive: true });
|
|
557
|
+
let config = {};
|
|
558
|
+
if (fs5.existsSync(hooksPath)) {
|
|
559
|
+
try {
|
|
560
|
+
config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
|
|
561
|
+
} catch {
|
|
562
|
+
fs5.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
|
|
563
|
+
config = {};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
config.version = config.version ?? 1;
|
|
567
|
+
config.hooks = config.hooks ?? {};
|
|
568
|
+
const command2 = hookCommand();
|
|
569
|
+
let changed = false;
|
|
570
|
+
for (const event of ["beforeSubmitPrompt", "stop"]) {
|
|
571
|
+
const entries = Array.isArray(config.hooks[event]) ? config.hooks[event] : [];
|
|
572
|
+
const ours = entries.find(
|
|
573
|
+
(e) => typeof e?.command === "string" && e.command.includes(HOOK_MARKER2)
|
|
574
|
+
);
|
|
575
|
+
if (!ours) {
|
|
576
|
+
entries.push({ command: command2 });
|
|
577
|
+
config.hooks[event] = entries;
|
|
578
|
+
changed = true;
|
|
579
|
+
} else if (ours.command !== command2) {
|
|
580
|
+
ours.command = command2;
|
|
581
|
+
changed = true;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (changed || !fs5.existsSync(hooksPath)) {
|
|
585
|
+
fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
|
|
586
|
+
}
|
|
587
|
+
return { changed, hooksPath };
|
|
588
|
+
}
|
|
589
|
+
function uninstallCursorHooks() {
|
|
590
|
+
const hooksPath = cursorHooksJsonPath();
|
|
591
|
+
if (!fs5.existsSync(hooksPath)) return { changed: false };
|
|
592
|
+
let config;
|
|
593
|
+
try {
|
|
594
|
+
config = JSON.parse(fs5.readFileSync(hooksPath, "utf8"));
|
|
595
|
+
} catch {
|
|
596
|
+
return { changed: false };
|
|
597
|
+
}
|
|
598
|
+
if (!config.hooks) return { changed: false };
|
|
599
|
+
let changed = false;
|
|
600
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
601
|
+
if (!Array.isArray(entries)) continue;
|
|
602
|
+
const kept = entries.filter(
|
|
603
|
+
(e) => !(typeof e?.command === "string" && e.command.includes(HOOK_MARKER2))
|
|
604
|
+
);
|
|
605
|
+
if (kept.length !== entries.length) {
|
|
606
|
+
config.hooks[event] = kept;
|
|
607
|
+
changed = true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (changed) {
|
|
611
|
+
fs5.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
|
|
612
|
+
}
|
|
613
|
+
return { changed };
|
|
614
|
+
}
|
|
615
|
+
function extensionIsActive() {
|
|
616
|
+
const infoPath = path4.join(os4.homedir(), ".cursor", "promptai-listener.json");
|
|
617
|
+
try {
|
|
618
|
+
const info = JSON.parse(fs5.readFileSync(infoPath, "utf8"));
|
|
619
|
+
if (typeof info.pid !== "number") return false;
|
|
620
|
+
process.kill(info.pid, 0);
|
|
621
|
+
return true;
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function acquireOnceLock(key) {
|
|
627
|
+
const dir = path4.join(promptaiDir(), "locks");
|
|
628
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
629
|
+
try {
|
|
630
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
631
|
+
for (const name of fs5.readdirSync(dir)) {
|
|
632
|
+
const p = path4.join(dir, name);
|
|
633
|
+
if (fs5.statSync(p).mtimeMs < cutoff) fs5.rmSync(p, { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
fs5.mkdirSync(path4.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
|
|
639
|
+
return true;
|
|
640
|
+
} catch {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function watermarkKey(conversationId) {
|
|
645
|
+
return `glass:${conversationId}`;
|
|
646
|
+
}
|
|
647
|
+
function handleCursorPromptSubmitted(payload) {
|
|
648
|
+
const config = loadConfig();
|
|
649
|
+
const state = loadState();
|
|
650
|
+
const convId = payload.conversation_id ?? "unknown";
|
|
651
|
+
const key = watermarkKey(convId);
|
|
652
|
+
if (!(key in state.watermarks) && payload.transcript_path) {
|
|
653
|
+
try {
|
|
654
|
+
const usage = readGlassTranscriptUsage(payload.transcript_path, { seenCount: 0 });
|
|
655
|
+
state.watermarks[key] = { lastTs: 0, seenCount: usage.seenCount };
|
|
656
|
+
} catch {
|
|
657
|
+
state.watermarks[key] = { lastTs: 0, seenCount: 0 };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const remote = process.env.CURSOR_CODE_REMOTE === "true";
|
|
661
|
+
if (config.adsOptIn && !extensionIsActive() && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS2) {
|
|
662
|
+
const url = watchUrl(config.serverUrl, config.deviceId, CURSOR_SOURCE);
|
|
663
|
+
if (!remote && openInBrowser(url)) {
|
|
664
|
+
state.lastAdOpenedAt = Date.now();
|
|
665
|
+
log(`[cursor] opened ad tab for conv=${convId}`);
|
|
666
|
+
} else if (remote || !hasDisplay()) {
|
|
667
|
+
log(`[cursor] no display here - watch ads at ${url}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
saveState(state);
|
|
671
|
+
}
|
|
672
|
+
async function handleCursorStop(payload) {
|
|
673
|
+
const config = loadConfig();
|
|
674
|
+
const convId = payload.conversation_id ?? "unknown";
|
|
675
|
+
if (extensionIsActive()) {
|
|
676
|
+
log(`[cursor] extension host active, deferring settle for conv=${convId}`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!payload.transcript_path) {
|
|
680
|
+
log(`[cursor] stop without transcript_path conv=${convId} (transcripts disabled?)`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!acquireOnceLock(`stop-${convId}-${payload.generation_id ?? "nogen"}`)) {
|
|
684
|
+
log(`[cursor] duplicate stop event for conv=${convId}, skipping`);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const state = loadState();
|
|
688
|
+
const key = watermarkKey(convId);
|
|
689
|
+
const watermark = state.watermarks[key] ?? { lastTs: 0, seenCount: 0 };
|
|
690
|
+
const model = payload.model_id || payload.model || "unknown";
|
|
691
|
+
let inputTokens = 0;
|
|
692
|
+
let outputTokens = 0;
|
|
693
|
+
let estimated = false;
|
|
694
|
+
const reportedInput = Number(payload.input_tokens ?? 0);
|
|
695
|
+
const reportedOutput = Number(payload.output_tokens ?? 0);
|
|
696
|
+
if (reportedInput + reportedOutput > 0) {
|
|
697
|
+
const cacheRead = Number(payload.cache_read_tokens ?? 0);
|
|
698
|
+
const cacheWrite = Number(payload.cache_write_tokens ?? 0);
|
|
699
|
+
const uncached = Math.max(0, reportedInput - cacheRead - cacheWrite);
|
|
700
|
+
inputTokens = Math.round(uncached + cacheWrite * 1.25 + cacheRead * 0.1);
|
|
701
|
+
outputTokens = reportedOutput;
|
|
702
|
+
} else {
|
|
703
|
+
estimated = true;
|
|
704
|
+
let usage;
|
|
705
|
+
for (const delay of SETTLE_RETRIES2) {
|
|
706
|
+
try {
|
|
707
|
+
usage = readGlassTranscriptUsage(payload.transcript_path, watermark);
|
|
708
|
+
if (usage.newEntries > 0) break;
|
|
709
|
+
} catch (err) {
|
|
710
|
+
log(`[cursor] transcript read failed: ${String(err)}`);
|
|
711
|
+
}
|
|
712
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
713
|
+
}
|
|
714
|
+
if (!usage || usage.newEntries === 0) {
|
|
715
|
+
log(`[cursor] no new usage for conv=${convId}, skipping`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
inputTokens = usage.inputTokens;
|
|
719
|
+
outputTokens = usage.outputTokens;
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
const snapshot = readGlassTranscriptUsage(payload.transcript_path, { seenCount: 0 });
|
|
723
|
+
state.watermarks[key] = { lastTs: 0, seenCount: snapshot.seenCount };
|
|
724
|
+
} catch {
|
|
725
|
+
state.watermarks[key] = watermark;
|
|
726
|
+
}
|
|
727
|
+
const cost = costUsd(model, inputTokens, outputTokens);
|
|
728
|
+
const promptId = crypto4.randomUUID();
|
|
729
|
+
let verified = false;
|
|
730
|
+
if (config.adsOptIn && cost > 0) {
|
|
731
|
+
verified = await redeemAgainstAd2(
|
|
732
|
+
config.serverUrl,
|
|
733
|
+
config.deviceId,
|
|
734
|
+
promptId,
|
|
735
|
+
cost,
|
|
736
|
+
resolveEmail(config)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
state.prompts.unshift({
|
|
740
|
+
id: promptId,
|
|
741
|
+
ts: Date.now(),
|
|
742
|
+
agent: "cursor",
|
|
743
|
+
sessionId: convId,
|
|
744
|
+
model,
|
|
745
|
+
inputTokens,
|
|
746
|
+
outputTokens,
|
|
747
|
+
costUsd: cost,
|
|
748
|
+
verified,
|
|
749
|
+
estimated
|
|
750
|
+
});
|
|
751
|
+
saveState(state);
|
|
752
|
+
log(
|
|
753
|
+
`[cursor] settled conv=${convId} model=${model} in=${inputTokens} out=${outputTokens} cost=$${cost}${estimated ? " (estimated)" : ""} verified=${verified}`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
|
|
757
|
+
try {
|
|
758
|
+
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
759
|
+
if (sessions.length === 0) {
|
|
760
|
+
log(`[cursor] no verified ad session available for prompt ${promptId}`);
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
await redeemCredit(serverUrl, {
|
|
764
|
+
sessionId: sessions[0].sessionId,
|
|
765
|
+
deviceId,
|
|
766
|
+
promptId,
|
|
767
|
+
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
|
|
768
|
+
email: email || void 0
|
|
769
|
+
});
|
|
770
|
+
return true;
|
|
771
|
+
} catch (err) {
|
|
772
|
+
log(`[cursor] credit redeem failed for prompt ${promptId}: ${String(err)}`);
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
385
777
|
// src/index.ts
|
|
386
778
|
var HELP = `promptai - ad-subsidized prompt credits for terminal agents
|
|
387
779
|
|
|
388
780
|
Usage:
|
|
389
781
|
promptai install claude Wire hooks into ~/.claude/settings.json
|
|
390
|
-
promptai
|
|
782
|
+
promptai install cursor Wire hooks into ~/.cursor/hooks.json (Agents Window)
|
|
783
|
+
promptai uninstall <agent> Remove our hooks (other hooks untouched)
|
|
391
784
|
promptai status Device, balance, banked ad credits, recent prompts
|
|
392
785
|
promptai watch Open a rewarded ad in the browser now
|
|
393
|
-
promptai claim [address] Claim your verified balance as USDC (Base
|
|
786
|
+
promptai claim [address] Claim your verified balance as USDC (Base)
|
|
394
787
|
promptai set wallet 0x... Set the payout wallet
|
|
788
|
+
promptai set email <email> Link your email (required for USDC claims; groups devices)
|
|
395
789
|
promptai set server <url> Point at a different API server
|
|
396
790
|
promptai set ads on|off Toggle the rewarded-ads opt-in
|
|
397
791
|
promptai hook (internal) invoked by agent hooks, JSON on stdin
|
|
@@ -404,15 +798,31 @@ function readStdin() {
|
|
|
404
798
|
});
|
|
405
799
|
}
|
|
406
800
|
async function cmdHook() {
|
|
801
|
+
const raw = await readStdin();
|
|
407
802
|
let payload = {};
|
|
408
803
|
try {
|
|
409
|
-
payload = JSON.parse(
|
|
804
|
+
payload = JSON.parse(raw);
|
|
410
805
|
} catch {
|
|
411
806
|
return;
|
|
412
807
|
}
|
|
413
|
-
const event = payload.hook_event_name ?? "";
|
|
808
|
+
const event = String(payload.hook_event_name ?? "");
|
|
809
|
+
if (process.env.PROMPTAI_DUMP === "1" || fs6.existsSync(path5.join(promptaiDir(), "debug"))) {
|
|
810
|
+
try {
|
|
811
|
+
const dir = path5.join(promptaiDir(), "dump");
|
|
812
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
813
|
+
fs6.writeFileSync(path5.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
}
|
|
414
817
|
try {
|
|
415
|
-
if (
|
|
818
|
+
if (isCursorPayload(payload)) {
|
|
819
|
+
const cursorPayload = payload;
|
|
820
|
+
if (event === "beforeSubmitPrompt" || event === "UserPromptSubmit") {
|
|
821
|
+
handleCursorPromptSubmitted(cursorPayload);
|
|
822
|
+
} else if (event === "stop" || event === "Stop") {
|
|
823
|
+
await handleCursorStop(cursorPayload);
|
|
824
|
+
}
|
|
825
|
+
} else if (event === "UserPromptSubmit") {
|
|
416
826
|
handleUserPromptSubmit(payload);
|
|
417
827
|
} else if (event === "Stop") {
|
|
418
828
|
await handleStop(payload);
|
|
@@ -424,10 +834,13 @@ async function cmdHook() {
|
|
|
424
834
|
async function cmdStatus() {
|
|
425
835
|
const config = loadConfig();
|
|
426
836
|
const state = loadState();
|
|
837
|
+
const email = resolveEmail(config);
|
|
427
838
|
console.log(`device ${config.deviceId}`);
|
|
428
839
|
console.log(`server ${config.serverUrl}`);
|
|
429
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)"}`);
|
|
430
842
|
console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
|
|
843
|
+
console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
|
|
431
844
|
try {
|
|
432
845
|
const [balance, verified] = await Promise.all([
|
|
433
846
|
fetchBalance(config.serverUrl, config.deviceId),
|
|
@@ -436,6 +849,12 @@ async function cmdStatus() {
|
|
|
436
849
|
console.log(
|
|
437
850
|
`balance $${balance.balanceUsd.toFixed(4)} (earned $${balance.earnedUsd.toFixed(4)}, claimed $${balance.claimedUsd.toFixed(4)})`
|
|
438
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
|
+
);
|
|
439
858
|
console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
|
|
440
859
|
} catch (err) {
|
|
441
860
|
console.log(`balance unavailable (${String(err)})`);
|
|
@@ -472,6 +891,13 @@ async function cmdClaim(addressArg) {
|
|
|
472
891
|
process.exitCode = 1;
|
|
473
892
|
return;
|
|
474
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
|
+
}
|
|
475
901
|
const balance = await fetchBalance(config.serverUrl, config.deviceId);
|
|
476
902
|
if (balance.balanceUsd < 1e-6) {
|
|
477
903
|
console.log("Nothing to claim yet - watch an ad (promptai watch) and run some agent prompts first.");
|
|
@@ -486,7 +912,7 @@ async function cmdClaim(addressArg) {
|
|
|
486
912
|
console.log(`Sent: ${result.txHash}`);
|
|
487
913
|
console.log(result.explorerUrl);
|
|
488
914
|
}
|
|
489
|
-
function cmdSet(key, value) {
|
|
915
|
+
async function cmdSet(key, value) {
|
|
490
916
|
const config = loadConfig();
|
|
491
917
|
if (key === "wallet" && value && /^0x[0-9a-fA-F]{40}$/.test(value)) {
|
|
492
918
|
config.wallet = value;
|
|
@@ -494,34 +920,101 @@ function cmdSet(key, value) {
|
|
|
494
920
|
config.serverUrl = value.replace(/\/$/, "");
|
|
495
921
|
} else if (key === "ads" && (value === "on" || value === "off")) {
|
|
496
922
|
config.adsOptIn = value === "on";
|
|
923
|
+
} else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
|
|
924
|
+
config.email = value.trim().toLowerCase();
|
|
497
925
|
} else {
|
|
498
|
-
console.error(
|
|
926
|
+
console.error(
|
|
927
|
+
"Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set email you@example.com"
|
|
928
|
+
);
|
|
499
929
|
process.exitCode = 1;
|
|
500
930
|
return;
|
|
501
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
|
+
}
|
|
502
965
|
saveConfig(config);
|
|
503
966
|
console.log(`${key} updated.`);
|
|
504
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
|
+
}
|
|
505
983
|
function cmdInstall(agent) {
|
|
506
|
-
if (agent
|
|
507
|
-
|
|
508
|
-
|
|
984
|
+
if (agent === "claude" || agent === "claude-code") {
|
|
985
|
+
const { changed, settingsPath } = installClaudeHooks();
|
|
986
|
+
console.log(
|
|
987
|
+
changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
|
|
988
|
+
);
|
|
989
|
+
console.log("Claude Code picks them up on its next session. Try: promptai watch, then run a prompt.");
|
|
509
990
|
return;
|
|
510
991
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
992
|
+
if (agent === "cursor" || agent === "cursor-agents") {
|
|
993
|
+
const { changed, hooksPath } = installCursorHooks();
|
|
994
|
+
console.log(
|
|
995
|
+
changed ? `Hooks installed into ${hooksPath}.` : `Hooks already installed in ${hooksPath}.`
|
|
996
|
+
);
|
|
997
|
+
console.log(
|
|
998
|
+
"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
|
+
);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
console.error("Supported agents: claude, cursor (Codex CLI coming next). Usage: promptai install <agent>");
|
|
1003
|
+
process.exitCode = 1;
|
|
516
1004
|
}
|
|
517
1005
|
function cmdUninstall(agent) {
|
|
518
|
-
if (agent
|
|
519
|
-
|
|
520
|
-
|
|
1006
|
+
if (agent === "claude" || agent === "claude-code") {
|
|
1007
|
+
const { changed } = uninstallClaudeHooks();
|
|
1008
|
+
console.log(changed ? "Hooks removed." : "No promptai hooks found.");
|
|
521
1009
|
return;
|
|
522
1010
|
}
|
|
523
|
-
|
|
524
|
-
|
|
1011
|
+
if (agent === "cursor" || agent === "cursor-agents") {
|
|
1012
|
+
const { changed } = uninstallCursorHooks();
|
|
1013
|
+
console.log(changed ? "Hooks removed." : "No promptai hooks found.");
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
console.error("Usage: promptai uninstall claude|cursor");
|
|
1017
|
+
process.exitCode = 1;
|
|
525
1018
|
}
|
|
526
1019
|
var [, , command, ...args] = process.argv;
|
|
527
1020
|
try {
|
|
@@ -545,7 +1038,7 @@ try {
|
|
|
545
1038
|
await cmdClaim(args[0]);
|
|
546
1039
|
break;
|
|
547
1040
|
case "set":
|
|
548
|
-
cmdSet(args[0], args[1]);
|
|
1041
|
+
await cmdSet(args[0], args[1]);
|
|
549
1042
|
break;
|
|
550
1043
|
default:
|
|
551
1044
|
console.log(HELP);
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptai.credit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Earn ad-subsidized prompt credits from terminal AI agents (Claude Code). Watch a dev-tool ad while your agent works; verified watches pay your prompt's token cost in USDC.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/Vib-UX/promptai.git",
|
|
13
|
-
"directory": "product/cli"
|
|
14
|
-
},
|
|
15
10
|
"homepage": "https://promptai.credit",
|
|
16
11
|
"keywords": [
|
|
17
12
|
"claude-code",
|