@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.
- package/assets/pty-overlay.mjs +3 -2
- package/assets/wrapper.sh.overlay +1 -1
- package/dist/assets/pty-overlay.mjs +3 -2
- package/dist/assets/wrapper.sh.overlay +1 -1
- package/dist/install.js +2 -2
- package/dist/surfaces/pty-inject.d.ts +2 -1
- package/dist/surfaces/pty-inject.js +87 -4
- package/dist/surfaces/wrap.d.ts +1 -1
- package/dist/surfaces/wrap.js +3 -2
- package/package.json +1 -1
package/assets/pty-overlay.mjs
CHANGED
|
@@ -44,11 +44,12 @@ if (typeof runWithAdOverlay !== "function") {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const binary = process.argv[2];
|
|
47
|
-
const
|
|
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
|
|
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
|
-
|
|
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", () => {
|
package/dist/surfaces/wrap.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/surfaces/wrap.js
CHANGED
|
@@ -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
|
+
"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",
|