adsinagents 0.1.0 → 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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # AdsInAgents
2
2
 
3
3
  Get paid while you build. AdsInAgents puts a single sponsored line in your
4
- Claude Code status bar and pays you for every verified impression.
4
+ coding agent's status line and pays you for every verified impression.
5
5
 
6
6
  ```sh
7
7
  npm install -g adsinagents
@@ -17,6 +17,6 @@ to attach a login and cash out.
17
17
 
18
18
  The ad is always disclosed (`· sponsored`), never injected into model context,
19
19
  and the daemon only counts an impression when the session is real, focused, and
20
- on-screen. macOS today; Windows next.
20
+ on-screen. Claude Code on macOS today; more agents and Windows next.
21
21
 
22
22
  [adsinagents.com](https://adsinagents.com) · advertisers: [adsinagents.com/advertise](https://adsinagents.com/advertise)
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,8 +4148,16 @@ 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),
4136
- /** AdLine server base URL the daemon polls for ads / sync / flags. */
4137
- serverUrl: external_exports.string().default("http://127.0.0.1:3000"),
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([]),
4157
+ /** AdLine server base URL the daemon polls for ads / sync / flags.
4158
+ * Defaults to production — a fresh `npm i -g adsinagents` install must work
4159
+ * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
4160
+ serverUrl: external_exports.string().default("https://adsinagents.com"),
4138
4161
  /** Device bearer token minted by `adsinagents init` (server /api/register). */
4139
4162
  deviceToken: external_exports.string().default(""),
4140
4163
  /** Stable per-install device id. */
@@ -4149,10 +4172,18 @@ var ConfigSchema = external_exports.object({
4149
4172
  rateCapSec: external_exports.number().int().positive().default(60),
4150
4173
  /** Continuous focus seconds required before an impression verifies. */
4151
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),
4152
4181
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4153
4182
  killSwitchUrl: external_exports.string().default(""),
4154
4183
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4155
- 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")
4156
4187
  });
4157
4188
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4158
4189
  var DaemonStateSchema = external_exports.object({
@@ -4171,7 +4202,10 @@ var DaemonStateSchema = external_exports.object({
4171
4202
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4172
4203
  balanceCents: external_exports.number().int().nonnegative().default(0),
4173
4204
  /** Earnings today, cents. From the last sync response. Optional. */
4174
- 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)
4175
4209
  });
4176
4210
  var ImpressionSchema = external_exports.object({
4177
4211
  id: external_exports.string(),
@@ -4420,7 +4454,7 @@ function getPlatform() {
4420
4454
  }
4421
4455
 
4422
4456
  // ../../shared/dist/agent/claude-code.js
4423
- 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";
4424
4458
  import { dirname as dirname2, join as join3 } from "node:path";
4425
4459
  import { execFileSync as execFileSync2 } from "node:child_process";
4426
4460
  var ClaudeCodeAdapter = class {
@@ -4428,6 +4462,9 @@ var ClaudeCodeAdapter = class {
4428
4462
  hasStatusLine: true,
4429
4463
  hasSpinner: true,
4430
4464
  hasHttpHooks: true,
4465
+ // CC strips OSC 8 in the status line (anthropics/claude-code#21586).
4466
+ statuslineHyperlinks: false,
4467
+ hasSlashCommands: true,
4431
4468
  displayName: "Claude Code",
4432
4469
  binaryName: "claude"
4433
4470
  };
@@ -4482,7 +4519,8 @@ var ClaudeCodeAdapter = class {
4482
4519
  next.statusLine = {
4483
4520
  type: "command",
4484
4521
  command: plan.statuslineCommand,
4485
- refreshInterval: 2
4522
+ // 1s so the working glyph animates at 1 frame/sec (was 2).
4523
+ refreshInterval: 1
4486
4524
  };
4487
4525
  }
4488
4526
  if (plan.placement === "spinner" || plan.placement === "both") {
@@ -4524,8 +4562,27 @@ var ClaudeCodeAdapter = class {
4524
4562
  const backup = this.backup();
4525
4563
  const { next, diff } = this.merge(plan);
4526
4564
  this.write(next);
4565
+ this.writeSlashCommand();
4527
4566
  return { backup, diff };
4528
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
+ }
4529
4586
  removeInstall() {
4530
4587
  const cur = this.read();
4531
4588
  const marker = cur[ADLINE_SENTINEL];
@@ -4550,6 +4607,13 @@ var ClaudeCodeAdapter = class {
4550
4607
  }
4551
4608
  delete next[ADLINE_SENTINEL];
4552
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
+ }
4553
4617
  return { restored: true };
4554
4618
  }
4555
4619
  /**
@@ -4612,6 +4676,30 @@ var ClaudeCodeAdapter = class {
4612
4676
  return rows;
4613
4677
  }
4614
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
+ `;
4615
4703
  function spinnerVerbsFor(ad) {
4616
4704
  if (!ad)
4617
4705
  return [];
@@ -4944,11 +5032,49 @@ function writeConfigPatch(patch) {
4944
5032
  writeFileSync3(PATHS.config, JSON.stringify(next, null, 2) + "\n");
4945
5033
  return next;
4946
5034
  }
5035
+ var THEME_VALUES = ["plain", "dim", "accent"];
4947
5036
  function cmdConfig(positional, flags) {
4948
5037
  if (positional[0] === "set" && positional[1] === "show-earnings") {
4949
5038
  const on = positional[2] !== "false";
4950
5039
  writeConfigPatch({ showEarnings: on });
4951
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}
4952
5078
  `);
4953
5079
  return;
4954
5080
  }
@@ -4957,7 +5083,9 @@ function cmdConfig(positional, flags) {
4957
5083
  process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
4958
5084
  return;
4959
5085
  }
4960
- 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
+ );
4961
5089
  }
4962
5090
  async function main() {
4963
5091
  const [, , cmd, ...rest] = process.argv;
@@ -4988,7 +5116,7 @@ async function main() {
4988
5116
  doctor
4989
5117
  stats
4990
5118
  claim print the URL to attach a login + get paid
4991
- config get | config set show-earnings <true|false>
5119
+ config get | config set show-earnings <true|false> | config set theme <plain|dim|accent>
4992
5120
  `
4993
5121
  );
4994
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,8 +4153,16 @@ 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),
4131
- /** AdLine server base URL the daemon polls for ads / sync / flags. */
4132
- serverUrl: external_exports.string().default("http://127.0.0.1:3000"),
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([]),
4162
+ /** AdLine server base URL the daemon polls for ads / sync / flags.
4163
+ * Defaults to production — a fresh `npm i -g adsinagents` install must work
4164
+ * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
4165
+ serverUrl: external_exports.string().default("https://adsinagents.com"),
4133
4166
  /** Device bearer token minted by `adsinagents init` (server /api/register). */
