@promptai.credit/cli 0.4.0 → 0.4.2

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/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as fs7 from "node:fs";
5
- import * as path6 from "node:path";
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 fs5 from "node:fs";
10
- import * as os3 from "node:os";
11
- import * as path4 from "node:path";
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=${url}`,
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,6 +377,113 @@ 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
489
  { match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
@@ -402,9 +514,9 @@ function costUsd(model, inputTokens, outputTokens) {
402
514
  }
403
515
 
404
516
  // src/transcript.ts
405
- import * as fs4 from "node:fs";
517
+ import * as fs5 from "node:fs";
406
518
  function readTranscriptUsage(transcriptPath, watermark) {
407
- const raw = fs4.readFileSync(transcriptPath, "utf8");
519
+ const raw = fs5.readFileSync(transcriptPath, "utf8");
408
520
  const byMessageId = /* @__PURE__ */ new Map();
409
521
  let parsedCount = 0;
410
522
  for (const line of raw.split("\n")) {
@@ -446,7 +558,7 @@ function readTranscriptUsage(transcriptPath, watermark) {
446
558
  };
447
559
  }
448
560
  function readGlassTranscriptUsage(transcriptPath, watermark) {
449
- const raw = fs4.readFileSync(transcriptPath, "utf8");
561
+ const raw = fs5.readFileSync(transcriptPath, "utf8");
450
562
  const lines = raw.split("\n").filter((l) => l.trim());
451
563
  let inputChars = 0;
452
564
  let outputChars = 0;
@@ -481,13 +593,13 @@ var MAX_CREDIT_PER_AD_USD = 5;
481
593
  var AD_MIN_INTERVAL_MS = 9e4;
482
594
  var SETTLE_RETRIES = [400, 800, 1500];
483
595
  function claudeSettingsPath() {
484
- return path4.join(os3.homedir(), ".claude", "settings.json");
596
+ return path5.join(os4.homedir(), ".claude", "settings.json");
485
597
  }
486
598
  function hookCommand() {
487
- let script = path4.resolve(process.argv[1] ?? "");
599
+ let script = path5.resolve(process.argv[1] ?? "");
488
600
  if (script.endsWith(".ts")) {
489
- const dist = path4.resolve(path4.dirname(script), "..", "dist", "index.js");
490
- if (!fs5.existsSync(dist)) {
601
+ const dist = path5.resolve(path5.dirname(script), "..", "dist", "index.js");
602
+ if (!fs6.existsSync(dist)) {
491
603
  throw new Error(
492
604
  `Build the CLI first (pnpm --filter @promptai.credit/cli build); hooks cannot run ${script} directly.`
493
605
  );
@@ -496,21 +608,34 @@ function hookCommand() {
496
608
  }
497
609
  return `"${process.execPath}" "${script}" hook`;
498
610
  }
611
+ var MODS_FLAG = "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS";
612
+ var MODS_FLAG_MARKER = "mods-flag-added";
613
+ function modsFlagMarkerPath() {
614
+ return path5.join(promptaiDir(), MODS_FLAG_MARKER);
615
+ }
499
616
  function installClaudeHooks() {
500
617
  const settingsPath = claudeSettingsPath();
501
- fs5.mkdirSync(path4.dirname(settingsPath), { recursive: true });
618
+ fs6.mkdirSync(path5.dirname(settingsPath), { recursive: true });
502
619
  let settings = {};
503
- if (fs5.existsSync(settingsPath)) {
620
+ if (fs6.existsSync(settingsPath)) {
504
621
  try {
505
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
622
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
506
623
  } catch {
507
- fs5.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
624
+ fs6.copyFileSync(settingsPath, settingsPath + ".promptai-backup");
508
625
  settings = {};
509
626
  }
510
627
  }
511
628
  settings.hooks = settings.hooks ?? {};
512
629
  const command2 = hookCommand();
513
630
  let changed = false;
631
+ settings.env = settings.env && typeof settings.env === "object" ? settings.env : {};
632
+ if (settings.env[MODS_FLAG] === void 0) {
633
+ settings.env[MODS_FLAG] = "1";
634
+ fs6.mkdirSync(promptaiDir(), { recursive: true });
635
+ fs6.writeFileSync(modsFlagMarkerPath(), `${(/* @__PURE__ */ new Date()).toISOString()}
636
+ `);
637
+ changed = true;
638
+ }
514
639
  for (const event of ["UserPromptSubmit", "Stop"]) {
515
640
  const groups = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
516
641
  let ours = groups.flatMap((g) => g.hooks ?? []).find((h) => typeof h.command === "string" && h.command.includes(HOOK_MARKER));
@@ -524,23 +649,30 @@ function installClaudeHooks() {
524
649
  changed = true;
525
650
  }
526
651
  }
527
- if (changed || !fs5.existsSync(settingsPath)) {
528
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
652
+ if (changed || !fs6.existsSync(settingsPath)) {
653
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
529
654
  }
530
655
  return { changed, settingsPath };
531
656
  }
532
657
  function uninstallClaudeHooks() {
533
658
  const settingsPath = claudeSettingsPath();
534
- if (!fs5.existsSync(settingsPath)) return { changed: false };
659
+ if (!fs6.existsSync(settingsPath)) return { changed: false };
535
660
  let settings;
536
661
  try {
537
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
662
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
538
663
  } catch {
539
664
  return { changed: false };
540
665
  }
541
- if (!settings.hooks) return { changed: false };
542
666
  let changed = false;
543
- for (const [event, groups] of Object.entries(settings.hooks)) {
667
+ if (fs6.existsSync(modsFlagMarkerPath())) {
668
+ if (settings.env?.[MODS_FLAG] === "1") {
669
+ delete settings.env[MODS_FLAG];
670
+ if (Object.keys(settings.env).length === 0) delete settings.env;
671
+ changed = true;
672
+ }
673
+ fs6.rmSync(modsFlagMarkerPath(), { force: true });
674
+ }
675
+ for (const [event, groups] of Object.entries(settings.hooks ?? {})) {
544
676
  if (!Array.isArray(groups)) continue;
545
677
  const kept = groups.map((g) => ({
546
678
  ...g,
@@ -554,7 +686,7 @@ function uninstallClaudeHooks() {
554
686
  }
555
687
  }
556
688
  if (changed) {
557
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
689
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
558
690
  }
559
691
  return { changed };
560
692
  }
@@ -571,12 +703,16 @@ function handleUserPromptSubmit(payload) {
571
703
  }
572
704
  }
573
705
  if (config.adsOptIn && Date.now() - state.lastAdOpenedAt >= AD_MIN_INTERVAL_MS) {
574
- const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
575
- if (openInBrowser(url, config.adSide)) {
576
- state.lastAdOpenedAt = Date.now();
577
- log(`[claude] opened ad tab for session=${sessionId}`);
578
- } else if (!hasDisplay()) {
579
- log(`[claude] headless environment, skipped ad tab`);
706
+ if (isNativePluginActive()) {
707
+ log(`[claude] native Mods plugin active, skipped browser ad tab session=${sessionId}`);
708
+ } else {
709
+ const url = watchUrl(config.serverUrl, config.deviceId, SOURCE);
710
+ if (openInBrowser(url, config.adSide)) {
711
+ state.lastAdOpenedAt = Date.now();
712
+ log(`[claude] opened ad tab for session=${sessionId}`);
713
+ } else if (!hasDisplay()) {
714
+ log(`[claude] headless environment, skipped ad tab`);
715
+ }
580
716
  }
581
717
  }
582
718
  saveState(state);
@@ -656,9 +792,9 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
656
792
 
657
793
  // src/cursor.ts
658
794
  import * as crypto4 from "node:crypto";
659
- import * as fs6 from "node:fs";
660
- import * as os4 from "node:os";
661
- import * as path5 from "node:path";
795
+ import * as fs7 from "node:fs";
796
+ import * as os5 from "node:os";
797
+ import * as path6 from "node:path";
662
798
  var CURSOR_SOURCE = "cursor-agents";
663
799
  var HOOK_MARKER2 = "promptai";
664
800
  var MAX_CREDIT_PER_AD_USD2 = 5;
@@ -668,17 +804,17 @@ function isCursorPayload(payload) {
668
804
  return typeof payload.cursor_version === "string" || typeof payload.conversation_id === "string";
669
805
  }
670
806
  function cursorHooksJsonPath() {
671
- return path5.join(os4.homedir(), ".cursor", "hooks.json");
807
+ return path6.join(os5.homedir(), ".cursor", "hooks.json");
672
808
  }
673
809
  function installCursorHooks() {
674
810
  const hooksPath = cursorHooksJsonPath();
675
- fs6.mkdirSync(path5.dirname(hooksPath), { recursive: true });
811
+ fs7.mkdirSync(path6.dirname(hooksPath), { recursive: true });
676
812
  let config = {};
677
- if (fs6.existsSync(hooksPath)) {
813
+ if (fs7.existsSync(hooksPath)) {
678
814
  try {
679
- config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
815
+ config = JSON.parse(fs7.readFileSync(hooksPath, "utf8"));
680
816
  } catch {
681
- fs6.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
817
+ fs7.copyFileSync(hooksPath, hooksPath + ".promptai-backup");
682
818
  config = {};
683
819
  }
684
820
  }
@@ -700,17 +836,17 @@ function installCursorHooks() {
700
836
  changed = true;
701
837
  }
702
838
  }
703
- if (changed || !fs6.existsSync(hooksPath)) {
704
- fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
839
+ if (changed || !fs7.existsSync(hooksPath)) {
840
+ fs7.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
705
841
  }
706
842
  return { changed, hooksPath };
707
843
  }
708
844
  function uninstallCursorHooks() {
709
845
  const hooksPath = cursorHooksJsonPath();
710
- if (!fs6.existsSync(hooksPath)) return { changed: false };
846
+ if (!fs7.existsSync(hooksPath)) return { changed: false };
711
847
  let config;
712
848
  try {
713
- config = JSON.parse(fs6.readFileSync(hooksPath, "utf8"));
849
+ config = JSON.parse(fs7.readFileSync(hooksPath, "utf8"));
714
850
  } catch {
715
851
  return { changed: false };
716
852
  }
@@ -727,14 +863,14 @@ function uninstallCursorHooks() {
727
863
  }
728
864
  }
729
865
  if (changed) {
730
- fs6.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
866
+ fs7.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + "\n");
731
867
  }
732
868
  return { changed };
733
869
  }
734
870
  function extensionIsActive() {
735
- const infoPath = path5.join(os4.homedir(), ".cursor", "promptai-listener.json");
871
+ const infoPath = path6.join(os5.homedir(), ".cursor", "promptai-listener.json");
736
872
  try {
737
- const info = JSON.parse(fs6.readFileSync(infoPath, "utf8"));
873
+ const info = JSON.parse(fs7.readFileSync(infoPath, "utf8"));
738
874
  if (typeof info.pid !== "number") return false;
739
875
  process.kill(info.pid, 0);
740
876
  return true;
@@ -743,18 +879,18 @@ function extensionIsActive() {
743
879
  }
744
880
  }
745
881
  function acquireOnceLock(key) {
746
- const dir = path5.join(promptaiDir(), "locks");
747
- fs6.mkdirSync(dir, { recursive: true });
882
+ const dir = path6.join(promptaiDir(), "locks");
883
+ fs7.mkdirSync(dir, { recursive: true });
748
884
  try {
749
885
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
750
- for (const name of fs6.readdirSync(dir)) {
751
- const p = path5.join(dir, name);
752
- if (fs6.statSync(p).mtimeMs < cutoff) fs6.rmSync(p, { recursive: true, force: true });
886
+ for (const name of fs7.readdirSync(dir)) {
887
+ const p = path6.join(dir, name);
888
+ if (fs7.statSync(p).mtimeMs < cutoff) fs7.rmSync(p, { recursive: true, force: true });
753
889
  }
754
890
  } catch {
755
891
  }
756
892
  try {
757
- fs6.mkdirSync(path5.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
893
+ fs7.mkdirSync(path6.join(dir, key.replace(/[^a-zA-Z0-9_-]/g, "_")));
758
894
  return true;
759
895
  } catch {
760
896
  return false;
@@ -898,12 +1034,12 @@ import * as readline from "node:readline";
898
1034
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
899
1035
  var WALLET_RE = /^0x[0-9a-fA-F]{40}$/;
900
1036
  function promptLine(question) {
901
- return new Promise((resolve2) => {
1037
+ return new Promise((resolve3) => {
902
1038
  process.stdout.write(question);
903
1039
  const rl = readline.createInterface({ input: process.stdin });
904
1040
  rl.once("line", (line) => {
905
1041
  rl.close();
906
- resolve2(line);
1042
+ resolve3(line);
907
1043
  });
908
1044
  });
909
1045
  }
@@ -941,17 +1077,6 @@ async function linkEmailInteractive(config, email, source) {
941
1077
  return false;
942
1078
  }
943
1079
  }
944
- function parseYes(raw, defaultYes) {
945
- const v = raw.trim().toLowerCase();
946
- if (!v) return defaultYes;
947
- return v === "y" || v === "yes";
948
- }
949
- function parseSide(raw) {
950
- const v = raw.trim().toLowerCase();
951
- if (v === "left" || v === "l") return "left";
952
- if (v === "default" || v === "browser" || v === "n" || v === "no") return "default";
953
- return "right";
954
- }
955
1080
  async function onboard(config) {
956
1081
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
957
1082
  if (!config.adSide) config.adSide = "right";
@@ -990,11 +1115,7 @@ async function onboard(config) {
990
1115
  } else {
991
1116
  console.log("Skipped wallet. Set it later: promptai set wallet 0x...");
992
1117
  }
993
- const sideRaw = await promptLine("Show ads on the right while your agent works? [Y/n]: ");
994
- config.adSide = parseYes(sideRaw, true) ? "right" : parseSide(sideRaw);
995
- console.log(
996
- config.adSide === "right" ? "Ads will open on the right." : config.adSide === "left" ? "Ads will open on the left." : "Ads will open in your default browser."
997
- );
1118
+ config.adSide = config.adSide || "right";
998
1119
  saveConfig(config);
999
1120
  console.log("");
1000
1121
  }
@@ -1003,24 +1124,24 @@ async function onboard(config) {
1003
1124
  var HELP = `promptai - ad-subsidized prompt credits for terminal agents
