@expitc/termad 0.1.3 → 0.1.4

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.
@@ -44,11 +44,12 @@ if (typeof runWithAdOverlay !== "function") {
44
44
  }
45
45
 
46
46
  const binary = process.argv[2];
47
- const args = process.argv.slice(3);
47
+ const surface = process.argv[3];
48
+ const args = process.argv.slice(4);
48
49
 
49
50
  if (!binary) {
50
51
  console.error("termad: usage: pty-overlay.mjs <binary> [args...]");
51
52
  process.exit(1);
52
53
  }
53
54
 
54
- await runWithAdOverlay(binary, args);
55
+ await runWithAdOverlay(binary, args, surface);
@@ -20,7 +20,7 @@ fi
20
20
 
21
21
  if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
22
22
  export TERMAD_AD_FILE TERMAD_BACKUP TERMAD_COUNT
23
- exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "$@"
23
+ exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "__TERMAD_SURFACE__" "$@"
24
24
  fi
25
25
 
26
26
  # Fallback: pre-banner
@@ -44,11 +44,12 @@ if (typeof runWithAdOverlay !== "function") {
44
44
  }
45
45
 
46
46
  const binary = process.argv[2];
47
- const args = process.argv.slice(3);
47
+ const surface = process.argv[3];
48
+ const args = process.argv.slice(4);
48
49
 
49
50
  if (!binary) {
50
51
  console.error("termad: usage: pty-overlay.mjs <binary> [args...]");
51
52
  process.exit(1);
52
53
  }
53
54
 
54
- await runWithAdOverlay(binary, args);
55
+ await runWithAdOverlay(binary, args, surface);
@@ -20,7 +20,7 @@ fi
20
20
 
21
21
  if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
22
22
  export TERMAD_AD_FILE TERMAD_BACKUP TERMAD_COUNT
23
- exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "$@"
23
+ exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "__TERMAD_SURFACE__" "$@"
24
24
  fi
25
25
 
26
26
  # Fallback: pre-banner
package/dist/install.js CHANGED
@@ -72,7 +72,7 @@ if (typeof mod.runWithAdOverlay !== "function") {
72
72
  console.error("termad: overlay unavailable");
73
73
  process.exit(1);
74
74
  }
