adsinagents 0.1.11 → 0.1.12
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 +35 -9
- package/daemon/dist/main.js +89 -13
- package/package.json +1 -1
- package/statusline/dist/index.js +106 -19
package/cli/dist/index.js
CHANGED
|
@@ -4124,7 +4124,16 @@ var AdSchema = external_exports.object({
|
|
|
4124
4124
|
* the daemon is running standalone (no server) — those imps earn nothing.
|
|
4125
4125
|
* See shared/src/nonce.ts.
|
|
4126
4126
|
*/
|
|
4127
|
-
nonce: external_exports.string().default("")
|
|
4127
|
+
nonce: external_exports.string().default(""),
|
|
4128
|
+
/**
|
|
4129
|
+
* Publisher payout RATES for this ad, in millicents, computed server-side from
|
|
4130
|
+
* the live CPM + share + click multiplier (earnings.ts) so the device never
|
|
4131
|
+
* hardcodes the math — change the campaign cpm and these move with it. Shown in
|
|
4132
|
+
* the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
|
|
4133
|
+
* ads / standalone daemons (no server rate) simply render nothing.
|
|
4134
|
+
*/
|
|
4135
|
+
payoutPerImpMillicents: external_exports.number().min(0).default(0),
|
|
4136
|
+
payoutPerClickMillicents: external_exports.number().min(0).default(0)
|
|
4128
4137
|
});
|
|
4129
4138
|
var GravityAdSchema = external_exports.object({
|
|
4130
4139
|
adText: external_exports.string(),
|
|
@@ -4166,16 +4175,20 @@ var ConfigSchema = external_exports.object({
|
|
|
4166
4175
|
deviceId: external_exports.string().default(""),
|
|
4167
4176
|
/** Single-use token to claim this install at /claim (attach a login + payout). */
|
|
4168
4177
|
claimToken: external_exports.string().default(""),
|
|
4169
|
-
/** Ad cache poll interval (seconds) — how often a new ad rotates in.
|
|
4170
|
-
*
|
|
4171
|
-
*
|
|
4178
|
+
/** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
|
|
4179
|
+
* with the fire: rateCapSec defaults to the SAME value, so every new ad fires
|
|
4180
|
+
* exactly once (no silent "every other ad skipped"). The next ad is preloaded
|
|
4181
|
+
* a few seconds early so the swap is instant. */
|
|
4172
4182
|
pollIntervalSec: external_exports.number().int().positive().default(30),
|
|
4173
4183
|
/** Hard daily fired-impression cap. */
|
|
4174
4184
|
dailyCap: external_exports.number().int().positive().default(300),
|
|
4175
|
-
/** Min seconds of verified view between two fired impressions.
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4185
|
+
/** Min seconds of verified view between two fired impressions. Defaults to the
|
|
4186
|
+
* rotation period (30s) so fire == rotation: each new ad booms once. */
|
|
4187
|
+
rateCapSec: external_exports.number().int().positive().default(30),
|
|
4188
|
+
/** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
|
|
4189
|
+
* viewability standard (≥1 continuous second in view). A real glance counts;
|
|
4190
|
+
* a sub-second flick-past still doesn't bill the advertiser. */
|
|
4191
|
+
focusDwellSec: external_exports.number().int().positive().default(1),
|
|
4179
4192
|
/**
|
|
4180
4193
|
* Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
|
|
4181
4194
|
* the status line goes clean until the next prompt. 0 = hide on Stop.
|
|
@@ -4186,6 +4199,8 @@ var ConfigSchema = external_exports.object({
|
|
|
4186
4199
|
killSwitchUrl: external_exports.string().default(""),
|
|
4187
4200
|
/** Show a live earnings segment in the status line (separate from the ad). Off by default. */
|
|
4188
4201
|
showEarnings: external_exports.boolean().default(false),
|
|
4202
|
+
/** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
|
|
4203
|
+
showPayout: external_exports.boolean().default(true),
|
|
4189
4204
|
/** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
|
|
4190
4205
|
theme: ThemeSchema.default("plain")
|
|
4191
4206
|
});
|
|
@@ -4216,7 +4231,18 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4216
4231
|
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4217
4232
|
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4218
4233
|
* Additive — 0 from older daemons hides the bar. */
|
|
4219
|
-
rotationMs: external_exports.number().int().nonnegative().default(0)
|
|
4234
|
+
rotationMs: external_exports.number().int().nonnegative().default(0),
|
|
4235
|
+
/** Unix ms of the last counted impression fire. Anchors the transient
|
|
4236
|
+
* "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
|
|
4237
|
+
* Additive — 0 from older daemons hides the flash. */
|
|
4238
|
+
lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4239
|
+
/** Per-impression payout (millicents) of the last counted fire — the amount
|
|
4240
|
+
* the impact flash pops. Additive — 0 hides the flash. */
|
|
4241
|
+
lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
|
|
4242
|
+
/** Viewability machine's current reason string (why this tick did/didn't
|
|
4243
|
+
* fire). Drives the "why no boom" hint when the rotation bar is near full.
|
|
4244
|
+
* Additive — "" from older daemons hides the hint. */
|
|
4245
|
+
viewReason: external_exports.string().default("")
|
|
4220
4246
|
});
|
|
4221
4247
|
var ImpressionSchema = external_exports.object({
|
|
4222
4248
|
id: external_exports.string(),
|
package/daemon/dist/main.js
CHANGED
|
@@ -4131,7 +4131,16 @@ var AdSchema = external_exports.object({
|
|
|
4131
4131
|
* the daemon is running standalone (no server) — those imps earn nothing.
|
|
4132
4132
|
* See shared/src/nonce.ts.
|
|
4133
4133
|
*/
|
|
4134
|
-
nonce: external_exports.string().default("")
|
|
4134
|
+
nonce: external_exports.string().default(""),
|
|
4135
|
+
/**
|
|
4136
|
+
* Publisher payout RATES for this ad, in millicents, computed server-side from
|
|
4137
|
+
* the live CPM + share + click multiplier (earnings.ts) so the device never
|
|
4138
|
+
* hardcodes the math — change the campaign cpm and these move with it. Shown in
|
|
4139
|
+
* the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
|
|
4140
|
+
* ads / standalone daemons (no server rate) simply render nothing.
|
|
4141
|
+
*/
|
|
4142
|
+
payoutPerImpMillicents: external_exports.number().min(0).default(0),
|
|
4143
|
+
payoutPerClickMillicents: external_exports.number().min(0).default(0)
|
|
4135
4144
|
});
|
|
4136
4145
|
var GravityAdSchema = external_exports.object({
|
|
4137
4146
|
adText: external_exports.string(),
|
|
@@ -4173,16 +4182,20 @@ var ConfigSchema = external_exports.object({
|
|
|
4173
4182
|
deviceId: external_exports.string().default(""),
|
|
4174
4183
|
/** Single-use token to claim this install at /claim (attach a login + payout). */
|
|
4175
4184
|
claimToken: external_exports.string().default(""),
|
|
4176
|
-
/** Ad cache poll interval (seconds) — how often a new ad rotates in.
|
|
4177
|
-
*
|
|
4178
|
-
*
|
|
4185
|
+
/** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
|
|
4186
|
+
* with the fire: rateCapSec defaults to the SAME value, so every new ad fires
|
|
4187
|
+
* exactly once (no silent "every other ad skipped"). The next ad is preloaded
|
|
4188
|
+
* a few seconds early so the swap is instant. */
|
|
4179
4189
|
pollIntervalSec: external_exports.number().int().positive().default(30),
|
|
4180
4190
|
/** Hard daily fired-impression cap. */
|
|
4181
4191
|
dailyCap: external_exports.number().int().positive().default(300),
|
|
4182
|
-
/** Min seconds of verified view between two fired impressions.
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4192
|
+
/** Min seconds of verified view between two fired impressions. Defaults to the
|
|
4193
|
+
* rotation period (30s) so fire == rotation: each new ad booms once. */
|
|
4194
|
+
rateCapSec: external_exports.number().int().positive().default(30),
|
|
4195
|
+
/** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
|
|
4196
|
+
* viewability standard (≥1 continuous second in view). A real glance counts;
|
|
4197
|
+
* a sub-second flick-past still doesn't bill the advertiser. */
|
|
4198
|
+
focusDwellSec: external_exports.number().int().positive().default(1),
|
|
4186
4199
|
/**
|
|
4187
4200
|
* Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
|
|
4188
4201
|
* the status line goes clean until the next prompt. 0 = hide on Stop.
|
|
@@ -4193,11 +4206,13 @@ var ConfigSchema = external_exports.object({
|
|
|
4193
4206
|
killSwitchUrl: external_exports.string().default(""),
|
|
4194
4207
|
/** Show a live earnings segment in the status line (separate from the ad). Off by default. */
|
|
4195
4208
|
showEarnings: external_exports.boolean().default(false),
|
|
4209
|
+
/** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
|
|
4210
|
+
showPayout: external_exports.boolean().default(true),
|
|
4196
4211
|
/** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
|
|
4197
4212
|
theme: ThemeSchema.default("plain")
|
|
4198
4213
|
});
|
|
4199
4214
|
var DEFAULT_CONFIG = ConfigSchema.parse({});
|
|
4200
|
-
var DAEMON_STATE_SCHEMA_VERSION =
|
|
4215
|
+
var DAEMON_STATE_SCHEMA_VERSION = 2;
|
|
4201
4216
|
var DaemonStateSchema = external_exports.object({
|
|
4202
4217
|
/** Contract version (see DAEMON_STATE_SCHEMA_VERSION). Missing => 1. */
|
|
4203
4218
|
schemaVersion: external_exports.number().int().positive().default(1),
|
|
@@ -4224,7 +4239,18 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4224
4239
|
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4225
4240
|
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4226
4241
|
* Additive — 0 from older daemons hides the bar. */
|
|
4227
|
-
rotationMs: external_exports.number().int().nonnegative().default(0)
|
|
4242
|
+
rotationMs: external_exports.number().int().nonnegative().default(0),
|
|
4243
|
+
/** Unix ms of the last counted impression fire. Anchors the transient
|
|
4244
|
+
* "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
|
|
4245
|
+
* Additive — 0 from older daemons hides the flash. */
|
|
4246
|
+
lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4247
|
+
/** Per-impression payout (millicents) of the last counted fire — the amount
|
|
4248
|
+
* the impact flash pops. Additive — 0 hides the flash. */
|
|
4249
|
+
lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
|
|
4250
|
+
/** Viewability machine's current reason string (why this tick did/didn't
|
|
4251
|
+
* fire). Drives the "why no boom" hint when the rotation bar is near full.
|
|
4252
|
+
* Additive — "" from older daemons hides the hint. */
|
|
4253
|
+
viewReason: external_exports.string().default("")
|
|
4228
4254
|
});
|
|
4229
4255
|
var ImpressionSchema = external_exports.object({
|
|
4230
4256
|
id: external_exports.string(),
|
|
@@ -5122,11 +5148,44 @@ var AdSource = class {
|
|
|
5122
5148
|
this.cfg = cfg;
|
|
5123
5149
|
}
|
|
5124
5150
|
rotation = 0;
|
|
5151
|
+
// Preloaded next ad — fetched a few seconds before the swap so the rotation is
|
|
5152
|
+
// instant (no network gap, no "loading" flicker). Holding it is NOT a bill:
|
|
5153
|
+
// the impression only fires once this ad becomes the visible one and the
|
|
5154
|
+
// viewability gates pass. Fetch != fire.
|
|
5155
|
+
preloaded = null;
|
|
5156
|
+
preloading = false;
|
|
5125
5157
|
setConfig(cfg) {
|
|
5126
5158
|
this.cfg = cfg;
|
|
5127
5159
|
}
|
|
5128
|
-
/**
|
|
5160
|
+
/**
|
|
5161
|
+
* Fetch the next ad. Returns null on explicit no-fill. Consumes a preloaded ad
|
|
5162
|
+
* if one is in hand (instant swap); otherwise fetches synchronously.
|
|
5163
|
+
*/
|
|
5129
5164
|
async next(sessionId, nowMs) {
|
|
5165
|
+
if (this.preloaded) {
|
|
5166
|
+
const ad = this.preloaded;
|
|
5167
|
+
this.preloaded = null;
|
|
5168
|
+
return ad;
|
|
5169
|
+
}
|
|
5170
|
+
return this.fetchOne(sessionId, nowMs);
|
|
5171
|
+
}
|
|
5172
|
+
/**
|
|
5173
|
+
* Warm the next ad into the buffer ahead of the swap. Idempotent + non-blocking
|
|
5174
|
+
* for the caller: if a fetch is already in flight or one is buffered, no-op.
|
|
5175
|
+
* Call this a few seconds before the rotation deadline.
|
|
5176
|
+
*/
|
|
5177
|
+
async preloadNext(sessionId, nowMs) {
|
|
5178
|
+
if (this.preloaded || this.preloading) return;
|
|
5179
|
+
this.preloading = true;
|
|
5180
|
+
try {
|
|
5181
|
+
const ad = await this.fetchOne(sessionId, nowMs);
|
|
5182
|
+
if (ad) this.preloaded = ad;
|
|
5183
|
+
} finally {
|
|
5184
|
+
this.preloading = false;
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
/** One ad from the server, or the built-in test rotation if unreachable. */
|
|
5188
|
+
async fetchOne(sessionId, nowMs) {
|
|
5130
5189
|
const fromServer = await this.fromServer(sessionId, nowMs);
|
|
5131
5190
|
if (fromServer !== void 0) return fromServer;
|
|
5132
5191
|
return this.builtinTestAd(nowMs);
|
|
@@ -5365,6 +5424,7 @@ function pendingReason(s) {
|
|
|
5365
5424
|
|
|
5366
5425
|
// ../daemon/src/main.ts
|
|
5367
5426
|
var TICK_MS = 1e3;
|
|
5427
|
+
var AD_PRELOAD_LEAD_MS = 3e3;
|
|
5368
5428
|
var SESSION_REAP_MS = 6 * 60 * 60 * 1e3;
|
|
5369
5429
|
var SYNC_EVERY_TICKS = 30;
|
|
5370
5430
|
var UNKNOWN_TERM_WARN_MS = 10 * 60 * 1e3;
|
|
@@ -5387,6 +5447,8 @@ async function main() {
|
|
|
5387
5447
|
let currentAd = null;
|
|
5388
5448
|
let lastPollMs = 0;
|
|
5389
5449
|
let tickCount = 0;
|
|
5450
|
+
let lastImpFiredAtMs = 0;
|
|
5451
|
+
let lastImpEarnedMillicents = 0;
|
|
5390
5452
|
const unknownTermWarnedAt = /* @__PURE__ */ new Map();
|
|
5391
5453
|
let earnings = { balanceCents: 0, todayCents: 0 };
|
|
5392
5454
|
const now = () => Date.now();
|
|
@@ -5414,7 +5476,15 @@ async function main() {
|
|
|
5414
5476
|
if (tickCount % 10 === 0) await killSwitch.refreshRemote();
|
|
5415
5477
|
const disabled = killSwitch.disabled() || cfg.placement === "off";
|
|
5416
5478
|
const sessionLive = sessions.sessionLive();
|
|
5417
|
-
|
|
5479
|
+
const pollMs = cfg.pollIntervalSec * 1e3;
|
|
5480
|
+
if (!disabled && sessionLive && lastPollMs > 0) {
|
|
5481
|
+
const untilSwap = pollMs - (nowMs - lastPollMs);
|
|
5482
|
+
if (untilSwap <= AD_PRELOAD_LEAD_MS && untilSwap > 0) {
|
|
5483
|
+
const sid = sessions.currentSessionId() ?? "anonymous";
|
|
5484
|
+
void adSource.preloadNext(sid, nowMs).catch((e) => fileLog(`preload error: ${String(e)}`));
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
if (!disabled && sessionLive && nowMs - lastPollMs >= pollMs) {
|
|
5418
5488
|
lastPollMs = nowMs;
|
|
5419
5489
|
const sid = sessions.currentSessionId() ?? "anonymous";
|
|
5420
5490
|
try {
|
|
@@ -5481,6 +5551,8 @@ async function main() {
|
|
|
5481
5551
|
if (isNew) {
|
|
5482
5552
|
view = markFired(view, result.fireAdId, nowMs);
|
|
5483
5553
|
if (currentAd.impUrl) firePixel(currentAd.impUrl);
|
|
5554
|
+
lastImpFiredAtMs = nowMs;
|
|
5555
|
+
lastImpEarnedMillicents = currentAd.payoutPerImpMillicents;
|
|
5484
5556
|
await fileLog(`FIRED ${result.fireAdId} (${currentAd.brandName})`);
|
|
5485
5557
|
}
|
|
5486
5558
|
}
|
|
@@ -5497,7 +5569,11 @@ async function main() {
|
|
|
5497
5569
|
// Anchor + period for the statusline rotation countdown bar. lastPollMs
|
|
5498
5570
|
// is the last ad swap; the bar fills over pollIntervalSec toward the next.
|
|
5499
5571
|
rotatedAtMs: lastPollMs,
|
|
5500
|
-
rotationMs: cfg.pollIntervalSec * 1e3
|
|
5572
|
+
rotationMs: cfg.pollIntervalSec * 1e3,
|
|
5573
|
+
lastImpFiredAtMs,
|
|
5574
|
+
lastImpEarnedMillicents,
|
|
5575
|
+
// Why this tick did/didn't fire — drives the "why no boom" hint.
|
|
5576
|
+
viewReason: result.reason
|
|
5501
5577
|
};
|
|
5502
5578
|
writeDaemonState(state);
|
|
5503
5579
|
if (tickCount % SYNC_EVERY_TICKS === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adsinagents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
package/statusline/dist/index.js
CHANGED
|
@@ -4108,7 +4108,16 @@ var AdSchema = external_exports.object({
|
|
|
4108
4108
|
* the daemon is running standalone (no server) — those imps earn nothing.
|
|
4109
4109
|
* See shared/src/nonce.ts.
|
|
4110
4110
|
*/
|
|
4111
|
-
nonce: external_exports.string().default("")
|
|
4111
|
+
nonce: external_exports.string().default(""),
|
|
4112
|
+
/**
|
|
4113
|
+
* Publisher payout RATES for this ad, in millicents, computed server-side from
|
|
4114
|
+
* the live CPM + share + click multiplier (earnings.ts) so the device never
|
|
4115
|
+
* hardcodes the math — change the campaign cpm and these move with it. Shown in
|
|
4116
|
+
* the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
|
|
4117
|
+
* ads / standalone daemons (no server rate) simply render nothing.
|
|
4118
|
+
*/
|
|
4119
|
+
payoutPerImpMillicents: external_exports.number().min(0).default(0),
|
|
4120
|
+
payoutPerClickMillicents: external_exports.number().min(0).default(0)
|
|
4112
4121
|
});
|
|
4113
4122
|
var GravityAdSchema = external_exports.object({
|
|
4114
4123
|
adText: external_exports.string(),
|
|
@@ -4150,16 +4159,20 @@ var ConfigSchema = external_exports.object({
|
|
|
4150
4159
|
deviceId: external_exports.string().default(""),
|
|
4151
4160
|
/** Single-use token to claim this install at /claim (attach a login + payout). */
|
|
4152
4161
|
claimToken: external_exports.string().default(""),
|
|
4153
|
-
/** Ad cache poll interval (seconds) — how often a new ad rotates in.
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4162
|
+
/** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
|
|
4163
|
+
* with the fire: rateCapSec defaults to the SAME value, so every new ad fires
|
|
4164
|
+
* exactly once (no silent "every other ad skipped"). The next ad is preloaded
|
|
4165
|
+
* a few seconds early so the swap is instant. */
|
|
4156
4166
|
pollIntervalSec: external_exports.number().int().positive().default(30),
|
|
4157
4167
|
/** Hard daily fired-impression cap. */
|
|
4158
4168
|
dailyCap: external_exports.number().int().positive().default(300),
|
|
4159
|
-
/** Min seconds of verified view between two fired impressions.
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4169
|
+
/** Min seconds of verified view between two fired impressions. Defaults to the
|
|
4170
|
+
* rotation period (30s) so fire == rotation: each new ad booms once. */
|
|
4171
|
+
rateCapSec: external_exports.number().int().positive().default(30),
|
|
4172
|
+
/** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
|
|
4173
|
+
* viewability standard (≥1 continuous second in view). A real glance counts;
|
|
4174
|
+
* a sub-second flick-past still doesn't bill the advertiser. */
|
|
4175
|
+
focusDwellSec: external_exports.number().int().positive().default(1),
|
|
4163
4176
|
/**
|
|
4164
4177
|
* Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
|
|
4165
4178
|
* the status line goes clean until the next prompt. 0 = hide on Stop.
|
|
@@ -4170,6 +4183,8 @@ var ConfigSchema = external_exports.object({
|
|
|
4170
4183
|
killSwitchUrl: external_exports.string().default(""),
|
|
4171
4184
|
/** Show a live earnings segment in the status line (separate from the ad). Off by default. */
|
|
4172
4185
|
showEarnings: external_exports.boolean().default(false),
|
|
4186
|
+
/** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
|
|
4187
|
+
showPayout: external_exports.boolean().default(true),
|
|
4173
4188
|
/** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
|
|
4174
4189
|
theme: ThemeSchema.default("plain")
|
|
4175
4190
|
});
|
|
@@ -4200,7 +4215,18 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4200
4215
|
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4201
4216
|
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4202
4217
|
* Additive — 0 from older daemons hides the bar. */
|
|
4203
|
-
rotationMs: external_exports.number().int().nonnegative().default(0)
|
|
4218
|
+
rotationMs: external_exports.number().int().nonnegative().default(0),
|
|
4219
|
+
/** Unix ms of the last counted impression fire. Anchors the transient
|
|
4220
|
+
* "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
|
|
4221
|
+
* Additive — 0 from older daemons hides the flash. */
|
|
4222
|
+
lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4223
|
+
/** Per-impression payout (millicents) of the last counted fire — the amount
|
|
4224
|
+
* the impact flash pops. Additive — 0 hides the flash. */
|
|
4225
|
+
lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
|
|
4226
|
+
/** Viewability machine's current reason string (why this tick did/didn't
|
|
4227
|
+
* fire). Drives the "why no boom" hint when the rotation bar is near full.
|
|
4228
|
+
* Additive — "" from older daemons hides the hint. */
|
|
4229
|
+
viewReason: external_exports.string().default("")
|
|
4204
4230
|
});
|
|
4205
4231
|
var ImpressionSchema = external_exports.object({
|
|
4206
4232
|
id: external_exports.string(),
|
|
@@ -4240,11 +4266,16 @@ var DIM = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
|
4240
4266
|
var BOLD = (s) => `\x1B[1m${s}\x1B[22m`;
|
|
4241
4267
|
var GREEN = (s) => `\x1B[32m${s}\x1B[39m`;
|
|
4242
4268
|
var AMBER = (s) => `\x1B[38;5;215m${s}\x1B[39m`;
|
|
4269
|
+
var FLASH = (s) => `\x1B[1;92m${s}\x1B[22;39m`;
|
|
4243
4270
|
var THEMES = {
|
|
4244
|
-
plain
|
|
4245
|
-
|
|
4246
|
-
|
|
4271
|
+
// plain MUST emit zero ANSI (VS Code) — the flash/hint are plain text there,
|
|
4272
|
+
// still appearing/disappearing, just uncolored.
|
|
4273
|
+
plain: { disclosure: ID, mark: ID, brand: ID, money: ID, flash: ID, hint: ID },
|
|
4274
|
+
// hint = amber even in dim, so an actionable "why no boom" block stands out.
|
|
4275
|
+
dim: { disclosure: DIM, mark: ID, brand: ID, money: ID, flash: FLASH, hint: AMBER },
|
|
4276
|
+
accent: { disclosure: DIM, mark: AMBER, brand: BOLD, money: GREEN, flash: FLASH, hint: AMBER }
|
|
4247
4277
|
};
|
|
4278
|
+
var IMPACT_FLASH_MS = 3e3;
|
|
4248
4279
|
var MAX_AD_TEXT = 60;
|
|
4249
4280
|
function renderAdSegment(ad, opts) {
|
|
4250
4281
|
const t = THEMES[opts.theme ?? "plain"];
|
|
@@ -4254,13 +4285,57 @@ function renderAdSegment(ad, opts) {
|
|
|
4254
4285
|
const label = ad.brandName ? `${text} \xB7 ${t.brand(ad.brandName)}` : text;
|
|
4255
4286
|
const clickUrl = `${opts.clickBase}/click/${encodeURIComponent(ad.id)}`;
|
|
4256
4287
|
const linked = opts.supportsOsc8 ? osc8(clickUrl, `${star} ${label} ${arrow}`) : opts.plainLink ? `${star} ${label} \xB7 ${opts.plainLink} ${arrow}` : `${star} ${label} ${arrow}`;
|
|
4257
|
-
return `${t.disclosure("Ad:")} ${linked}
|
|
4288
|
+
return `${t.disclosure("Ad:")} ${linked}`;
|
|
4258
4289
|
}
|
|
4259
4290
|
function renderEarningsSegment(balanceCents, todayCents, theme = "plain") {
|
|
4260
4291
|
const t = THEMES[theme];
|
|
4261
4292
|
const d = (c) => t.money(`$${(Math.max(0, c) / 100).toFixed(2)}`);
|
|
4262
4293
|
return `${d(todayCents)} today \xB7 ${d(balanceCents)}`;
|
|
4263
4294
|
}
|
|
4295
|
+
function renderPayoutSegment(perImpMillicents, perClickMillicents, theme = "plain") {
|
|
4296
|
+
const imp = Math.max(0, perImpMillicents);
|
|
4297
|
+
const click = Math.max(0, perClickMillicents);
|
|
4298
|
+
if (imp === 0 && click === 0)
|
|
4299
|
+
return "";
|
|
4300
|
+
const t = THEMES[theme];
|
|
4301
|
+
const fmt = (millicents) => {
|
|
4302
|
+
const dollars = millicents / 1e5;
|
|
4303
|
+
const s = dollars >= 0.01 ? dollars.toFixed(2) : dollars.toFixed(4).replace(/0+$/, "");
|
|
4304
|
+
return `$${s}`;
|
|
4305
|
+
};
|
|
4306
|
+
return `${t.money(`+${fmt(imp)}`)}/view \xB7 ${t.money(`+${fmt(click)}`)}/click`;
|
|
4307
|
+
}
|
|
4308
|
+
function renderImpactFlash(nowMs, firedAtMs, earnedMillicents, theme = "plain") {
|
|
4309
|
+
if (firedAtMs <= 0 || earnedMillicents <= 0)
|
|
4310
|
+
return "";
|
|
4311
|
+
if (nowMs - firedAtMs > IMPACT_FLASH_MS || nowMs < firedAtMs)
|
|
4312
|
+
return "";
|
|
4313
|
+
const dollars = earnedMillicents / 1e5;
|
|
4314
|
+
const s = dollars >= 0.01 ? dollars.toFixed(2) : dollars.toFixed(4).replace(/0+$/, "");
|
|
4315
|
+
return THEMES[theme].flash(`+$${s}`);
|
|
4316
|
+
}
|
|
4317
|
+
function classifyEarningState(reason) {
|
|
4318
|
+
if (reason === "verified \u2014 fire" || reason === "already fired this ad" || reason === "rate-capped" || reason.startsWith("dwell ")) {
|
|
4319
|
+
return { earning: true, why: "" };
|
|
4320
|
+
}
|
|
4321
|
+
const why = reason === "no live session" ? "no active session" : reason === "terminal not focused" ? "terminal not focused" : reason === "no recent prompt (idle)" ? "idle - prompt to resume" : reason === "screen locked" ? "screen locked" : reason === "display asleep" ? "display asleep" : reason === "disabled (kill switch)" ? "turned off" : reason === "daily cap reached" ? "daily max reached" : reason === "no ad cached" ? "loading ad" : "paused";
|
|
4322
|
+
return { earning: false, why };
|
|
4323
|
+
}
|
|
4324
|
+
var EARNING_PULSE_FRAMES = ["\u25CF", "\u25CD", "\u25C9", "\u25CD"];
|
|
4325
|
+
function renderEarningDot(reason, theme = "plain", nowMs) {
|
|
4326
|
+
const t = THEMES[theme];
|
|
4327
|
+
const { earning, why } = classifyEarningState(reason);
|
|
4328
|
+
if (earning) {
|
|
4329
|
+
if (nowMs === void 0)
|
|
4330
|
+
return t.flash("\u25CF earning");
|
|
4331
|
+
const step = Math.floor(nowMs / 1e3);
|
|
4332
|
+
const dot = EARNING_PULSE_FRAMES[step % EARNING_PULSE_FRAMES.length];
|
|
4333
|
+
const bright = dot === "\u25CF" || dot === "\u25C9";
|
|
4334
|
+
const paint = bright ? t.flash : t.money;
|
|
4335
|
+
return paint(`${dot} earning`);
|
|
4336
|
+
}
|
|
4337
|
+
return t.disclosure(`\u25CB paused: ${why}`);
|
|
4338
|
+
}
|
|
4264
4339
|
var ROTATION_BAR_CELLS = 8;
|
|
4265
4340
|
function renderRotationBar(nowMs, rotatedAtMs, rotationMs, theme = "plain") {
|
|
4266
4341
|
if (rotationMs <= 0 || rotatedAtMs <= 0)
|
|
@@ -4373,6 +4448,7 @@ function main() {
|
|
|
4373
4448
|
const stale = Date.now() - state.updatedAt > STATE_STALE_MS;
|
|
4374
4449
|
const cfg = ConfigSchema.safeParse(readJson(PATHS.config));
|
|
4375
4450
|
const showEarnings = cfg.success && cfg.data.showEarnings;
|
|
4451
|
+
const showPayout = cfg.success && cfg.data.showPayout;
|
|
4376
4452
|
const theme = cfg.success ? cfg.data.theme : "plain";
|
|
4377
4453
|
const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents, theme) : "";
|
|
4378
4454
|
const adParsed = AdSchema.safeParse(readJson(PATHS.currentAd));
|
|
@@ -4389,13 +4465,24 @@ function main() {
|
|
|
4389
4465
|
plainLink: plainClickLink(adParsed.data, serverUrl, clickBase),
|
|
4390
4466
|
theme
|
|
4391
4467
|
});
|
|
4392
|
-
const seg =
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
state.rotationMs,
|
|
4468
|
+
const seg = adSeg;
|
|
4469
|
+
const payoutSeg = showPayout ? renderPayoutSegment(
|
|
4470
|
+
adParsed.data.payoutPerImpMillicents,
|
|
4471
|
+
adParsed.data.payoutPerClickMillicents,
|
|
4397
4472
|
theme
|
|
4473
|
+
) : "";
|
|
4474
|
+
const now = Date.now();
|
|
4475
|
+
const flashSeg = renderImpactFlash(
|
|
4476
|
+
now,
|
|
4477
|
+
state.lastImpFiredAtMs,
|
|
4478
|
+
state.lastImpEarnedMillicents,
|
|
4479
|
+
theme
|
|
4480
|
+
);
|
|
4481
|
+
const earningDotSeg = flashSeg === "" ? renderEarningDot(state.viewReason, theme, now) : "";
|
|
4482
|
+
const rotationSeg = renderRotationBar(now, state.rotatedAtMs, state.rotationMs, theme);
|
|
4483
|
+
const spinnerSeg = state.working ? workingGlyph(now, theme) : "";
|
|
4484
|
+
process.stdout.write(
|
|
4485
|
+
composeStatusLine(prior, seg, payoutSeg, flashSeg, earningDotSeg, rotationSeg, spinnerSeg, earningsSeg) + "\n"
|
|
4398
4486
|
);
|
|
4399
|
-
process.stdout.write(composeStatusLine(prior, seg, rotationSeg, earningsSeg) + "\n");
|
|
4400
4487
|
}
|
|
4401
4488
|
main();
|