4134
4167
  deviceToken: external_exports.string().default(""),
4135
4168
  /** Stable per-install device id. */
@@ -4144,10 +4177,18 @@ var ConfigSchema = external_exports.object({
4144
4177
  rateCapSec: external_exports.number().int().positive().default(60),
4145
4178
  /** Continuous focus seconds required before an impression verifies. */
4146
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),
4147
4186
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4148
4187
  killSwitchUrl: external_exports.string().default(""),
4149
4188
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4150
- 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")
4151
4192
  });
4152
4193
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4153
4194
  var DAEMON_STATE_SCHEMA_VERSION = 1;
@@ -4167,7 +4208,10 @@ var DaemonStateSchema = external_exports.object({
4167
4208
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4168
4209
  balanceCents: external_exports.number().int().nonnegative().default(0),
4169
4210
  /** Earnings today, cents. From the last sync response. Optional. */
4170
- 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)
4171
4215
  });
4172
4216
  var ImpressionSchema = external_exports.object({
4173
4217
  id: external_exports.string(),
@@ -4416,7 +4460,7 @@ function getPlatform() {
4416
4460
  }
4417
4461
 
4418
4462
  // ../../shared/dist/agent/claude-code.js
4419
- 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";
4420
4464
  import { dirname as dirname2, join as join3 } from "node:path";
4421
4465
  import { execFileSync as execFileSync2 } from "node:child_process";
4422
4466
  var ClaudeCodeAdapter = class {
@@ -4424,6 +4468,9 @@ var ClaudeCodeAdapter = class {
4424
4468
  hasStatusLine: true,
4425
4469
  hasSpinner: true,
4426
4470
  hasHttpHooks: true,
4471
+ // CC strips OSC 8 in the status line (anthropics/claude-code#21586).
4472
+ statuslineHyperlinks: false,
4473
+ hasSlashCommands: true,
4427
4474
  displayName: "Claude Code",
4428
4475
  binaryName: "claude"
4429
4476
  };
@@ -4478,7 +4525,8 @@ var ClaudeCodeAdapter = class {
4478
4525
  next.statusLine = {
4479
4526
  type: "command",
4480
4527
  command: plan.statuslineCommand,
4481
- refreshInterval: 2
4528
+ // 1s so the working glyph animates at 1 frame/sec (was 2).
4529
+ refreshInterval: 1
4482
4530
  };
4483
4531
  }
4484
4532
  if (plan.placement === "spinner" || plan.placement === "both") {
@@ -4520,8 +4568,27 @@ var ClaudeCodeAdapter = class {
4520
4568
  const backup = this.backup();
4521
4569
  const { next, diff } = this.merge(plan);
4522
4570
  this.write(next);
4571
+ this.writeSlashCommand();
4523
4572
  return { backup, diff };
4524
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
+ }
4525
4592
  removeInstall() {
4526
4593
  const cur = this.read();
4527
4594
  const marker = cur[ADLINE_SENTINEL];
@@ -4546,6 +4613,13 @@ var ClaudeCodeAdapter = class {
4546
4613
  }
4547
4614
  delete next[ADLINE_SENTINEL];
4548
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
+ }
4549
4623
  return { restored: true };
4550
4624
  }
4551
4625
  /**
@@ -4608,6 +4682,30 @@ var ClaudeCodeAdapter = class {
4608
4682
  return rows;
4609
4683
  }
4610
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
+ `;
4611
4709
  function spinnerVerbsFor(ad) {
4612
4710
  if (!ad)
4613
4711
  return [];
@@ -4845,6 +4943,7 @@ var SessionTracker = class {
4845
4943
  /** session_id -> startedAt ms. A session is live while present here. */
4846
4944
  live = /* @__PURE__ */ new Map();
4847
4945
  lastPromptAtMs = null;
4946
+ lastStopAtMs = null;
4848
4947
  onSessionStart(sessionId, nowMs) {
4849
4948
  this.live.set(sessionId, nowMs);
4850
4949
  }
@@ -4856,7 +4955,16 @@ var SessionTracker = class {
4856
4955
  this.lastPromptAtMs = nowMs;
4857
4956
  }
4858
4957
  /** Stop hook: assistant turn ended. Session stays live (only End closes it). */
4859
- 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;
4860
4968
  }
4861
4969
  sessionLive() {
4862
4970
  return this.live.size > 0;
@@ -4985,15 +5093,21 @@ var AdSource = class {
4985
5093
  id: json.id ?? newImpId(nowMs),
4986
5094
  fetchedAt: nowMs
4987
5095
  });
4988
- 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;
4989
5099
  } catch {
4990
5100
  return void 0;
4991
5101
  }
4992
5102
  }
