@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.
- package/README.md +49 -0
- package/dist/index.js +557 -0
- 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
|
+
}
|