@expitc/termad 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EXPITC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/POLICY.md ADDED
@@ -0,0 +1,91 @@
1
+ # termad — Content & Platform Policy
2
+
3
+ This policy governs (a) ads served through a termad endpoint, and (b) the
4
+ termad platform (advertisers, runners, revenue share). It exists so every
5
+ party — advertiser, runner, and the person who sees the ad — knows exactly
6
+ what is and isn't allowed. terms of use for the platform should reference
7
+ this file.
8
+
9
+ ## 1. What an ad is
10
+
11
+ An ad in termad is **one line of plain text plus an optional URL**, printed
12
+ above the CLI's own output with a fixed `[ad]` label. It is never mixed into
13
+ the CLI's output, never styled to look like the CLI's own messages, and is
14
+ always removable with `termad uninstall`.
15
+
16
+ ## 2. Allowed ad content
17
+
18
+ - Honest, legal products and services aimed at developers.
19
+ - Clear claims. No "your machine is infected" urgency, no fake system
20
+ warnings, no impersonation of terminals, OS dialogs, or AI assistants.
21
+ - URLs must resolve to the advertised destination (no cloaking, no
22
+ redirect chains to unrelated domains).
23
+
24
+ ## 3. Prohibited ad content
25
+
26
+ - Malware, phishing, credential harvesting, or any link whose landing page
27
+ would fail a safe-browsing check.
28
+ - Illegal goods/services; adult content; violence; deceptive financial
29
+ offers ("guaranteed returns"); counterfeit goods.
30
+ - Content that mimics terminal output, error messages, or the branding of
31
+ claude/codex/opencode/gemini or their companies.
32
+ - Ads claiming affiliation with, or endorsement by, the CLIs termad runs in.
33
+ - Self-promotion loops: an ad for an endpoint is fine; an ad that instructs
34
+ viewers to click it, or to run commands that generate ad impressions, is
35
+ not (see §5).
36
+
37
+ Enforcement: the operator of an endpoint is responsible for its inventory.
38
+ Anyone may propose additions/exceptions via PR to this file; removals for
39
+ policy violations are immediate.
40
+
41
+ ## 4. Measurement honesty
42
+
43
+ - An **impression** means: the ad was rendered into a terminal the viewer
44
+ was already using. Counting starts when the wrapper prints the line.
45
+ - termad clients never click their own ads and never simulate engagement.
46
+ No auto-follow of ad URLs, no beacon on render, no fingerprinting, no
47
+ cross-site identifiers. The viewer's `uid` is a random local identifier
48
+ used only for per-install ad rotation and revenue accounting.
49
+ - Reported numbers in the dashboard are raw counts (per ad, per surface)
50
+ — not modeled, not extrapolated. Gaps (e.g. offline periods) are gaps.
51
+
52
+ ## 5. Revenue share (the 50%)
53
+
54
+ - Advertisers prepay credit (via the platform's payment provider).
55
+ - For each reported impression: **50% of the ad's CPM value is credited to
56
+ the runner** (the person/organization whose terminals display the ad),
57
+ 50% stays with the platform to cover payment processing and operations.
58
+ - Runners never need to (and never should) click ads, ask others to click,
59
+ or inflate counts mechanically — counts come from actual renders only.
60
+ - Payouts are made against the runner's recorded balance; the ledger in
61
+ the dashboard is the source of truth and shows every credited impression.
62
+
63
+ ## 6. Trademarks & non-affiliation
64
+
65
+ `claude`, `codex`, `opencode`, and `gemini` are trademarks of their
66
+ respective owners. termad uses these names only to identify compatible
67
+ software. **termad is not affiliated with, sponsored by, or endorsed by
68
+ Anthropic, OpenAI, Google, or the opencode project.** Ad content may not
69
+ claim such affiliation either.
70
+
71
+ ## 7. Privacy
72
+
73
+ - Collected: random install `uid`, impression timestamps, ad id, surface.
74
+ - Not collected: prompts, conversations, file paths, machine identifiers,
75
+ IP-based profiles. Impressions are counted by the client and reported in
76
+ batches; no per-render network activity occurs.
77
+ - Deletion: `termad uninstall` removes local state; operators may purge
78
+ `uid` records from their ledger at any time on request.
79
+
80
+ ## 8. Reversibility commitment
81
+
82
+ Every termad change is (a) shown to the user before consent,
83
+ (b) sha256-recorded in a local manifest, and (c) fully reverted by
84
+ `termad uninstall`. `termad audit` re-verifies (b) and refuses to restore
85
+ from a backup that fails verification.
86
+
87
+ ## 9. Contact / takedown
88
+
89
+ Report policy-violating ads to the operator of the endpoint that served
90
+ them (the endpoint is always identifiable by the serving domain). For
91
+ platform-level issues, open an issue in this repository.
package/README.md CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # termad
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Opt-in, fully reversible text ads for terminal AI CLIs — **claude**, **codex**,
4
+ **opencode**, **gemini**. Your words, your endpoint, one line in the terminal.
5
+
6
+ ```
7
+ $ opencode --version
8
+
9
+ [ad] termad — put your words in the terminal
10
+ https://github.com/yourname/termad
11
+
12
+ opencode v2.1.1
13
+ ```
14
+
15
+ ## How it works
16
+
17
+ | Surface | Mechanism | Risk |
18
+ |---|---|---|
19
+ | **claude** | Public `statusLine` in `~/.claude/settings.json` (chained — your existing statusline is captured and still runs) | none — documented feature |
20
+ | **codex / opencode / gemini** | Marker-tagged wrapper script in front of the binary; original backed up as `<bin>.termad-orig` | fully reversible |
21
+
22
+ - Ads are fetched from **your** HTTP endpoint once, cached to
23
+ `~/.termad/ad.txt`, and printed by the wrapper — zero network in the CLI's
24
+ hot path, and CLIs keep working if the endpoint is down.
25
+ - Click = a bare URL the terminal auto-links. No tracking, no beacons.
26
+ - `termad uninstall` restores every binary and your settings verbatim.
27
+ If a CLI self-updates over the wrapper, termad detects the marker is gone
28
+ and leaves the new binary alone.
29
+
30
+ ## Quick start
31
+
32
+ ```sh
33
+ npm install && npm run build
34
+
35
+ # 1. start your ad server (zero deps)
36
+ node server/server.mjs 8787
37
+
38
+ # 2. point termad at it
39
+ node dist/cli.js set-endpoint http://localhost:8787
40
+
41
+ # 3. install (choose surfaces, or omit for all opted-in)
42
+ node dist/cli.js install claude codex opencode
43
+
44
+ # 4. anytime: full revert
45
+ node dist/cli.js uninstall
46
+ ```
47
+
48
+ Other commands: `status`, `sync`, `watch` (poll endpoint), `style line|banner`.
49
+
50
+ ## Trust & compliance
51
+
52
+ ```sh
53
+ node dist/cli.js install # shows the exact plan, asks for consent
54
+ # (non-interactive: --yes required)
55
+ node dist/cli.js audit # sha256-verifies every recorded change
56
+ ```
57
+
58
+ - **Consent first** — nothing is modified until the plan (files, backups)
59
+ is shown and confirmed.
60
+ - **Tamper-evident** — every change is recorded in `~/.termad/manifest.json`
61
+ with sha256 of original and installed content. `audit` flags tampering,
62
+ and `uninstall` *refuses* to restore from a backup that fails its check.
63
+ - **No tracking** — no beacons, no click counting, no fingerprints. One
64
+ impression = one render, counted locally, reported in batches.
65
+ - Content rules: [POLICY.md](POLICY.md).
66
+ - **Non-affiliation**: claude, codex, opencode, and gemini are trademarks of
67
+ their respective owners (Anthropic, OpenAI, Google, opencode project).
68
+ termad is not affiliated with, sponsored by, or endorsed by any of them.
69
+
70
+ ## Ad inventory
71
+
72
+ Edit `server/ads.json` — plain JSON, hot-reloads every 30s:
73
+
74
+ ```json
75
+ { "ads": [
76
+ { "id": "sponsor:acme", "text": "Acme — ship faster", "url": "https://acme.dev" }
77
+ ]}
78
+ ```
79
+
80
+ Round-robin rotation per install `uid`; impressions counted in memory
81
+ (`GET /v1/stats`). Swap in a DB when it's real.
82
+
83
+ ## Programmatic use
84
+
85
+ ```ts
86
+ import { install, uninstall, renderBanner, fetchAd } from "termad";
87
+ ```
88
+
89
+ ## Design notes
90
+
91
+ - **Never breaks the CLI**: every ad operation is wrapped; worst case is a
92
+ missing banner.
93
+ - **Opt-in per surface**, stored in `~/.termad/config.json`.
94
+ - `TERMAD_HOME`, `TERMAD_<CLI>_BIN`, `TERMAD_CLAUDE_HOME` override all paths
95
+ (used for testing; nothing touches your real setup without `install`).
@@ -0,0 +1,31 @@
1
+ @ECHO off
2
+ REM ===== TERMAD-CLI =====
3
+ REM Reversible termad wrapper. Prints a one-line ad banner read from
4
+ REM "__TERMAD_AD__" (line 1 = wording, line 2 = click URL, line 4 = ad id)
5
+ REM then delegates to the pristine binary at "__TERMAD_BACKUP__".
6
+ REM Each render is appended to "__TERMAD_COUNT__" (one line of ad id — the
7
+ REM batch counter `termad sync` reports; no network happens here).
8
+ REM Restore: copy the backup over this file, or run `termad uninstall`.
9
+ setlocal enabledelayedexpansion
10
+ set "__AD_FILE=__TERMAD_AD__"
11
+ set "__COUNT_FILE=__TERMAD_COUNT__"
12
+ set "__AD="
13
+ set "__URL="
14
+ set "__ID="
15
+ set "__N=0"
16
+ if exist "%__AD_FILE%" (
17
+ for /f "usebackq delims=" %%A in ("%__AD_FILE%") do (
18
+ if !__N! EQU 0 set "__AD=%%A"
19
+ if !__N! EQU 1 set "__URL=%%A"
20
+ if !__N! EQU 3 set "__ID=%%A"
21
+ set /a __N+=1
22
+ )
23
+ )
24
+ if defined __AD (
25
+ echo.
26
+ echo [ad] !__AD!
27
+ if defined __URL if not "!__URL!"=="-" echo !__URL!
28
+ echo.
29
+ if defined __ID echo !__ID!>>"%__COUNT_FILE%"
30
+ )
31
+ endlocal & call "__TERMAD_BACKUP__" %*
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # ===== TERMAD-CLI =====
3
+ # Reversible termad wrapper. Prints a one-line ad banner read from
4
+ # "__TERMAD_AD__" (line 1 = wording, line 2 = click URL, line 4 = ad id)
5
+ # then exec's the pristine binary at "__TERMAD_BACKUP__".
6
+ # Each render is appended to "__TERMAD_COUNT__" (one line of ad id — the
7
+ # batch counter `termad sync` reports; no network happens here).
8
+ # Restore: copy the backup over this file, or run `termad uninstall`.
9
+ AD_FILE="__TERMAD_AD__"
10
+ COUNT_FILE="__TERMAD_COUNT__"
11
+ AD_TEXT=""
12
+ AD_URL=""
13
+ AD_ID=""
14
+ if [ -r "$AD_FILE" ]; then
15
+ AD_TEXT=$(head -n 1 "$AD_FILE" 2>/dev/null)
16
+ AD_URL=$(sed -n 2p "$AD_FILE" 2>/dev/null)
17
+ AD_ID=$(sed -n 4p "$AD_FILE" 2>/dev/null)
18
+ fi
19
+ if [ -n "$AD_TEXT" ]; then
20
+ printf '\n [ad] %s\n' "$AD_TEXT"
21
+ case "$AD_URL" in
22
+ http://*|https://*) printf ' %s\n\n' "$AD_URL" ;;
23
+ *) printf '\n' ;;
24
+ esac
25
+ if [ -n "$AD_ID" ]; then
26
+ printf '%s\n' "$AD_ID" >> "$COUNT_FILE" 2>/dev/null || true
27
+ fi
28
+ fi
29
+ exec "__TERMAD_BACKUP__" "$@"
@@ -0,0 +1,31 @@
1
+ @ECHO off
2
+ REM ===== TERMAD-CLI =====
3
+ REM Reversible termad wrapper. Prints a one-line ad banner read from
4
+ REM "__TERMAD_AD__" (line 1 = wording, line 2 = click URL, line 4 = ad id)
5
+ REM then delegates to the pristine binary at "__TERMAD_BACKUP__".
6
+ REM Each render is appended to "__TERMAD_COUNT__" (one line of ad id — the
7
+ REM batch counter `termad sync` reports; no network happens here).
8
+ REM Restore: copy the backup over this file, or run `termad uninstall`.
9
+ setlocal enabledelayedexpansion
10
+ set "__AD_FILE=__TERMAD_AD__"
11
+ set "__COUNT_FILE=__TERMAD_COUNT__"
12
+ set "__AD="
13
+ set "__URL="
14
+ set "__ID="
15
+ set "__N=0"
16
+ if exist "%__AD_FILE%" (
17
+ for /f "usebackq delims=" %%A in ("%__AD_FILE%") do (
18
+ if !__N! EQU 0 set "__AD=%%A"
19
+ if !__N! EQU 1 set "__URL=%%A"
20
+ if !__N! EQU 3 set "__ID=%%A"
21
+ set /a __N+=1
22
+ )
23
+ )
24
+ if defined __AD (
25
+ echo.
26
+ echo [ad] !__AD!
27
+ if defined __URL if not "!__URL!"=="-" echo !__URL!
28
+ echo.
29
+ if defined __ID echo !__ID!>>"%__COUNT_FILE%"
30
+ )
31
+ endlocal & call "__TERMAD_BACKUP__" %*
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # ===== TERMAD-CLI =====
3
+ # Reversible termad wrapper. Prints a one-line ad banner read from
4
+ # "__TERMAD_AD__" (line 1 = wording, line 2 = click URL, line 4 = ad id)
5
+ # then exec's the pristine binary at "__TERMAD_BACKUP__".
6
+ # Each render is appended to "__TERMAD_COUNT__" (one line of ad id — the
7
+ # batch counter `termad sync` reports; no network happens here).
8
+ # Restore: copy the backup over this file, or run `termad uninstall`.
9
+ AD_FILE="__TERMAD_AD__"
10
+ COUNT_FILE="__TERMAD_COUNT__"
11
+ AD_TEXT=""
12
+ AD_URL=""
13
+ AD_ID=""
14
+ if [ -r "$AD_FILE" ]; then
15
+ AD_TEXT=$(head -n 1 "$AD_FILE" 2>/dev/null)
16
+ AD_URL=$(sed -n 2p "$AD_FILE" 2>/dev/null)
17
+ AD_ID=$(sed -n 4p "$AD_FILE" 2>/dev/null)
18
+ fi
19
+ if [ -n "$AD_TEXT" ]; then
20
+ printf '\n [ad] %s\n' "$AD_TEXT"
21
+ case "$AD_URL" in
22
+ http://*|https://*) printf ' %s\n\n' "$AD_URL" ;;
23
+ *) printf '\n' ;;
24
+ esac
25
+ if [ -n "$AD_ID" ]; then
26
+ printf '%s\n' "$AD_ID" >> "$COUNT_FILE" 2>/dev/null || true
27
+ fi
28
+ fi
29
+ exec "__TERMAD_BACKUP__" "$@"
@@ -0,0 +1,9 @@
1
+ export interface AuditRow {
2
+ surface: string;
3
+ target: string;
4
+ status: "ok" | "restored" | "updated" | "tampered" | "missing" | "unknown";
5
+ detail: string;
6
+ }
7
+ export declare function audit(): AuditRow[];
8
+ /** Exit-code helper: only true integrity problems fail an audit. */
9
+ export declare function auditFailed(rows: AuditRow[]): boolean;
package/dist/audit.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `termad audit` — re-verify every recorded change against the filesystem.
3
+ *
4
+ * Statuses:
5
+ * ok wrapper/statusline installed, backup matches the recorded original
6
+ * restored target matches the recorded original (uninstalled or reinstalled by the CLI)
7
+ * updated marker gone AND content changed — the CLI self-updated; termad left it alone
8
+ * tampered termad's own wrapper was altered, or the backup no longer matches
9
+ * the recorded original — NOT something termad did silently
10
+ * missing the target file no longer exists
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { loadManifest, sha256File } from "./core/manifest.js";
14
+ import { isWrapped } from "./surfaces/wrap.js";
15
+ export function audit() {
16
+ const m = loadManifest();
17
+ return m.entries.map((e) => {
18
+ const base = { surface: e.surface, target: e.target };
19
+ if (!existsSync(e.target))
20
+ return { ...base, status: "missing", detail: "target file no longer exists" };
21
+ if (e.mechanism === "statusline") {
22
+ let installed = false;
23
+ try {
24
+ installed = readFileSync(e.target, "utf8").includes("termad");
25
+ }
26
+ catch { }
27
+ if (installed)
28
+ return { ...base, status: "ok", detail: "statusline installed, original captured in backup" };
29
+ return { ...base, status: "restored", detail: "settings no longer reference termad" };
30
+ }
31
+ const cur = sha256File(e.target);
32
+ const wrapped = isWrapped(e.target);
33
+ if (wrapped) {
34
+ if (cur !== e.sha256Installed)
35
+ return { ...base, status: "tampered", detail: "wrapper content differs from what termad installed" };
36
+ if (!e.backup || sha256File(e.backup) === null)
37
+ return { ...base, status: "tampered", detail: "wrapper present but original backup is missing" };
38
+ if (sha256File(e.backup) !== e.sha256Original)
39
+ return { ...base, status: "tampered", detail: "backup no longer matches the recorded original" };
40
+ return { ...base, status: "ok", detail: "wrapper installed, original backup verified (sha256)" };
41
+ }
42
+ if (cur === e.sha256Original)
43
+ return { ...base, status: "restored", detail: "target byte-identical to recorded original" };
44
+ return {
45
+ ...base,
46
+ status: "updated",
47
+ detail: "marker absent and content changed — CLI replaced the wrapper; termad left it untouched",
48
+ };
49
+ });
50
+ }
51
+ /** Exit-code helper: only true integrity problems fail an audit. */
52
+ export function auditFailed(rows) {
53
+ return rows.some((r) => r.status === "tampered" || r.status === "missing");
54
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * termad — opt-in, reversible text ads for terminal AI CLIs.
4
+ *
5
+ * termad install [claude|codex|opencode|gemini ...] install (default: all opted-in)
6
+ * termad uninstall [surface ...] full revert
7
+ * termad audit re-verify every recorded change
8
+ * termad status what's installed where
9
+ * termad sync fetch ad now, write cache
10
+ * termad watch poll endpoint, refresh cache
11
+ * termad set-endpoint <url> point at your ad server
12
+ * termad style line|banner render style
13
+ */
14
+ import { ALL_SURFACES, loadConfig, saveConfig } from "./core/config.js";
15
+ import { readSync } from "node:fs";
16
+ import { paths, termadHome } from "./core/paths.js";
17
+ import { readAdCache, fetchAd } from "./core/source.js";
18
+ import { install, uninstall, sync, planInstall } from "./install.js";
19
+ import { surfaces } from "./surfaces/surfaces.js";
20
+ import { isWrapped } from "./surfaces/wrap.js";
21
+ import { isStatuslineInstalled } from "./surfaces/statusline.js";
22
+ import { render } from "./render/render.js";
23
+ import { audit, auditFailed } from "./audit.js";
24
+ const [cmd, ...args] = process.argv.slice(2);
25
+ import { reportDisplays } from "./report.js";
26
+ function confirmPlan(surfaces, force = false) {
27
+ if (args.includes("--yes"))
28
+ return;
29
+ const plan = planInstall(surfaces, force);
30
+ console.log("termad will make these changes (nothing happens without your consent):\n");
31
+ for (const p of plan) {
32
+ console.log(` ${p.surface.padEnd(9)} ${p.action}`);
33
+ console.log(` ${"".padEnd(9)} → ${p.target}`);
34
+ }
35
+ console.log(`
36
+ • every change is sha256-recorded in ${paths.manifest()}
37
+ • \`termad uninstall\` restores everything byte-for-byte
38
+ • \`termad audit\` re-verifies all of it at any time`);
39
+ if (!process.stdin.isTTY) {
40
+ console.error("\nnon-interactive session: re-run with --yes to confirm");
41
+ process.exit(1);
42
+ }
43
+ process.stdout.write("\nProceed? [y/N] ");
44
+ const answer = readlineSync();
45
+ if (!/^y(es)?$/i.test(answer.trim())) {
46
+ console.log("aborted — nothing changed");
47
+ process.exit(1);
48
+ }
49
+ }
50
+ function readlineSync() {
51
+ // Minimal blocking read for the consent prompt (no deps).
52
+ try {
53
+ const buf = Buffer.alloc(1024);
54
+ const n = readSync(0, buf, 0, 1024, null);
55
+ return buf.subarray(0, n).toString("utf8");
56
+ }
57
+ catch {
58
+ return "";
59
+ }
60
+ }
61
+ function parseSurfaces(argv) {
62
+ const picked = argv.filter((a) => ALL_SURFACES.includes(a));
63
+ return picked.length ? picked : undefined;
64
+ }
65
+ function printOutcomes(out) {
66
+ for (const o of out)
67
+ console.log(` ${o.ok ? "✓" : "✗"} ${o.surface.padEnd(9)} ${o.detail}`);
68
+ if (out.some((o) => !o.ok))
69
+ process.exitCode = 1;
70
+ }
71
+ switch (cmd) {
72
+ case "install": {
73
+ const force = args.includes("--force");
74
+ confirmPlan(parseSurfaces(args), force);
75
+ const out = await install(parseSurfaces(args), { force });
76
+ printOutcomes(out);
77
+ const ad = readAdCache();
78
+ if (ad)
79
+ console.log(`\n ad cache: ${paths.adCache()}\n ${render(ad, loadConfig().style)}`);
80
+ const rows = audit();
81
+ if (rows.length)
82
+ console.log(` audit: ${rows.length} change(s) recorded → \`termad audit\` to re-verify`);
83
+ break;
84
+ }
85
+ case "uninstall": {
86
+ const out = uninstall(parseSurfaces(args));
87
+ printOutcomes(out);
88
+ break;
89
+ }
90
+ case "status": {
91
+ const cfg = loadConfig();
92
+ console.log(`termad home : ${termadHome()}`);
93
+ console.log(`endpoint : ${cfg.endpoint ?? "(none — local/house ads only)"}`);
94
+ console.log(`style : ${cfg.style}`);
95
+ console.log("");
96
+ for (const s of surfaces) {
97
+ const bin = s.locate();
98
+ const state = s.id === "claude"
99
+ ? isStatuslineInstalled()
100
+ ? "installed (statusline)"
101
+ : "—"
102
+ : bin && isWrapped(bin)
103
+ ? `installed (wrapper)`
104
+ : "—";
105
+ console.log(` ${s.id.padEnd(9)} ${String(bin ?? "not found").padEnd(42)} ${state}`);
106
+ }
107
+ const ad = readAdCache();
108
+ if (ad)
109
+ console.log(`\ncached ad:\n${render(ad, cfg.style)}`);
110
+ break;
111
+ }
112
+ case "audit": {
113
+ const rows = audit();
114
+ if (!rows.length) {
115
+ console.log(` no recorded changes (nothing installed by this CLI yet)`);
116
+ break;
117
+ }
118
+ console.log(` ${"SURFACE".padEnd(9)} ${"STATUS".padEnd(9)} DETAIL`);
119
+ for (const r of rows) {
120
+ const icon = r.status === "ok" ? "✓" : r.status === "tampered" || r.status === "missing" ? "✗" : "•";
121
+ console.log(` ${icon} ${r.surface.padEnd(9)} ${r.status.padEnd(9)} ${r.detail}`);
122
+ console.log(` ${"".padEnd(9)} ${"".padEnd(9)} ${r.target}`);
123
+ }
124
+ if (auditFailed(rows)) {
125
+ console.log(`\n integrity problems found — see entries above`);
126
+ process.exit(1);
127
+ }
128
+ console.log(`\n all recorded changes verified (sha256)`);
129
+ break;
130
+ }
131
+ case "sync": {
132
+ const text = await sync();
133
+ const rep = await reportDisplays();
134
+ console.log(` ad cache refreshed: ${text}`);
135
+ if (rep)
136
+ console.log(` display report: ${rep}`);
137
+ else
138
+ console.log(` display report: nothing pending (or endpoint unreachable — counts retained)`);
139
+ break;
140
+ }
141
+ case "watch": {
142
+ const cfg = loadConfig();
143
+ if (!cfg.endpoint) {
144
+ console.error(" no endpoint configured — run: termad set-endpoint <url>");
145
+ process.exit(1);
146
+ }
147
+ console.log(` watching ${cfg.endpoint} every ${cfg.pollSeconds}s (ctrl-c to stop)`);
148
+ for (;;) {
149
+ const text = await sync().catch(() => null);
150
+ if (text)
151
+ process.stdout.write(` ${new Date().toISOString()} ${text}\n`);
152
+ await new Promise((r) => setTimeout(r, cfg.pollSeconds * 1000));
153
+ }
154
+ break;
155
+ }
156
+ case "set-endpoint": {
157
+ if (!args[0]) {
158
+ console.error(" usage: termad set-endpoint <url>");
159
+ process.exit(1);
160
+ }
161
+ const cfg = loadConfig();
162
+ cfg.endpoint = args[0].replace(/\/+$/, "");
163
+ saveConfig(cfg);
164
+ console.log(` endpoint set: ${cfg.endpoint}`);
165
+ // Pull the first ad immediately so the cache isn't empty.
166
+ const ad = await fetchAd("claude").catch(() => null);
167
+ if (ad)
168
+ console.log(` first ad: ${ad.text}`);
169
+ break;
170
+ }
171
+ case "style": {
172
+ if (args[0] !== "line" && args[0] !== "banner") {
173
+ console.error(" usage: termad style line|banner");
174
+ process.exit(1);
175
+ }
176
+ const cfg = loadConfig();
177
+ cfg.style = args[0];
178
+ saveConfig(cfg);
179
+ console.log(` style set: ${args[0]}`);
180
+ break;
181
+ }
182
+ default:
183
+ console.log(`termad — opt-in, reversible text ads for terminal AI CLIs
184
+
185
+ termad install [surface ...] install (claude | codex | opencode | gemini)
186
+ termad uninstall [surface ...] remove everything termad added
187
+ termad audit re-verify every recorded change (sha256)
188
+ termad status show installed surfaces + cached ad
189
+ termad set-endpoint <url> point at your ad server
190
+ termad sync fetch ad now + report pending displays
191
+ termad watch poll the endpoint continuously
192
+ termad style line|banner render style`);
193
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The smallest possible ad: one line of wording, optionally a URL.
3
+ * No images, no beacons, no HTML — the terminal renders it.
4
+ */
5
+ export interface Ad {
6
+ /** Unique id (server-assigned or slug of the text). */
7
+ id: string;
8
+ /** The wording shown to the user. Kept short — terminal line. */
9
+ text: string;
10
+ /** Click target. Rendered as a bare URL so the terminal auto-links it. */
11
+ url?: string;
12
+ /** Optional second line (e.g. "sponsored by …"). */
13
+ sub?: string;
14
+ }
15
+ /** Shape the HTTP endpoint returns. */
16
+ export interface AdResponse {
17
+ ad: Ad;
18
+ }
19
+ export declare function isValidAd(v: unknown): v is Ad;
@@ -0,0 +1,12 @@
1
+ export function isValidAd(v) {
2
+ if (typeof v !== "object" || v === null)
3
+ return false;
4
+ const a = v;
5
+ return (typeof a.id === "string" &&
6
+ a.id.length > 0 &&
7
+ typeof a.text === "string" &&
8
+ a.text.length > 0 &&
9
+ a.text.length <= 300 &&
10
+ (a.url === undefined ||
11
+ (typeof a.url === "string" && /^https?:\/\//.test(a.url) && a.url.length <= 500)));
12
+ }
@@ -0,0 +1,18 @@
1
+ export type SurfaceId = "claude" | "codex" | "opencode" | "gemini";
2
+ export declare const ALL_SURFACES: SurfaceId[];
3
+ export interface TermadConfig {
4
+ /** Your ad endpoint. GET <endpoint>/v1/ad?surface=<s>&uid=<u> → {ad:{...}} */
5
+ endpoint: string | null;
6
+ /** Stable anonymous install id (rotation/frequency capping server-side). */
7
+ uid: string;
8
+ /** How often `termad watch` re-fetches, seconds. */
9
+ pollSeconds: number;
10
+ /** Which surfaces are opted in. */
11
+ surfaces: Record<SurfaceId, boolean>;
12
+ /** "line" | "banner" — render style in the terminal. */
13
+ style: "line" | "banner";
14
+ }
15
+ export declare function loadConfig(): TermadConfig;
16
+ export declare function saveConfig(cfg: TermadConfig): void;
17
+ /** Atomic-ish write so wrappers never read a half-written cache. */
18
+ export declare function writeFileAtomic(path: string, data: string): void;