4993
5103
  builtinTestAd(nowMs) {
4994
- const base = BUILTIN_TEST_ADS[this.rotation % BUILTIN_TEST_ADS.length];
4995
- this.rotation += 1;
4996
- 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;
4997
5111
  }
4998
5112
  };
4999
5113
 
@@ -5254,6 +5368,9 @@ async function main() {
5254
5368
  await fileLog(`ad fetch error: ${String(e)}`);
5255
5369
  }
5256
5370
  }
5371
+ const working = sessions.working();
5372
+ const lastStop = sessions.lastStop();
5373
+ const adDisplayable = working || lastStop !== null && nowMs - lastStop <= cfg.idleGraceSec * 1e3;
5257
5374
  const probe = focus.current();
5258
5375
  const sample = {
5259
5376
  nowMs,
@@ -5262,7 +5379,7 @@ async function main() {
5262
5379
  screenUnlocked: !probe.locked,
5263
5380
  displayAwake: probe.screenAwake,
5264
5381
  disabled,
5265
- adPresent: currentAd !== null,
5382
+ adPresent: currentAd !== null && adDisplayable,
5266
5383
  lastPromptAtMs: sessions.lastPrompt(),
5267
5384
  adId: currentAd?.id ?? null,
5268
5385
  focusDwellSec: cfg.focusDwellSec,
@@ -5283,11 +5400,12 @@ async function main() {
5283
5400
  schemaVersion: DAEMON_STATE_SCHEMA_VERSION,
5284
5401
  sessionLive,
5285
5402
  disabled,
5286
- adVisible: !disabled && sessionLive && currentAd !== null,
5403
+ adVisible: !disabled && sessionLive && currentAd !== null && adDisplayable,
5287
5404
  port,
5288
5405
  updatedAt: nowMs,
5289
5406
  balanceCents: earnings.balanceCents,
5290
- todayCents: earnings.todayCents
5407
+ todayCents: earnings.todayCents,
5408
+ working
5291
5409
  };
5292
5410
  writeDaemonState(state);
5293
5411
  if (tickCount % SYNC_EVERY_TICKS === 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adsinagents",
3
- "version": "0.1.0",
4
- "description": "Get paid while you build. A terminal-native ad layer for Claude Code that pays you for every verified impression.",
3
+ "version": "0.1.2",
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",
7
7
  "type": "module",
@@ -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,8 +4133,16 @@ 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),
4124
- /** AdLine server base URL the daemon polls for ads / sync / flags. */
4125
- serverUrl: external_exports.string().default("http://127.0.0.1:3000"),
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([]),
4142
+ /** AdLine server base URL the daemon polls for ads / sync / flags.
4143
+ * Defaults to production — a fresh `npm i -g adsinagents` install must work
4144
+ * with zero config. Local dev: set serverUrl in ~/.adsinagents/config.json. */
4145
+ serverUrl: external_exports.string().default("https://adsinagents.com"),
4126
4146
  /** Device bearer token minted by `adsinagents init` (server /api/register). */
4127
4147
  deviceToken: external_exports.string().default(""),
4128
4148
  /** Stable per-install device id. */
@@ -4137,10 +4157,18 @@ var ConfigSchema = external_exports.object({
4137
4157
  rateCapSec: external_exports.number().int().positive().default(60),
4138
4158
  /** Continuous focus seconds required before an impression verifies. */
4139
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),
4140
4166
  /** Remote kill-switch flag URL (optional; falls back to serverUrl/api/flags). */
4141
4167
  killSwitchUrl: external_exports.string().default(""),
4142
4168
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4143
- 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")
4144
4172
  });
4145
4173
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4146
4174
  var DaemonStateSchema = external_exports.object({
@@ -4159,7 +4187,10 @@ var DaemonStateSchema = external_exports.object({
4159
4187
  /** Publisher lifetime balance, cents. From the last sync response. Optional. */
4160
4188
  balanceCents: external_exports.number().int().nonnegative().default(0),
4161
4189
  /** Earnings today, cents. From the last sync response. Optional. */
4162
- 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)
4163
4194
  });
4164
4195
  var ImpressionSchema = external_exports.object({
4165
4196
  id: external_exports.string(),
@@ -4189,19 +4220,41 @@ var FocusProbeSchema = external_exports.object({
4189
4220
  function osc8(url, text) {
4190
4221
  return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
4191
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;
4192
4234
  function renderAdSegment(ad, opts) {
4193
- const star = "\u2736";
4194
- const arrow = "\u2197";
4195
- const label = ad.brandName ? `${ad.text} \xB7 ${ad.brandName}` : ad.text;
4196
- 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;
4197
4240
  const clickUrl = `${opts.clickBase}/click/${encodeURIComponent(ad.id)}`;
4198
- const linked = opts.supportsOsc8 ? osc8(clickUrl, core) : core;
4199
- 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")}`;
4200
4243
  }
4201
- function renderEarningsSegment(balanceCents, todayCents) {
4202
- 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)}`);
4203
4247
  return `${d(todayCents)} today \xB7 ${d(balanceCents)}`;
4204
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
+ }
4205
4258
  function composeStatusLine(priorOutput, ...segments) {
4206
4259
  const parts = [priorOutput.trim(), ...segments.map((s) => s.trim())].filter(Boolean);
4207
4260
  return parts.join(" \u2502 ");
@@ -4215,6 +4268,32 @@ import { execFile, execFileSync } from "node:child_process";
4215
4268
  import { promisify } from "node:util";
4216
4269
  var pExecFile = promisify(execFile);
4217
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
+
4218
4297
  // ../statusline/src/index.ts
4219
4298
  var STATE_STALE_MS = 3e4;
4220
4299
  function readJson(path) {
@@ -4225,11 +4304,15 @@ function readJson(path) {
4225
4304
  return null;
4226
4305
  }
4227
4306
  }
4228
- function terminalSupportsOsc8() {
4229
- if (process.env.FORCE_HYPERLINK === "1") return true;
4230
- const term = process.env.TERM_PROGRAM ?? "";
4231
- if (term === "Apple_Terminal") return false;
4232
- 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)}`;
4233
4316
  }
4234
4317
  function priorStatusline(stdinJson) {
4235
4318
  const cmd = process.env.ADSINAGENTS_PRIOR_STATUSLINE;
@@ -4263,20 +4346,23 @@ function main() {
4263
4346
  const stale = Date.now() - state.updatedAt > STATE_STALE_MS;
4264
4347
  const cfg = ConfigSchema.safeParse(readJson(PATHS.config));
4265
4348
  const showEarnings = cfg.success && cfg.data.showEarnings;
4266
- const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents) : "";
4267
- if (stale || state.disabled || !state.adVisible) {
4268
- process.stdout.write(composeStatusLine(prior, earningsSeg) + "\n");
4269
- return;
4270
- }
4349
+ const theme = cfg.success ? cfg.data.theme : "plain";
4350
+ const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents, theme) : "";
4271
4351
  const adParsed = AdSchema.safeParse(readJson(PATHS.currentAd));
4272
- if (!adParsed.success) {
4273
- 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");
4274
4355
  return;
4275
4356
  }
4357
+ const clickBase = `http://127.0.0.1:${state.port}`;
4358
+ const serverUrl = cfg.success ? cfg.data.serverUrl : "";
4276
4359
  const adSeg = renderAdSegment(adParsed.data, {
4277
- clickBase: `http://127.0.0.1:${state.port}`,
4278
- supportsOsc8: terminalSupportsOsc8()
4360
+ clickBase,
4361
+ supportsOsc8: hostPreservesOsc8(),
4362
+ plainLink: plainClickLink(adParsed.data, serverUrl, clickBase),
4363
+ theme
4279
4364
  });
4280
- 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");
4281
4367
  }
4282
4368
  main();