@promptai.credit/cli 0.4.1 → 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/README.md +25 -6
- package/claude-plugin/.claude-plugin/plugin.json +11 -0
- package/claude-plugin/README.md +37 -0
- package/claude-plugin/hooks/ads.tsx +729 -0
- package/claude-plugin/hooks/hooks.json +4 -0
- package/claude-plugin/hooks/logo.ts +167 -0
- package/dist/index.js +283 -79
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as fs8 from "node:fs";
|
|
5
|
+
import * as path7 from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/claude.ts
|
|
8
8
|
import * as crypto3 from "node:crypto";
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
9
|
+
import * as fs6 from "node:fs";
|
|
10
|
+
import * as os4 from "node:os";
|
|
11
|
+
import * as path5 from "node:path";
|
|
12
12
|
|
|
13
13
|
// src/api.ts
|
|
14
14
|
import * as crypto from "node:crypto";
|
|
@@ -246,15 +246,20 @@ function openInBrowser(url, side = "right") {
|
|
|
246
246
|
const chrome = chromeBin();
|
|
247
247
|
if (chrome) {
|
|
248
248
|
const profile = path2.join(promptaiDir(), "ad-window");
|
|
249
|
+
const appUrl = `${url}${url.includes("?") ? "&" : "?"}_=${Date.now()}`;
|
|
249
250
|
try {
|
|
250
251
|
spawn(
|
|
251
252
|
chrome,
|
|
252
253
|
[
|
|
253
254
|
`--user-data-dir=${profile}`,
|
|
254
|
-
`--app=${
|
|
255
|
+
`--app=${appUrl}`,
|
|
255
256
|
`--window-position=${rect.x},${rect.y}`,
|
|
256
257
|
`--window-size=${rect.w},${rect.h}`,
|
|
257
|
-
"--new-window"
|
|
258
|
+
"--new-window",
|
|
259
|
+
"--no-first-run",
|
|
260
|
+
"--no-default-browser-check",
|
|
261
|
+
"--disable-session-crashed-bubble",
|
|
262
|
+
"--hide-crash-restore-bubble"
|
|
258
263
|
],
|
|
259
264
|
{ detached: true, stdio: "ignore" }
|
|
260
265
|
).unref();
|
|
@@ -372,17 +377,127 @@ function resolveEmail(config) {
|
|
|
372
377
|
return detected.email;
|
|
373
378
|
}
|
|
374
379
|
|
|
380
|
+
// src/plugin.ts
|
|
381
|
+
import * as fs4 from "node:fs";
|
|
382
|
+
import * as os3 from "node:os";
|
|
383
|
+
import * as path4 from "node:path";
|
|
384
|
+
import { fileURLToPath } from "node:url";
|
|
385
|
+
var PLUGIN_NAME = "promptai";
|
|
386
|
+
var HEARTBEAT_FILE = "native-plugin.json";
|
|
387
|
+
var HEARTBEAT_FRESH_MS = 2 * 60 * 60 * 1e3;
|
|
388
|
+
function claudePluginInstallDir() {
|
|
389
|
+
return path4.join(os3.homedir(), ".claude", "skills", PLUGIN_NAME);
|
|
390
|
+
}
|
|
391
|
+
function heartbeatPath() {
|
|
392
|
+
return path4.join(promptaiDir(), HEARTBEAT_FILE);
|
|
393
|
+
}
|
|
394
|
+
function pluginSourceDir() {
|
|
395
|
+
const here = path4.dirname(fileURLToPath(import.meta.url));
|
|
396
|
+
const candidates = [
|
|
397
|
+
// npm package / after build: claude-plugin/ next to dist/
|
|
398
|
+
path4.resolve(here, "..", "claude-plugin"),
|
|
399
|
+
// monorepo: product/cli/dist|src → product/claude-plugin
|
|
400
|
+
path4.resolve(here, "..", "..", "claude-plugin")
|
|
401
|
+
];
|
|
402
|
+
for (const dir of candidates) {
|
|
403
|
+
if (fs4.existsSync(path4.join(dir, ".claude-plugin", "plugin.json"))) {
|
|
404
|
+
return dir;
|
|
405
|
+
}
|
|
406
|
+
if (fs4.existsSync(path4.join(dir, "hooks", "ads.tsx"))) {
|
|
407
|
+
return dir;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function copyDirSync(src, dest) {
|
|
413
|
+
fs4.mkdirSync(dest, { recursive: true });
|
|
414
|
+
for (const entry of fs4.readdirSync(src, { withFileTypes: true })) {
|
|
415
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
416
|
+
const from = path4.join(src, entry.name);
|
|
417
|
+
const to = path4.join(dest, entry.name);
|
|
418
|
+
if (entry.isDirectory()) {
|
|
419
|
+
copyDirSync(from, to);
|
|
420
|
+
} else if (entry.isFile()) {
|
|
421
|
+
fs4.copyFileSync(from, to);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function isClaudePluginInstalled() {
|
|
426
|
+
const dir = claudePluginInstallDir();
|
|
427
|
+
return fs4.existsSync(path4.join(dir, ".claude-plugin", "plugin.json")) || fs4.existsSync(path4.join(dir, "hooks", "ads.tsx"));
|
|
428
|
+
}
|
|
429
|
+
function isNativePluginActive(now = Date.now()) {
|
|
430
|
+
try {
|
|
431
|
+
const raw = fs4.readFileSync(heartbeatPath(), "utf8");
|
|
432
|
+
const data = JSON.parse(raw);
|
|
433
|
+
if (data.active !== true) return false;
|
|
434
|
+
const at = Number(data.at ?? 0);
|
|
435
|
+
const freshMs = Number(data.freshMs ?? HEARTBEAT_FRESH_MS);
|
|
436
|
+
if (!Number.isFinite(at) || at <= 0) return false;
|
|
437
|
+
return now - at < freshMs;
|
|
438
|
+
} catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function clearNativeHeartbeat() {
|
|
443
|
+
try {
|
|
444
|
+
fs4.unlinkSync(heartbeatPath());
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function installClaudePlugin() {
|
|
449
|
+
const source = pluginSourceDir();
|
|
450
|
+
const installPath = claudePluginInstallDir();
|
|
451
|
+
if (!source) {
|
|
452
|
+
return {
|
|
453
|
+
changed: false,
|
|
454
|
+
installPath,
|
|
455
|
+
sourcePath: null,
|
|
456
|
+
error: "Claude Mods plugin sources not found next to the CLI. Reinstall @promptai.credit/cli."
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
fs4.mkdirSync(path4.dirname(installPath), { recursive: true });
|
|
460
|
+
fs4.rmSync(installPath, { recursive: true, force: true });
|
|
461
|
+
copyDirSync(source, installPath);
|
|
462
|
+
fs4.writeFileSync(
|
|
463
|
+
heartbeatPath(),
|
|
464
|
+
JSON.stringify(
|
|
465
|
+
{
|
|
466
|
+
active: false,
|
|
467
|
+
installed: true,
|
|
468
|
+
installedAt: Date.now(),
|
|
469
|
+
installPath,
|
|
470
|
+
source,
|
|
471
|
+
note: "Becomes active when Claude Code loads the plugin (Mods / function hooks)."
|
|
472
|
+
},
|
|
473
|
+
null,
|
|
474
|
+
2
|
|
475
|
+
) + "\n"
|
|
476
|
+
);
|
|
477
|
+
return { changed: true, installPath, sourcePath: source };
|
|
478
|
+
}
|
|
479
|
+
function uninstallClaudePlugin() {
|
|
480
|
+
const installPath = claudePluginInstallDir();
|
|
481
|
+
const existed = fs4.existsSync(installPath);
|
|
482
|
+
fs4.rmSync(installPath, { recursive: true, force: true });
|
|
483
|
+
clearNativeHeartbeat();
|
|
484
|
+
return { changed: existed, installPath };
|
|
485
|
+
}
|
|
486
|
+
|
|
375
487
|
// src/pricing.ts
|
|
376
488
|
var TABLE = [
|
|
377
|
-
{ 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 },
|
|
378
491
|
{ match: ["opus"], inputPerMtok: 15, outputPerMtok: 75 },
|
|
379
492
|
{ match: ["sonnet"], inputPerMtok: 3, outputPerMtok: 15 },
|
|
380
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 },
|
|
381
495
|
{ match: ["gpt-5", "gpt5", "codex"], inputPerMtok: 1.25, outputPerMtok: 10 },
|
|
382
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 },
|
|
383
498
|
{ match: ["gemini"], inputPerMtok: 1.25, outputPerMtok: 10 },
|
|
384
|
-
{ match: ["grok"], inputPerMtok:
|
|
385
|
-
{ match: ["composer"], inputPerMtok:
|
|
499
|
+
{ match: ["grok"], inputPerMtok: 2, outputPerMtok: 6.25 },
|
|
500
|
+
{ match: ["composer"], inputPerMtok: 0.3, outputPerMtok: 4 },
|
|
386
501
|
{ match: ["deepseek", "kimi", "qwen"], inputPerMtok: 0.6, outputPerMtok: 2.5 }
|
|
387
502
|
];
|
|
388
503
|
var DEFAULT_PRICE = { inputPerMtok: 2, outputPerMtok: 8 };
|
|
@@ -402,9 +517,9 @@ function costUsd(model, inputTokens, outputTokens) {
|
|
|
402
517
|
}
|
|
403
518
|
|
|
404
519
|
// src/transcript.ts
|
|
405
|
-
import * as
|
|
520
|
+
import * as fs5 from "node:fs";
|
|
406
521
|
function readTranscriptUsage(transcriptPath, watermark) {
|
|
407
|
-
const raw =
|
|
522
|
+
const raw = fs5.readFileSync(transcriptPath, "utf8");
|
|
408
523
|
const byMessageId = /* @__PURE__ */ new Map();
|
|
409
524
|
let parsedCount = 0;
|
|
410
525
|
for (const line of raw.split("\n")) {
|
|
@@ -446,7 +561,7 @@ function readTranscriptUsage(transcriptPath, watermark) {
|
|
|
446
561
|
};
|
|
447
562
|
}
|
|
448
563
|
function readGlassTranscriptUsage(transcriptPath, watermark) {
|
|
449
|
-
const raw =
|
|
564
|
+
const raw = fs5.readFileSync(transcriptPath, "utf8");
|
|
450
565
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
451
566
|
let inputChars = 0;
|
|
452
567
|
let outputChars = 0;
|
|
@@ -481,13 +596,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
|
|
|
481
596
|
var AD_MIN_INTERVAL_MS = 9e4;
|
|
482
597
|
var SETTLE_RETRIES = [400, 800, 1500];
|
|
483
598
|
function claudeSettingsPath() {
|
|
484
|
-
return
|
|
599
|
+
return path5.join(os4.homedir(), ".claude", "settings.json");
|
|
485
600
|
}
|
|
486
601
|
function hookCommand() {
|
|
487
|
-
let script =
|
|
602
|
+
let script = path5.resolve(process.argv[1] ?? "");
|
|
488
603
|
if (script.endsWith(".ts")) {
|
|
489
|
-
const dist =
|
|
490
|
-
if (!
|
|
604
|
+
const dist = path5.resolve(path5.dirname(script), "..", "dist", "index.js");
|
|
605
|
+
if (!fs6.existsSync(dist)) {
|
|
491
606
|
throw new Error(
|
|
492
607
|
`Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
|
|
493
608
|
);
|
|
@@ -496,21 +611,34 @@ function hookCommand() {
|
|
|
496
611
|
}
|
|
497
612
|
return `"${process.execPath}" "${script}" hook`;
|
|
498
613
|
}
|
|
614
|
+
var MODS_FLAG = "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS";
|
|
615
|
+
var MODS_FLAG_MARKER = "mods-flag-added";
|
|
616
|
+
function modsFlagMarkerPath() {
|
|
617
|
+
return path5.join(promptaiDir(), MODS_FLAG_MARKER);
|
|
618
|
+
}
|
|
499
619
|
function installClaudeHooks() {
|
|
500
620
|
const settingsPath = claudeSettingsPath();
|
|
501
|
-
|
|
621
|
+
fs6.mkdirSync(path5.dirname(settingsPath), { recursive: true });
|
|
502
622
|
let settings = {};
|
|
503
|
-
if (
|
|
623
|
+
if (fs6.existsSync(settingsPath)) {
|
|
504
624
|
try {
|
|
505
|
-
settings = JSON.parse(
|
|
625
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
|
|
506
626
|
} catch {
|
|
507
|
-
|
|
627
|
+
fs6.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
|
|
508
628
|
settings = {};
|
|
509
629
|
}
|
|
510
630
|
}
|
|
511
631
|
settings.hooks = settings.hooks ?? {};
|
|
512
632
|
const command2 = hookCommand();
|
|
513
633
|
let changed = false;
|
|
634
|
+
settings.env = settings.env && typeof settings.env === "object" ? settings.env : {};
|
|
635
|
+
if (settings.env[MODS_FLAG] === void 0) {
|
|
636
|
+
settings.env[MODS_FLAG] = "1";
|
|
637
|
+
fs6.mkdirSync(promptaiDir(), { recursive: true });
|
|
638
|
+
fs6.writeFileSync(modsFlagMarkerPath(), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
639
|
+
`);
|
|
640
|
+
changed = true;
|
|
641
|
+
}
|
|
514
642
|
for (const event of ["UserPromptSubmit", "Stop"]) {
|
|
515
643
|
const groups = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
516
644
|
let ours = groups.flatMap((g) => g.hooks ?? []).find((h) => typeof h.command === "string" && h.command.includes(HOOK_MARKER));
|
|
@@ -524,23 +652,30 @@ function installClaudeHooks() {
|
|
|
524
652
|
changed = true;
|
|
525
653
|
}
|
|
526
654
|
}
|
|
527
|
-
if (changed || !
|
|
528
|
-
|
|
655
|
+
if (changed || !fs6.existsSync(settingsPath)) {
|
|
656
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
529
657
|
}
|
|
530
658
|
return { changed, settingsPath };
|
|
531
659
|
}
|
|
532
660
|
function uninstallClaudeHooks() {
|
|
533
661
|
const settingsPath = claudeSettingsPath();
|
|
534
|
-
if (!
|
|
662
|
+
if (!fs6.existsSync(settingsPath)) return { changed: false };
|
|
535
663
|
let settings;
|
|
536
664
|
try {
|
|
537
|
-
settings = JSON.parse(
|
|
665
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
|
|
538
666
|
} catch {
|
|
539
667
|
return { changed: false };
|
|
540
668
|
}
|
|
541
|
-
if (!settings.hooks) return { changed: false };
|
|
542
669
|
let changed = false;
|
|
543
|
-
|
|
670
|
+
if (fs6.existsSync(modsFlagMarkerPath())) {
|
|
671
|
+
if (settings.env?.[MODS_FLAG] === "1") {
|
|
672
|
+
delete settings.env[MODS_FLAG];
|
|
673
|
+
if (Object.keys(settings.env).length === 0) delete settings.env;
|
|
674
|
+
changed = true;
|
|
675
|
+
}
|
|
676
|
+
fs6.rmSync(modsFlagMarkerPath(), { force: true });
|
|
677
|
+
}
|
|
678
|
+
for (const [event, groups] of Object.entries(settings.hooks ?? {})) {
|
|
544
679
|
if (!Array.isArray(groups)) continue;
|
|
545
680
|
const kept = groups.map((g) => ({
|
|
546
681
|
...g,
|
|
@@ -554,7 +689,7 @@ function uninstallClaudeHooks() {
|
|
|
554
689
|
}
|
|
555
690
|
}
|
|
556
691
|
if (changed) {
|
|
557
|
-
|
|
692
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
558
693
|
}
|
|
559
694
|
return { changed };
|
|
560
695
|
}
|
|
@@ -571,12 +706,16 @@ function handleUserPromptSubmit(payload) {
|
|
|
571
706
|
}
|
|
572
707
|
}
|
|
573
708
|
if (config.adsOptIn && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
709
|
+
if (isNativePluginActive()) {
|
|
710
|
+
log(`[claude] native Mods plugin active, skipped browser ad tab session=${sessionId}`);
|
|
711
|
+
} else {
|
|
712
|
+
const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
|
|
713
|
+
if (openInBrowser(url, config.adSide)) {
|
|
714
|
+
state.lastAdOpenedAt = Date.now();
|
|
715
|
+
log(`[claude] opened ad tab for session=${sessionId}`);
|
|
716
|
+
} else if (!hasDisplay()) {
|
|
717
|
+
log(`[claude] headless environment, skipped ad tab`);
|
|
718
|
+
}
|
|
580
719
|
}
|
|
581
720
|
}
|
|
582
721
|
saveState(state);
|
|
@@ -608,14 +747,21 @@ async function handleStop(payload) {
|
|
|
608
747
|
const cost = costUsd(usage.model, usage.inputTokens, usage.outputTokens);
|
|
609
748
|
const promptId = crypto3.randomUUID();
|
|
610
749
|
let verified = false;
|
|
750
|
+
let creditedUsd;
|
|
751
|
+
let stocks;
|
|
611
752
|
if (config.adsOptIn && cost > 0) {
|
|
612
|
-
|
|
753
|
+
const redeem = await redeemAgainstAd(
|
|
613
754
|
config.serverUrl,
|
|
614
755
|
config.deviceId,
|
|
615
756
|
promptId,
|
|
616
757
|
cost,
|
|
617
758
|
resolveEmail(config)
|
|
618
759
|
);
|
|
760
|
+
if (redeem) {
|
|
761
|
+
verified = true;
|
|
762
|
+
creditedUsd = redeem.creditedUsd;
|
|
763
|
+
stocks = redeem.stocks;
|
|
764
|
+
}
|
|
619
765
|
}
|
|
620
766
|
state.prompts.unshift({
|
|
621
767
|
id: promptId,
|
|
@@ -626,7 +772,9 @@ async function handleStop(payload) {
|
|
|
626
772
|
inputTokens: usage.inputTokens,
|
|
627
773
|
outputTokens: usage.outputTokens,
|
|
628
774
|
costUsd: cost,
|
|
629
|
-
verified
|
|
775
|
+
verified,
|
|
776
|
+
creditedUsd,
|
|
777
|
+
stocks
|
|
630
778
|
});
|
|
631
779
|
saveState(state);
|
|
632
780
|
log(
|
|
@@ -638,27 +786,30 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
|
|
|
638
786
|
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
639
787
|
if (sessions.length === 0) {
|
|
640
788
|
log(`[claude] no verified ad session available for prompt ${promptId}`);
|
|
641
|
-
return
|
|
789
|
+
return null;
|
|
642
790
|
}
|
|
643
|
-
await redeemCredit(serverUrl, {
|
|
791
|
+
const result = await redeemCredit(serverUrl, {
|
|
644
792
|
sessionId: sessions[0].sessionId,
|
|
645
793
|
deviceId,
|
|
646
794
|
promptId,
|
|
647
795
|
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
|
|
648
796
|
email: email || void 0
|
|
649
797
|
});
|
|
650
|
-
return
|
|
798
|
+
return {
|
|
799
|
+
creditedUsd: result.creditedUsd,
|
|
800
|
+
stocks: result.stocks ?? []
|
|
801
|
+
};
|
|
651
802
|
} catch (err) {
|
|
652
803
|
log(`[claude] credit redeem failed for prompt ${promptId}: ${String(err)}`);
|
|
653
|
-
return
|
|
804
|
+
return null;
|
|
654
805
|
}
|
|
655
806
|
}
|
|
656
807
|
|
|
657
808
|
// src/cursor.ts
|
|
658
809
|
import * as crypto4 from "node:crypto";
|
|
659
|
-
import * as
|
|
660
|
-
import * as
|
|
661
|
-
import * as
|
|
810
|
+
import * as fs7 from "node:fs";
|
|
811
|
+
import * as os5 from "node:os";
|
|
812
|
+
import * as path6 from "node:path";
|
|
662
813
|
var CURSOR_SOURCE = "cursor-agents";
|
|
663
814
|
var HOOK_MARKER2 = "promptai";
|
|
664
815
|
var MAX_CREDIT_PER_AD_USD2 = 5;
|
|
@@ -668,17 +819,17 @@ function isCursorPayload(payload) {
|
|
|
668
819
|
return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
|
|
669
820
|
}
|
|
670
821
|
function cursorHooksJsonPath() {
|
|
671
|
-
return
|
|
822
|
+
return path6.join(os5.homedir(), ".cursor", "hooks.json");
|
|
672
823
|
}
|
|
673
824
|
function installCursorHooks() {
|
|
674
825
|
const hooksPath = cursorHooksJsonPath();
|
|
675
|
-
|
|
826
|
+
fs7.mkdirSync(path6.dirname(hooksPath), { recursive: true });
|
|
676
827
|
let config = {};
|
|
677
|
-
if (
|
|
828
|
+
if (fs7.existsSync(hooksPath)) {
|
|
678
829
|
try {
|
|
679
|
-
config = JSON.parse(
|
|
830
|
+
config = JSON.parse(fs7.readFileSync(hooksPath, "utf8"));
|
|
680
831
|
} catch {
|
|
681
|
-
|
|
832
|
+
fs7.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
|
|
682
833
|
config = {};
|
|
683
834
|
}
|
|
684
835
|
}
|
|
@@ -700,17 +851,17 @@ function installCursorHooks() {
|
|
|
700
851
|
changed = true;
|
|
701
852
|
}
|
|
702
853
|
}
|
|
703
|
-
if (changed || !
|
|
704
|
-
|
|
854
|
+
if (changed || !fs7.existsSync(hooksPath)) {
|
|
855
|
+
fs7.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
|
|
705
856
|
}
|
|
706
857
|
return { changed, hooksPath };
|
|
707
858
|
}
|
|
708
859
|
function uninstallCursorHooks() {
|
|
709
860
|
const hooksPath = cursorHooksJsonPath();
|
|
710
|
-
if (!
|
|
861
|
+
if (!fs7.existsSync(hooksPath)) return { changed: false };
|
|
711
862
|
let config;
|
|
712
863
|
try {
|
|
713
|
-
config = JSON.parse(
|
|
864
|
+
config = JSON.parse(fs7.readFileSync(hooksPath, "utf8"));
|
|
714
865
|
} catch {
|
|
715
866
|
return { changed: false };
|
|
716
867
|
}
|
|
@@ -727,14 +878,14 @@ function uninstallCursorHooks() {
|
|
|
727
878
|
}
|
|
728
879
|
}
|
|
729
880
|
if (changed) {
|
|
730
|
-
|
|
881
|
+
fs7.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
|
|
731
882
|
}
|
|
732
883
|
return { changed };
|
|
733
884
|
}
|
|
734
885
|
function extensionIsActive() {
|
|
735
|
-
const infoPath =
|
|
886
|
+
const infoPath = path6.join(os5.homedir(), ".cursor", "promptai-listener.json");
|
|
736
887
|
try {
|
|
737
|
-
const info = JSON.parse(
|
|
888
|
+
const info = JSON.parse(fs7.readFileSync(infoPath, "utf8"));
|
|
738
889
|
if (typeof info.pid !== "number") return false;
|
|
739
890
|
process.kill(info.pid, 0);
|
|
740
891
|
return true;
|
|
@@ -743,18 +894,18 @@ function extensionIsActive() {
|
|
|
743
894
|
}
|
|
744
895
|
}
|
|
745
896
|
function acquireOnceLock(key) {
|
|
746
|
-
const dir =
|
|
747
|
-
|
|
897
|
+
const dir = path6.join(promptaiDir(), "locks");
|
|
898
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
748
899
|
try {
|
|
749
900
|
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
750
|
-
for (const name of
|
|
751
|
-
const p =
|
|
752
|
-
if (
|
|
901
|
+
for (const name of fs7.readdirSync(dir)) {
|
|
902
|
+
const p = path6.join(dir, name);
|
|
903
|
+
if (fs7.statSync(p).mtimeMs < cutoff) fs7.rmSync(p, { recursive: true, force: true });
|
|
753
904
|
}
|
|
754
905
|
} catch {
|
|
755
906
|
}
|
|
756
907
|
try {
|
|
757
|
-
|
|
908
|
+
fs7.mkdirSync(path6.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
|
|
758
909
|
return true;
|
|
759
910
|
} catch {
|
|
760
911
|
return false;
|
|
@@ -846,14 +997,21 @@ async function handleCursorStop(payload) {
|
|
|
846
997
|
const cost = costUsd(model, inputTokens, outputTokens);
|
|
847
998
|
const promptId = crypto4.randomUUID();
|
|
848
999
|
let verified = false;
|
|
1000
|
+
let creditedUsd;
|
|
1001
|
+
let stocks;
|
|
849
1002
|
if (config.adsOptIn && cost > 0) {
|
|
850
|
-
|
|
1003
|
+
const redeem = await redeemAgainstAd2(
|
|
851
1004
|
config.serverUrl,
|
|
852
1005
|
config.deviceId,
|
|
853
1006
|
promptId,
|
|
854
1007
|
cost,
|
|
855
1008
|
resolveEmail(config)
|
|
856
1009
|
);
|
|
1010
|
+
if (redeem) {
|
|
1011
|
+
verified = true;
|
|
1012
|
+
creditedUsd = redeem.creditedUsd;
|
|
1013
|
+
stocks = redeem.stocks;
|
|
1014
|
+
}
|
|
857
1015
|
}
|
|
858
1016
|
state.prompts.unshift({
|
|
859
1017
|
id: promptId,
|
|
@@ -865,7 +1023,9 @@ async function handleCursorStop(payload) {
|
|
|
865
1023
|
outputTokens,
|
|
866
1024
|
costUsd: cost,
|
|
867
1025
|
verified,
|
|
868
|
-
estimated
|
|
1026
|
+
estimated,
|
|
1027
|
+
creditedUsd,
|
|
1028
|
+
stocks
|
|
869
1029
|
});
|
|
870
1030
|
saveState(state);
|
|
871
1031
|
log(
|
|
@@ -877,19 +1037,22 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
|
|
|
877
1037
|
const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
|
|
878
1038
|
if (sessions.length === 0) {
|
|
879
1039
|
log(`[cursor] no verified ad session available for prompt ${promptId}`);
|
|
880
|
-
return
|
|
1040
|
+
return null;
|
|
881
1041
|
}
|
|
882
|
-
await redeemCredit(serverUrl, {
|
|
1042
|
+
const result = await redeemCredit(serverUrl, {
|
|
883
1043
|
sessionId: sessions[0].sessionId,
|
|
884
1044
|
deviceId,
|
|
885
1045
|
promptId,
|
|
886
1046
|
amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
|
|
887
1047
|
email: email || void 0
|
|
888
1048
|
});
|
|
889
|
-
return
|
|
1049
|
+
return {
|
|
1050
|
+
creditedUsd: result.creditedUsd,
|
|
1051
|
+
stocks: result.stocks ?? []
|
|
1052
|
+
};
|
|
890
1053
|
} catch (err) {
|
|
891
1054
|
log(`[cursor] credit redeem failed for prompt ${promptId}: ${String(err)}`);
|
|
892
|
-
return
|
|
1055
|
+
return null;
|
|
893
1056
|
}
|
|
894
1057
|
}
|
|
895
1058
|
|
|
@@ -898,12 +1061,12 @@ import * as readline from "node:readline";
|
|
|
898
1061
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
899
1062
|
var WALLET_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
900
1063
|
function promptLine(question) {
|
|
901
|
-
return new Promise((
|
|
1064
|
+
return new Promise((resolve3) => {
|
|
902
1065
|
process.stdout.write(question);
|
|
903
1066
|
const rl = readline.createInterface({ input: process.stdin });
|
|
904
1067
|
rl.once("line", (line) => {
|
|
905
1068
|
rl.close();
|
|
906
|
-
|
|
1069
|
+
resolve3(line);
|
|
907
1070
|
});
|
|
908
1071
|
});
|
|
909
1072
|
}
|
|
@@ -988,9 +1151,9 @@ async function onboard(config) {
|
|
|
988
1151
|
var HELP = `promptai - ad-subsidized prompt credits for terminal agents
|
|
989
1152
|
|
|
990
1153
|
Usage:
|
|
991
|
-
promptai install claude Wire hooks
|
|
1154
|
+
promptai install claude Wire hooks + Claude Mods native ad plugin
|
|
992
1155
|
promptai install cursor Wire hooks into ~/.cursor/hooks.json (Agents Window)
|
|
993
|
-
promptai uninstall <agent> Remove our hooks (other hooks untouched)
|
|
1156
|
+
promptai uninstall <agent> Remove our hooks/plugin (other hooks untouched)
|
|
994
1157
|
promptai status Device, balance, banked ad credits, recent prompts
|
|
995
1158
|
promptai watch Open a rewarded ad in the browser now
|
|
996
1159
|
promptai claim [address] Claim your verified balance as USDC (Base)
|
|
@@ -1002,10 +1165,10 @@ Usage:
|
|
|
1002
1165
|
promptai hook (internal) invoked by agent hooks, JSON on stdin
|
|
1003
1166
|
`;
|
|
1004
1167
|
function readStdin() {
|
|
1005
|
-
return new Promise((
|
|
1168
|
+
return new Promise((resolve3) => {
|
|
1006
1169
|
const chunks = [];
|
|
1007
1170
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
1008
|
-
process.stdin.on("end", () =>
|
|
1171
|
+
process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
1009
1172
|
});
|
|
1010
1173
|
}
|
|
1011
1174
|
async function cmdHook() {
|
|
@@ -1017,11 +1180,11 @@ async function cmdHook() {
|
|
|
1017
1180
|
return;
|
|
1018
1181
|
}
|
|
1019
1182
|
const event = String(payload.hook_event_name ?? "");
|
|
1020
|
-
if (process.env.PROMPTAI_DUMP === "1" ||
|
|
1183
|
+
if (process.env.PROMPTAI_DUMP === "1" || fs8.existsSync(path7.join(promptaiDir(), "debug"))) {
|
|
1021
1184
|
try {
|
|
1022
|
-
const dir =
|
|
1023
|
-
|
|
1024
|
-
|
|
1185
|
+
const dir = path7.join(promptaiDir(), "dump");
|
|
1186
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1187
|
+
fs8.writeFileSync(path7.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
|
|
1025
1188
|
} catch {
|
|
1026
1189
|
}
|
|
1027
1190
|
}
|
|
@@ -1052,6 +1215,11 @@ async function cmdStatus() {
|
|
|
1052
1215
|
console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
|
|
1053
1216
|
console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
|
|
1054
1217
|
console.log(`side ${config.adSide}`);
|
|
1218
|
+
const nativeInstalled = isClaudePluginInstalled();
|
|
1219
|
+
const nativeActive = isNativePluginActive();
|
|
1220
|
+
console.log(
|
|
1221
|
+
`native ${nativeActive ? "active (AbovePrompt)" : nativeInstalled ? "installed (waiting for Mods session)" : "not installed"}`
|
|
1222
|
+
);
|
|
1055
1223
|
console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
|
|
1056
1224
|
try {
|
|
1057
1225
|
const [balance, verified] = await Promise.all([
|
|
@@ -1068,6 +1236,14 @@ async function cmdStatus() {
|
|
|
1068
1236
|
`claims ${!balance.email ? "blocked (link an email first)" : balance.claimActivated ? "activated" : "blocked (waiting for admin activation)"}`
|
|
1069
1237
|
);
|
|
1070
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
|
+
}
|
|
1071
1247
|
} catch (err) {
|
|
1072
1248
|
console.log(`balance unavailable (${String(err)})`);
|
|
1073
1249
|
}
|
|
@@ -1076,8 +1252,15 @@ async function cmdStatus() {
|
|
|
1076
1252
|
for (const p of state.prompts.slice(0, 8)) {
|
|
1077
1253
|
const when = new Date(p.ts).toISOString().slice(0, 16).replace("T", " ");
|
|
1078
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 ");
|
|
1079
1261
|
console.log(
|
|
1080
|
-
` ${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}` : "")
|
|
1081
1264
|
);
|
|
1082
1265
|
}
|
|
1083
1266
|
}
|
|
@@ -1165,13 +1348,30 @@ function printInstallNext() {
|
|
|
1165
1348
|
console.log("You can toggle ads with promptai on / promptai off.");
|
|
1166
1349
|
console.log("Try: promptai watch, then run a prompt.");
|
|
1167
1350
|
}
|
|
1351
|
+
function printClaudeModsHint(installPath) {
|
|
1352
|
+
console.log("");
|
|
1353
|
+
console.log("Native AbovePrompt ads use Claude Mods (function hooks, early access).");
|
|
1354
|
+
console.log("Enabled via env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in your Claude settings;");
|
|
1355
|
+
console.log("this also turns on any other function-hook plugins you have.");
|
|
1356
|
+
console.log(`Plugin path: ${installPath}`);
|
|
1357
|
+
console.log("Where Mods are unavailable, ads still open in the browser (/watch).");
|
|
1358
|
+
}
|
|
1168
1359
|
async function cmdInstall(agent) {
|
|
1169
1360
|
if (agent === "claude" || agent === "claude-code") {
|
|
1170
1361
|
const { changed, settingsPath } = installClaudeHooks();
|
|
1171
1362
|
console.log(
|
|
1172
1363
|
changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
|
|
1173
1364
|
);
|
|
1174
|
-
|
|
1365
|
+
const plugin = installClaudePlugin();
|
|
1366
|
+
if (plugin.error) {
|
|
1367
|
+
console.log(`Mods plugin: ${plugin.error}`);
|
|
1368
|
+
} else {
|
|
1369
|
+
console.log(
|
|
1370
|
+
plugin.changed ? `Mods plugin installed into ${plugin.installPath}.` : `Mods plugin already at ${plugin.installPath}.`
|
|
1371
|
+
);
|
|
1372
|
+
printClaudeModsHint(plugin.installPath);
|
|
1373
|
+
}
|
|
1374
|
+
console.log("Claude Code picks hooks up on its next session.");
|
|
1175
1375
|
await onboard(loadConfig());
|
|
1176
1376
|
printInstallNext();
|
|
1177
1377
|
return;
|
|
@@ -1195,6 +1395,10 @@ function cmdUninstall(agent) {
|
|
|
1195
1395
|
if (agent === "claude" || agent === "claude-code") {
|
|
1196
1396
|
const { changed } = uninstallClaudeHooks();
|
|
1197
1397
|
console.log(changed ? "Hooks removed." : "No promptai hooks found.");
|
|
1398
|
+
const plugin = uninstallClaudePlugin();
|
|
1399
|
+
console.log(
|
|
1400
|
+
plugin.changed ? `Mods plugin removed from ${plugin.installPath}.` : "No Mods plugin install found."
|
|
1401
|
+
);
|
|
1198
1402
|
return;
|
|
1199
1403
|
}
|
|
1200
1404
|
if (agent === "cursor" || agent === "cursor-agents") {
|