@promptai.credit/cli 0.1.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.
Files changed (3) hide show
  1. package/README.md +49 -0
  2. package/dist/index.js +557 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # promptai CLI
2
+
3
+ Brings the earn loop (prompt → opt-in ad during the wait → server-side verification →
4
+ credit → USDC claim) to terminal agents. Claude Code is supported today; Codex CLI is next.
5
+
6
+ ## How it works
7
+
8
+ 1. `promptai install claude` merges two hooks into `~/.claude/settings.json`:
9
+ - **UserPromptSubmit**: baselines the session's transcript watermark and (if opted
10
+ in, at most every 90s, never headless) opens the hosted rewarded-ad page
11
+ (`/watch`) in your browser. The ad plays while your agent works.
12
+ - **Stop** (async): reads the session transcript JSONL since the watermark, prices
13
+ the turn with the shared model price table, and redeems one banked
14
+ server-verified ad session against it via the API.
15
+ 2. The `/watch` page creates the ad session server-side on load, so SSV wall-clock
16
+ timing is enforced by the server; the browser countdown is cosmetic.
17
+ 3. `promptai claim` pays out your verified balance as USDC on Base Sepolia.
18
+
19
+ State lives in `~/.promptai/` (`config.json`, `state.json`, `cli.log`).
20
+
21
+ ## Setup
22
+
23
+ ```bash
24
+ npm install -g @promptai.credit/cli
25
+ promptai install claude
26
+
27
+ # then:
28
+ promptai status # device, balance, banked ad watches, recent prompts
29
+ promptai watch # open a rewarded ad now (banks a credit for later)
30
+ promptai set wallet 0x... # payout address
31
+ promptai claim # USDC on Base Sepolia
32
+ promptai set ads off # opt out any time
33
+ ```
34
+
35
+ The default server is `https://api.promptai.credit`; point elsewhere with
36
+ `promptai set server <url>`.
37
+
38
+ Developing from the repo instead: `pnpm --filter @promptai.credit/cli build`, then
39
+ `node product/cli/dist/index.js install claude` (hooks embed the absolute path).
40
+
41
+ ## Notes
42
+
43
+ - Credits only accrue for turns funded by a **verified** ad watch, enforced
44
+ server-side (same rules as the Cursor extension: one session funds one prompt,
45
+ capped at $5).
46
+ - Headless environments (CI, ssh without a display) never get browser tabs and
47
+ simply skip crediting.
48
+ - Uninstall with `promptai uninstall claude`; other hooks in your settings are
49
+ preserved.
package/dist/index.js ADDED
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/claude.ts
4
+ import * as crypto3 from "node:crypto";
5
+ import * as fs3 from "node:fs";
6
+ import * as os2 from "node:os";
7
+ import * as path2 from "node:path";
8
+
9
+ // src/api.ts
10
+ import * as crypto from "node:crypto";
11
+ var TIMEOUT_MS = 5e3;
12
+ async function request(url, init) {
13
+ const response = await fetch(url, {
14
+ ...init,
15
+ signal: AbortSignal.timeout(TIMEOUT_MS)
16
+ });
17
+ const json = await response.json().catch(() => ({}));
18
+ if (!response.ok) {
19
+ throw new Error(json.error ?? `${url} returned ${response.status}`);
20
+ }
21
+ return json;
22
+ }
23
+ function post(serverUrl, pathname, body) {
24
+ return request(`${serverUrl}${pathname}`, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify(body)
28
+ });
29
+ }
30
+ function watchUrl(serverUrl, deviceId, source) {
31
+ return `${serverUrl}/watch?device=${encodeURIComponent(deviceId)}&source=${encodeURIComponent(source)}`;
32
+ }
33
+ function listVerifiedSessions(serverUrl, deviceId) {
34
+ return request(`${serverUrl}/ads/verified/${deviceId}`);
35
+ }
36
+ function redeemCredit(serverUrl, params) {
37
+ return post(serverUrl, "/credits", params);
38
+ }
39
+ function fetchBalance(serverUrl, deviceId) {
40
+ return request(`${serverUrl}/balance/${deviceId}`);
41
+ }
42
+ function claim(serverUrl, params) {
43
+ return post(serverUrl, "/claim", {
44
+ ...params,
45
+ idempotencyKey: crypto.randomUUID()
46
+ });
47
+ }
48
+
49
+ // src/browser.ts
50
+ import { spawn } from "node:child_process";
51
+ function hasDisplay() {
52
+ if (process.platform === "darwin" || process.platform === "win32") return true;
53
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
54
+ }
55
+ function openInBrowser(url) {
56
+ if (!hasDisplay()) return false;
57
+ const [cmd, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
58
+ try {
59
+ spawn(cmd, args2, { detached: true, stdio: "ignore" }).unref();
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ // src/config.ts
67
+ import * as crypto2 from "node:crypto";
68
+ import * as fs from "node:fs";
69
+ import * as os from "node:os";
70
+ import * as path from "node:path";
71
+ var DIR = path.join(os.homedir(), ".promptai");
72
+ var CONFIG_FILE = path.join(DIR, "config.json");
73
+ var STATE_FILE = path.join(DIR, "state.json");
74
+ var LOG_FILE = path.join(DIR, "cli.log");
75
+ var MAX_PROMPTS = 100;
76
+ var MAX_WATERMARKS = 200;
77
+ function promptaiDir() {
78
+ fs.mkdirSync(DIR, { recursive: true });
79
+ return DIR;
80
+ }
81
+ function readJson(file, fallback) {
82
+ try {
83
+ return { ...fallback, ...JSON.parse(fs.readFileSync(file, "utf8")) };
84
+ } catch {
85
+ return fallback;
86
+ }
87
+ }
88
+ function loadConfig() {
89
+ const config = readJson(CONFIG_FILE, {
90
+ deviceId: "",
91
+ serverUrl: "https://api.promptai.credit",
92
+ wallet: "",
93
+ adsOptIn: true
94
+ });
95
+ if (!config.deviceId) {
96
+ config.deviceId = crypto2.randomUUID();
97
+ saveConfig(config);
98
+ }
99
+ return config;
100
+ }
101
+ function saveConfig(config) {
102
+ promptaiDir();
103
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
104
+ }
105
+ function loadState() {
106
+ return readJson(STATE_FILE, {
107
+ watermarks: {},
108
+ lastAdOpenedAt: 0,
109
+ prompts: []
110
+ });
111
+ }
112
+ function saveState(state) {
113
+ promptaiDir();
114
+ state.prompts = state.prompts.slice(0, MAX_PROMPTS);
115
+ const keys = Object.keys(state.watermarks);
116
+ if (keys.length > MAX_WATERMARKS) {
117
+ for (const key of keys.slice(0, keys.length - MAX_WATERMARKS)) {
118
+ delete state.watermarks[key];
119
+ }
120
+ }
121
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + "\n");
122
+ }
123
+ function log(message) {
124
+ promptaiDir();
125
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
126
+ `;
127
+ try {
128
+ fs.appendFileSync(LOG_FILE, line);
129
+ const { size } = fs.statSync(LOG_FILE);
130
+ if (size > 512 * 1024) {
131
+ const tail = fs.readFileSync(LOG_FILE, "utf8").slice(-256 * 1024);
132
+ fs.writeFileSync(LOG_FILE, tail);
133
+ }
134
+ } catch {
135
+ }
136
+ }
137
+
138
+ // src/pricing.ts
139
+ var TABLE = [
140
+ { match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
141
+ { match: ["opus"], inputPerMtok: 15, outputPerMtok: 75 },
142
+ { match: ["sonnet"], inputPerMtok: 3, outputPerMtok: 15 },
143
+ { match: ["haiku"], inputPerMtok: 0.8, outputPerMtok: 4 },
144
+ { match: ["gpt-5", "gpt5", "codex"], inputPerMtok: 1.25, outputPerMtok: 10 },
145
+ { match: ["gpt-4", "gpt4", "o3-", "o4-"], inputPerMtok: 2, outputPerMtok: 8 },
146
+ { match: ["gemini"], inputPerMtok: 1.25, outputPerMtok: 10 },
147
+ { match: ["grok"], inputPerMtok: 3, outputPerMtok: 15 },
148
+ { match: ["composer"], inputPerMtok: 1.25, outputPerMtok: 10 },
149
+ { match: ["deepseek", "kimi", "qwen"], inputPerMtok: 0.6, outputPerMtok: 2.5 }
150
+ ];
151
+ var DEFAULT_PRICE = { inputPerMtok: 2, outputPerMtok: 8 };
152
+ function priceFor(model) {
153
+ const m = (model ?? "").toLowerCase();
154
+ for (const row of TABLE) {
155
+ if (row.match.some((s) => m.includes(s))) {
156
+ return { inputPerMtok: row.inputPerMtok, outputPerMtok: row.outputPerMtok };
157
+ }
158
+ }
159
+ return DEFAULT_PRICE;
160
+ }
161
+ function costUsd(model, inputTokens, outputTokens) {
162
+ const p = priceFor(model);
163
+ const usd = (inputTokens * p.inputPerMtok + outputTokens * p.outputPerMtok) / 1e6;
164
+ return Math.round(usd * 1e6) / 1e6;
165
+ }
166
+
167
+ // src/transcript.ts
168
+ import * as fs2 from "node:fs";
169
+ function readTranscriptUsage(transcriptPath, watermark) {
170
+ const raw = fs2.readFileSync(transcriptPath, "utf8");
171
+ const byMessageId = /* @__PURE__ */ new Map();
172
+ let parsedCount = 0;
173
+ for (const line of raw.split("\n")) {
174
+ if (!line.trim()) continue;
175
+ let entry;
176
+ try {
177
+ entry = JSON.parse(line);
178
+ } catch {
179
+ continue;
180
+ }
181
+ if (entry.type !== "assistant") continue;
182
+ const message = entry.message;
183
+ const usage = message?.usage;
184
+ if (!usage) continue;
185
+ parsedCount += 1;
186
+ const ts = Date.parse(String(entry.timestamp ?? "")) || 0;
187
+ const inputTokens = Math.round(
188
+ (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) * 1.25 + (usage.cache_read_input_tokens ?? 0) * 0.1
189
+ );
190
+ const id = message?.id ?? `line-${parsedCount}`;
191
+ byMessageId.set(id, {
192
+ ts,
193
+ messageId: id,
194
+ model: message?.model ?? "unknown",
195
+ inputTokens,
196
+ outputTokens: usage.output_tokens ?? 0
197
+ });
198
+ }
199
+ const entries = [...byMessageId.values()].sort((a, b) => a.ts - b.ts);
200
+ const hasTimestamps = entries.some((e) => e.ts > 0);
201
+ const fresh = hasTimestamps ? entries.filter((e) => e.ts > watermark.lastTs) : entries.slice(watermark.seenCount);
202
+ return {
203
+ inputTokens: fresh.reduce((s, e) => s + e.inputTokens, 0),
204
+ outputTokens: fresh.reduce((s, e) => s + e.outputTokens, 0),
205
+ model: fresh.at(-1)?.model ?? entries.at(-1)?.model ?? "unknown",
206
+ newEntries: fresh.length,
207
+ lastTs: entries.reduce((m, e) => Math.max(m, e.ts), watermark.lastTs),
208
+ seenCount: entries.length
209
+ };
210
+ }
211
+
212
+ // src/claude.ts
213
+ var SOURCE = "claude-code";
214
+ var HOOK_MARKER = "promptai";
215
+ var MAX_CREDIT_PER_AD_USD = 5;
216
+ var AD_MIN_INTERVAL_MS = 9e4;
217
+ var SETTLE_RETRIES = [400, 800, 1500];
218
+ function claudeSettingsPath() {
219
+ return path2.join(os2.homedir(), ".claude", "settings.json");
220
+ }
221
+ function hookCommand() {
222
+ let script = path2.resolve(process.argv[1] ?? "");
223
+ if (script.endsWith(".ts")) {
224
+ const dist = path2.resolve(path2.dirname(script), "..", "dist", "index.js");
225
+ if (!fs3.existsSync(dist)) {
226
+ throw new Error(
227
+ `Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
228
+ );
229
+ }
230
+ script = dist;
231
+ }
232
+ return `"${process.execPath}" "${script}" hook`;
233
+ }
234
+ function installClaudeHooks() {
235
+ const settingsPath = claudeSettingsPath();
236
+ fs3.mkdirSync(path2.dirname(settingsPath), { recursive: true });
237
+ let settings = {};
238
+ if (fs3.existsSync(settingsPath)) {
239
+ try {
240
+ settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
241
+ } catch {
242
+ fs3.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
243
+ settings = {};
244
+ }
245
+ }
246
+ settings.hooks = settings.hooks ?? {};
247
+ const command2 = hookCommand();
248
+ let changed = false;
249
+ for (const event of ["UserPromptSubmit", "Stop"]) {
250
+ const groups = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
251
+ let ours = groups.flatMap((g) => g.hooks ?? []).find((h) => typeof h.command === "string" && h.command.includes(HOOK_MARKER));
252
+ if (!ours) {
253
+ const hook = event === "Stop" ? { type: "command", command: command2, async: true } : { type: "command", command: command2 };
254
+ groups.push({ hooks: [hook] });
255
+ settings.hooks[event] = groups;
256
+ changed = true;
257
+ } else if (ours.command !== command2) {
258
+ ours.command = command2;
259
+ changed = true;
260
+ }
261
+ }
262
+ if (changed || !fs3.existsSync(settingsPath)) {
263
+ fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
264
+ }
265
+ return { changed, settingsPath };
266
+ }
267
+ function uninstallClaudeHooks() {
268
+ const settingsPath = claudeSettingsPath();
269
+ if (!fs3.existsSync(settingsPath)) return { changed: false };
270
+ let settings;
271
+ try {
272
+ settings = JSON.parse(fs3.readFileSync(settingsPath, "utf8"));
273
+ } catch {
274
+ return { changed: false };
275
+ }
276
+ if (!settings.hooks) return { changed: false };
277
+ let changed = false;
278
+ for (const [event, groups] of Object.entries(settings.hooks)) {
279
+ if (!Array.isArray(groups)) continue;
280
+ const kept = groups.map((g) => ({
281
+ ...g,
282
+ hooks: (g.hooks ?? []).filter(
283
+ (h) => !(typeof h.command === "string" && h.command.includes(HOOK_MARKER))
284
+ )
285
+ })).filter((g) => (g.hooks?.length ?? 0) > 0 || Object.keys(g).length > 1);
286
+ if (JSON.stringify(kept) !== JSON.stringify(groups)) {
287
+ settings.hooks[event] = kept;
288
+ changed = true;
289
+ }
290
+ }
291
+ if (changed) {
292
+ fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
293
+ }
294
+ return { changed };
295
+ }
296
+ function handleUserPromptSubmit(payload) {
297
+ const config = loadConfig();
298
+ const state = loadState();
299
+ const sessionId = payload.session_id ?? "unknown";
300
+ if (!(sessionId in state.watermarks) && payload.transcript_path) {
301
+ try {
302
+ const usage = readTranscriptUsage(payload.transcript_path, { lastTs: 0, seenCount: 0 });
303
+ state.watermarks[sessionId] = { lastTs: usage.lastTs, seenCount: usage.seenCount };
304
+ } catch {
305
+ state.watermarks[sessionId] = { lastTs: 0, seenCount: 0 };
306
+ }
307
+ }
308
+ if (config.adsOptIn && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS) {
309
+ const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
310
+ if (openInBrowser(url)) {
311
+ state.lastAdOpenedAt = Date.now();
312
+ log(`[claude] opened ad tab for session=${sessionId}`);
313
+ } else if (!hasDisplay()) {
314
+ log(`[claude] headless environment, skipped ad tab`);
315
+ }
316
+ }
317
+ saveState(state);
318
+ }
319
+ async function handleStop(payload) {
320
+ const config = loadConfig();
321
+ const sessionId = payload.session_id ?? "unknown";
322
+ if (!payload.transcript_path) {
323
+ log(`[claude] stop without transcript_path session=${sessionId}`);
324
+ return;
325
+ }
326
+ const state = loadState();
327
+ const watermark = state.watermarks[sessionId] ?? { lastTs: 0, seenCount: 0 };
328
+ let usage;
329
+ for (const delay of SETTLE_RETRIES) {
330
+ try {
331
+ usage = readTranscriptUsage(payload.transcript_path, watermark);
332
+ if (usage.newEntries > 0) break;
333
+ } catch (err) {
334
+ log(`[claude] transcript read failed: ${String(err)}`);
335
+ }
336
+ await new Promise((r) => setTimeout(r, delay));
337
+ }
338
+ if (!usage || usage.newEntries === 0) {
339
+ log(`[claude] no new usage for session=${sessionId}, skipping`);
340
+ return;
341
+ }
342
+ state.watermarks[sessionId] = { lastTs: usage.lastTs, seenCount: usage.seenCount };
343
+ const cost = costUsd(usage.model, usage.inputTokens, usage.outputTokens);
344
+ const promptId = crypto3.randomUUID();
345
+ let verified = false;
346
+ if (config.adsOptIn && cost > 0) {
347
+ verified = await redeemAgainstAd(config.serverUrl, config.deviceId, promptId, cost);
348
+ }
349
+ state.prompts.unshift({
350
+ id: promptId,
351
+ ts: Date.now(),
352
+ agent: SOURCE,
353
+ sessionId,
354
+ model: usage.model,
355
+ inputTokens: usage.inputTokens,
356
+ outputTokens: usage.outputTokens,
357
+ costUsd: cost,
358
+ verified
359
+ });
360
+ saveState(state);
361
+ log(
362
+ `[claude] settled session=${sessionId} model=${usage.model} in=${usage.inputTokens} out=${usage.outputTokens} cost=$${cost} verified=${verified}`
363
+ );
364
+ }
365
+ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost) {
366
+ try {
367
+ const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
368
+ if (sessions.length === 0) {
369
+ log(`[claude] no verified ad session available for prompt ${promptId}`);
370
+ return false;
371
+ }
372
+ await redeemCredit(serverUrl, {
373
+ sessionId: sessions[0].sessionId,
374
+ deviceId,
375
+ promptId,
376
+ amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD)
377
+ });
378
+ return true;
379
+ } catch (err) {
380
+ log(`[claude] credit redeem failed for prompt ${promptId}: ${String(err)}`);
381
+ return false;
382
+ }
383
+ }
384
+
385
+ // src/index.ts
386
+ var HELP = `promptai - ad-subsidized prompt credits for terminal agents
387
+
388
+ Usage:
389
+ promptai install claude Wire hooks into ~/.claude/settings.json
390
+ promptai uninstall claude Remove our hooks (other hooks untouched)
391
+ promptai status Device, balance, banked ad credits, recent prompts
392
+ promptai watch Open a rewarded ad in the browser now
393
+ promptai claim [address] Claim your verified balance as USDC (Base Sepolia)
394
+ promptai set wallet 0x... Set the payout wallet
395
+ promptai set server <url> Point at a different API server
396
+ promptai set ads on|off Toggle the rewarded-ads opt-in
397
+ promptai hook (internal) invoked by agent hooks, JSON on stdin
398
+ `;
399
+ function readStdin() {
400
+ return new Promise((resolve2) => {
401
+ const chunks = [];
402
+ process.stdin.on("data", (c) => chunks.push(c));
403
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
404
+ });
405
+ }
406
+ async function cmdHook() {
407
+ let payload = {};
408
+ try {
409
+ payload = JSON.parse(await readStdin());
410
+ } catch {
411
+ return;
412
+ }
413
+ const event = payload.hook_event_name ?? "";
414
+ try {
415
+ if (event === "UserPromptSubmit") {
416
+ handleUserPromptSubmit(payload);
417
+ } else if (event === "Stop") {
418
+ await handleStop(payload);
419
+ }
420
+ } catch (err) {
421
+ log(`hook ${event} failed: ${String(err)}`);
422
+ }
423
+ }
424
+ async function cmdStatus() {
425
+ const config = loadConfig();
426
+ const state = loadState();
427
+ console.log(`device ${config.deviceId}`);
428
+ console.log(`server ${config.serverUrl}`);
429
+ console.log(`wallet ${config.wallet || "(not set - promptai set wallet 0x...)"}`);
430
+ console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
431
+ try {
432
+ const [balance, verified] = await Promise.all([
433
+ fetchBalance(config.serverUrl, config.deviceId),
434
+ listVerifiedSessions(config.serverUrl, config.deviceId)
435
+ ]);
436
+ console.log(
437
+ `balance $${balance.balanceUsd.toFixed(4)} (earned $${balance.earnedUsd.toFixed(4)}, claimed $${balance.claimedUsd.toFixed(4)})`
438
+ );
439
+ console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
440
+ } catch (err) {
441
+ console.log(`balance unavailable (${String(err)})`);
442
+ }
443
+ if (state.prompts.length > 0) {
444
+ console.log("\nrecent prompts:");
445
+ for (const p of state.prompts.slice(0, 8)) {
446
+ const when = new Date(p.ts).toISOString().slice(0, 16).replace("T", " ");
447
+ const badge = p.verified ? "ad verified" : "no ad";
448
+ console.log(
449
+ ` ${when} ${p.model} in=${p.inputTokens} out=${p.outputTokens} $${p.costUsd.toFixed(4)} [${badge}]`
450
+ );
451
+ }
452
+ }
453
+ }
454
+ async function cmdWatch() {
455
+ const config = loadConfig();
456
+ const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
457
+ if (openInBrowser(url)) {
458
+ console.log(`Opened ${url}`);
459
+ console.log("Watch the full ad; the credit banks automatically once the server verifies it.");
460
+ } else if (!hasDisplay()) {
461
+ console.log("No display detected. Open this URL in any browser:");
462
+ console.log(url);
463
+ } else {
464
+ console.log(`Could not launch a browser. Open manually: ${url}`);
465
+ }
466
+ }
467
+ async function cmdClaim(addressArg) {
468
+ const config = loadConfig();
469
+ const address = addressArg ?? config.wallet;
470
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
471
+ console.error("Set a valid wallet first: promptai set wallet 0x... (or pass one as an argument)");
472
+ process.exitCode = 1;
473
+ return;
474
+ }
475
+ const balance = await fetchBalance(config.serverUrl, config.deviceId);
476
+ if (balance.balanceUsd < 1e-6) {
477
+ console.log("Nothing to claim yet - watch an ad (promptai watch) and run some agent prompts first.");
478
+ return;
479
+ }
480
+ console.log(`Claiming $${balance.balanceUsd.toFixed(6)} USDC to ${address}...`);
481
+ const result = await claim(config.serverUrl, {
482
+ address,
483
+ amountUsd: balance.balanceUsd,
484
+ deviceId: config.deviceId
485
+ });
486
+ console.log(`Sent: ${result.txHash}`);
487
+ console.log(result.explorerUrl);
488
+ }
489
+ function cmdSet(key, value) {
490
+ const config = loadConfig();
491
+ if (key === "wallet" && value && /^0x[0-9a-fA-F]{40}$/.test(value)) {
492
+ config.wallet = value;
493
+ } else if (key === "server" && value && /^https?:\/\//.test(value)) {
494
+ config.serverUrl = value.replace(/\/$/, "");
495
+ } else if (key === "ads" && (value === "on" || value === "off")) {
496
+ config.adsOptIn = value === "on";
497
+ } else {
498
+ console.error("Usage: promptai set wallet 0x... | set server <url> | set ads on|off");
499
+ process.exitCode = 1;
500
+ return;
501
+ }
502
+ saveConfig(config);
503
+ console.log(`${key} updated.`);
504
+ }
505
+ function cmdInstall(agent) {
506
+ if (agent !== "claude" && agent !== "claude-code") {
507
+ console.error("Supported agents: claude (Codex CLI coming next). Usage: promptai install claude");
508
+ process.exitCode = 1;
509
+ return;
510
+ }
511
+ const { changed, settingsPath } = installClaudeHooks();
512
+ console.log(
513
+ changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
514
+ );
515
+ console.log("Claude Code picks them up on its next session. Try: promptai watch, then run a prompt.");
516
+ }
517
+ function cmdUninstall(agent) {
518
+ if (agent !== "claude" && agent !== "claude-code") {
519
+ console.error("Usage: promptai uninstall claude");
520
+ process.exitCode = 1;
521
+ return;
522
+ }
523
+ const { changed } = uninstallClaudeHooks();
524
+ console.log(changed ? "Hooks removed." : "No promptai hooks found.");
525
+ }
526
+ var [, , command, ...args] = process.argv;
527
+ try {
528
+ switch (command) {
529
+ case "hook":
530
+ await cmdHook();
531
+ break;
532
+ case "install":
533
+ cmdInstall(args[0]);
534
+ break;
535
+ case "uninstall":
536
+ cmdUninstall(args[0]);
537
+ break;
538
+ case "status":
539
+ await cmdStatus();
540
+ break;
541
+ case "watch":
542
+ await cmdWatch();
543
+ break;
544
+ case "claim":
545
+ await cmdClaim(args[0]);
546
+ break;
547
+ case "set":
548
+ cmdSet(args[0], args[1]);
549
+ break;
550
+ default:
551
+ console.log(HELP);
552
+ if (command && command !== "help" && command !== "--help") process.exitCode = 1;
553
+ }
554
+ } catch (err) {
555
+ console.error(err instanceof Error ? err.message : String(err));
556
+ process.exitCode = 1;
557
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@promptai.credit/cli",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
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
+ "type": "module",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Vib-UX/promptai.git",
13
+ "directory": "product/cli"
14
+ },
15
+ "homepage": "https://promptai.credit",
16
+ "keywords": [
17
+ "claude-code",
18
+ "ai",
19
+ "agents",
20
+ "prompt-credits",
21
+ "rewarded-ads",
22
+ "usdc",
23
+ "promptai"
24
+ ],
25
+ "bin": {
26
+ "promptai": "dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "dev": "tsx src/index.ts",
34
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\"",
35
+ "prepublishOnly": "pnpm build",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.14.0",
43
+ "esbuild": "^0.24.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.5.0"
46
+ }
47
+ }