adsinagents 0.1.7 → 0.1.9

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
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // ../cli/src/index.ts
11
- import { dirname as dirname4, join as join5 } from "node:path";
11
+ import { dirname as dirname4, join as join6 } from "node:path";
12
12
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13
13
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
14
14
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -4166,8 +4166,10 @@ var ConfigSchema = external_exports.object({
4166
4166
  deviceId: external_exports.string().default(""),
4167
4167
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4168
4168
  claimToken: external_exports.string().default(""),
4169
- /** Ad cache poll interval (seconds). */
4170
- pollIntervalSec: external_exports.number().int().positive().default(60),
4169
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4170
+ * is gated separately by rateCapSec, so a faster poll rotates creatives
4171
+ * without changing earnings. */
4172
+ pollIntervalSec: external_exports.number().int().positive().default(30),
4171
4173
  /** Hard daily fired-impression cap. */
4172
4174
  dailyCap: external_exports.number().int().positive().default(300),
4173
4175
  /** Min seconds of verified view between two fired impressions. */
@@ -4542,8 +4544,8 @@ var ClaudeCodeAdapter = class {
4542
4544
  else
4543
4545
  priorValues.hooks = cur.hooks;
4544
4546
  const mk = (path) => ({
4545
- type: "http",
4546
- url: `${plan.hookBaseUrl}${path}`
4547
+ type: "command",
4548
+ command: hookCommand(path)
4547
4549
  });
4548
4550
  next.hooks = {
4549
4551
  ...hooks,
@@ -4568,8 +4570,8 @@ var ClaudeCodeAdapter = class {
4568
4570
  const backup = this.backup();
4569
4571
  const { next, diff } = this.merge(plan);
4570
4572
  this.write(next);
4571
- this.writeSlashCommand();
4572
- return { backup, diff };
4573
+ const slashCommand = this.writeSlashCommand();
4574
+ return { backup, diff, slashCommand };
4573
4575
  }
4574
4576
  /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4575
4577
  slashCommandPath() {
@@ -4579,14 +4581,22 @@ var ClaudeCodeAdapter = class {
4579
4581
  * Install /adsinagents — the status line itself is display-only, so this is
4580
4582
  * the in-session settings surface: the user types /adsinagents and Claude
4581
4583
  * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4582
- * remove. Best-effort: a failure here never fails install.
4584
+ * remove. Reconciles: writes if missing or stale (content drift), skips if
4585
+ * already correct. Returns the outcome so init can REPORT failures instead of
4586
+ * silently swallowing them (the old try/catch hid a real install bug — the
4587
+ * file never wrote and doctor was the only thing that ever noticed).
4583
4588
  */
4584
4589
  writeSlashCommand() {
4590
+ const p = this.slashCommandPath();
4585
4591
  try {
4586
- const p = this.slashCommandPath();
4592
+ if (existsSync2(p) && readFileSync2(p, "utf8") === SLASH_COMMAND_MD) {
4593
+ return { ok: true };
4594
+ }
4587
4595
  mkdirSync2(dirname2(p), { recursive: true });
4588
4596
  writeFileSync2(p, SLASH_COMMAND_MD);
4589
- } catch {
4597
+ return { ok: true };
4598
+ } catch (e) {
4599
+ return { ok: false, reason: e.message };
4590
4600
  }
4591
4601
  }
4592
4602
  removeInstall() {
@@ -4716,10 +4726,18 @@ function spinnerVerbsFor(ad) {
4716
4726
  `Brought to you by ${brand}`
4717
4727
  ];
4718
4728
  }
4729
+ function hookCommand(path) {
4730
+ return [
4731
+ `p=$(node -e 'try{process.stdout.write(String(require(process.env.HOME+"/.adsinagents/daemon.json").port||""))}catch(e){}' 2>/dev/null)`,
4732
+ `[ -n "$p" ] && curl -s --max-time 2 -X POST "http://127.0.0.1:$p${path}" -H 'content-type: application/json' --data-binary @- >/dev/null 2>&1`,
4733
+ `true`
4734
+ ].join("; ");
4735
+ }
4719
4736
  function appendHook(existing, entry) {
4720
4737
  const arr = Array.isArray(existing) ? existing.slice() : existing ? [existing] : [];
4721
- arr.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4722
- return arr;
4738
+ const userOwned = arr.filter((e) => !(e && typeof e === "object" && e[ADLINE_SENTINEL]));
4739
+ userOwned.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4740
+ return userOwned;
4723
4741
  }
4724
4742
  function renderDiff(before, after) {
4725
4743
  const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
@@ -4761,6 +4779,9 @@ function getAgent(name = "claude-code") {
4761
4779
 
4762
4780
  // ../cli/src/settings.ts
4763
4781
  var cc = () => getAgent("claude-code");
4782
+ function isInstalled() {
4783
+ return cc().isInstalled();
4784
+ }
4764
4785
  function planMerge(plan) {
4765
4786
  return cc().planInstall(plan);
4766
4787
  }
@@ -4804,6 +4825,10 @@ function buildAndInstallNative() {
4804
4825
  if (existsSync3(src)) {
4805
4826
  copyFileSync2(src, dest);
4806
4827
  chmodSync2(dest, 493);
4828
+ try {
4829
+ execFileSync3("codesign", ["-f", "--sign", "-", dest], { stdio: "pipe" });
4830
+ } catch {
4831
+ }
4807
4832
  installed.push(dest);
4808
4833
  }
4809
4834
  }
@@ -4812,6 +4837,9 @@ function buildAndInstallNative() {
4812
4837
 
4813
4838
  // ../cli/src/doctor.ts
4814
4839
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
4840
+ import { spawnSync } from "node:child_process";
4841
+ import { homedir as homedir3 } from "node:os";
4842
+ import { join as join5 } from "node:path";
4815
4843
  function daemonAlive() {
4816
4844
  if (!existsSync4(PATHS.daemonInfo)) return { ok: false, detail: "no daemon.json (not started)" };
4817
4845
  try {
@@ -4863,8 +4891,133 @@ async function runDoctor() {
4863
4891
  ok: key !== null,
4864
4892
  detail: key ? "present (Keychain/file)" : "none \u2014 run `adsinagents login`"
4865
4893
  });
4894
+ checks.push(checkStatusLine());
4895
+ checks.push(checkHookReachable());
4896
+ checks.push(checkSlashCommand());
4866
4897
  return checks;
4867
4898
  }
4899
+ function checkStatusLine() {
4900
+ const name = "Status line renders";
4901
+ const settingsPath = join5(claudeConfigDir(), "settings.json");
4902
+ if (!existsSync4(settingsPath)) {
4903
+ return { name, ok: true, detail: "no statusLine command configured" };
4904
+ }
4905
+ let settings;
4906
+ try {
4907
+ settings = JSON.parse(readFileSync3(settingsPath, "utf8"));
4908
+ } catch {
4909
+ return { name, ok: true, detail: "settings.json unreadable (skipped)" };
4910
+ }
4911
+ const cmd = settings.statusLine?.command;
4912
+ if (!cmd) {
4913
+ return { name, ok: true, detail: "no statusLine command configured" };
4914
+ }
4915
+ let hasAd = false;
4916
+ try {
4917
+ if (existsSync4(PATHS.currentAd)) {
4918
+ const ad = JSON.parse(readFileSync3(PATHS.currentAd, "utf8"));
4919
+ hasAd = !!(ad.adId && String(ad.adId).trim().length > 0);
4920
+ }
4921
+ } catch {
4922
+ hasAd = false;
4923
+ }
4924
+ const parts = cmd.split(" ");
4925
+ const exe = parts[0];
4926
+ const args = parts.slice(1);
4927
+ const sessionJson = JSON.stringify({
4928
+ session_id: "doctor",
4929
+ workspace: { current_dir: "/tmp" },
4930
+ model: { display_name: "x" }
4931
+ });
4932
+ const result = spawnSync(exe, args, {
4933
+ input: sessionJson,
4934
+ encoding: "utf8",
4935
+ timeout: 3e3
4936
+ });
4937
+ if (result.error) {
4938
+ const errCode = result.error.code;
4939
+ const msg = errCode === "ENOENT" ? "not found" : result.error.message;
4940
+ return { name, ok: false, detail: `statusLine command not found or not executable: ${exe} (${msg})` };
4941
+ }
4942
+ if (result.status !== 0) {
4943
+ return { name, ok: false, detail: `statusLine command exited with error (code ${result.status ?? "?"})` };
4944
+ }
4945
+ const stdout = (result.stdout ?? "").trim();
4946
+ if (hasAd && stdout.length === 0) {
4947
+ return { name, ok: false, detail: "statusLine command produced no output (ad exists but nothing rendered)" };
4948
+ }
4949
+ if (stdout.length > 0) {
4950
+ return { name, ok: true, detail: `rendered ${stdout.length} chars` };
4951
+ }
4952
+ return { name, ok: true, detail: "ok (no ad to render right now)" };
4953
+ }
4954
+ function checkHookReachable() {
4955
+ const name = "Hook reachable";
4956
+ let daemonPort = null;
4957
+ if (existsSync4(PATHS.daemonInfo)) {
4958
+ try {
4959
+ const info = JSON.parse(readFileSync3(PATHS.daemonInfo, "utf8"));
4960
+ daemonPort = info.port ?? null;
4961
+ } catch {
4962
+ daemonPort = null;
4963
+ }
4964
+ }
4965
+ if (daemonPort === null) {
4966
+ return { name, ok: true, detail: "daemon not running (skipped)" };
4967
+ }
4968
+ const settingsPath = join5(claudeConfigDir(), "settings.json");
4969
+ let hookPort = null;
4970
+ if (existsSync4(settingsPath)) {
4971
+ try {
4972
+ const s = JSON.parse(readFileSync3(settingsPath, "utf8"));
4973
+ const hooksStr = JSON.stringify(s.hooks ?? {});
4974
+ const m = /http:\/\/127\.0\.0\.1:(\d+)/.exec(hooksStr);
4975
+ if (m) {
4976
+ hookPort = Number(m[1]);
4977
+ } else {
4978
+ hookPort = daemonPort;
4979
+ }
4980
+ } catch {
4981
+ hookPort = daemonPort;
4982
+ }
4983
+ }
4984
+ const result = spawnSync(
4985
+ "node",
4986
+ [
4987
+ "-e",
4988
+ `const h=require("node:http");const r=h.get("http://127.0.0.1:${daemonPort}/health",res=>{let b="";res.on("data",d=>b+=d);res.on("end",()=>{process.stdout.write(b);process.exit(0)});});r.on("error",e=>{process.exit(1)});r.setTimeout(2000,()=>{r.destroy();process.exit(2)});`
4989
+ ],
4990
+ { encoding: "utf8", timeout: 4e3 }
4991
+ );
4992
+ if (result.status !== 0) {
4993
+ return { name, ok: false, detail: `daemon unreachable on port ${daemonPort}` };
4994
+ }
4995
+ if (hookPort !== null && hookPort !== daemonPort) {
4996
+ return {
4997
+ name,
4998
+ ok: false,
4999
+ detail: `hooks point at port ${hookPort} but daemon is on ${daemonPort}`
5000
+ };
5001
+ }
5002
+ return { name, ok: true, detail: `daemon OK on port ${daemonPort}` };
5003
+ }
5004
+ function checkSlashCommand() {
5005
+ const name = "Slash command installed";
5006
+ const SENTINEL = "<!-- adsinagents:managed -->";
5007
+ const cmdPath = join5(claudeConfigDir(), "commands", "adsinagents.md");
5008
+ if (!existsSync4(cmdPath)) {
5009
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5010
+ }
5011
+ try {
5012
+ const contents = readFileSync3(cmdPath, "utf8");
5013
+ if (!contents.includes(SENTINEL)) {
5014
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5015
+ }
5016
+ } catch {
5017
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5018
+ }
5019
+ return { name, ok: true, detail: `present at ${cmdPath.replace(homedir3(), "~")}` };
5020
+ }
4868
5021
  function printDoctor(checks) {
4869
5022
  for (const c of checks) {
4870
5023
  const mark = c.ok ? "\u2713" : "\u2717";
@@ -4873,19 +5026,73 @@ function printDoctor(checks) {
4873
5026
  }
4874
5027
  }
4875
5028
 
5029
+ // ../cli/src/init-ui.ts
5030
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
5031
+ var ID = (s) => s;
5032
+ var dim = useColor ? (s) => `\x1B[2m${s}\x1B[0m` : ID;
5033
+ var bold = useColor ? (s) => `\x1B[1m${s}\x1B[0m` : ID;
5034
+ var green = useColor ? (s) => `\x1B[32m${s}\x1B[0m` : ID;
5035
+ var amber = useColor ? (s) => `\x1B[38;5;215m${s}\x1B[0m` : ID;
5036
+ function renderInitSummary(data) {
5037
+ const lines = [];
5038
+ const tick = green("\u2713");
5039
+ const warn = amber("!");
5040
+ const dot = dim("\xB7");
5041
+ if (data.firstRun) {
5042
+ lines.push(`${amber("\u258C")} ${bold("AdsInAgents")} ${dim("\u2014 sponsored status line for Claude Code")}`);
5043
+ lines.push("");
5044
+ const labelW = maxLabelWidth(data.steps);
5045
+ for (const step of data.steps) {
5046
+ const icon = step.ok ? tick : warn;
5047
+ const label = step.label.padEnd(labelW);
5048
+ const note = step.note ? ` ${dim("(" + step.note + ")")}` : "";
5049
+ lines.push(` ${icon} ${label} ${dot} ${step.detail}${note}`);
5050
+ }
5051
+ lines.push("");
5052
+ lines.push(` ${bold("You're live.")} Restart Claude Code to see your first ad.`);
5053
+ lines.push("");
5054
+ lines.push(` ${dim("Verify ")} adsinagents doctor`);
5055
+ if (data.claimUrl) {
5056
+ lines.push(` ${dim("Get paid")} ${amber(data.claimUrl)}`);
5057
+ }
5058
+ lines.push("");
5059
+ lines.push(` ${dim("Ads are test-only for now.")}`);
5060
+ } else {
5061
+ lines.push(`${amber("\u258C")} ${bold("AdsInAgents")} ${dim("\u2014 already set up, refreshed")}`);
5062
+ lines.push("");
5063
+ const labelW = maxLabelWidth(data.steps);
5064
+ for (const step of data.steps) {
5065
+ const icon = step.ok ? tick : warn;
5066
+ const label = step.label.padEnd(labelW);
5067
+ let detail = step.detail;
5068
+ if (step.label === "Device" && data.balanceCents !== void 0) {
5069
+ const dollars = `$${(data.balanceCents / 100).toFixed(2)}`;
5070
+ detail = `${detail} ${dot} balance ${green(dollars)}`;
5071
+ }
5072
+ lines.push(` ${icon} ${label} ${dot} ${detail}`);
5073
+ }
5074
+ lines.push("");
5075
+ lines.push(` ${dim("Nothing changed.")} Run ${bold("adsinagents doctor")} to check status.`);
5076
+ }
5077
+ return lines.join("\n") + "\n";
5078
+ }
5079
+ function maxLabelWidth(steps) {
5080
+ return steps.reduce((m, s) => Math.max(m, s.label.length), 0);
5081
+ }
5082
+
4876
5083
  // ../cli/src/index.ts
4877
5084
  var HERE2 = dirname4(fileURLToPath2(import.meta.url));
4878
5085
  function readVersion() {
4879
5086
  for (const rel of ["../../package.json", "../package.json"]) {
4880
5087
  try {
4881
- return JSON.parse(readFileSync4(join5(HERE2, rel), "utf8")).version ?? "0.0.0";
5088
+ return JSON.parse(readFileSync4(join6(HERE2, rel), "utf8")).version ?? "0.0.0";
4882
5089
  } catch {
4883
5090
  }
4884
5091
  }
4885
5092
  return "0.0.0";
4886
5093
  }
4887
5094
  var VERSION = readVersion();
4888
- var DAEMON_ENTRY = join5(HERE2, "..", "..", "daemon", "dist", "main.js");
5095
+ var DAEMON_ENTRY = join6(HERE2, "..", "..", "daemon", "dist", "main.js");
4889
5096
  function parseFlags(argv) {
4890
5097
  const positional = [];
4891
5098
  const flags = /* @__PURE__ */ new Map();
@@ -4901,8 +5108,8 @@ function parseFlags(argv) {
4901
5108
  return { positional, flags };
4902
5109
  }
4903
5110
  function buildPlan(placement, spinnerVerbs) {
4904
- const nativeStatusline = join5(dirname4(PATHS.frontmostBin), "adsinagents-statusline");
4905
- const statuslineCommand = existsSync5(nativeStatusline) ? nativeStatusline : `node ${join5(HERE2, "..", "..", "statusline", "dist", "index.js")}`;
5111
+ const nativeStatusline = join6(dirname4(PATHS.frontmostBin), "adsinagents-statusline");
5112
+ const statuslineCommand = existsSync5(nativeStatusline) ? nativeStatusline : `node ${join6(HERE2, "..", "..", "statusline", "dist", "index.js")}`;
4906
5113
  return {
4907
5114
  statuslineCommand,
4908
5115
  hookBaseUrl: `http://127.0.0.1:${HOOK_PORT}`,
@@ -4922,17 +5129,33 @@ async function cmdInit(flags) {
4922
5129
  process.stdout.write(diff + "\n");
4923
5130
  return;
4924
5131
  }
5132
+ const firstRun = !isInstalled();
5133
+ const steps = [];
4925
5134
  const native = buildAndInstallNative();
4926
- process.stdout.write(
4927
- native.ok ? `\u2713 native helpers installed (${native.installed.length})
4928
- ` : `\u26A0 ${native.reason} (degraded mode)
4929
- `
4930
- );
4931
- const { backup } = applyMerge(plan);
4932
- process.stdout.write(`\u2713 settings merged${backup ? ` (backup: ${backup})` : ""}
4933
- `);
5135
+ steps.push({
5136
+ ok: native.ok,
5137
+ label: "Native helpers",
5138
+ detail: native.ok ? `${native.installed.length} installed` : `degraded mode`,
5139
+ note: native.ok ? void 0 : native.reason
5140
+ });
5141
+ const { backup, slashCommand } = applyMerge(plan);
5142
+ steps.push({
5143
+ ok: true,
5144
+ label: "Settings",
5145
+ detail: "merged",
5146
+ note: backup ? "backup saved" : void 0
5147
+ });
5148
+ steps.push({
5149
+ ok: slashCommand.ok,
5150
+ label: "Slash command",
5151
+ detail: slashCommand.ok ? "/adsinagents ready" : "not installed",
5152
+ note: slashCommand.ok ? void 0 : slashCommand.reason
5153
+ });
4934
5154
  const cfg = loadConfigSafe();
4935
5155
  let claimUrl = "";
5156
+ let deviceStepOk = true;
5157
+ let deviceDetail = "";
5158
+ let deviceNote;
4936
5159
  if (!cfg.deviceToken) {
4937
5160
  try {
4938
5161
  const deviceId = `dev_${randomBytes(8).toString("hex")}`;
@@ -4945,36 +5168,43 @@ async function cmdInit(flags) {
4945
5168
  const reg = await res.json();
4946
5169
  writeConfigPatch({ deviceId: reg.deviceId, deviceToken: reg.deviceToken, claimToken: reg.claimToken });
4947
5170
  claimUrl = reg.claimUrl;
4948
- process.stdout.write("\u2713 device registered (earning anonymously)\n");
5171
+ deviceDetail = "registered, earning anonymously";
4949
5172
  } catch (e) {
4950
- process.stdout.write(`\u26A0 device registration skipped (${e.message}); run \`adsinagents login\` later
4951
- `);
5173
+ deviceStepOk = false;
5174
+ deviceDetail = "registration skipped";
5175
+ deviceNote = e.message;
4952
5176
  }
4953
5177
  } else {
4954
- process.stdout.write("\u2022 device already registered\n");
5178
+ deviceDetail = "registered";
4955
5179
  if (cfg.claimToken) claimUrl = `${cfg.serverUrl}/claim?t=${cfg.claimToken}`;
4956
5180
  }
5181
+ steps.push({ ok: deviceStepOk, label: "Device", detail: deviceDetail, note: deviceNote });
4957
5182
  const platform = getPlatform();
4958
5183
  const nodeBin = process.execPath;
4959
- await platform.installAutostart(nodeBin, [DAEMON_ENTRY]);
4960
- process.stdout.write("\u2713 daemon autostart installed + loaded\n");
5184
+ let daemonOk = true;
5185
+ let daemonNote;
5186
+ try {
5187
+ await platform.installAutostart(nodeBin, [DAEMON_ENTRY]);
5188
+ } catch (e) {
5189
+ daemonOk = false;
5190
+ daemonNote = e.message;
5191
+ }
5192
+ steps.push({ ok: daemonOk, label: "Daemon", detail: daemonOk ? "running" : "not loaded", note: daemonNote });
4961
5193
  if (flags.has("show-earnings")) {
4962
5194
  writeConfigPatch({ showEarnings: flags.get("show-earnings") !== "false" });
4963
- process.stdout.write("\u2713 live earnings segment enabled\n");
4964
5195
  }
4965
- process.stdout.write(`
4966
- AdsInAgents is live (placement=${placement}). Run \`adsinagents doctor\` to verify.
4967
- `);
4968
- process.stdout.write("Restart Claude Code (quit + reopen) so the status line picks up the ad.\n");
4969
- if (claimUrl) {
4970
- process.stdout.write(
4971
- `
4972
- You're earning anonymously. To see earnings + get paid, claim this install:
4973
- ${claimUrl}
4974
- (or run \`adsinagents claim\` anytime to reprint the link)
4975
- `
4976
- );
5196
+ let balanceCents;
5197
+ if (!firstRun && existsSync5(PATHS.daemonState)) {
5198
+ try {
5199
+ const state = JSON.parse(readFileSync4(PATHS.daemonState, "utf8"));
5200
+ if (typeof state.balanceCents === "number") balanceCents = state.balanceCents;
5201
+ } catch {
5202
+ }
4977
5203
  }
5204
+ const summarySteps = firstRun ? steps : steps.filter((s) => s.label !== "Native helpers");
5205
+ process.stdout.write(
5206
+ renderInitSummary({ firstRun, steps: summarySteps, claimUrl: claimUrl || void 0, balanceCents })
5207
+ );
4978
5208
  }
4979
5209
  async function cmdRemove() {
4980
5210
  const platform = getPlatform();
@@ -4173,8 +4173,10 @@ var ConfigSchema = external_exports.object({
4173
4173
  deviceId: external_exports.string().default(""),
4174
4174
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4175
4175
  claimToken: external_exports.string().default(""),
4176
- /** Ad cache poll interval (seconds). */
4177
- pollIntervalSec: external_exports.number().int().positive().default(60),
4176
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4177
+ * is gated separately by rateCapSec, so a faster poll rotates creatives
4178
+ * without changing earnings. */
4179
+ pollIntervalSec: external_exports.number().int().positive().default(30),
4178
4180
  /** Hard daily fired-impression cap. */
4179
4181
  dailyCap: external_exports.number().int().positive().default(300),
4180
4182
  /** Min seconds of verified view between two fired impressions. */
@@ -4550,8 +4552,8 @@ var ClaudeCodeAdapter = class {
4550
4552
  else
4551
4553
  priorValues.hooks = cur.hooks;
4552
4554
  const mk = (path) => ({
4553
- type: "http",
4554
- url: `${plan.hookBaseUrl}${path}`
4555
+ type: "command",
4556
+ command: hookCommand(path)
4555
4557
  });
4556
4558
  next.hooks = {
4557
4559
  ...hooks,
@@ -4576,8 +4578,8 @@ var ClaudeCodeAdapter = class {
4576
4578
  const backup = this.backup();
4577
4579
  const { next, diff } = this.merge(plan);
4578
4580
  this.write(next);
4579
- this.writeSlashCommand();
4580
- return { backup, diff };
4581
+ const slashCommand = this.writeSlashCommand();
4582
+ return { backup, diff, slashCommand };
4581
4583
  }
4582
4584
  /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4583
4585
  slashCommandPath() {
@@ -4587,14 +4589,22 @@ var ClaudeCodeAdapter = class {
4587
4589
  * Install /adsinagents — the status line itself is display-only, so this is
4588
4590
  * the in-session settings surface: the user types /adsinagents and Claude
4589
4591
  * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4590
- * remove. Best-effort: a failure here never fails install.
4592
+ * remove. Reconciles: writes if missing or stale (content drift), skips if
4593
+ * already correct. Returns the outcome so init can REPORT failures instead of
4594
+ * silently swallowing them (the old try/catch hid a real install bug — the
4595
+ * file never wrote and doctor was the only thing that ever noticed).
4591
4596
  */
4592
4597
  writeSlashCommand() {
4598
+ const p = this.slashCommandPath();
4593
4599
  try {
4594
- const p = this.slashCommandPath();
4600
+ if (existsSync2(p) && readFileSync2(p, "utf8") === SLASH_COMMAND_MD) {
4601
+ return { ok: true };
4602
+ }
4595
4603
  mkdirSync2(dirname2(p), { recursive: true });
4596
4604
  writeFileSync2(p, SLASH_COMMAND_MD);
4597
- } catch {
4605
+ return { ok: true };
4606
+ } catch (e) {
4607
+ return { ok: false, reason: e.message };
4598
4608
  }
4599
4609
  }
4600
4610
  removeInstall() {
@@ -4724,10 +4734,18 @@ function spinnerVerbsFor(ad) {
4724
4734
  `Brought to you by ${brand}`
4725
4735
  ];
4726
4736
  }
4737
+ function hookCommand(path) {
4738
+ return [
4739
+ `p=$(node -e 'try{process.stdout.write(String(require(process.env.HOME+"/.adsinagents/daemon.json").port||""))}catch(e){}' 2>/dev/null)`,
4740
+ `[ -n "$p" ] && curl -s --max-time 2 -X POST "http://127.0.0.1:$p${path}" -H 'content-type: application/json' --data-binary @- >/dev/null 2>&1`,
4741
+ `true`
4742
+ ].join("; ");
4743
+ }
4727
4744
  function appendHook(existing, entry) {
4728
4745
  const arr = Array.isArray(existing) ? existing.slice() : existing ? [existing] : [];
4729
- arr.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4730
- return arr;
4746
+ const userOwned = arr.filter((e) => !(e && typeof e === "object" && e[ADLINE_SENTINEL]));
4747
+ userOwned.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4748
+ return userOwned;
4731
4749
  }
4732
4750
  function renderDiff(before, after) {
4733
4751
  const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
@@ -5437,7 +5455,8 @@ async function main() {
5437
5455
  if (tickCount % SYNC_EVERY_TICKS === 0) {
5438
5456
  void syncLedger(ledger, cfg).then((e) => {
5439
5457
  if (e) earnings = e;
5440
- }).catch(() => {
5458
+ }).catch((err) => {
5459
+ void fileLog(`sync error: ${String(err)}`);
5441
5460
  });
5442
5461
  }
5443
5462
  }, TICK_MS);
@@ -5466,9 +5485,17 @@ async function syncLedger(ledger, cfg) {
5466
5485
  }),
5467
5486
  signal: AbortSignal.timeout(8e3)
5468
5487
  });
5469
- if (!res.ok) return null;
5488
+ if (!res.ok) {
5489
+ const detail = await res.text().catch(() => "");
5490
+ await fileLog(`sync HTTP ${res.status}: ${detail.slice(0, 200)}`);
5491
+ return null;
5492
+ }
5470
5493
  if (rows.length) ledger.markSynced(rows.map((r) => r.id));
5471
5494
  const body = await res.json().catch(() => ({}));
5495
+ if (body.rejected && body.rejected > 0) {
5496
+ const why = Object.entries(body.rejectReasons ?? {}).map(([r, n]) => `${r}=${n}`).join(" ");
5497
+ await fileLog(`sync rejected ${body.rejected}${why ? ` (${why})` : ""}`);
5498
+ }
5472
5499
  return {
5473
5500
  balanceCents: Math.max(0, body.balanceCents ?? 0),
5474
5501
  todayCents: Math.max(0, body.todayCents ?? 0)
package/native/build.sh CHANGED
@@ -27,4 +27,14 @@ swiftc -O "$HERE/statusline.swift" -o "$OUT/adsinagents-statusline"
27
27
  echo "Compiling frontmost..." >&2
28
28
  swiftc -O "$HERE/frontmost.swift" -o "$OUT/frontmost"
29
29
 
30
+ # Re-sign with ad-hoc signature. macOS 26 (Tahoe) Taskgated invalidates the
31
+ # linker-signed signature when the binary is copied across paths (copyFileSync /
32
+ # install step). A fresh codesign -f after compile ensures the installed copy
33
+ # passes Taskgated validation without needing a Developer ID.
34
+ if command -v codesign >/dev/null 2>&1; then
35
+ echo "Signing binaries (ad-hoc)..." >&2
36
+ codesign -f --sign - "$OUT/adsinagents-statusline" >/dev/null 2>&1 || true
37
+ codesign -f --sign - "$OUT/frontmost" >/dev/null 2>&1 || true
38
+ fi
39
+
30
40
  echo "Built: $OUT/adsinagents-statusline, $OUT/frontmost" >&2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adsinagents",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",
@@ -4150,8 +4150,10 @@ var ConfigSchema = external_exports.object({
4150
4150
  deviceId: external_exports.string().default(""),
4151
4151
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4152
4152
  claimToken: external_exports.string().default(""),
4153
- /** Ad cache poll interval (seconds). */
4154
- pollIntervalSec: external_exports.number().int().positive().default(60),
4153
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4154
+ * is gated separately by rateCapSec, so a faster poll rotates creatives
4155
+ * without changing earnings. */
4156
+ pollIntervalSec: external_exports.number().int().positive().default(30),
4155
4157
  /** Hard daily fired-impression cap. */
4156
4158
  dailyCap: external_exports.number().int().positive().default(300),
4157
4159
  /** Min seconds of verified view between two fired impressions. */