1004
1125
 
1005
1126
  Usage:
1006
- promptai install claude Wire hooks into ~/.claude/settings.json
1127
+ promptai install claude Wire hooks + Claude Mods native ad plugin
1007
1128
  promptai install cursor Wire hooks into ~/.cursor/hooks.json (Agents Window)
1008
- promptai uninstall <agent> Remove our hooks (other hooks untouched)
1129
+ promptai uninstall <agent> Remove our hooks/plugin (other hooks untouched)
1009
1130
  promptai status Device, balance, banked ad credits, recent prompts
1010
1131
  promptai watch Open a rewarded ad in the browser now
1011
1132
  promptai claim [address] Claim your verified balance as USDC (Base)
1012
1133
  promptai set wallet 0x... Set the payout wallet
1013
1134
  promptai set email <email> Link your email (required for USDC claims; groups devices)
1014
1135
  promptai set server <url> Point at a different API server
1015
- promptai set ads on|off Toggle the rewarded-ads opt-in
1136
+ promptai on | off Toggle rewarded ads
1016
1137
  promptai set side right|left|default Where the ad window docks (default: right)
1017
1138
  promptai hook (internal) invoked by agent hooks, JSON on stdin
1018
1139
  `;
1019
1140
  function readStdin() {
1020
- return new Promise((resolve2) => {
1141
+ return new Promise((resolve3) => {
1021
1142
  const chunks = [];
1022
1143
  process.stdin.on("data", (c) => chunks.push(c));
1023
- process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
1144
+ process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1024
1145
  });
1025
1146
  }
1026
1147
  async function cmdHook() {
@@ -1032,11 +1153,11 @@ async function cmdHook() {
1032
1153
  return;
1033
1154
  }
1034
1155
  const event = String(payload.hook_event_name ?? "");
1035
- if (process.env.PROMPTAI_DUMP === "1" || fs7.existsSync(path6.join(promptaiDir(), "debug"))) {
1156
+ if (process.env.PROMPTAI_DUMP === "1" || fs8.existsSync(path7.join(promptaiDir(), "debug"))) {
1036
1157
  try {
1037
- const dir = path6.join(promptaiDir(), "dump");
1038
- fs7.mkdirSync(dir, { recursive: true });
1039
- fs7.writeFileSync(path6.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
1158
+ const dir = path7.join(promptaiDir(), "dump");
1159
+ fs8.mkdirSync(dir, { recursive: true });
1160
+ fs8.writeFileSync(path7.join(dir, `${Date.now()}-${event || "unknown"}.json`), raw);
1040
1161
  } catch {
1041
1162
  }
1042
1163
  }
@@ -1067,6 +1188,11 @@ async function cmdStatus() {
1067
1188
  console.log(`email ${email || "(not detected - promptai set email you@example.com)"}`);
1068
1189
  console.log(`ads ${config.adsOptIn ? "opted in" : "opted out"}`);
1069
1190
  console.log(`side ${config.adSide}`);
1191
+ const nativeInstalled = isClaudePluginInstalled();
1192
+ const nativeActive = isNativePluginActive();
1193
+ console.log(
1194
+ `native ${nativeActive ? "active (AbovePrompt)" : nativeInstalled ? "installed (waiting for Mods session)" : "not installed"}`
1195
+ );
1070
1196
  console.log(`watch ${watchUrl(config.serverUrl, config.deviceId, "manual")}`);
1071
1197
  try {
1072
1198
  const [balance, verified] = await Promise.all([
@@ -1145,15 +1271,13 @@ async function cmdSet(key, value) {
1145
1271
  config.wallet = value;
1146
1272
  } else if (key === "server" && value && /^https?:\/\//.test(value)) {
1147
1273
  config.serverUrl = value.replace(/\/$/, "");
1148
- } else if (key === "ads" && (value === "on" || value === "off")) {
1149
- config.adsOptIn = value === "on";
1150
1274
  } else if (key === "side" && (value === "right" || value === "left" || value === "default")) {
1151
1275
  config.adSide = value;
1152
1276
  } else if (key === "email" && value && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
1153
1277
  config.email = value.trim().toLowerCase();
1154
1278
  } else {
1155
1279
  console.error(
1156
- "Usage: promptai set wallet 0x... | set server <url> | set ads on|off | set side right|left|default | set email you@example.com"
1280
+ "Usage: promptai set wallet 0x... | set server <url> | set side right|left|default | set email you@example.com"
1157
1281
  );
1158
1282
  process.exitCode = 1;
1159
1283
  return;
@@ -1172,15 +1296,42 @@ async function cmdSet(key, value) {
1172
1296
  saveConfig(config);
1173
1297
  console.log(`${key} updated.`);
1174
1298
  }
1299
+ function cmdAds(on) {
1300
+ const config = loadConfig();
1301
+ config.adsOptIn = on;
1302
+ saveConfig(config);
1303
+ console.log(on ? "Ads on." : "Ads off.");
1304
+ }
1305
+ function printInstallNext() {
1306
+ console.log("You can toggle ads with promptai on / promptai off.");
1307
+ console.log("Try: promptai watch, then run a prompt.");
1308
+ }
1309
+ function printClaudeModsHint(installPath) {
1310
+ console.log("");
1311
+ console.log("Native AbovePrompt ads use Claude Mods (function hooks, early access).");
1312
+ console.log("Enabled via env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in your Claude settings;");
1313
+ console.log("this also turns on any other function-hook plugins you have.");
1314
+ console.log(`Plugin path: ${installPath}`);
1315
+ console.log("Where Mods are unavailable, ads still open in the browser (/watch).");
1316
+ }
1175
1317
  async function cmdInstall(agent) {
1176
1318
  if (agent === "claude" || agent === "claude-code") {
1177
1319
  const { changed, settingsPath } = installClaudeHooks();
1178
1320
  console.log(
1179
1321
  changed ? `Hooks installed into ${settingsPath}.` : `Hooks already installed in ${settingsPath}.`
1180
1322
  );
1181
- console.log("Claude Code picks them up on its next session.");
1323
+ const plugin = installClaudePlugin();
1324
+ if (plugin.error) {
1325
+ console.log(`Mods plugin: ${plugin.error}`);
1326
+ } else {
1327
+ console.log(
1328
+ plugin.changed ? `Mods plugin installed into ${plugin.installPath}.` : `Mods plugin already at ${plugin.installPath}.`
1329
+ );
1330
+ printClaudeModsHint(plugin.installPath);
1331
+ }
1332
+ console.log("Claude Code picks hooks up on its next session.");
1182
1333
  await onboard(loadConfig());
1183
- console.log("Try: promptai watch, then run a prompt.");
1334
+ printInstallNext();
1184
1335
  return;
1185
1336
  }
1186
1337
  if (agent === "cursor" || agent === "cursor-agents") {
@@ -1192,7 +1343,7 @@ async function cmdInstall(agent) {
1192
1343
  "Works in the Agents Window and the classic IDE (the CLI stands down when the promptai extension is running). Cursor hot-reloads hooks.json."
1193
1344
  );
1194
1345
  await onboard(loadConfig());
1195
- console.log("Try: promptai watch, then run a prompt.");
1346
+ printInstallNext();
1196
1347
  return;
1197
1348
  }
1198
1349
  console.error("Supported agents: claude, cursor (Codex CLI coming next). Usage: promptai install <agent>");
@@ -1202,6 +1353,10 @@ function cmdUninstall(agent) {
1202
1353
  if (agent === "claude" || agent === "claude-code") {
1203
1354
  const { changed } = uninstallClaudeHooks();
1204
1355
  console.log(changed ? "Hooks removed." : "No promptai hooks found.");
1356
+ const plugin = uninstallClaudePlugin();
1357
+ console.log(
1358
+ plugin.changed ? `Mods plugin removed from ${plugin.installPath}.` : "No Mods plugin install found."
1359
+ );
1205
1360
  return;
1206
1361
  }
1207
1362
  if (agent === "cursor" || agent === "cursor-agents") {
@@ -1236,6 +1391,12 @@ try {
1236
1391
  case "set":
1237
1392
  await cmdSet(args[0], args[1]);
1238
1393
  break;
1394
+ case "on":
1395
+ cmdAds(true);
1396
+ break;
1397
+ case "off":
1398
+ cmdAds(false);
1399
+ break;
1239
1400
  default:
1240
1401
  console.log(HELP);
1241
1402
  if (command && command !== "help" && command !== "--help") process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptai.credit/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -22,11 +22,12 @@
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
- "README.md"
25
+ "README.md",
26
+ "claude-plugin"
26
27
  ],
27
28
  "scripts": {
28
29
  "dev": "tsx src/index.ts",
29
- "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\"",
30
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" && node scripts/copy-plugin.mjs",
30
31
  "prepublishOnly": "pnpm build",
31
32
  "typecheck": "tsc --noEmit"
32
33
  },