75
- await mod.runWithAdOverlay(process.argv[2], process.argv.slice(3));
75
+ await mod.runWithAdOverlay(process.argv[2], process.argv.slice(4), process.argv[3]);
76
76
  `;
77
77
  writeFileSync(target, body);
78
78
  chmodSync(target, 0o755);
@@ -121,7 +121,7 @@ export async function install(surfaces, opts = {}) {
121
121
  }
122
122
  const wasWrapped = isWrapped(bin);
123
123
  const shaBefore = sha256File(bin);
124
- const r = installWrapper(bin, paths.adCache(), force);
124
+ const r = installWrapper(bin, paths.adCache(), force, id);
125
125
  if (r.ok) {
126
126
  const entry = loadManifest().entries.find((e) => e.surface === id && e.target === bin);
127
127
  if (!entry) {
@@ -1 +1,2 @@
1
- export declare function runWithAdOverlay(binary: string, args?: string[]): Promise<void>;
1
+ import { type SurfaceId } from "../core/config.js";
2
+ export declare function runWithAdOverlay(binary: string, args?: string[], surface?: SurfaceId): Promise<void>;
@@ -1,10 +1,13 @@
1
- import { readFileSync, appendFileSync, existsSync } from "node:fs";
1
+ import { readFileSync, appendFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { isValidAd } from "../core/ad.js";
5
+ import { writeAdCache } from "../core/source.js";
4
6
  const TERMAD_HOME = process.env.TERMAD_HOME || join(homedir(), ".termad");
5
7
  const AD_FILE = join(TERMAD_HOME, "ad.txt");
6
8
  const COUNT_FILE = join(TERMAD_HOME, "display.log");
7
9
  const CONFIG_FILE = join(TERMAD_HOME, "config.json");
10
+ const STATS_FILE = join(TERMAD_HOME, "stats.json");
8
11
  function readAd() {
9
12
  try {
10
13
  const raw = readFileSync(AD_FILE, "utf8").replace(/\r/g, "").split("\n");
@@ -38,6 +41,39 @@ function count(id) {
38
41
  }
39
42
  catch { }
40
43
  }
44
+ function recordPrompt() {
45
+ try {
46
+ mkdirSync(TERMAD_HOME, { recursive: true });
47
+ let stats = { prompts: 0, sessions: 0 };
48
+ try {
49
+ stats = { ...stats, ...JSON.parse(readFileSync(STATS_FILE, "utf8")) };
50
+ }
51
+ catch { }
52
+ stats.prompts++;
53
+ writeFileSync(STATS_FILE, JSON.stringify(stats));
54
+ }
55
+ catch { }
56
+ }
57
+ function refreshAd(surface) {
58
+ try {
59
+ const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
60
+ if (!cfg.endpoint || !cfg.uid)
61
+ return Promise.resolve(false);
62
+ const url = `${String(cfg.endpoint).replace(/\/+$/, "")}/v1/ad?surface=${surface}&uid=${encodeURIComponent(cfg.uid)}`;
63
+ return fetch(url, { signal: AbortSignal.timeout(4000) }).then(async (res) => {
64
+ if (!res.ok)
65
+ return false;
66
+ const body = await res.json();
67
+ if (!isValidAd(body.ad))
68
+ return false;
69
+ writeAdCache(body.ad);
70
+ return true;
71
+ }).catch(() => false);
72
+ }
73
+ catch {
74
+ return Promise.resolve(false);
75
+ }
76
+ }
41
77
  function osc8(url, text) {
42
78
  if (!url)
43
79
  return text;
@@ -93,10 +129,10 @@ function buildFooter(ad, style, width, offset = 0) {
93
129
  line += ` ${linked}`;
94
130
  return line;
95
131
  }
96
- export function runWithAdOverlay(binary, args = []) {
97
- return runWithAdOverlayAsync(binary, args);
132
+ export function runWithAdOverlay(binary, args = [], surface = "codex") {
133
+ return runWithAdOverlayAsync(binary, args, surface);
98
134
  }
99
- async function runWithAdOverlayAsync(binary, args = []) {
135
+ async function runWithAdOverlayAsync(binary, args = [], surface) {
100
136
  let pty;
101
137
  try {
102
138
  // @ts-ignore
@@ -138,7 +174,18 @@ async function runWithAdOverlayAsync(binary, args = []) {
138
174
  let footerVisible = !!ad;
139
175
  let lastAd = ad;
140
176
  let marqueeOffset = 0;
177
+ let fetching = false;
178
+ let fetchFrame = 0;
179
+ let promptBytes = [];
141
180
  function drawFooter() {
181
+ if (fetching) {
182
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
183
+ const status = ` \x1b[36m${frames[fetchFrame++ % frames.length]} validating prompt • selecting an ad\x1b[0m`;
184
+ const termRows = ptyProcess.rows || rows;
185
+ const footerRow = style === "banner" ? Math.max(1, termRows - 2) : Math.max(1, termRows - 1);
186
+ process.stdout.write(`\x1b[s\x1b[${footerRow};1H\x1b[2K${status}\x1b[u`);
187
+ return;
188
+ }
142
189
  if (!footerVisible || !lastAd)
143
190
  return;
144
191
  const width = process.stdout.columns || cols;
@@ -175,6 +222,42 @@ async function runWithAdOverlayAsync(binary, args = []) {
175
222
  catch { }
176
223
  }
177
224
  process.stdin.on("data", (data) => {
225
+ for (const byte of data) {
226
+ if (byte === 3 || byte === 4) {
227
+ promptBytes = [];
228
+ continue;
229
+ }
230
+ if (byte === 13 || byte === 10) {
231
+ const hasPrompt = promptBytes.some((b) => b >= 32 && b !== 127);
232
+ promptBytes = [];
233
+ if (hasPrompt && !fetching) {
234
+ recordPrompt();
235
+ fetching = true;
236
+ drawFooter();
237
+ void refreshAd(surface).then((updated) => {
238
+ if (updated) {
239
+ const next = readAd();
240
+ if (next) {
241
+ lastAd = next;
242
+ footerVisible = true;
243
+ if (next.id)
244
+ count(next.id);
245
+ }
246
+ }
247
+ fetching = false;
248
+ drawFooter();
249
+ });
250
+ }
251
+ }
252
+ else if (byte === 127 || byte === 8) {
253
+ promptBytes.pop();
254
+ }
255
+ else if (byte >= 32 && byte !== 127) {
256
+ promptBytes.push(byte);
257
+ if (promptBytes.length > 20000)
258
+ promptBytes = promptBytes.slice(-20000);
259
+ }
260
+ }
178
261
  ptyProcess.write(data);
179
262
  });
180
263
  process.stdout.on("resize", () => {
@@ -7,7 +7,7 @@ export declare function isWrapped(target: string): boolean;
7
7
  /** Wrap a binary/shim with the ad-printing wrapper. Idempotent.
8
8
  * `force` re-bakes an existing wrapper (e.g. after a template upgrade)
9
9
  * while keeping the pristine backup untouched. */
10
- export declare function installWrapper(target: string, adCachePath: string, force?: boolean): WrapResult;
10
+ export declare function installWrapper(target: string, adCachePath: string, force?: boolean, surface?: string): WrapResult;
11
11
  /** Restore the pristine binary and remove wrapper + ad cache. Idempotent. */
12
12
  export declare function uninstallWrapper(target: string, adCachePath: string): WrapResult;
13
13
  /**
@@ -33,7 +33,7 @@ export function isWrapped(target) {
33
33
  /** Wrap a binary/shim with the ad-printing wrapper. Idempotent.
34
34
  * `force` re-bakes an existing wrapper (e.g. after a template upgrade)
35
35
  * while keeping the pristine backup untouched. */
36
- export function installWrapper(target, adCachePath, force = false) {
36
+ export function installWrapper(target, adCachePath, force = false, surface = "codex") {
37
37
  if (!existsSync(target))
38
38
  return { ok: false, reason: `target not found: ${target}` };
39
39
  if (isWrapped(target) && !force)
@@ -53,7 +53,8 @@ export function installWrapper(target, adCachePath, force = false) {
53
53
  const wrapper = template
54
54
  .split("__TERMAD_AD__").join(adCachePath)
55
55
  .split("__TERMAD_COUNT__").join(paths.displayLog())
56
- .split("__TERMAD_BACKUP__").join(bak);
56
+ .split("__TERMAD_BACKUP__").join(bak)
57
+ .split("__TERMAD_SURFACE__").join(surface);
57
58
  if (wrapper.includes("__TERMAD_"))
58
59
  return { ok: false, reason: "unbaked template token" };
59
60
  const mode = statSync(target).mode & 0o777;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expitc/termad",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Opt-in, fully reversible text ads for terminal AI CLIs (claude, codex, opencode, gemini). Consent-first, tamper-evident audit, 50% runner revenue share.",
5
5
  "type": "module",
6
6
  "license": "MIT",