adsinagents 0.1.5 → 0.1.8

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";
@@ -19,6 +19,7 @@ import { hostname } from "node:os";
19
19
  import { homedir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  var ADLINE_DIR = join(homedir(), ".adsinagents");
22
+ var HOOK_PORT = 8473;
22
23
  var PATHS = {
23
24
  root: ADLINE_DIR,
24
25
  config: join(ADLINE_DIR, "config.json"),
@@ -4097,6 +4098,7 @@ function isCategory(value) {
4097
4098
  }
4098
4099
 
4099
4100
  // ../../shared/dist/schemas.js
4101
+ var AdSourceSchema = external_exports.enum(["gravity", "direct"]);
4100
4102
  var AdSchema = external_exports.object({
4101
4103
  /** Stable impression id for this rotation; idempotency key for firing. */
4102
4104
  id: external_exports.string().min(1),
@@ -4113,7 +4115,7 @@ var AdSchema = external_exports.object({
4113
4115
  /** Gravity click pixel (GET). Empty for direct campaigns. */
4114
4116
  clickUrl: external_exports.string().default(""),
4115
4117
  /** Source of demand. */
4116
- source: external_exports.enum(["gravity", "direct"]),
4118
+ source: AdSourceSchema,
4117
4119
  /** Unix ms when this ad was fetched into cache. */
4118
4120
  fetchedAt: external_exports.number().int(),
4119
4121
  /**
@@ -4216,7 +4218,11 @@ var ImpressionSchema = external_exports.object({
4216
4218
  verifiedAt: external_exports.number().int().nullable(),
4217
4219
  impFired: external_exports.boolean(),
4218
4220
  clicked: external_exports.boolean(),
4219
- source: external_exports.enum(["gravity", "direct"]),
4221
+ source: AdSourceSchema,
4222
+ /** Render surface the ad painted on. Only "statusline" is billable today
4223
+ * (spinner reach is un-billable and never writes a row). Recorded so the
4224
+ * publisher log can show *where* the ad showed. Older daemons omit it. */
4225
+ slot: external_exports.enum(["statusline", "spinner"]).default("statusline"),
4220
4226
  /** Server serve-proof echoed back on sync. Empty for standalone/unverified. */
4221
4227
  nonce: external_exports.string().default("")
4222
4228
  });
@@ -4536,8 +4542,8 @@ var ClaudeCodeAdapter = class {
4536
4542
  else
4537
4543
  priorValues.hooks = cur.hooks;
4538
4544
  const mk = (path) => ({
4539
- type: "http",
4540
- url: `${plan.hookBaseUrl}${path}`
4545
+ type: "command",
4546
+ command: hookCommand(path)
4541
4547
  });
4542
4548
  next.hooks = {
4543
4549
  ...hooks,
@@ -4562,8 +4568,8 @@ var ClaudeCodeAdapter = class {
4562
4568
  const backup = this.backup();
4563
4569
  const { next, diff } = this.merge(plan);
4564
4570
  this.write(next);
4565
- this.writeSlashCommand();
4566
- return { backup, diff };
4571
+ const slashCommand = this.writeSlashCommand();
4572
+ return { backup, diff, slashCommand };
4567
4573
  }
4568
4574
  /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4569
4575
  slashCommandPath() {
@@ -4573,14 +4579,22 @@ var ClaudeCodeAdapter = class {
4573
4579
  * Install /adsinagents — the status line itself is display-only, so this is
4574
4580
  * the in-session settings surface: the user types /adsinagents and Claude
4575
4581
  * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4576
- * remove. Best-effort: a failure here never fails install.
4582
+ * remove. Reconciles: writes if missing or stale (content drift), skips if
4583
+ * already correct. Returns the outcome so init can REPORT failures instead of
4584
+ * silently swallowing them (the old try/catch hid a real install bug — the
4585
+ * file never wrote and doctor was the only thing that ever noticed).
4577
4586
  */
4578
4587
  writeSlashCommand() {
4588
+ const p = this.slashCommandPath();
4579
4589
  try {
4580
- const p = this.slashCommandPath();
4590
+ if (existsSync2(p) && readFileSync2(p, "utf8") === SLASH_COMMAND_MD) {
4591
+ return { ok: true };
4592
+ }
4581
4593
  mkdirSync2(dirname2(p), { recursive: true });
4582
4594
  writeFileSync2(p, SLASH_COMMAND_MD);
4583
- } catch {
4595
+ return { ok: true };
4596
+ } catch (e) {
4597
+ return { ok: false, reason: e.message };
4584
4598
  }
4585
4599
  }
4586
4600
  removeInstall() {
@@ -4710,10 +4724,18 @@ function spinnerVerbsFor(ad) {
4710
4724
  `Brought to you by ${brand}`
4711
4725
  ];
4712
4726
  }
4727
+ function hookCommand(path) {
4728
+ return [
4729
+ `p=$(node -e 'try{process.stdout.write(String(require(process.env.HOME+"/.adsinagents/daemon.json").port||""))}catch(e){}' 2>/dev/null)`,
4730
+ `[ -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`,
4731
+ `true`
4732
+ ].join("; ");
4733
+ }
4713
4734
  function appendHook(existing, entry) {
4714
4735
  const arr = Array.isArray(existing) ? existing.slice() : existing ? [existing] : [];
4715
- arr.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4716
- return arr;
4736
+ const userOwned = arr.filter((e) => !(e && typeof e === "object" && e[ADLINE_SENTINEL]));
4737
+ userOwned.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4738
+ return userOwned;
4717
4739
  }
4718
4740
  function renderDiff(before, after) {
4719
4741
  const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
@@ -4755,6 +4777,9 @@ function getAgent(name = "claude-code") {
4755
4777
 
4756
4778
  // ../cli/src/settings.ts
4757
4779
  var cc = () => getAgent("claude-code");
4780
+ function isInstalled() {
4781
+ return cc().isInstalled();
4782
+ }
4758
4783
  function planMerge(plan) {
4759
4784
  return cc().planInstall(plan);
4760
4785
  }
@@ -4798,6 +4823,10 @@ function buildAndInstallNative() {
4798
4823
  if (existsSync3(src)) {
4799
4824
  copyFileSync2(src, dest);
4800
4825
  chmodSync2(dest, 493);
4826
+ try {
4827
+ execFileSync3("codesign", ["-f", "--sign", "-", dest], { stdio: "pipe" });
4828
+ } catch {
4829
+ }
4801
4830
  installed.push(dest);
4802
4831
  }
4803
4832
  }
@@ -4806,6 +4835,9 @@ function buildAndInstallNative() {
4806
4835
 
4807
4836
  // ../cli/src/doctor.ts
4808
4837
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
4838
+ import { spawnSync } from "node:child_process";
4839
+ import { homedir as homedir3 } from "node:os";
4840
+ import { join as join5 } from "node:path";
4809
4841
  function daemonAlive() {
4810
4842
  if (!existsSync4(PATHS.daemonInfo)) return { ok: false, detail: "no daemon.json (not started)" };
4811
4843
  try {
@@ -4857,8 +4889,133 @@ async function runDoctor() {
4857
4889
  ok: key !== null,
4858
4890
  detail: key ? "present (Keychain/file)" : "none \u2014 run `adsinagents login`"
4859
4891
  });
4892
+ checks.push(checkStatusLine());
4893
+ checks.push(checkHookReachable());
4894
+ checks.push(checkSlashCommand());
4860
4895
  return checks;
4861
4896
  }
4897
+ function checkStatusLine() {
4898
+ const name = "Status line renders";
4899
+ const settingsPath = join5(claudeConfigDir(), "settings.json");
4900
+ if (!existsSync4(settingsPath)) {
4901
+ return { name, ok: true, detail: "no statusLine command configured" };
4902
+ }
4903
+ let settings;
4904
+ try {
4905
+ settings = JSON.parse(readFileSync3(settingsPath, "utf8"));
4906
+ } catch {
4907
+ return { name, ok: true, detail: "settings.json unreadable (skipped)" };
4908
+ }
4909
+ const cmd = settings.statusLine?.command;
4910
+ if (!cmd) {
4911
+ return { name, ok: true, detail: "no statusLine command configured" };
4912
+ }
4913
+ let hasAd = false;
4914
+ try {
4915
+ if (existsSync4(PATHS.currentAd)) {
4916
+ const ad = JSON.parse(readFileSync3(PATHS.currentAd, "utf8"));
4917
+ hasAd = !!(ad.adId && String(ad.adId).trim().length > 0);
4918
+ }
4919
+ } catch {
4920
+ hasAd = false;
4921
+ }
4922
+ const parts = cmd.split(" ");
4923
+ const exe = parts[0];
4924
+ const args = parts.slice(1);
4925
+ const sessionJson = JSON.stringify({
4926
+ session_id: "doctor",
4927
+ workspace: { current_dir: "/tmp" },
4928
+ model: { display_name: "x" }
4929
+ });
4930
+ const result = spawnSync(exe, args, {
4931
+ input: sessionJson,
4932
+ encoding: "utf8",
4933
+ timeout: 3e3
4934
+ });
4935
+ if (result.error) {
4936
+ const errCode = result.error.code;
4937
+ const msg = errCode === "ENOENT" ? "not found" : result.error.message;
4938
+ return { name, ok: false, detail: `statusLine command not found or not executable: ${exe} (${msg})` };
4939
+ }
4940
+ if (result.status !== 0) {
4941
+ return { name, ok: false, detail: `statusLine command exited with error (code ${result.status ?? "?"})` };
4942
+ }
4943
+ const stdout = (result.stdout ?? "").trim();
4944
+ if (hasAd && stdout.length === 0) {
4945
+ return { name, ok: false, detail: "statusLine command produced no output (ad exists but nothing rendered)" };
4946
+ }
4947
+ if (stdout.length > 0) {
4948
+ return { name, ok: true, detail: `rendered ${stdout.length} chars` };
4949
+ }
4950
+ return { name, ok: true, detail: "ok (no ad to render right now)" };
4951
+ }
4952
+ function checkHookReachable() {
4953
+ const name = "Hook reachable";
4954
+ let daemonPort = null;
4955
+ if (existsSync4(PATHS.daemonInfo)) {
4956
+ try {
4957
+ const info = JSON.parse(readFileSync3(PATHS.daemonInfo, "utf8"));
4958
+ daemonPort = info.port ?? null;
4959
+ } catch {
4960
+ daemonPort = null;
4961
+ }
4962
+ }
4963
+ if (daemonPort === null) {
4964
+ return { name, ok: true, detail: "daemon not running (skipped)" };
4965
+ }
4966
+ const settingsPath = join5(claudeConfigDir(), "settings.json");
4967
+ let hookPort = null;
4968
+ if (existsSync4(settingsPath)) {
4969
+ try {
4970
+ const s = JSON.parse(readFileSync3(settingsPath, "utf8"));
4971
+ const hooksStr = JSON.stringify(s.hooks ?? {});
4972
+ const m = /http:\/\/127\.0\.0\.1:(\d+)/.exec(hooksStr);
4973
+ if (m) {
4974
+ hookPort = Number(m[1]);
4975
+ } else {
4976
+ hookPort = daemonPort;
4977
+ }
4978
+ } catch {
4979
+ hookPort = daemonPort;
4980
+ }
4981
+ }
4982
+ const result = spawnSync(
4983
+ "node",
4984
+ [
4985
+ "-e",
4986
+ `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)});`
4987
+ ],
4988
+ { encoding: "utf8", timeout: 4e3 }
4989
+ );
4990
+ if (result.status !== 0) {
4991
+ return { name, ok: false, detail: `daemon unreachable on port ${daemonPort}` };
4992
+ }
4993
+ if (hookPort !== null && hookPort !== daemonPort) {
4994
+ return {
4995
+ name,
4996
+ ok: false,
4997
+ detail: `hooks point at port ${hookPort} but daemon is on ${daemonPort}`
4998
+ };
4999
+ }
5000
+ return { name, ok: true, detail: `daemon OK on port ${daemonPort}` };
5001
+ }
5002
+ function checkSlashCommand() {
5003
+ const name = "Slash command installed";
5004
+ const SENTINEL = "<!-- adsinagents:managed -->";
5005
+ const cmdPath = join5(claudeConfigDir(), "commands", "adsinagents.md");
5006
+ if (!existsSync4(cmdPath)) {
5007
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5008
+ }
5009
+ try {
5010
+ const contents = readFileSync3(cmdPath, "utf8");
5011
+ if (!contents.includes(SENTINEL)) {
5012
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5013
+ }
5014
+ } catch {
5015
+ return { name, ok: false, detail: "run `adsinagents init` to reinstall the /adsinagents command" };
5016
+ }
5017
+ return { name, ok: true, detail: `present at ${cmdPath.replace(homedir3(), "~")}` };
5018
+ }
4862
5019
  function printDoctor(checks) {
4863
5020
  for (const c of checks) {
4864
5021
  const mark = c.ok ? "\u2713" : "\u2717";
@@ -4867,19 +5024,73 @@ function printDoctor(checks) {
4867
5024
  }
4868
5025
  }
4869
5026
 
5027
+ // ../cli/src/init-ui.ts
5028
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
5029
+ var ID = (s) => s;
5030
+ var dim = useColor ? (s) => `\x1B[2m${s}\x1B[0m` : ID;
5031
+ var bold = useColor ? (s) => `\x1B[1m${s}\x1B[0m` : ID;
5032
+ var green = useColor ? (s) => `\x1B[32m${s}\x1B[0m` : ID;
5033
+ var amber = useColor ? (s) => `\x1B[38;5;215m${s}\x1B[0m` : ID;
5034
+ function renderInitSummary(data) {
5035
+ const lines = [];
5036
+ const tick = green("\u2713");
5037
+ const warn = amber("!");
5038
+ const dot = dim("\xB7");
5039
+ if (data.firstRun) {
5040
+ lines.push(`${amber("\u258C")} ${bold("AdsInAgents")} ${dim("\u2014 sponsored status line for Claude Code")}`);
5041
+ lines.push("");
5042
+ const labelW = maxLabelWidth(data.steps);
5043
+ for (const step of data.steps) {
5044
+ const icon = step.ok ? tick : warn;
5045
+ const label = step.label.padEnd(labelW);
5046
+ const note = step.note ? ` ${dim("(" + step.note + ")")}` : "";
5047
+ lines.push(` ${icon} ${label} ${dot} ${step.detail}${note}`);
5048
+ }
5049
+ lines.push("");
5050
+ lines.push(` ${bold("You're live.")} Restart Claude Code to see your first ad.`);
5051
+ lines.push("");
5052
+ lines.push(` ${dim("Verify ")} adsinagents doctor`);
5053
+ if (data.claimUrl) {
5054
+ lines.push(` ${dim("Get paid")} ${amber(data.claimUrl)}`);
5055
+ }
5056
+ lines.push("");
5057
+ lines.push(` ${dim("Ads are test-only for now.")}`);
5058
+ } else {
5059
+ lines.push(`${amber("\u258C")} ${bold("AdsInAgents")} ${dim("\u2014 already set up, refreshed")}`);
5060
+ lines.push("");
5061
+ const labelW = maxLabelWidth(data.steps);
5062
+ for (const step of data.steps) {
5063
+ const icon = step.ok ? tick : warn;
5064
+ const label = step.label.padEnd(labelW);
5065
+ let detail = step.detail;
5066
+ if (step.label === "Device" && data.balanceCents !== void 0) {
5067
+ const dollars = `$${(data.balanceCents / 100).toFixed(2)}`;
5068
+ detail = `${detail} ${dot} balance ${green(dollars)}`;
5069
+ }
5070
+ lines.push(` ${icon} ${label} ${dot} ${detail}`);
5071
+ }
5072
+ lines.push("");
5073
+ lines.push(` ${dim("Nothing changed.")} Run ${bold("adsinagents doctor")} to check status.`);
5074
+ }
5075
+ return lines.join("\n") + "\n";
5076
+ }
5077
+ function maxLabelWidth(steps) {
5078
+ return steps.reduce((m, s) => Math.max(m, s.label.length), 0);
5079
+ }
5080
+
4870
5081
  // ../cli/src/index.ts
4871
5082
  var HERE2 = dirname4(fileURLToPath2(import.meta.url));
4872
5083
  function readVersion() {
4873
5084
  for (const rel of ["../../package.json", "../package.json"]) {
4874
5085
  try {
4875
- return JSON.parse(readFileSync4(join5(HERE2, rel), "utf8")).version ?? "0.0.0";
5086
+ return JSON.parse(readFileSync4(join6(HERE2, rel), "utf8")).version ?? "0.0.0";
4876
5087
  } catch {
4877
5088
  }
4878
5089
  }
4879
5090
  return "0.0.0";
4880
5091
  }
4881
5092
  var VERSION = readVersion();
4882
- var DAEMON_ENTRY = join5(HERE2, "..", "..", "daemon", "dist", "main.js");
5093
+ var DAEMON_ENTRY = join6(HERE2, "..", "..", "daemon", "dist", "main.js");
4883
5094
  function parseFlags(argv) {
4884
5095
  const positional = [];
4885
5096
  const flags = /* @__PURE__ */ new Map();
@@ -4895,11 +5106,11 @@ function parseFlags(argv) {
4895
5106
  return { positional, flags };
4896
5107
  }
4897
5108
  function buildPlan(placement, spinnerVerbs) {
4898
- const nativeStatusline = join5(dirname4(PATHS.frontmostBin), "adsinagents-statusline");
4899
- const statuslineCommand = existsSync5(nativeStatusline) ? nativeStatusline : `node ${join5(HERE2, "..", "..", "statusline", "dist", "index.js")}`;
5109
+ const nativeStatusline = join6(dirname4(PATHS.frontmostBin), "adsinagents-statusline");
5110
+ const statuslineCommand = existsSync5(nativeStatusline) ? nativeStatusline : `node ${join6(HERE2, "..", "..", "statusline", "dist", "index.js")}`;
4900
5111
  return {
4901
5112
  statuslineCommand,
4902
- hookBaseUrl: "http://127.0.0.1:8473",
5113
+ hookBaseUrl: `http://127.0.0.1:${HOOK_PORT}`,
4903
5114
  placement,
4904
5115
  spinnerVerbs,
4905
5116
  version: VERSION
@@ -4916,17 +5127,33 @@ async function cmdInit(flags) {
4916
5127
  process.stdout.write(diff + "\n");
4917
5128
  return;
4918
5129
  }
5130
+ const firstRun = !isInstalled();
5131
+ const steps = [];
4919
5132
  const native = buildAndInstallNative();
4920
- process.stdout.write(
4921
- native.ok ? `\u2713 native helpers installed (${native.installed.length})
4922
- ` : `\u26A0 ${native.reason} (degraded mode)
4923
- `
4924
- );
4925
- const { backup } = applyMerge(plan);
4926
- process.stdout.write(`\u2713 settings merged${backup ? ` (backup: ${backup})` : ""}
4927
- `);
5133
+ steps.push({
5134
+ ok: native.ok,
5135
+ label: "Native helpers",
5136
+ detail: native.ok ? `${native.installed.length} installed` : `degraded mode`,
5137
+ note: native.ok ? void 0 : native.reason
5138
+ });
5139
+ const { backup, slashCommand } = applyMerge(plan);
5140
+ steps.push({
5141
+ ok: true,
5142
+ label: "Settings",
5143
+ detail: "merged",
5144
+ note: backup ? "backup saved" : void 0
5145
+ });
5146
+ steps.push({
5147
+ ok: slashCommand.ok,
5148
+ label: "Slash command",
5149
+ detail: slashCommand.ok ? "/adsinagents ready" : "not installed",
5150
+ note: slashCommand.ok ? void 0 : slashCommand.reason
5151
+ });
4928
5152
  const cfg = loadConfigSafe();
4929
5153
  let claimUrl = "";
5154
+ let deviceStepOk = true;
5155
+ let deviceDetail = "";
5156
+ let deviceNote;
4930
5157
  if (!cfg.deviceToken) {
4931
5158
  try {
4932
5159
  const deviceId = `dev_${randomBytes(8).toString("hex")}`;
@@ -4939,34 +5166,43 @@ async function cmdInit(flags) {
4939
5166
  const reg = await res.json();
4940
5167
  writeConfigPatch({ deviceId: reg.deviceId, deviceToken: reg.deviceToken, claimToken: reg.claimToken });
4941
5168
  claimUrl = reg.claimUrl;
4942
- process.stdout.write("\u2713 device registered (earning anonymously)\n");
5169
+ deviceDetail = "registered, earning anonymously";
4943
5170
  } catch (e) {
4944
- process.stdout.write(`\u26A0 device registration skipped (${e.message}); run \`adsinagents login\` later
4945
- `);
5171
+ deviceStepOk = false;
5172
+ deviceDetail = "registration skipped";
5173
+ deviceNote = e.message;
4946
5174
  }
4947
5175
  } else {
4948
- process.stdout.write("\u2022 device already registered\n");
5176
+ deviceDetail = "registered";
5177
+ if (cfg.claimToken) claimUrl = `${cfg.serverUrl}/claim?t=${cfg.claimToken}`;
4949
5178
  }
5179
+ steps.push({ ok: deviceStepOk, label: "Device", detail: deviceDetail, note: deviceNote });
4950
5180
  const platform = getPlatform();
4951
5181
  const nodeBin = process.execPath;
4952
- await platform.installAutostart(nodeBin, [DAEMON_ENTRY]);
4953
- process.stdout.write("\u2713 daemon autostart installed + loaded\n");
5182
+ let daemonOk = true;
5183
+ let daemonNote;
5184
+ try {
5185
+ await platform.installAutostart(nodeBin, [DAEMON_ENTRY]);
5186
+ } catch (e) {
5187
+ daemonOk = false;
5188
+ daemonNote = e.message;
5189
+ }
5190
+ steps.push({ ok: daemonOk, label: "Daemon", detail: daemonOk ? "running" : "not loaded", note: daemonNote });
4954
5191
  if (flags.has("show-earnings")) {
4955
5192
  writeConfigPatch({ showEarnings: flags.get("show-earnings") !== "false" });
4956
- process.stdout.write("\u2713 live earnings segment enabled\n");
4957
5193
  }
4958
- process.stdout.write(`
4959
- AdsInAgents is live (placement=${placement}). Run \`adsinagents doctor\` to verify.
4960
- `);
4961
- if (claimUrl) {
4962
- process.stdout.write(
4963
- `
4964
- You're earning anonymously. To see earnings + get paid, claim this install:
4965
- ${claimUrl}
4966
- (or run \`adsinagents claim\` anytime to reprint the link)
4967
- `
4968
- );
5194
+ let balanceCents;
5195
+ if (!firstRun && existsSync5(PATHS.daemonState)) {
5196
+ try {
5197
+ const state = JSON.parse(readFileSync4(PATHS.daemonState, "utf8"));
5198
+ if (typeof state.balanceCents === "number") balanceCents = state.balanceCents;
5199
+ } catch {
5200
+ }
4969
5201
  }
5202
+ const summarySteps = firstRun ? steps : steps.filter((s) => s.label !== "Native helpers");
5203
+ process.stdout.write(
5204
+ renderInitSummary({ firstRun, steps: summarySteps, claimUrl: claimUrl || void 0, balanceCents })
5205
+ );
4970
5206
  }
4971
5207
  async function cmdRemove() {
4972
5208
  const platform = getPlatform();
@@ -14,6 +14,7 @@ import { writeFileSync as writeFileSync4 } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  var ADLINE_DIR = join(homedir(), ".adsinagents");
17
+ var HOOK_PORT = 8473;
17
18
  var PATHS = {
18
19
  root: ADLINE_DIR,
19
20
  config: join(ADLINE_DIR, "config.json"),
@@ -4102,6 +4103,9 @@ function matchesBlocked(ad, blocked) {
4102
4103
  }
4103
4104
 
4104
4105
  // ../../shared/dist/schemas.js
4106
+ var AdSourceSchema = external_exports.enum(["gravity", "direct"]);
4107
+ var GRAVITY_CAMPAIGN_ID = "gravity";
4108
+ var FALLBACK_AD_URL = "https://trygravity.ai";
4105
4109
  var AdSchema = external_exports.object({
4106
4110
  /** Stable impression id for this rotation; idempotency key for firing. */
4107
4111
  id: external_exports.string().min(1),
@@ -4118,7 +4122,7 @@ var AdSchema = external_exports.object({
4118
4122
  /** Gravity click pixel (GET). Empty for direct campaigns. */
4119
4123
  clickUrl: external_exports.string().default(""),
4120
4124
  /** Source of demand. */
4121
- source: external_exports.enum(["gravity", "direct"]),
4125
+ source: AdSourceSchema,
4122
4126
  /** Unix ms when this ad was fetched into cache. */
4123
4127
  fetchedAt: external_exports.number().int(),
4124
4128
  /**
@@ -4222,7 +4226,11 @@ var ImpressionSchema = external_exports.object({
4222
4226
  verifiedAt: external_exports.number().int().nullable(),
4223
4227
  impFired: external_exports.boolean(),
4224
4228
  clicked: external_exports.boolean(),
4225
- source: external_exports.enum(["gravity", "direct"]),
4229
+ source: AdSourceSchema,
4230
+ /** Render surface the ad painted on. Only "statusline" is billable today
4231
+ * (spinner reach is un-billable and never writes a row). Recorded so the
4232
+ * publisher log can show *where* the ad showed. Older daemons omit it. */
4233
+ slot: external_exports.enum(["statusline", "spinner"]).default("statusline"),
4226
4234
  /** Server serve-proof echoed back on sync. Empty for standalone/unverified. */
4227
4235
  nonce: external_exports.string().default("")
4228
4236
  });
@@ -4542,8 +4550,8 @@ var ClaudeCodeAdapter = class {
4542
4550
  else
4543
4551
  priorValues.hooks = cur.hooks;
4544
4552
  const mk = (path) => ({
4545
- type: "http",
4546
- url: `${plan.hookBaseUrl}${path}`
4553
+ type: "command",
4554
+ command: hookCommand(path)
4547
4555
  });
4548
4556
  next.hooks = {
4549
4557
  ...hooks,
@@ -4568,8 +4576,8 @@ var ClaudeCodeAdapter = class {
4568
4576
  const backup = this.backup();
4569
4577
  const { next, diff } = this.merge(plan);
4570
4578
  this.write(next);
4571
- this.writeSlashCommand();
4572
- return { backup, diff };
4579
+ const slashCommand = this.writeSlashCommand();
4580
+ return { backup, diff, slashCommand };
4573
4581
  }
4574
4582
  /** Path of the in-session settings command, e.g. ~/.claude/commands/adsinagents.md. */
4575
4583
  slashCommandPath() {
@@ -4579,14 +4587,22 @@ var ClaudeCodeAdapter = class {
4579
4587
  * Install /adsinagents — the status line itself is display-only, so this is
4580
4588
  * the in-session settings surface: the user types /adsinagents and Claude
4581
4589
  * drives the CLI. File is fully ours (sentinel comment), safe to delete on
4582
- * remove. Best-effort: a failure here never fails install.
4590
+ * remove. Reconciles: writes if missing or stale (content drift), skips if
4591
+ * already correct. Returns the outcome so init can REPORT failures instead of
4592
+ * silently swallowing them (the old try/catch hid a real install bug — the
4593
+ * file never wrote and doctor was the only thing that ever noticed).
4583
4594
  */
4584
4595
  writeSlashCommand() {
4596
+ const p = this.slashCommandPath();
4585
4597
  try {
4586
- const p = this.slashCommandPath();
4598
+ if (existsSync2(p) && readFileSync2(p, "utf8") === SLASH_COMMAND_MD) {
4599
+ return { ok: true };
4600
+ }
4587
4601
  mkdirSync2(dirname2(p), { recursive: true });
4588
4602
  writeFileSync2(p, SLASH_COMMAND_MD);
4589
- } catch {
4603
+ return { ok: true };
4604
+ } catch (e) {
4605
+ return { ok: false, reason: e.message };
4590
4606
  }
4591
4607
  }
4592
4608
  removeInstall() {
@@ -4716,10 +4732,18 @@ function spinnerVerbsFor(ad) {
4716
4732
  `Brought to you by ${brand}`
4717
4733
  ];
4718
4734
  }
4735
+ function hookCommand(path) {
4736
+ return [
4737
+ `p=$(node -e 'try{process.stdout.write(String(require(process.env.HOME+"/.adsinagents/daemon.json").port||""))}catch(e){}' 2>/dev/null)`,
4738
+ `[ -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`,
4739
+ `true`
4740
+ ].join("; ");
4741
+ }
4719
4742
  function appendHook(existing, entry) {
4720
4743
  const arr = Array.isArray(existing) ? existing.slice() : existing ? [existing] : [];
4721
- arr.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4722
- return arr;
4744
+ const userOwned = arr.filter((e) => !(e && typeof e === "object" && e[ADLINE_SENTINEL]));
4745
+ userOwned.push({ hooks: [entry], [ADLINE_SENTINEL]: true });
4746
+ return userOwned;
4723
4747
  }
4724
4748
  function renderDiff(before, after) {
4725
4749
  const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
@@ -4818,6 +4842,7 @@ var Ledger = class {
4818
4842
  imp_fired INTEGER NOT NULL DEFAULT 0,
4819
4843
  clicked INTEGER NOT NULL DEFAULT 0,
4820
4844
  nonce TEXT NOT NULL DEFAULT '',
4845
+ slot TEXT NOT NULL DEFAULT 'statusline',
4821
4846
  synced INTEGER NOT NULL DEFAULT 0
4822
4847
  );
4823
4848
  CREATE INDEX IF NOT EXISTS idx_imp_unsynced ON impressions(synced);
@@ -4827,14 +4852,17 @@ var Ledger = class {
4827
4852
  if (!cols.some((c) => c.name === "nonce")) {
4828
4853
  this.db.exec(`ALTER TABLE impressions ADD COLUMN nonce TEXT NOT NULL DEFAULT ''`);
4829
4854
  }
4855
+ if (!cols.some((c) => c.name === "slot")) {
4856
+ this.db.exec(`ALTER TABLE impressions ADD COLUMN slot TEXT NOT NULL DEFAULT 'statusline'`);
4857
+ }
4830
4858
  }
4831
4859
  /** Record an impression starting (idempotent — ignore if id already present). */
4832
4860
  startImpression(row) {
4833
4861
  this.db.prepare(
4834
4862
  `INSERT OR IGNORE INTO impressions
4835
- (id, ad_id, campaign_id, session_id, source, started_at, nonce)
4836
- VALUES (@id, @adId, @campaignId, @sessionId, @source, @startedAt, @nonce)`
4837
- ).run({ ...row, nonce: row.nonce ?? "" });
4863
+ (id, ad_id, campaign_id, session_id, source, started_at, nonce, slot)
4864
+ VALUES (@id, @adId, @campaignId, @sessionId, @source, @startedAt, @nonce, @slot)`
4865
+ ).run({ ...row, nonce: row.nonce ?? "", slot: row.slot ?? "statusline" });
4838
4866
  }
4839
4867
  /**
4840
4868
  * Mark verified + fired. Persist-before-fire: call this, then fire impUrl.
@@ -4874,7 +4902,7 @@ var Ledger = class {
4874
4902
  const rows = this.db.prepare(
4875
4903
  `SELECT id, ad_id as adId, campaign_id as campaignId, session_id as sessionId,
4876
4904
  source, started_at as startedAt, verified_at as verifiedAt,
4877
- imp_fired as impFired, clicked, nonce
4905
+ imp_fired as impFired, clicked, nonce, slot
4878
4906
  FROM impressions WHERE synced = 0 ORDER BY started_at LIMIT @limit`
4879
4907
  ).all({ limit });
4880
4908
  return rows.map((r) => ({
@@ -4887,6 +4915,7 @@ var Ledger = class {
4887
4915
  verifiedAt: r.verifiedAt ?? null,
4888
4916
  impFired: !!r.impFired,
4889
4917
  clicked: !!r.clicked,
4918
+ slot: r.slot ?? "statusline",
4890
4919
  nonce: r.nonce ?? ""
4891
4920
  }));
4892
4921
  }
@@ -5027,7 +5056,7 @@ function newImpId(nowMs) {
5027
5056
  }
5028
5057
  var BUILTIN_TEST_ADS = [
5029
5058
  {
5030
- campaignId: "gravity",
5059
+ campaignId: GRAVITY_CAMPAIGN_ID,
5031
5060
  text: "Ship faster \u2014 catch errors before users do",
5032
5061
  brandName: "Sentry",
5033
5062
  url: "https://sentry.io",
@@ -5036,7 +5065,7 @@ var BUILTIN_TEST_ADS = [
5036
5065
  source: "gravity"
5037
5066
  },
5038
5067
  {
5039
- campaignId: "gravity",
5068
+ campaignId: GRAVITY_CAMPAIGN_ID,
5040
5069
  text: "Postgres without the ops \u2014 serverless branches",
5041
5070
  brandName: "Neon",
5042
5071
  url: "https://neon.tech",
@@ -5045,7 +5074,7 @@ var BUILTIN_TEST_ADS = [
5045
5074
  source: "gravity"
5046
5075
  },
5047
5076
  {
5048
- campaignId: "gravity",
5077
+ campaignId: GRAVITY_CAMPAIGN_ID,
5049
5078
  text: "Deploy in seconds, scale to zero",
5050
5079
  brandName: "Vercel",
5051
5080
  url: "https://vercel.com",
@@ -5133,7 +5162,7 @@ function startServer(deps) {
5133
5162
  const ad = readCurrentAd();
5134
5163
  deps.ledger.markClicked(impId);
5135
5164
  if (ad && ad.id === impId && ad.clickUrl) deps.firePixel(ad.clickUrl);
5136
- const dest = ad?.url || "https://trygravity.ai";
5165
+ const dest = ad?.url || FALLBACK_AD_URL;
5137
5166
  res.writeHead(302, { location: dest });
5138
5167
  res.end();
5139
5168
  return;
@@ -5172,12 +5201,22 @@ function startServer(deps) {
5172
5201
  res.end("error");
5173
5202
  }
5174
5203
  });
5175
- return new Promise((resolve) => {
5176
- server.listen(0, "127.0.0.1", () => {
5204
+ return new Promise((resolve, reject) => {
5205
+ const onListening = () => {
5177
5206
  const addr = server.address();
5178
5207
  const port = typeof addr === "object" && addr ? addr.port : 0;
5179
5208
  resolve({ server, port });
5180
- });
5209
+ };
5210
+ const onError = (err) => {
5211
+ if (err.code === "EADDRINUSE") {
5212
+ server.once("error", reject);
5213
+ server.listen(0, "127.0.0.1", onListening);
5214
+ return;
5215
+ }
5216
+ reject(err);
5217
+ };
5218
+ server.once("error", onError);
5219
+ server.listen(HOOK_PORT, "127.0.0.1", onListening);
5181
5220
  });
5182
5221
  }
5183
5222
 
@@ -5361,7 +5400,10 @@ async function main() {
5361
5400
  sessionId: sid,
5362
5401
  source: ad.source,
5363
5402
  startedAt: nowMs,
5364
- nonce: ad.nonce
5403
+ nonce: ad.nonce,
5404
+ // Only the status line writes a billable impression — spinner reach
5405
+ // is un-billable and never reaches this path. Recorded for the log.
5406
+ slot: "statusline"
5365
5407
  });
5366
5408
  }
5367
5409
  } catch (e) {
package/native/build.sh CHANGED
@@ -7,6 +7,13 @@ set -euo pipefail
7
7
  HERE="$(cd "$(dirname "$0")" && pwd)"
8
8
  OUT="${1:-$HERE/build}"
9
9
 
10
+ # macOS-only: these helpers import AppKit. On any non-Darwin host (Linux CI),
11
+ # skip cleanly — the binary is compiled on the user's Mac at runtime, never in CI.
12
+ if [ "$(uname -s)" != "Darwin" ]; then
13
+ echo "native: skipping Swift build on non-macOS ($(uname -s))" >&2
14
+ exit 0
15
+ fi
16
+
10
17
  if ! xcrun --find swiftc >/dev/null 2>&1 && ! command -v swiftc >/dev/null 2>&1; then
11
18
  echo "swiftc not found (install Xcode Command Line Tools: xcode-select --install)" >&2
12
19
  exit 3
@@ -20,4 +27,14 @@ swiftc -O "$HERE/statusline.swift" -o "$OUT/adsinagents-statusline"
20
27
  echo "Compiling frontmost..." >&2
21
28
  swiftc -O "$HERE/frontmost.swift" -o "$OUT/frontmost"
22
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
+
23
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.5",
3
+ "version": "0.1.8",
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",
@@ -4082,6 +4082,7 @@ var CATEGORY_KEYS = Object.keys(CATEGORIES);
4082
4082
  var CategorySchema = external_exports.enum(CATEGORY_KEYS);
4083
4083
 
4084
4084
  // ../../shared/dist/schemas.js
4085
+ var AdSourceSchema = external_exports.enum(["gravity", "direct"]);
4085
4086
  var AdSchema = external_exports.object({
4086
4087
  /** Stable impression id for this rotation; idempotency key for firing. */
4087
4088
  id: external_exports.string().min(1),
@@ -4098,7 +4099,7 @@ var AdSchema = external_exports.object({
4098
4099
  /** Gravity click pixel (GET). Empty for direct campaigns. */
4099
4100
  clickUrl: external_exports.string().default(""),
4100
4101
  /** Source of demand. */
4101
- source: external_exports.enum(["gravity", "direct"]),
4102
+ source: AdSourceSchema,
4102
4103
  /** Unix ms when this ad was fetched into cache. */
4103
4104
  fetchedAt: external_exports.number().int(),
4104
4105
  /**
@@ -4201,7 +4202,11 @@ var ImpressionSchema = external_exports.object({
4201
4202
  verifiedAt: external_exports.number().int().nullable(),
4202
4203
  impFired: external_exports.boolean(),
4203
4204
  clicked: external_exports.boolean(),
4204
- source: external_exports.enum(["gravity", "direct"]),
4205
+ source: AdSourceSchema,
4206
+ /** Render surface the ad painted on. Only "statusline" is billable today
4207
+ * (spinner reach is un-billable and never writes a row). Recorded so the
4208
+ * publisher log can show *where* the ad showed. Older daemons omit it. */
4209
+ slot: external_exports.enum(["statusline", "spinner"]).default("statusline"),
4205
4210
  /** Server serve-proof echoed back on sync. Empty for standalone/unverified. */
4206
4211
  nonce: external_exports.string().default("")
4207
4212
  });