adsinagents 0.1.1 → 0.1.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/cli/dist/index.js CHANGED
@@ -4082,6 +4082,20 @@ var coerce = {
4082
4082
  };
4083
4083
  var NEVER = INVALID;
4084
4084
 
4085
+ // ../../shared/dist/categories.js
4086
+ var CATEGORIES = {
4087
+ crypto: ["crypto", "web3", "blockchain", "nft", "token", "defi", "bitcoin", "ethereum", "wallet"],
4088
+ gambling: ["casino", "betting", "sportsbook", "poker", "lottery", "wager", "slots"],
4089
+ alcohol: ["beer", "whiskey", "whisky", "vodka", "wine", "tequila", "liquor", "cocktail"],
4090
+ dating: ["dating", "hookup", "singles", "match with", "find love"],
4091
+ politics: ["campaign", "vote for", "super pac", "ballot", "elect "]
4092
+ };
4093
+ var CATEGORY_KEYS = Object.keys(CATEGORIES);
4094
+ var CategorySchema = external_exports.enum(CATEGORY_KEYS);
4095
+ function isCategory(value) {
4096
+ return CATEGORY_KEYS.includes(value);
4097
+ }
4098
+
4085
4099
  // ../../shared/dist/schemas.js
4086
4100
  var AdSchema = external_exports.object({
4087
4101
  /** Stable impression id for this rotation; idempotency key for firing. */
@@ -4124,6 +4138,7 @@ var GravityAdSchema = external_exports.object({
4124
4138
  });
4125
4139
  var GravityResponseSchema = external_exports.array(GravityAdSchema);
4126
4140
  var PlacementSchema = external_exports.enum(["statusline", "spinner", "both", "off"]);
4141
+ var ThemeSchema = external_exports.enum(["plain", "dim", "accent"]);
4127
4142
  var AgentNameSchema = external_exports.enum(["claude-code"]);
4128
4143
  var ConfigSchema = external_exports.object({
4129
4144
  placement: PlacementSchema.default("statusline"),
@@ -4133,6 +4148,12 @@ var ConfigSchema = external_exports.object({
4133
4148
  production: external_exports.boolean().default(false),
4134
4149
  /** Send prompt text to Gravity for contextual matching. PRIVACY: off by default. */
4135
4150
  contextualAds: external_exports.boolean().default(false),
4151
+ /**
4152
+ * Ad categories the user opted out of. Best-effort keyword match on the ad's
4153
+ * own text/brand, applied client-side in the daemon — a matched ad is dropped
4154
+ * before it paints (no impression, not billed). See shared/src/categories.ts.
4155
+ */
4156
+ blockedCategories: external_exports.array(CategorySchema).default([]),
4136
4157
  /** AdLine server base URL the daemon polls for ads / sync / flags.
4137
4158
  * Defaults to production — a fresh `npm i -g adsinagents` install must work
4138
4159
  * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
@@ -4151,10 +4172,18 @@ var ConfigSchema = external_exports.object({
4151
4172
  rateCapSec: external_exports.number().int().positive().default(60),
4152
4173
  /** Continuous focus seconds required before an impression verifies. */
4153
4174
  focusDwellSec: external_exports.number().int().positive().default(5),
4175
+ /**
4176
+ * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4177
+ * the status line goes clean until the next prompt. 0 = hide on Stop.
4178
+ * Display AND billing follow this gate together (billed ⇒ on screen).
4179
+ */
4180
+ idleGraceSec: external_exports.number().int().nonnegative().default(120),
4154
4181
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4155
4182
  killSwitchUrl: external_exports.string().default(""),
4156
4183
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4157
- showEarnings: external_exports.boolean().default(false)
4184
+ showEarnings: external_exports.boolean().default(false),
4185
+ /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4186
+ theme: ThemeSchema.default("plain")
4158
4187
  });
4159
4188
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4160
4189
  var DaemonStateSchema = external_exports.object({
@@ -4173,7 +4202,10 @@ var DaemonStateSchema = external_exports.object({
4173
4202
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4174
4203
  balanceCents: external_exports.number().int().nonnegative().default(0),
4175
4204
  /** Earnings today, cents. From the last sync response. Optional. */
4176
- todayCents: external_exports.number().int().nonnegative().default(0)
4205
+ todayCents: external_exports.number().int().nonnegative().default(0),
4206
+ /** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
4207
+ * working glyph. Additive — older daemons omit it. */
4208
+ working: external_exports.boolean().default(false)
4177
4209
  });
4178
4210
  var ImpressionSchema = external_exports.object({
4179
4211
  id: external_exports.string(),
@@ -4422,7 +4454,7 @@ function getPlatform() {
4422
4454
  }
4423
4455
 
4424
4456
  // ../../shared/dist/agent/claude-code.js
4425
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, copyFileSync, renameSync } from "node:fs";
4457
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, copyFileSync, renameSync, rmSync } from "node:fs";
4426
4458
  import { dirname as dirname2, join as join3 } from "node:path";
4427
4459
  import { execFileSync as execFileSync2 } from "node:child_process";
4428
4460
  var ClaudeCodeAdapter = class {
@@ -4430,6 +4462,9 @@ var ClaudeCodeAdapter = class {
4430
4462
  hasStatusLine: true,
4431
4463
  hasSpinner: true,
4432
4464
  hasHttpHooks: true,
4465
+ // CC strips OSC 8 in the status line (anthropics/claude-code#21586).
4466
+ statuslineHyperlinks: false,
4467
+ hasSlashCommands: true,
4433
4468
  displayName: "Claude Code",
4434
4469
  binaryName: "claude"
4435
4470
  };
@@ -4484,7 +4519,8 @@ var ClaudeCodeAdapter = class {
4484
4519
  next.statusLine = {
4485
4520
  type: "command",
4486
4521
  command: plan.statuslineCommand,
4487
- refreshInterval: 2
4522
+ // 1s so the working glyph animates at 1 frame/sec (was 2).
4523
+ refreshInterval: 1
4488
4524
  };
4489
4525
  }
4490
4526
  if (plan.placement === "spinner" || plan.placement === "both") {
@@ -4526,8 +4562,27 @@ var ClaudeCodeAdapter = class {
4526
4562
  const backup = this.backup();
4527
4563
  const { next, diff } = this.merge(plan);
4528
4564
  this.write(next);
4565
+ this.writeSlashCommand();
4529
4566
  return { backup, diff };
4530
4567
  }
4568
+ /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4569
+ slashCommandPath() {
4570
+ return join3(claudeConfigDir(), "commands", "adsinagents.md");
4571
+ }
4572
+ /**
4573
+ * Install /adsinagents — the status line itself is display-only, so this is
4574
+ * the in-session settings surface: the user types /adsinagents and Claude
4575
+ * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4576
+ * remove. Best-effort: a failure here never fails install.
4577
+ */
4578
+ writeSlashCommand() {
4579
+ try {
4580
+ const p = this.slashCommandPath();
4581
+ mkdirSync2(dirname2(p), { recursive: true });
4582
+ writeFileSync2(p, SLASH_COMMAND_MD);
4583
+ } catch {
4584
+ }
4585
+ }
4531
4586
  removeInstall() {
4532
4587
  const cur = this.read();
4533
4588
  const marker = cur[ADLINE_SENTINEL];
@@ -4552,6 +4607,13 @@ var ClaudeCodeAdapter = class {
4552
4607
  }
4553
4608
  delete next[ADLINE_SENTINEL];
4554
4609
  this.write(next);
4610
+ try {
4611
+ const p = this.slashCommandPath();
4612
+ if (existsSync2(p) && readFileSync2(p, "utf8").includes(SLASH_SENTINEL)) {
4613
+ rmSync(p);
4614
+ }
4615
+ } catch {
4616
+ }
4555
4617
  return { restored: true };
4556
4618
  }
4557
4619
  /**
@@ -4614,6 +4676,30 @@ var ClaudeCodeAdapter = class {
4614
4676
  return rows;
4615
4677
  }
4616
4678
  };
4679
+ var SLASH_SENTINEL = "<!-- adsinagents:managed -->";
4680
+ var SLASH_COMMAND_MD = `---
4681
+ description: AdsInAgents \u2014 earnings, settings, doctor for the sponsored status line
4682
+ allowed-tools: Bash(adsinagents:*)
4683
+ ---
4684
+ ${SLASH_SENTINEL}
4685
+
4686
+ You manage AdsInAgents (the sponsored status line) for the user via its CLI.
4687
+
4688
+ Current state:
4689
+ - Config: !\`adsinagents config get\`
4690
+ - Stats: !\`adsinagents stats\`
4691
+
4692
+ User request: $ARGUMENTS
4693
+
4694
+ If the request is empty, show current config + stats and list what can be changed.
4695
+ Apply changes with the CLI (never edit config files directly):
4696
+ - \`adsinagents config set show-earnings <true|false>\` \u2014 toggle the earnings readout
4697
+ - \`adsinagents config set theme <plain|dim|accent>\` \u2014 status line colors (accent = amber mark, bold brand, green earnings; note: Claude Code currently dims all status line colors upstream)
4698
+ - \`adsinagents config block <category>\` / \`config unblock <category>\` \u2014 opt out of ad categories (crypto, gambling, alcohol, dating, politics). Best-effort keyword match on the ad's text; a blocked ad is dropped before it shows and is never billed. Run \`config blocked\` to list. Map natural requests like "no crypto ads" to \`config block crypto\`.
4699
+ - \`adsinagents doctor\` \u2014 diagnose setup issues
4700
+ - \`adsinagents stats\` \u2014 impressions, clicks, balance
4701
+ - \`adsinagents remove\` \u2014 uninstall (confirm with the user first)
4702
+ `;
4617
4703
  function spinnerVerbsFor(ad) {
4618
4704
  if (!ad)
4619
4705
  return [];
@@ -4946,11 +5032,49 @@ function writeConfigPatch(patch) {
4946
5032
  writeFileSync3(PATHS.config, JSON.stringify(next, null, 2) + "\n");
4947
5033
  return next;
4948
5034
  }
5035
+ var THEME_VALUES = ["plain", "dim", "accent"];
4949
5036
  function cmdConfig(positional, flags) {
4950
5037
  if (positional[0] === "set" && positional[1] === "show-earnings") {
4951
5038
  const on = positional[2] !== "false";
4952
5039
  writeConfigPatch({ showEarnings: on });
4953
5040
  process.stdout.write(`\u2713 showEarnings = ${on}
5041
+ `);
5042
+ return;
5043
+ }
5044
+ if (positional[0] === "block" || positional[0] === "unblock") {
5045
+ const cat = positional[1];
5046
+ if (!cat || !isCategory(cat)) {
5047
+ process.stdout.write(`category must be one of: ${CATEGORY_KEYS.join(" | ")}
5048
+ `);
5049
+ process.exitCode = 1;
5050
+ return;
5051
+ }
5052
+ const cur = loadConfigSafe().blockedCategories;
5053
+ const next = positional[0] === "block" ? Array.from(/* @__PURE__ */ new Set([...cur, cat])) : cur.filter((c) => c !== cat);
5054
+ writeConfigPatch({ blockedCategories: next });
5055
+ const verb = positional[0] === "block" ? "blocking" : "showing";
5056
+ process.stdout.write(`\u2713 now ${verb} ${cat} ads \xB7 blocked: ${next.join(", ") || "(none)"}
5057
+ `);
5058
+ return;
5059
+ }
5060
+ if (positional[0] === "blocked") {
5061
+ const cur = loadConfigSafe().blockedCategories;
5062
+ process.stdout.write(`blocked: ${cur.join(", ") || "(none)"}
5063
+ `);
5064
+ process.stdout.write(`available: ${CATEGORY_KEYS.join(", ")}
5065
+ `);
5066
+ return;
5067
+ }
5068
+ if (positional[0] === "set" && positional[1] === "theme") {
5069
+ const t = positional[2];
5070
+ if (!THEME_VALUES.includes(t)) {
5071
+ process.stdout.write(`theme must be one of: ${THEME_VALUES.join(" | ")}
5072
+ `);
5073
+ process.exitCode = 1;
5074
+ return;
5075
+ }
5076
+ writeConfigPatch({ theme: t });
5077
+ process.stdout.write(`\u2713 theme = ${t}
4954
5078
  `);
4955
5079
  return;
4956
5080
  }
@@ -4959,7 +5083,9 @@ function cmdConfig(positional, flags) {
4959
5083
  process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
4960
5084
  return;
4961
5085
  }
4962
- process.stdout.write("usage: adsinagents config get | config set show-earnings <true|false>\n");
5086
+ process.stdout.write(
5087
+ "usage: adsinagents config get | config set show-earnings <true|false> | config set theme <plain|dim|accent> | config block <category> | config unblock <category> | config blocked\n"
5088
+ );
4963
5089
  }
4964
5090
  async function main() {
4965
5091
  const [, , cmd, ...rest] = process.argv;
@@ -4990,7 +5116,7 @@ async function main() {
4990
5116
  doctor
4991
5117
  stats
4992
5118
  claim print the URL to attach a login + get paid
4993
- config get | config set show-earnings <true|false>
5119
+ config get | config set show-earnings <true|false> | config set theme <plain|dim|accent>
4994
5120
  `
4995
5121
  );
4996
5122
  return;
@@ -4077,6 +4077,30 @@ var coerce = {
4077
4077
  };
4078
4078
  var NEVER = INVALID;
4079
4079
 
4080
+ // ../../shared/dist/categories.js
4081
+ var CATEGORIES = {
4082
+ crypto: ["crypto", "web3", "blockchain", "nft", "token", "defi", "bitcoin", "ethereum", "wallet"],
4083
+ gambling: ["casino", "betting", "sportsbook", "poker", "lottery", "wager", "slots"],
4084
+ alcohol: ["beer", "whiskey", "whisky", "vodka", "wine", "tequila", "liquor", "cocktail"],
4085
+ dating: ["dating", "hookup", "singles", "match with", "find love"],
4086
+ politics: ["campaign", "vote for", "super pac", "ballot", "elect "]
4087
+ };
4088
+ var CATEGORY_KEYS = Object.keys(CATEGORIES);
4089
+ var CategorySchema = external_exports.enum(CATEGORY_KEYS);
4090
+ function matchesBlocked(ad, blocked) {
4091
+ if (blocked.length === 0)
4092
+ return false;
4093
+ const haystack = `${ad.text} ${ad.brandName}`.toLowerCase();
4094
+ for (const cat of blocked) {
4095
+ const keywords = CATEGORIES[cat];
4096
+ if (!keywords)
4097
+ continue;
4098
+ if (keywords.some((kw) => haystack.includes(kw)))
4099
+ return true;
4100
+ }
4101
+ return false;
4102
+ }
4103
+
4080
4104
  // ../../shared/dist/schemas.js
4081
4105
  var AdSchema = external_exports.object({
4082
4106
  /** Stable impression id for this rotation; idempotency key for firing. */
@@ -4119,6 +4143,7 @@ var GravityAdSchema = external_exports.object({
4119
4143
  });
4120
4144
  var GravityResponseSchema = external_exports.array(GravityAdSchema);
4121
4145
  var PlacementSchema = external_exports.enum(["statusline", "spinner", "both", "off"]);
4146
+ var ThemeSchema = external_exports.enum(["plain", "dim", "accent"]);
4122
4147
  var AgentNameSchema = external_exports.enum(["claude-code"]);
4123
4148
  var ConfigSchema = external_exports.object({
4124
4149
  placement: PlacementSchema.default("statusline"),
@@ -4128,6 +4153,12 @@ var ConfigSchema = external_exports.object({
4128
4153
  production: external_exports.boolean().default(false),
4129
4154
  /** Send prompt text to Gravity for contextual matching. PRIVACY: off by default. */
4130
4155
  contextualAds: external_exports.boolean().default(false),
4156
+ /**
4157
+ * Ad categories the user opted out of. Best-effort keyword match on the ad's
4158
+ * own text/brand, applied client-side in the daemon — a matched ad is dropped
4159
+ * before it paints (no impression, not billed). See shared/src/categories.ts.
4160
+ */
4161
+ blockedCategories: external_exports.array(CategorySchema).default([]),
4131
4162
  /** AdLine server base URL the daemon polls for ads / sync / flags.
4132
4163
  * Defaults to production — a fresh `npm i -g adsinagents` install must work
4133
4164
  * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
@@ -4146,10 +4177,18 @@ var ConfigSchema = external_exports.object({
4146
4177
  rateCapSec: external_exports.number().int().positive().default(60),
4147
4178
  /** Continuous focus seconds required before an impression verifies. */
4148
4179
  focusDwellSec: external_exports.number().int().positive().default(5),
4180
+ /**
4181
+ * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4182
+ * the status line goes clean until the next prompt. 0 = hide on Stop.
4183
+ * Display AND billing follow this gate together (billed ⇒ on screen).
4184
+ */
4185
+ idleGraceSec: external_exports.number().int().nonnegative().default(120),
4149
4186
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4150
4187
  killSwitchUrl: external_exports.string().default(""),
4151
4188
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4152
- showEarnings: external_exports.boolean().default(false)
4189
+ showEarnings: external_exports.boolean().default(false),
4190
+ /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4191
+ theme: ThemeSchema.default("plain")
4153
4192
  });
4154
4193
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4155
4194
  var DAEMON_STATE_SCHEMA_VERSION = 1;
@@ -4169,7 +4208,10 @@ var DaemonStateSchema = external_exports.object({
4169
4208
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4170
4209
  balanceCents: external_exports.number().int().nonnegative().default(0),
4171
4210
  /** Earnings today, cents. From the last sync response. Optional. */
4172
- todayCents: external_exports.number().int().nonnegative().default(0)
4211
+ todayCents: external_exports.number().int().nonnegative().default(0),
4212
+ /** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
4213
+ * working glyph. Additive — older daemons omit it. */
4214
+ working: external_exports.boolean().default(false)
4173
4215
  });
4174
4216
  var ImpressionSchema = external_exports.object({
4175
4217
  id: external_exports.string(),
@@ -4418,7 +4460,7 @@ function getPlatform() {
4418
4460
  }
4419
4461
 
4420
4462
  // ../../shared/dist/agent/claude-code.js
4421
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, copyFileSync, renameSync } from "node:fs";
4463
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, copyFileSync, renameSync, rmSync } from "node:fs";
4422
4464
  import { dirname as dirname2, join as join3 } from "node:path";
4423
4465
  import { execFileSync as execFileSync2 } from "node:child_process";
4424
4466
  var ClaudeCodeAdapter = class {
@@ -4426,6 +4468,9 @@ var ClaudeCodeAdapter = class {
4426
4468
  hasStatusLine: true,
4427
4469
  hasSpinner: true,
4428
4470
  hasHttpHooks: true,
4471
+ // CC strips OSC 8 in the status line (anthropics/claude-code#21586).
4472
+ statuslineHyperlinks: false,
4473
+ hasSlashCommands: true,
4429
4474
  displayName: "Claude Code",
4430
4475
  binaryName: "claude"
4431
4476
  };
@@ -4480,7 +4525,8 @@ var ClaudeCodeAdapter = class {
4480
4525
  next.statusLine = {
4481
4526
  type: "command",
4482
4527
  command: plan.statuslineCommand,
4483
- refreshInterval: 2
4528
+ // 1s so the working glyph animates at 1 frame/sec (was 2).
4529
+ refreshInterval: 1
4484
4530
  };
4485
4531
  }
4486
4532
  if (plan.placement === "spinner" || plan.placement === "both") {
@@ -4522,8 +4568,27 @@ var ClaudeCodeAdapter = class {
4522
4568
  const backup = this.backup();
4523
4569
  const { next, diff } = this.merge(plan);
4524
4570
  this.write(next);
4571
+ this.writeSlashCommand();
4525
4572
  return { backup, diff };
4526
4573
  }
4574
+ /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4575
+ slashCommandPath() {
4576
+ return join3(claudeConfigDir(), "commands", "adsinagents.md");
4577
+ }
4578
+ /**
4579
+ * Install /adsinagents — the status line itself is display-only, so this is
4580
+ * the in-session settings surface: the user types /adsinagents and Claude
4581
+ * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4582
+ * remove. Best-effort: a failure here never fails install.
4583
+ */
4584
+ writeSlashCommand() {
4585
+ try {
4586
+ const p = this.slashCommandPath();
4587
+ mkdirSync2(dirname2(p), { recursive: true });
4588
+ writeFileSync2(p, SLASH_COMMAND_MD);
4589
+ } catch {
4590
+ }
4591
+ }
4527
4592
  removeInstall() {
4528
4593
  const cur = this.read();
4529
4594
  const marker = cur[ADLINE_SENTINEL];
@@ -4548,6 +4613,13 @@ var ClaudeCodeAdapter = class {
4548
4613
  }
4549
4614
  delete next[ADLINE_SENTINEL];
4550
4615
  this.write(next);
4616
+ try {
4617
+ const p = this.slashCommandPath();
4618
+ if (existsSync2(p) && readFileSync2(p, "utf8").includes(SLASH_SENTINEL)) {
4619
+ rmSync(p);
4620
+ }
4621
+ } catch {
4622
+ }
4551
4623
  return { restored: true };
4552
4624
  }
4553
4625
  /**
@@ -4610,6 +4682,30 @@ var ClaudeCodeAdapter = class {
4610
4682
  return rows;
4611
4683
  }
4612
4684
  };
4685
+ var SLASH_SENTINEL = "<!-- adsinagents:managed -->";
4686
+ var SLASH_COMMAND_MD = `---
4687
+ description: AdsInAgents \u2014 earnings, settings, doctor for the sponsored status line
4688
+ allowed-tools: Bash(adsinagents:*)
4689
+ ---
4690
+ ${SLASH_SENTINEL}
4691
+
4692
+ You manage AdsInAgents (the sponsored status line) for the user via its CLI.
4693
+
4694
+ Current state:
4695
+ - Config: !\`adsinagents config get\`
4696
+ - Stats: !\`adsinagents stats\`
4697
+
4698
+ User request: $ARGUMENTS
4699
+
4700
+ If the request is empty, show current config + stats and list what can be changed.
4701
+ Apply changes with the CLI (never edit config files directly):
4702
+ - \`adsinagents config set show-earnings <true|false>\` \u2014 toggle the earnings readout
4703
+ - \`adsinagents config set theme <plain|dim|accent>\` \u2014 status line colors (accent = amber mark, bold brand, green earnings; note: Claude Code currently dims all status line colors upstream)
4704
+ - \`adsinagents config block <category>\` / \`config unblock <category>\` \u2014 opt out of ad categories (crypto, gambling, alcohol, dating, politics). Best-effort keyword match on the ad's text; a blocked ad is dropped before it shows and is never billed. Run \`config blocked\` to list. Map natural requests like "no crypto ads" to \`config block crypto\`.
4705
+ - \`adsinagents doctor\` \u2014 diagnose setup issues
4706
+ - \`adsinagents stats\` \u2014 impressions, clicks, balance
4707
+ - \`adsinagents remove\` \u2014 uninstall (confirm with the user first)
4708
+ `;
4613
4709
  function spinnerVerbsFor(ad) {
4614
4710
  if (!ad)
4615
4711
  return [];
@@ -4847,6 +4943,7 @@ var SessionTracker = class {
4847
4943
  /** session_id -> startedAt ms. A session is live while present here. */
4848
4944
  live = /* @__PURE__ */ new Map();
4849
4945
  lastPromptAtMs = null;
4946
+ lastStopAtMs = null;
4850
4947
  onSessionStart(sessionId, nowMs) {
4851
4948
  this.live.set(sessionId, nowMs);
4852
4949
  }
@@ -4858,7 +4955,16 @@ var SessionTracker = class {
4858
4955
  this.lastPromptAtMs = nowMs;
4859
4956
  }
4860
4957
  /** Stop hook: assistant turn ended. Session stays live (only End closes it). */
4861
- onStop(_sessionId, _nowMs) {
4958
+ onStop(_sessionId, nowMs) {
4959
+ this.lastStopAtMs = nowMs;
4960
+ }
4961
+ /** Mid-turn: a prompt arrived after the last Stop (Claude is "working"). */
4962
+ working() {
4963
+ if (!this.sessionLive() || this.lastPromptAtMs === null) return false;
4964
+ return this.lastStopAtMs === null || this.lastPromptAtMs > this.lastStopAtMs;
4965
+ }
4966
+ lastStop() {
4967
+ return this.lastStopAtMs;
4862
4968
  }
4863
4969
  sessionLive() {
4864
4970
  return this.live.size > 0;
@@ -4987,15 +5093,21 @@ var AdSource = class {
4987
5093
  id: json.id ?? newImpId(nowMs),
4988
5094
  fetchedAt: nowMs
4989
5095
  });
4990
- return parsed.success ? parsed.data : void 0;
5096
+ if (!parsed.success) return void 0;
5097
+ if (matchesBlocked(parsed.data, this.cfg.blockedCategories)) return null;
5098
+ return parsed.data;
4991
5099
  } catch {
4992
5100
  return void 0;
4993
5101
  }
4994
5102
  }
4995
5103
  builtinTestAd(nowMs) {
4996
- const base = BUILTIN_TEST_ADS[this.rotation % BUILTIN_TEST_ADS.length];
4997
- this.rotation += 1;
4998
- return AdSchema.parse({ ...base, id: newImpId(nowMs), fetchedAt: nowMs, nonce: "" });
5104
+ for (let i = 0; i < BUILTIN_TEST_ADS.length; i++) {
5105
+ const base = BUILTIN_TEST_ADS[this.rotation % BUILTIN_TEST_ADS.length];
5106
+ this.rotation += 1;
5107
+ const ad = AdSchema.parse({ ...base, id: newImpId(nowMs), fetchedAt: nowMs, nonce: "" });
5108
+ if (!matchesBlocked(ad, this.cfg.blockedCategories)) return ad;
5109
+ }
5110
+ return null;
4999
5111
  }
5000
5112
  };
5001
5113
 
@@ -5256,6 +5368,9 @@ async function main() {
5256
5368
  await fileLog(`ad fetch error: ${String(e)}`);
5257
5369
  }
5258
5370
  }
5371
+ const working = sessions.working();
5372
+ const lastStop = sessions.lastStop();
5373
+ const adDisplayable = working || lastStop !== null && nowMs - lastStop <= cfg.idleGraceSec * 1e3;
5259
5374
  const probe = focus.current();
5260
5375
  const sample = {
5261
5376
  nowMs,
@@ -5264,7 +5379,7 @@ async function main() {
5264
5379
  screenUnlocked: !probe.locked,
5265
5380
  displayAwake: probe.screenAwake,
5266
5381
  disabled,
5267
- adPresent: currentAd !== null,
5382
+ adPresent: currentAd !== null && adDisplayable,
5268
5383
  lastPromptAtMs: sessions.lastPrompt(),
5269
5384
  adId: currentAd?.id ?? null,
5270
5385
  focusDwellSec: cfg.focusDwellSec,
@@ -5285,11 +5400,12 @@ async function main() {
5285
5400
  schemaVersion: DAEMON_STATE_SCHEMA_VERSION,
5286
5401
  sessionLive,
5287
5402
  disabled,
5288
- adVisible: !disabled && sessionLive && currentAd !== null,
5403
+ adVisible: !disabled && sessionLive && currentAd !== null && adDisplayable,
5289
5404
  port,
5290
5405
  updatedAt: nowMs,
5291
5406
  balanceCents: earnings.balanceCents,
5292
- todayCents: earnings.todayCents
5407
+ todayCents: earnings.todayCents,
5408
+ working
5293
5409
  };
5294
5410
  writeDaemonState(state);
5295
5411
  if (tickCount % SYNC_EVERY_TICKS === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adsinagents",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Get paid while you build. AdsInAgents is the terminal-native ad layer for AI coding agents. Verified impressions pay you.",
5
5
  "homepage": "https://adsinagents.com",
6
6
  "license": "UNLICENSED",
@@ -4070,6 +4070,17 @@ var coerce = {
4070
4070
  };
4071
4071
  var NEVER = INVALID;
4072
4072
 
4073
+ // ../../shared/dist/categories.js
4074
+ var CATEGORIES = {
4075
+ crypto: ["crypto", "web3", "blockchain", "nft", "token", "defi", "bitcoin", "ethereum", "wallet"],
4076
+ gambling: ["casino", "betting", "sportsbook", "poker", "lottery", "wager", "slots"],
4077
+ alcohol: ["beer", "whiskey", "whisky", "vodka", "wine", "tequila", "liquor", "cocktail"],
4078
+ dating: ["dating", "hookup", "singles", "match with", "find love"],
4079
+ politics: ["campaign", "vote for", "super pac", "ballot", "elect "]
4080
+ };
4081
+ var CATEGORY_KEYS = Object.keys(CATEGORIES);
4082
+ var CategorySchema = external_exports.enum(CATEGORY_KEYS);
4083
+
4073
4084
  // ../../shared/dist/schemas.js
4074
4085
  var AdSchema = external_exports.object({
4075
4086
  /** Stable impression id for this rotation; idempotency key for firing. */
@@ -4112,6 +4123,7 @@ var GravityAdSchema = external_exports.object({
4112
4123
  });
4113
4124
  var GravityResponseSchema = external_exports.array(GravityAdSchema);
4114
4125
  var PlacementSchema = external_exports.enum(["statusline", "spinner", "both", "off"]);
4126
+ var ThemeSchema = external_exports.enum(["plain", "dim", "accent"]);
4115
4127
  var AgentNameSchema = external_exports.enum(["claude-code"]);
4116
4128
  var ConfigSchema = external_exports.object({
4117
4129
  placement: PlacementSchema.default("statusline"),
@@ -4121,6 +4133,12 @@ var ConfigSchema = external_exports.object({
4121
4133
  production: external_exports.boolean().default(false),
4122
4134
  /** Send prompt text to Gravity for contextual matching. PRIVACY: off by default. */
4123
4135
  contextualAds: external_exports.boolean().default(false),
4136
+ /**
4137
+ * Ad categories the user opted out of. Best-effort keyword match on the ad's
4138
+ * own text/brand, applied client-side in the daemon — a matched ad is dropped
4139
+ * before it paints (no impression, not billed). See shared/src/categories.ts.
4140
+ */
4141
+ blockedCategories: external_exports.array(CategorySchema).default([]),
4124
4142
  /** AdLine server base URL the daemon polls for ads / sync / flags.
4125
4143
  * Defaults to production — a fresh `npm i -g adsinagents` install must work
4126
4144
  * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
@@ -4139,10 +4157,18 @@ var ConfigSchema = external_exports.object({
4139
4157
  rateCapSec: external_exports.number().int().positive().default(60),
4140
4158
  /** Continuous focus seconds required before an impression verifies. */
4141
4159
  focusDwellSec: external_exports.number().int().positive().default(5),
4160
+ /**
4161
+ * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4162
+ * the status line goes clean until the next prompt. 0 = hide on Stop.
4163
+ * Display AND billing follow this gate together (billed ⇒ on screen).
4164
+ */
4165
+ idleGraceSec: external_exports.number().int().nonnegative().default(120),
4142
4166
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4143
4167
  killSwitchUrl: external_exports.string().default(""),
4144
4168
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4145
- showEarnings: external_exports.boolean().default(false)
4169
+ showEarnings: external_exports.boolean().default(false),
4170
+ /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4171
+ theme: ThemeSchema.default("plain")
4146
4172
  });
4147
4173
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4148
4174
  var DaemonStateSchema = external_exports.object({
@@ -4161,7 +4187,10 @@ var DaemonStateSchema = external_exports.object({
4161
4187
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4162
4188
  balanceCents: external_exports.number().int().nonnegative().default(0),
4163
4189
  /** Earnings today, cents. From the last sync response. Optional. */
4164
- todayCents: external_exports.number().int().nonnegative().default(0)
4190
+ todayCents: external_exports.number().int().nonnegative().default(0),
4191
+ /** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
4192
+ * working glyph. Additive — older daemons omit it. */
4193
+ working: external_exports.boolean().default(false)
4165
4194
  });
4166
4195
  var ImpressionSchema = external_exports.object({
4167
4196
  id: external_exports.string(),
@@ -4191,19 +4220,41 @@ var FocusProbeSchema = external_exports.object({
4191
4220
  function osc8(url, text) {
4192
4221
  return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
4193
4222
  }
4223
+ var ID = (s) => s;
4224
+ var DIM = (s) => `\x1B[2m${s}\x1B[22m`;
4225
+ var BOLD = (s) => `\x1B[1m${s}\x1B[22m`;
4226
+ var GREEN = (s) => `\x1B[32m${s}\x1B[39m`;
4227
+ var AMBER = (s) => `\x1B[38;5;215m${s}\x1B[39m`;
4228
+ var THEMES = {
4229
+ plain: { disclosure: ID, mark: ID, brand: ID, money: ID },
4230
+ dim: { disclosure: DIM, mark: ID, brand: ID, money: ID },
4231
+ accent: { disclosure: DIM, mark: AMBER, brand: BOLD, money: GREEN }
4232
+ };
4233
+ var MAX_AD_TEXT = 60;
4194
4234
  function renderAdSegment(ad, opts) {
4195
- const star = "\u2736";
4196
- const arrow = "\u2197";
4197
- const label = ad.brandName ? `${ad.text} \xB7 ${ad.brandName}` : ad.text;
4198
- const core = `${star} ${label} ${arrow}`;
4235
+ const t = THEMES[opts.theme ?? "plain"];
4236
+ const star = t.mark("\u2736");
4237
+ const arrow = t.mark("\u2197");
4238
+ const text = ad.text.length > MAX_AD_TEXT ? ad.text.slice(0, MAX_AD_TEXT - 1).trimEnd() + "\u2026" : ad.text;
4239
+ const label = ad.brandName ? `${text} \xB7 ${t.brand(ad.brandName)}` : text;
4199
4240
  const clickUrl = `${opts.clickBase}/click/${encodeURIComponent(ad.id)}`;
4200
- const linked = opts.supportsOsc8 ? osc8(clickUrl, core) : core;
4201
- return `Ad: ${linked} \xB7 sponsored`;
4241
+ const linked = opts.supportsOsc8 ? osc8(clickUrl, `${star} ${label} ${arrow}`) : opts.plainLink ? `${star} ${label} \xB7 ${opts.plainLink} ${arrow}` : `${star} ${label} ${arrow}`;
4242
+ return `${t.disclosure("Ad:")} ${linked} ${t.disclosure("\xB7 sponsored")}`;
4202
4243
  }
4203
- function renderEarningsSegment(balanceCents, todayCents) {
4204
- const d = (c) => `$${(Math.max(0, c) / 100).toFixed(2)}`;
4244
+ function renderEarningsSegment(balanceCents, todayCents, theme = "plain") {
4245
+ const t = THEMES[theme];
4246
+ const d = (c) => t.money(`$${(Math.max(0, c) / 100).toFixed(2)}`);
4205
4247
  return `${d(todayCents)} today \xB7 ${d(balanceCents)}`;
4206
4248
  }
4249
+ function renderLoadingSegment(theme = "plain") {
4250
+ const s = "Ad: loading\u2026";
4251
+ return theme === "plain" ? s : DIM(s);
4252
+ }
4253
+ var WORKING_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4254
+ function workingGlyph(nowMs, theme = "plain") {
4255
+ const frame = WORKING_FRAMES[Math.floor(nowMs / 1e3) % WORKING_FRAMES.length];
4256
+ return THEMES[theme ?? "plain"].mark(frame);
4257
+ }
4207
4258
  function composeStatusLine(priorOutput, ...segments) {
4208
4259
  const parts = [priorOutput.trim(), ...segments.map((s) => s.trim())].filter(Boolean);
4209
4260
  return parts.join(" \u2502 ");
@@ -4217,6 +4268,32 @@ import { execFile, execFileSync } from "node:child_process";
4217
4268
  import { promisify } from "node:util";
4218
4269
  var pExecFile = promisify(execFile);
4219
4270
 
4271
+ // ../../shared/dist/agent/claude-code.js
4272
+ var SLASH_SENTINEL = "<!-- adsinagents:managed -->";
4273
+ var SLASH_COMMAND_MD = `---
4274
+ description: AdsInAgents \u2014 earnings, settings, doctor for the sponsored status line
4275
+ allowed-tools: Bash(adsinagents:*)
4276
+ ---
4277
+ ${SLASH_SENTINEL}
4278
+
4279
+ You manage AdsInAgents (the sponsored status line) for the user via its CLI.
4280
+
4281
+ Current state:
4282
+ - Config: !\`adsinagents config get\`
4283
+ - Stats: !\`adsinagents stats\`
4284
+
4285
+ User request: $ARGUMENTS
4286
+
4287
+ If the request is empty, show current config + stats and list what can be changed.
4288
+ Apply changes with the CLI (never edit config files directly):
4289
+ - \`adsinagents config set show-earnings <true|false>\` \u2014 toggle the earnings readout
4290
+ - \`adsinagents config set theme <plain|dim|accent>\` \u2014 status line colors (accent = amber mark, bold brand, green earnings; note: Claude Code currently dims all status line colors upstream)
4291
+ - \`adsinagents config block <category>\` / \`config unblock <category>\` \u2014 opt out of ad categories (crypto, gambling, alcohol, dating, politics). Best-effort keyword match on the ad's text; a blocked ad is dropped before it shows and is never billed. Run \`config blocked\` to list. Map natural requests like "no crypto ads" to \`config block crypto\`.
4292
+ - \`adsinagents doctor\` \u2014 diagnose setup issues
4293
+ - \`adsinagents stats\` \u2014 impressions, clicks, balance
4294
+ - \`adsinagents remove\` \u2014 uninstall (confirm with the user first)
4295
+ `;
4296
+
4220
4297
  // ../statusline/src/index.ts
4221
4298
  var STATE_STALE_MS = 3e4;
4222
4299
  function readJson(path) {
@@ -4227,11 +4304,15 @@ function readJson(path) {
4227
4304
  return null;
4228
4305
  }
4229
4306
  }
4230
- function terminalSupportsOsc8() {
4231
- if (process.env.FORCE_HYPERLINK === "1") return true;
4232
- const term = process.env.TERM_PROGRAM ?? "";
4233
- if (term === "Apple_Terminal") return false;
4234
- return true;
4307
+ function hostPreservesOsc8() {
4308
+ return process.env.FORCE_HYPERLINK === "1";
4309
+ }
4310
+ function plainClickLink(ad, serverUrl, clickBase) {
4311
+ const localServer = /127\.0\.0\.1|localhost/.test(serverUrl);
4312
+ if (ad.source === "direct" && !localServer) {
4313
+ return `${serverUrl.replace(/\/$/, "")}/c/${encodeURIComponent(ad.campaignId)}`;
4314
+ }
4315
+ return `${clickBase}/click/${encodeURIComponent(ad.id)}`;
4235
4316
  }
4236
4317
  function priorStatusline(stdinJson) {
4237
4318
  const cmd = process.env.ADSINAGENTS_PRIOR_STATUSLINE;
@@ -4265,20 +4346,23 @@ function main() {
4265
4346
  const stale = Date.now() - state.updatedAt > STATE_STALE_MS;
4266
4347
  const cfg = ConfigSchema.safeParse(readJson(PATHS.config));
4267
4348
  const showEarnings = cfg.success && cfg.data.showEarnings;
4268
- const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents) : "";
4269
- if (stale || state.disabled || !state.adVisible) {
4270
- process.stdout.write(composeStatusLine(prior, earningsSeg) + "\n");
4271
- return;
4272
- }
4349
+ const theme = cfg.success ? cfg.data.theme : "plain";
4350
+ const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents, theme) : "";
4273
4351
  const adParsed = AdSchema.safeParse(readJson(PATHS.currentAd));
4274
- if (!adParsed.success) {
4275
- process.stdout.write(composeStatusLine(prior, earningsSeg) + "\n");
4352
+ if (stale || state.disabled || !state.adVisible || !adParsed.success) {
4353
+ const loadingSeg = !stale && !state.disabled && state.working && !adParsed.success ? renderLoadingSegment(theme) : "";
4354
+ process.stdout.write(composeStatusLine(prior, loadingSeg, earningsSeg) + "\n");
4276
4355
  return;
4277
4356
  }
4357
+ const clickBase = `http://127.0.0.1:${state.port}`;
4358
+ const serverUrl = cfg.success ? cfg.data.serverUrl : "";
4278
4359
  const adSeg = renderAdSegment(adParsed.data, {
4279
- clickBase: `http://127.0.0.1:${state.port}`,
4280
- supportsOsc8: terminalSupportsOsc8()
4360
+ clickBase,
4361
+ supportsOsc8: hostPreservesOsc8(),
4362
+ plainLink: plainClickLink(adParsed.data, serverUrl, clickBase),
4363
+ theme
4281
4364
  });
4282
- process.stdout.write(composeStatusLine(prior, adSeg, earningsSeg) + "\n");
4365
+ const seg = state.working ? `${workingGlyph(Date.now(), theme)} ${adSeg}` : adSeg;
4366
+ process.stdout.write(composeStatusLine(prior, seg, earningsSeg) + "\n");
4283
4367
  }
4284
4368
  main();