@promptai.credit/cli 0.4.2 → 0.4.3
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/claude-plugin/README.md +4 -0
- package/claude-plugin/hooks/ads.tsx +210 -38
- package/dist/index.js +58 -16
- package/package.json +2 -2
package/claude-plugin/README.md
CHANGED
|
@@ -31,3 +31,7 @@ CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir /path/to/product/claude-
|
|
|
31
31
|
terminal. It is fetched with `curl` and converted with `sips` (macOS) or ImageMagick;
|
|
32
32
|
SVGs need `qlmanage` (macOS) or `rsvg-convert`. If any step is missing the card simply
|
|
33
33
|
draws without a logo. Results are cached under `~/.promptai/logos/`.
|
|
34
|
+
- Jev ticker: the plugin polls `GET /stocks/ticker` (Jev's current top picks, with
|
|
35
|
+
Robinhood Chain prices and 24h change from DefiLlama). The picks show on the ad
|
|
36
|
+
card, the credited slices show on the verified strip, and a compact ticker rides the
|
|
37
|
+
prompt hint while the agent works. If the server has Jev off, the ticker stays hidden.
|
|
@@ -7,21 +7,28 @@
|
|
|
7
7
|
* 3. ui.render AbovePrompt — compact sponsored card + countdown
|
|
8
8
|
* 4. clock tick — when minWatchMs elapsed, POST /ads/complete
|
|
9
9
|
* 5. CTA press — beacon click, open destination in OS browser
|
|
10
|
+
* 6. Jev ticker — GET /stocks/ticker (top picks + Robinhood Chain quotes),
|
|
11
|
+
* drawn on the ad card and on the prompt hint while working
|
|
10
12
|
*
|
|
11
13
|
* Stop / transcript pricing / credit redeem stay on the command-hook CLI path.
|
|
12
14
|
*
|
|
13
15
|
* Requires CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 until Mods are generally available.
|
|
14
16
|
*/
|
|
15
|
-
import type { EngineInterface as Engine, Register, Timer } from "claude-code";
|
|
17
|
+
import type { EngineInterface as Engine, Register, RenderChildren, Timer } from "claude-code";
|
|
16
18
|
import { WORK_PX, bitmapToCells, decodeBase64, hashKey, parseBmp, type LogoCells } from "./logo.ts";
|
|
17
19
|
|
|
18
20
|
const SOURCE = "claude-mods";
|
|
19
21
|
/** Logo cell grid: half-blocks, so 12 columns × 6 rows is a square of 12×12 pixels. */
|
|
20
22
|
const LOGO_COLS = 12;
|
|
21
23
|
const LOGO_ROWS = 6;
|
|
24
|
+
/** Stock logos in the Jev tiles: 6×3 cells, one tile tall. */
|
|
25
|
+
const STOCK_LOGO_COLS = 6;
|
|
26
|
+
const STOCK_LOGO_ROWS = 3;
|
|
22
27
|
const HEARTBEAT_FILE = "native-plugin.json";
|
|
23
28
|
/** How long the CLI treats a heartbeat as "plugin is live" before falling back to /watch. */
|
|
24
29
|
const HEARTBEAT_FRESH_MS = 2 * 60 * 60 * 1000;
|
|
30
|
+
/** Refetch the Jev ticker at most this often (the server caches it for 60s). */
|
|
31
|
+
const TICKER_STALE_MS = 5 * 60 * 1000;
|
|
25
32
|
|
|
26
33
|
interface AdCreative {
|
|
27
34
|
id: string;
|
|
@@ -42,6 +49,27 @@ interface PromptaiConfig {
|
|
|
42
49
|
adsOptIn: boolean;
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
interface TickerName {
|
|
53
|
+
ticker: string;
|
|
54
|
+
name: string;
|
|
55
|
+
logoUrl?: string;
|
|
56
|
+
price: number | null;
|
|
57
|
+
change24h: number | null;
|
|
58
|
+
weight: number | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface JevTicker {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
rewardUsd: number;
|
|
64
|
+
picks: TickerName[];
|
|
65
|
+
names: TickerName[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface GrantedStock {
|
|
69
|
+
ticker: string;
|
|
70
|
+
notionalUsd: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
type AdPhase = "idle" | "loading" | "showing" | "verified" | "failed";
|
|
46
74
|
|
|
47
75
|
interface ActiveAd {
|
|
@@ -52,12 +80,19 @@ interface ActiveAd {
|
|
|
52
80
|
error?: string;
|
|
53
81
|
tick?: Timer;
|
|
54
82
|
logo?: LogoCells;
|
|
83
|
+
/** Paper stock slices granted on this verified watch (Jev inside promptai). */
|
|
84
|
+
stocks?: GrantedStock[];
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
const active: ActiveAd = { phase: "idle" };
|
|
58
88
|
let config: PromptaiConfig | null = null;
|
|
59
89
|
let homeDir = "";
|
|
60
90
|
let startingSession = false;
|
|
91
|
+
let jev: JevTicker | null = null;
|
|
92
|
+
let jevFetchedAt = 0;
|
|
93
|
+
let jevFetching = false;
|
|
94
|
+
/** Stock logo cells by ticker; null once a load failed so it is not retried. */
|
|
95
|
+
const stockLogos = new Map<string, LogoCells | null>();
|
|
61
96
|
|
|
62
97
|
function configPath(): string {
|
|
63
98
|
return `${homeDir}/.promptai/config.json`;
|
|
@@ -159,6 +194,7 @@ function resetAd(phase: AdPhase = "idle"): void {
|
|
|
159
194
|
active.shownAt = undefined;
|
|
160
195
|
active.error = undefined;
|
|
161
196
|
active.logo = undefined;
|
|
197
|
+
active.stocks = undefined;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
200
|
function parseBody<T>(text: string): T {
|
|
@@ -193,16 +229,18 @@ async function completeAd($: Engine): Promise<void> {
|
|
|
193
229
|
$.ui.invalidate("ui.render");
|
|
194
230
|
return;
|
|
195
231
|
}
|
|
232
|
+
const body = parseBody<{
|
|
233
|
+
verified?: boolean;
|
|
234
|
+
stocks?: Array<{ ticker?: string; notionalUsd?: number | string }>;
|
|
235
|
+
}>(res.text);
|
|
236
|
+
active.stocks = (body.stocks ?? [])
|
|
237
|
+
.filter((s) => typeof s.ticker === "string" && s.ticker)
|
|
238
|
+
.map((s) => ({ ticker: s.ticker as string, notionalUsd: Number(s.notionalUsd) || 0 }))
|
|
239
|
+
.slice(0, 5);
|
|
196
240
|
active.phase = "verified";
|
|
197
241
|
clearTick();
|
|
242
|
+
// The card stays open once banked; Hide or the next prompt's ad replaces it.
|
|
198
243
|
$.ui.invalidate("ui.render");
|
|
199
|
-
// Auto-collapse the verified strip after a short beat.
|
|
200
|
-
$.clock.after(8_000, () => {
|
|
201
|
-
if (active.phase === "verified") {
|
|
202
|
-
resetAd("idle");
|
|
203
|
-
$.ui.invalidate("ui.render");
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
244
|
} catch (err) {
|
|
207
245
|
active.phase = "failed";
|
|
208
246
|
active.error = String(err);
|
|
@@ -380,7 +418,8 @@ async function beginAdSession($: Engine): Promise<void> {
|
|
|
380
418
|
if (active.phase === "loading" || active.phase === "showing") return;
|
|
381
419
|
|
|
382
420
|
startingSession = true;
|
|
383
|
-
|
|
421
|
+
// A banked card stays open until now; drop its logo and credited stocks.
|
|
422
|
+
resetAd("loading");
|
|
384
423
|
$.ui.invalidate("ui.render");
|
|
385
424
|
|
|
386
425
|
try {
|
|
@@ -432,6 +471,73 @@ async function beginAdSession($: Engine): Promise<void> {
|
|
|
432
471
|
}
|
|
433
472
|
}
|
|
434
473
|
|
|
474
|
+
// --- Jev ticker ---
|
|
475
|
+
|
|
476
|
+
/** Fire-and-forget; keeps the last good ticker when a refresh fails. */
|
|
477
|
+
async function refreshTicker($: Engine, force = false): Promise<void> {
|
|
478
|
+
if (!config || jevFetching) return;
|
|
479
|
+
if (!force && Date.now() - jevFetchedAt < TICKER_STALE_MS) return;
|
|
480
|
+
jevFetching = true;
|
|
481
|
+
try {
|
|
482
|
+
const res = await $.http.fetch(`${config.serverUrl}/stocks/ticker`, { method: "GET" });
|
|
483
|
+
if (!res.ok) return;
|
|
484
|
+
const body = parseBody<Partial<JevTicker>>(res.text);
|
|
485
|
+
if (!Array.isArray(body.names)) return;
|
|
486
|
+
jev = {
|
|
487
|
+
enabled: Boolean(body.enabled),
|
|
488
|
+
rewardUsd: Number(body.rewardUsd) || 0,
|
|
489
|
+
picks: Array.isArray(body.picks) ? body.picks : [],
|
|
490
|
+
names: body.names,
|
|
491
|
+
};
|
|
492
|
+
jevFetchedAt = Date.now();
|
|
493
|
+
$.ui.invalidate("ui.render");
|
|
494
|
+
for (const name of jev.names) void loadStockLogo($, name);
|
|
495
|
+
} catch {
|
|
496
|
+
// ticker is decoration; never surface a fetch error
|
|
497
|
+
} finally {
|
|
498
|
+
jevFetching = false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Fire-and-forget: tiles draw a text badge until the logo lands. */
|
|
503
|
+
async function loadStockLogo($: Engine, name: TickerName): Promise<void> {
|
|
504
|
+
if (!name.logoUrl || stockLogos.has(name.ticker)) return;
|
|
505
|
+
stockLogos.set(name.ticker, null);
|
|
506
|
+
const logo = await loadLogoCells($, {
|
|
507
|
+
url: name.logoUrl,
|
|
508
|
+
home: homeDir,
|
|
509
|
+
columns: STOCK_LOGO_COLS,
|
|
510
|
+
rows: STOCK_LOGO_ROWS,
|
|
511
|
+
});
|
|
512
|
+
if (logo) {
|
|
513
|
+
stockLogos.set(name.ticker, logo);
|
|
514
|
+
$.ui.invalidate("ui.render");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function tickerQuote(ticker: string): TickerName | undefined {
|
|
519
|
+
return jev?.names.find((n) => n.ticker === ticker);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function fmtPrice(price: number | null): string {
|
|
523
|
+
if (price == null) return "";
|
|
524
|
+
return price >= 1000 ? price.toFixed(0) : price.toFixed(2);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function fmtChange(change: number | null): { text: string; color: string } | null {
|
|
528
|
+
if (change == null) return null;
|
|
529
|
+
const up = change >= 0;
|
|
530
|
+
return { text: `${up ? "▲" : "▼"}${Math.abs(change).toFixed(1)}%`, color: up ? C.green : C.red };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Plain-text ticker for the one-string prompt hint: `NVDA 182.40 ▲1.2% · TSLA …`. */
|
|
534
|
+
function tickerLine(): string {
|
|
535
|
+
const names = jev?.picks.length ? jev.picks : [];
|
|
536
|
+
return names
|
|
537
|
+
.map((n) => [n.ticker, fmtPrice(n.price), fmtChange(n.change24h)?.text ?? ""].filter(Boolean).join(" "))
|
|
538
|
+
.join(" · ");
|
|
539
|
+
}
|
|
540
|
+
|
|
435
541
|
function remainingSeconds($: Engine): number {
|
|
436
542
|
if (!active.ad || active.shownAt == null) return 0;
|
|
437
543
|
const left = active.ad.minWatchMs - (Date.now() - active.shownAt);
|
|
@@ -444,6 +550,7 @@ export const register: Register = (on) => {
|
|
|
444
550
|
config = await loadConfig($);
|
|
445
551
|
if (config?.adsOptIn) {
|
|
446
552
|
await writeHeartbeat($);
|
|
553
|
+
void refreshTicker($, true);
|
|
447
554
|
}
|
|
448
555
|
return next(e);
|
|
449
556
|
});
|
|
@@ -453,16 +560,67 @@ export const register: Register = (on) => {
|
|
|
453
560
|
if (config?.adsOptIn) {
|
|
454
561
|
void writeHeartbeat($);
|
|
455
562
|
void beginAdSession($);
|
|
563
|
+
void refreshTicker($);
|
|
456
564
|
}
|
|
457
565
|
return next(e);
|
|
458
566
|
});
|
|
459
567
|
|
|
568
|
+
// While the agent works and no card is up, ride Jev's picks on the hint line.
|
|
569
|
+
on("ui.render", { component: "PromptHint" }, ($, e, next) => {
|
|
570
|
+
if (!config?.adsOptIn || active.phase !== "idle" || !e.props.isWorking || e.props.isDraft) {
|
|
571
|
+
return next(e);
|
|
572
|
+
}
|
|
573
|
+
const line = tickerLine();
|
|
574
|
+
if (!line) return next(e);
|
|
575
|
+
const hint = e.props.hint ? `${e.props.hint} · ` : "";
|
|
576
|
+
return next({ ...e, props: { ...e.props, hint: `${hint}Jev ${line}` } });
|
|
577
|
+
});
|
|
578
|
+
|
|
460
579
|
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
|
|
461
580
|
if (!config?.adsOptIn || active.phase === "idle") {
|
|
462
581
|
return next(e);
|
|
463
582
|
}
|
|
464
583
|
|
|
465
584
|
const t = await $.ui.resolve(e);
|
|
585
|
+
const Raster = "Raster" in t ? t.Raster : null;
|
|
586
|
+
|
|
587
|
+
// One Jev stock tile: logo, then ticker / quote / detail stacked beside it.
|
|
588
|
+
const stockTile = (ticker: string, detail: string, detailColor: string) => {
|
|
589
|
+
const quote = tickerQuote(ticker);
|
|
590
|
+
const change = fmtChange(quote?.change24h ?? null);
|
|
591
|
+
const logo = stockLogos.get(ticker);
|
|
592
|
+
return (
|
|
593
|
+
<t.Box key={`tile-${ticker}`} gap={1}>
|
|
594
|
+
{Raster && logo ? (
|
|
595
|
+
<Raster key={`logo-${ticker}`} columns={logo.columns} rows={logo.rows} cells={logo.cells} />
|
|
596
|
+
) : (
|
|
597
|
+
<t.Box width={STOCK_LOGO_COLS} height={STOCK_LOGO_ROWS} justifyContent="center" alignItems="center">
|
|
598
|
+
<t.Text color={C.dim} bold>{ticker.slice(0, 2)}</t.Text>
|
|
599
|
+
</t.Box>
|
|
600
|
+
)}
|
|
601
|
+
<t.Box flexDirection="column">
|
|
602
|
+
<t.Text color={C.fg} bold>{ticker}</t.Text>
|
|
603
|
+
<t.Box gap={1}>
|
|
604
|
+
{quote?.price != null ? <t.Text color={C.soft}>{fmtPrice(quote.price)}</t.Text> : null}
|
|
605
|
+
{change ? <t.Text color={change.color}>{change.text}</t.Text> : null}
|
|
606
|
+
</t.Box>
|
|
607
|
+
<t.Text color={detailColor}>{detail}</t.Text>
|
|
608
|
+
</t.Box>
|
|
609
|
+
</t.Box>
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const jevSection = (title: string, aside: string, tiles: RenderChildren[]) => (
|
|
614
|
+
<t.Box flexDirection="column" borderStyle="round" borderColor={C.faint} paddingX={1} marginTop={1}>
|
|
615
|
+
<t.Box justifyContent="space-between">
|
|
616
|
+
<t.Text color={C.amber} bold>{`◇ ${title}`}</t.Text>
|
|
617
|
+
<t.Text color={C.dim}>{aside}</t.Text>
|
|
618
|
+
</t.Box>
|
|
619
|
+
<t.Box gap={4} flexWrap="wrap" marginTop={1}>
|
|
620
|
+
{tiles}
|
|
621
|
+
</t.Box>
|
|
622
|
+
</t.Box>
|
|
623
|
+
);
|
|
466
624
|
|
|
467
625
|
if (active.phase === "loading") {
|
|
468
626
|
return (
|
|
@@ -482,26 +640,8 @@ export const register: Register = (on) => {
|
|
|
482
640
|
);
|
|
483
641
|
}
|
|
484
642
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
<t.Box borderStyle="round" borderColor={C.green} paddingX={1} gap={1}>
|
|
488
|
-
<t.Text color={C.green} bold>✓ verified</t.Text>
|
|
489
|
-
<t.Text color={C.soft}>credit banked</t.Text>
|
|
490
|
-
<t.Text color={C.faint}>·</t.Text>
|
|
491
|
-
<t.Button
|
|
492
|
-
key="hide"
|
|
493
|
-
label="Hide"
|
|
494
|
-
plain
|
|
495
|
-
onPress={() => {
|
|
496
|
-
resetAd("idle");
|
|
497
|
-
$.ui.invalidate("ui.render");
|
|
498
|
-
}}
|
|
499
|
-
/>
|
|
500
|
-
</t.Box>
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (active.phase !== "showing" || !active.ad || active.shownAt == null) {
|
|
643
|
+
const verified = active.phase === "verified";
|
|
644
|
+
if ((active.phase !== "showing" && !verified) || !active.ad || active.shownAt == null) {
|
|
505
645
|
return next(e);
|
|
506
646
|
}
|
|
507
647
|
|
|
@@ -511,8 +651,8 @@ export const register: Register = (on) => {
|
|
|
511
651
|
const secs = remainingSeconds($);
|
|
512
652
|
const shownLines = (ad.lines ?? []).slice(0, 4);
|
|
513
653
|
|
|
514
|
-
const Raster = "Raster" in t ? t.Raster : null;
|
|
515
654
|
const logo = active.logo;
|
|
655
|
+
const granted = verified ? (active.stocks ?? []) : [];
|
|
516
656
|
|
|
517
657
|
const details = (
|
|
518
658
|
<t.Box flexDirection="column" flexGrow={1}>
|
|
@@ -533,7 +673,7 @@ export const register: Register = (on) => {
|
|
|
533
673
|
);
|
|
534
674
|
|
|
535
675
|
return (
|
|
536
|
-
<t.Box flexDirection="column" borderStyle="round" borderColor={C.faint} paddingX={1}>
|
|
676
|
+
<t.Box flexDirection="column" borderStyle="round" borderColor={verified ? C.green : C.faint} paddingX={1}>
|
|
537
677
|
{Raster && logo ? (
|
|
538
678
|
<t.Box gap={2}>
|
|
539
679
|
<Raster key="logo" columns={logo.columns} rows={logo.rows} cells={logo.cells} />
|
|
@@ -542,14 +682,46 @@ export const register: Register = (on) => {
|
|
|
542
682
|
) : (
|
|
543
683
|
details
|
|
544
684
|
)}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
685
|
+
{granted.length
|
|
686
|
+
? jevSection(
|
|
687
|
+
"Jev credited",
|
|
688
|
+
`$${granted.reduce((sum, g) => sum + g.notionalUsd, 0).toFixed(2)} paper`,
|
|
689
|
+
granted.map((g) => stockTile(g.ticker, `+$${g.notionalUsd.toFixed(4)}`, C.green)),
|
|
690
|
+
)
|
|
691
|
+
: jev?.enabled && jev.picks.length && !verified
|
|
692
|
+
? jevSection(
|
|
693
|
+
"Jev top picks",
|
|
694
|
+
`watch → $${jev.rewardUsd.toFixed(2)} slice`,
|
|
695
|
+
jev.picks.map((p) =>
|
|
696
|
+
stockTile(p.ticker, p.weight != null ? `${Math.round(p.weight * 100)}% of book` : "", C.faint),
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
: null}
|
|
700
|
+
{verified ? (
|
|
701
|
+
<t.Box gap={1} marginTop={1}>
|
|
702
|
+
<t.Text color={C.green} bold>✓ verified</t.Text>
|
|
703
|
+
<t.Text color={C.soft}>credit banked</t.Text>
|
|
704
|
+
<t.Text color={C.faint}>·</t.Text>
|
|
705
|
+
<t.Button
|
|
706
|
+
key="hide"
|
|
707
|
+
label="Hide"
|
|
708
|
+
plain
|
|
709
|
+
onPress={() => {
|
|
710
|
+
resetAd("idle");
|
|
711
|
+
$.ui.invalidate("ui.render");
|
|
712
|
+
}}
|
|
713
|
+
/>
|
|
549
714
|
</t.Box>
|
|
550
|
-
|
|
551
|
-
<t.
|
|
552
|
-
|
|
715
|
+
) : (
|
|
716
|
+
<t.Box gap={1} marginTop={1}>
|
|
717
|
+
<t.Box>
|
|
718
|
+
<t.Text color={C.green}>{bar.filled}</t.Text>
|
|
719
|
+
<t.Text color={C.faint}>{bar.empty}</t.Text>
|
|
720
|
+
</t.Box>
|
|
721
|
+
<t.Text color={C.fg} bold>{`${secs}s`}</t.Text>
|
|
722
|
+
<t.Text color={C.faint}>opt-in rewarded ad</t.Text>
|
|
723
|
+
</t.Box>
|
|
724
|
+
)}
|
|
553
725
|
<t.Button key="cta" label={`${ad.ctaLabel || "Learn more"} →`} onPress={() => void onCtaPress($)} />
|
|
554
726
|
</t.Box>
|
|
555
727
|
);
|
package/dist/index.js
CHANGED
|
@@ -486,15 +486,18 @@ function uninstallClaudePlugin() {
|
|
|
486
486
|
|
|
487
487
|
// src/pricing.ts
|
|
488
488
|
var TABLE = [
|
|
489
|
-
{ match: ["fable"], inputPerMtok:
|
|
489
|
+
{ match: ["fable"], inputPerMtok: 10, outputPerMtok: 50 },
|
|
490
|
+
{ match: ["opus-5.5", "opus 5.5", "claude-opus-5"], inputPerMtok: 4, outputPerMtok: 20 },
|
|
490
491
|
{ match: ["opus"], inputPerMtok: 15, outputPerMtok: 75 },
|
|
491
492
|
{ match: ["sonnet"], inputPerMtok: 3, outputPerMtok: 15 },
|
|
492
493
|
{ match: ["haiku"], inputPerMtok: 0.8, outputPerMtok: 4 },
|
|
494
|
+
{ match: ["gpt-5.6", "gpt5.6", "5.6-sol", "5.6 sol"], inputPerMtok: 4, outputPerMtok: 20 },
|
|
493
495
|
{ match: ["gpt-5", "gpt5", "codex"], inputPerMtok: 1.25, outputPerMtok: 10 },
|
|
494
496
|
{ match: ["gpt-4", "gpt4", "o3-", "o4-"], inputPerMtok: 2, outputPerMtok: 8 },
|
|
497
|
+
{ match: ["gemini-3.8", "gemini 3.8", "3.8-flash", "3.8 flash"], inputPerMtok: 0.5, outputPerMtok: 5 },
|
|
495
498
|
{ match: ["gemini"], inputPerMtok: 1.25, outputPerMtok: 10 },
|
|
496
|
-
{ match: ["grok"], inputPerMtok:
|
|
497
|
-
{ match: ["composer"], inputPerMtok:
|
|
499
|
+
{ match: ["grok"], inputPerMtok: 2, outputPerMtok: 6.25 },
|
|
500
|
+
{ match: ["composer"], inputPerMtok: 0.3, outputPerMtok: 4 },
|
|
498
501
|
{ match: ["deepseek", "kimi", "qwen"], inputPerMtok: 0.6, outputPerMtok: 2.5 }
|
|
499
502
|
];
|
|
500
503
|
var DEFAULT_PRICE = { inputPerMtok: 2, outputPerMtok: 8 };
|
|
@@ -744,14 +747,21 @@ async function handleStop(payload) {
|
|
|
744
747
|
const cost = costUsd(usage.model, usage.inputTokens, usage.outputTokens);
|
|
745
748
|
const promptId = crypto3.randomUUID();
|
|
746
749
|
let verified = false;
|
|
750
|
+
let creditedUsd;
|
|
751
|
+
let stocks;
|
|
747
752
|
if (config.adsOptIn && cost > 0) {
|
|
748
|
-
|
|
753
|
+
const redeem = await redeemAgainstAd(
|
|
749
754
|
config.serverUrl,
|
|
750
755
|
config.deviceId,
|
|
751
756
|
promptId,
|
|
752
757
|
cost,
|
|
753
758
|
resolveEmail(config)
|
|
754
759
|
);
|
|
760
|
+
if (redeem) {
|
|
761
|
+
verified = true;
|
|
762
|
+
creditedUsd = redeem.creditedUsd;
|
|
763
|
+
stocks = redeem.stocks;
|
|
764
|
+
}
|
|
755
765
|
}
|
|
756
766
|
state.prompts.unshift({
|
|
757
767
|
id: promptId,
|
|
@@ -762,7 +772,9 @@ async function handleStop(payload) {
|
|
|
762
772
|
inputTokens: usage.inputTokens,
|
|
763
773
|
outputTokens: usage.outputTokens,
|
|
764
774
|
costUsd: cost,
|
|
765
|
-
verified
|
|
775
|
+
verified,
|
|
776
|
+
creditedUsd,
|
|
777
|
+
stocks
|
|
766
778
|
});
|
|
767
779
|
saveState(state);
|
|
768
780
|
log(
|
|
@@ -774,19 +786,22 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
|
|
|
774
786
|
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
775
787
|
if (sessions.length === 0) {
|
|
776
788
|
log(`[claude] no verified ad session available for prompt ${promptId}`);
|
|
777
|
-
return
|
|
789
|
+
return null;
|
|
778
790
|
}
|
|
779
|
-
await redeemCredit(serverUrl, {
|
|
791
|
+
const result = await redeemCredit(serverUrl, {
|
|
780
792
|
sessionId: sessions[0].sessionId,
|
|
781
793
|
deviceId,
|
|
782
794
|
promptId,
|
|
783
795
|
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
|
|
784
796
|
email: email || void 0
|
|
785
797
|
});
|
|
786
|
-
return
|
|
798
|
+
return {
|
|
799
|
+
creditedUsd: result.creditedUsd,
|
|
800
|
+
stocks: result.stocks ?? []
|
|
801
|
+
};
|
|
787
802
|
} catch (err) {
|
|
788
803
|
log(`[claude] credit redeem failed for prompt ${promptId}: ${String(err)}`);
|
|
789
|
-
return
|
|
804
|
+
return null;
|
|
790
805
|
}
|
|
791
806
|
}
|
|
792
807
|
|
|
@@ -982,14 +997,21 @@ async function handleCursorStop(payload) {
|
|
|
982
997
|
const cost = costUsd(model, inputTokens, outputTokens);
|
|
983
998
|
const promptId = crypto4.randomUUID();
|
|
984
999
|
let verified = false;
|
|
1000
|
+
let creditedUsd;
|
|
1001
|
+
let stocks;
|
|
985
1002
|
if (config.adsOptIn && cost > 0) {
|
|
986
|
-
|
|
1003
|
+
const redeem = await redeemAgainstAd2(
|
|
987
1004
|
config.serverUrl,
|
|
988
1005
|
config.deviceId,
|
|
989
1006
|
promptId,
|
|
990
1007
|
cost,
|
|
991
1008
|
resolveEmail(config)
|
|
992
1009
|
);
|
|
1010
|
+
if (redeem) {
|
|
1011
|
+
verified = true;
|
|
1012
|
+
creditedUsd = redeem.creditedUsd;
|
|
1013
|
+
stocks = redeem.stocks;
|
|
1014
|
+
}
|
|
993
1015
|
}
|
|
994
1016
|
state.prompts.unshift({
|
|
995
1017
|
id: promptId,
|
|
@@ -1001,7 +1023,9 @@ async function handleCursorStop(payload) {
|
|
|
1001
1023
|
outputTokens,
|
|
1002
1024
|
costUsd: cost,
|
|
1003
1025
|
verified,
|
|
1004
|
-
estimated
|
|
1026
|
+
estimated,
|
|
1027
|
+
creditedUsd,
|
|
1028
|
+
stocks
|
|
1005
1029
|
});
|
|
1006
1030
|
saveState(state);
|
|
1007
1031
|
log(
|
|
@@ -1013,19 +1037,22 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
|
|
|
1013
1037
|
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
1014
1038
|
if (sessions.length === 0) {
|
|
1015
1039
|
log(`[cursor] no verified ad session available for prompt ${promptId}`);
|
|
1016
|
-
return
|
|
1040
|
+
return null;
|
|
1017
1041
|
}
|
|
1018
|
-
await redeemCredit(serverUrl, {
|
|
1042
|
+
const result = await redeemCredit(serverUrl, {
|
|
1019
1043
|
sessionId: sessions[0].sessionId,
|
|
1020
1044
|
deviceId,
|
|
1021
1045
|
promptId,
|
|
1022
1046
|
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
|
|
1023
1047
|
email: email || void 0
|
|
1024
1048
|
});
|
|
1025
|
-
return
|
|
1049
|
+
return {
|
|
1050
|
+
creditedUsd: result.creditedUsd,
|
|
1051
|
+
stocks: result.stocks ?? []
|
|
1052
|
+
};
|
|
1026
1053
|
} catch (err) {
|
|
1027
1054
|
log(`[cursor] credit redeem failed for prompt ${promptId}: ${String(err)}`);
|
|
1028
|
-
return
|
|
1055
|
+
return null;
|
|
1029
1056
|
}
|
|
1030
1057
|
}
|
|
1031
1058
|
|
|
@@ -1209,6 +1236,14 @@ async function cmdStatus() {
|
|
|
1209
1236
|
`claims ${!balance.email ? "blocked (link an email first)" : balance.claimActivated ? "activated" : "blocked (waiting for admin activation)"}`
|
|
1210
1237
|
);
|
|
1211
1238
|
console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
|
|
1239
|
+
const stocks = balance.stocks ?? [];
|
|
1240
|
+
if (stocks.length) {
|
|
1241
|
+
console.log(
|
|
1242
|
+
`stocks ${stocks.map((s) => `${s.ticker} $${Number(s.notionalUsd).toFixed(2)}`).join(" \xB7 ")} (paper \xB7 not claimable \xB7 Jev)`
|
|
1243
|
+
);
|
|
1244
|
+
} else {
|
|
1245
|
+
console.log(`stocks none yet (Jev picks NVDA, TSLA, Google, or SpaceX after a verified watch)`);
|
|
1246
|
+
}
|
|
1212
1247
|
} catch (err) {
|
|
1213
1248
|
console.log(`balance unavailable (${String(err)})`);
|
|
1214
1249
|
}
|
|
@@ -1217,8 +1252,15 @@ async function cmdStatus() {
|
|
|
1217
1252
|
for (const p of state.prompts.slice(0, 8)) {
|
|
1218
1253
|
const when = new Date(p.ts).toISOString().slice(0, 16).replace("T", " ");
|
|
1219
1254
|
const badge = p.verified ? "ad verified" : "no ad";
|
|
1255
|
+
const stocks = p.stocks ?? [];
|
|
1256
|
+
const stockBits = stocks.map((s) => `${s.ticker} $${Number(s.notionalUsd).toFixed(2)}`).join(" \xB7 ");
|
|
1257
|
+
const extras = [
|
|
1258
|
+
p.verified ? `credit $${Number(p.creditedUsd ?? p.costUsd).toFixed(4)}` : null,
|
|
1259
|
+
stockBits || null
|
|
1260
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1220
1261
|
console.log(
|
|
1221
|
-
` ${when} ${p.model} in=${p.inputTokens} out=${p.outputTokens} $${p.costUsd.toFixed(4)} [${badge}]`
|
|
1262
|
+
` ${when} ${p.model} in=${p.inputTokens} out=${p.outputTokens} $${p.costUsd.toFixed(4)} [${badge}]` + (extras ? `
|
|
1263
|
+
${extras}` : "")
|
|
1222
1264
|
);
|
|
1223
1265
|
}
|
|
1224
1266
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptai.credit/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
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
|
|
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 in USDC and bank Jev-picked tokenised stock credits.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"homepage": "https://promptai.credit",
|