adsinagents 0.1.10 → 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 +75 -12
- package/daemon/dist/main.js +140 -16
- package/package.json +1 -1
- package/statusline/dist/index.js +128 -15
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
|
});
|
|
@@ -4209,7 +4224,25 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4209
4224
|
todayCents: external_exports.number().int().nonnegative().default(0),
|
|
4210
4225
|
/** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
|
|
4211
4226
|
* working glyph. Additive — older daemons omit it. */
|
|
4212
|
-
working: external_exports.boolean().default(false)
|
|
4227
|
+
working: external_exports.boolean().default(false),
|
|
4228
|
+
/** Unix ms of the last ad rotation (== lastPollMs). Anchors the rotation
|
|
4229
|
+
* countdown bar; the statusline derives fill from (now - rotatedAtMs).
|
|
4230
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
4231
|
+
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4232
|
+
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4233
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
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("")
|
|
4213
4246
|
});
|
|
4214
4247
|
var ImpressionSchema = external_exports.object({
|
|
4215
4248
|
id: external_exports.string(),
|
|
@@ -4246,11 +4279,20 @@ var TERMINAL_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
|
4246
4279
|
"com.mitchellh.ghostty",
|
|
4247
4280
|
"dev.warp.Warp-Stable",
|
|
4248
4281
|
"dev.warp.Warp-Preview",
|
|
4282
|
+
"com.cmuxterm.app",
|
|
4283
|
+
// cmux
|
|
4249
4284
|
// VS Code / Cursor integrated terminal => coarser confidence, still counts.
|
|
4250
4285
|
"com.microsoft.VSCode",
|
|
4251
4286
|
"com.todesktop.230313mzl4w4u92"
|
|
4252
4287
|
// Cursor
|
|
4253
4288
|
]);
|
|
4289
|
+
var ENV_TERMINAL_BUNDLE_IDS = new Set((process.env.ADSINAGENTS_TERMINAL_BUNDLE_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
4290
|
+
function isTerminalBundle(bundleId) {
|
|
4291
|
+
return TERMINAL_BUNDLE_IDS.has(bundleId) || ENV_TERMINAL_BUNDLE_IDS.has(bundleId);
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// ../../shared/dist/earnings.js
|
|
4295
|
+
var DAILY_CAP_MILLICENTS = 1e4 * 1e3;
|
|
4254
4296
|
|
|
4255
4297
|
// ../../shared/dist/nonce.js
|
|
4256
4298
|
var NONCE_TTL_MS = 30 * 60 * 1e3;
|
|
@@ -4277,10 +4319,12 @@ var DarwinPlatform = class {
|
|
|
4277
4319
|
const { stdout } = await pExecFile(PATHS.frontmostBin, [], { timeout: 800 });
|
|
4278
4320
|
const parsed = FocusProbeSchema.safeParse(JSON.parse(stdout));
|
|
4279
4321
|
if (parsed.success) {
|
|
4322
|
+
const focused = isTerminalBundle(parsed.data.bundleId);
|
|
4280
4323
|
return {
|
|
4281
|
-
focused
|
|
4324
|
+
focused,
|
|
4282
4325
|
locked: parsed.data.locked,
|
|
4283
|
-
screenAwake: parsed.data.screenAwake
|
|
4326
|
+
screenAwake: parsed.data.screenAwake,
|
|
4327
|
+
unknownFrontApp: focused ? void 0 : parsed.data.bundleId || void 0
|
|
4284
4328
|
};
|
|
4285
4329
|
}
|
|
4286
4330
|
} catch {
|
|
@@ -4296,7 +4340,13 @@ var DarwinPlatform = class {
|
|
|
4296
4340
|
'tell application "System Events" to get bundle identifier of first application process whose frontmost is true'
|
|
4297
4341
|
], { timeout: 800 });
|
|
4298
4342
|
const bundleId = stdout.trim();
|
|
4299
|
-
|
|
4343
|
+
const focused = isTerminalBundle(bundleId);
|
|
4344
|
+
return {
|
|
4345
|
+
focused,
|
|
4346
|
+
locked: false,
|
|
4347
|
+
screenAwake: true,
|
|
4348
|
+
unknownFrontApp: focused ? void 0 : bundleId || void 0
|
|
4349
|
+
};
|
|
4300
4350
|
} catch {
|
|
4301
4351
|
return { focused: false, locked: false, screenAwake: true };
|
|
4302
4352
|
}
|
|
@@ -4880,6 +4930,19 @@ async function runDoctor() {
|
|
|
4880
4930
|
ok: focusOk,
|
|
4881
4931
|
detail: focusOk ? "native frontmost helper present" : "Node fallback (osascript)"
|
|
4882
4932
|
});
|
|
4933
|
+
try {
|
|
4934
|
+
const fm = await platform.frontmost();
|
|
4935
|
+
if (fm.unknownFrontApp) {
|
|
4936
|
+
checks.push({
|
|
4937
|
+
name: "Terminal recognized",
|
|
4938
|
+
ok: false,
|
|
4939
|
+
detail: `frontmost app "${fm.unknownFrontApp}" isn't a known terminal \u2014 impressions won't fire. If this is your terminal: set ADSINAGENTS_TERMINAL_BUNDLE_IDS=${fm.unknownFrontApp} and restart the daemon.`
|
|
4940
|
+
});
|
|
4941
|
+
} else if (fm.focused) {
|
|
4942
|
+
checks.push({ name: "Terminal recognized", ok: true, detail: "frontmost app is a known terminal" });
|
|
4943
|
+
}
|
|
4944
|
+
} catch {
|
|
4945
|
+
}
|
|
4883
4946
|
let key = null;
|
|
4884
4947
|
try {
|
|
4885
4948
|
key = await platform.getSecret("publisher-key");
|
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),
|
|
@@ -4217,7 +4232,25 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4217
4232
|
todayCents: external_exports.number().int().nonnegative().default(0),
|
|
4218
4233
|
/** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
|
|
4219
4234
|
* working glyph. Additive — older daemons omit it. */
|
|
4220
|
-
working: external_exports.boolean().default(false)
|
|
4235
|
+
working: external_exports.boolean().default(false),
|
|
4236
|
+
/** Unix ms of the last ad rotation (== lastPollMs). Anchors the rotation
|
|
4237
|
+
* countdown bar; the statusline derives fill from (now - rotatedAtMs).
|
|
4238
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
4239
|
+
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4240
|
+
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4241
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
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("")
|
|
4221
4254
|
});
|
|
4222
4255
|
var ImpressionSchema = external_exports.object({
|
|
4223
4256
|
id: external_exports.string(),
|
|
@@ -4254,11 +4287,20 @@ var TERMINAL_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
|
4254
4287
|
"com.mitchellh.ghostty",
|
|
4255
4288
|
"dev.warp.Warp-Stable",
|
|
4256
4289
|
"dev.warp.Warp-Preview",
|
|
4290
|
+
"com.cmuxterm.app",
|
|
4291
|
+
// cmux
|
|
4257
4292
|
// VS Code / Cursor integrated terminal => coarser confidence, still counts.
|
|
4258
4293
|
"com.microsoft.VSCode",
|
|
4259
4294
|
"com.todesktop.230313mzl4w4u92"
|
|
4260
4295
|
// Cursor
|
|
4261
4296
|
]);
|
|
4297
|
+
var ENV_TERMINAL_BUNDLE_IDS = new Set((process.env.ADSINAGENTS_TERMINAL_BUNDLE_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
4298
|
+
function isTerminalBundle(bundleId) {
|
|
4299
|
+
return TERMINAL_BUNDLE_IDS.has(bundleId) || ENV_TERMINAL_BUNDLE_IDS.has(bundleId);
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
// ../../shared/dist/earnings.js
|
|
4303
|
+
var DAILY_CAP_MILLICENTS = 1e4 * 1e3;
|
|
4262
4304
|
|
|
4263
4305
|
// ../../shared/dist/nonce.js
|
|
4264
4306
|
var NONCE_TTL_MS = 30 * 60 * 1e3;
|
|
@@ -4285,10 +4327,12 @@ var DarwinPlatform = class {
|
|
|
4285
4327
|
const { stdout } = await pExecFile(PATHS.frontmostBin, [], { timeout: 800 });
|
|
4286
4328
|
const parsed = FocusProbeSchema.safeParse(JSON.parse(stdout));
|
|
4287
4329
|
if (parsed.success) {
|
|
4330
|
+
const focused = isTerminalBundle(parsed.data.bundleId);
|
|
4288
4331
|
return {
|
|
4289
|
-
focused
|
|
4332
|
+
focused,
|
|
4290
4333
|
locked: parsed.data.locked,
|
|
4291
|
-
screenAwake: parsed.data.screenAwake
|
|
4334
|
+
screenAwake: parsed.data.screenAwake,
|
|
4335
|
+
unknownFrontApp: focused ? void 0 : parsed.data.bundleId || void 0
|
|
4292
4336
|
};
|
|
4293
4337
|
}
|
|
4294
4338
|
} catch {
|
|
@@ -4304,7 +4348,13 @@ var DarwinPlatform = class {
|
|
|
4304
4348
|
'tell application "System Events" to get bundle identifier of first application process whose frontmost is true'
|
|
4305
4349
|
], { timeout: 800 });
|
|
4306
4350
|
const bundleId = stdout.trim();
|
|
4307
|
-
|
|
4351
|
+
const focused = isTerminalBundle(bundleId);
|
|
4352
|
+
return {
|
|
4353
|
+
focused,
|
|
4354
|
+
locked: false,
|
|
4355
|
+
screenAwake: true,
|
|
4356
|
+
unknownFrontApp: focused ? void 0 : bundleId || void 0
|
|
4357
|
+
};
|
|
4308
4358
|
} catch {
|
|
4309
4359
|
return { focused: false, locked: false, screenAwake: true };
|
|
4310
4360
|
}
|
|
@@ -4961,6 +5011,14 @@ var FocusPoller = class {
|
|
|
4961
5011
|
terminalFocused() {
|
|
4962
5012
|
return this.last.focused;
|
|
4963
5013
|
}
|
|
5014
|
+
/**
|
|
5015
|
+
* Bundle id of the frontmost app when it's a foreground app we DON'T recognize
|
|
5016
|
+
* as a terminal — the daemon warns on it so an unlisted terminal can't silently
|
|
5017
|
+
* zero out earnings. Undefined when on a known terminal. Diagnostics only.
|
|
5018
|
+
*/
|
|
5019
|
+
unknownFrontApp() {
|
|
5020
|
+
return this.last.unknownFrontApp;
|
|
5021
|
+
}
|
|
4964
5022
|
async poll() {
|
|
4965
5023
|
try {
|
|
4966
5024
|
this.last = await this.platform.frontmost();
|
|
@@ -5090,11 +5148,44 @@ var AdSource = class {
|
|
|
5090
5148
|
this.cfg = cfg;
|
|
5091
5149
|
}
|
|
5092
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;
|
|
5093
5157
|
setConfig(cfg) {
|
|
5094
5158
|
this.cfg = cfg;
|
|
5095
5159
|
}
|
|
5096
|
-
/**
|
|
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
|
+
*/
|
|
5097
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) {
|
|
5098
5189
|
const fromServer = await this.fromServer(sessionId, nowMs);
|
|
5099
5190
|
if (fromServer !== void 0) return fromServer;
|
|
5100
5191
|
return this.builtinTestAd(nowMs);
|
|
@@ -5333,8 +5424,10 @@ function pendingReason(s) {
|
|
|
5333
5424
|
|
|
5334
5425
|
// ../daemon/src/main.ts
|
|
5335
5426
|
var TICK_MS = 1e3;
|
|
5427
|
+
var AD_PRELOAD_LEAD_MS = 3e3;
|
|
5336
5428
|
var SESSION_REAP_MS = 6 * 60 * 60 * 1e3;
|
|
5337
5429
|
var SYNC_EVERY_TICKS = 30;
|
|
5430
|
+
var UNKNOWN_TERM_WARN_MS = 10 * 60 * 1e3;
|
|
5338
5431
|
async function fileLog(line) {
|
|
5339
5432
|
try {
|
|
5340
5433
|
writeFileSync4(PATHS.daemonLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
@@ -5354,6 +5447,9 @@ async function main() {
|
|
|
5354
5447
|
let currentAd = null;
|
|
5355
5448
|
let lastPollMs = 0;
|
|
5356
5449
|
let tickCount = 0;
|
|
5450
|
+
let lastImpFiredAtMs = 0;
|
|
5451
|
+
let lastImpEarnedMillicents = 0;
|
|
5452
|
+
const unknownTermWarnedAt = /* @__PURE__ */ new Map();
|
|
5357
5453
|
let earnings = { balanceCents: 0, todayCents: 0 };
|
|
5358
5454
|
const now = () => Date.now();
|
|
5359
5455
|
const { server, port } = await startServer({
|
|
@@ -5380,7 +5476,15 @@ async function main() {
|
|
|
5380
5476
|
if (tickCount % 10 === 0) await killSwitch.refreshRemote();
|
|
5381
5477
|
const disabled = killSwitch.disabled() || cfg.placement === "off";
|
|
5382
5478
|
const sessionLive = sessions.sessionLive();
|
|
5383
|
-
|
|
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) {
|
|
5384
5488
|
lastPollMs = nowMs;
|
|
5385
5489
|
const sid = sessions.currentSessionId() ?? "anonymous";
|
|
5386
5490
|
try {
|
|
@@ -5416,6 +5520,16 @@ async function main() {
|
|
|
5416
5520
|
const lastStop = sessions.lastStop();
|
|
5417
5521
|
const adDisplayable = working || lastStop !== null && nowMs - lastStop <= cfg.idleGraceSec * 1e3;
|
|
5418
5522
|
const probe = focus.current();
|
|
5523
|
+
const unknownTerm = focus.unknownFrontApp();
|
|
5524
|
+
if (sessionLive && currentAd !== null && adDisplayable && unknownTerm) {
|
|
5525
|
+
const lastWarn = unknownTermWarnedAt.get(unknownTerm) ?? 0;
|
|
5526
|
+
if (nowMs - lastWarn >= UNKNOWN_TERM_WARN_MS) {
|
|
5527
|
+
unknownTermWarnedAt.set(unknownTerm, nowMs);
|
|
5528
|
+
await fileLog(
|
|
5529
|
+
`WARN unrecognized terminal "${unknownTerm}" is frontmost \u2014 impressions can't fire. If this is your terminal, set ADSINAGENTS_TERMINAL_BUNDLE_IDS=${unknownTerm} and restart the daemon.`
|
|
5530
|
+
);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5419
5533
|
const sample = {
|
|
5420
5534
|
nowMs,
|
|
5421
5535
|
sessionLive,
|
|
@@ -5437,6 +5551,8 @@ async function main() {
|
|
|
5437
5551
|
if (isNew) {
|
|
5438
5552
|
view = markFired(view, result.fireAdId, nowMs);
|
|
5439
5553
|
if (currentAd.impUrl) firePixel(currentAd.impUrl);
|
|
5554
|
+
lastImpFiredAtMs = nowMs;
|
|
5555
|
+
lastImpEarnedMillicents = currentAd.payoutPerImpMillicents;
|
|
5440
5556
|
await fileLog(`FIRED ${result.fireAdId} (${currentAd.brandName})`);
|
|
5441
5557
|
}
|
|
5442
5558
|
}
|
|
@@ -5449,7 +5565,15 @@ async function main() {
|
|
|
5449
5565
|
updatedAt: nowMs,
|
|
5450
5566
|
balanceCents: earnings.balanceCents,
|
|
5451
5567
|
todayCents: earnings.todayCents,
|
|
5452
|
-
working
|
|
5568
|
+
working,
|
|
5569
|
+
// Anchor + period for the statusline rotation countdown bar. lastPollMs
|
|
5570
|
+
// is the last ad swap; the bar fills over pollIntervalSec toward the next.
|
|
5571
|
+
rotatedAtMs: lastPollMs,
|
|
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
|
|
5453
5577
|
};
|
|
5454
5578
|
writeDaemonState(state);
|
|
5455
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
|
});
|
|
@@ -4193,7 +4208,25 @@ var DaemonStateSchema = external_exports.object({
|
|
|
4193
4208
|
todayCents: external_exports.number().int().nonnegative().default(0),
|
|
4194
4209
|
/** Claude is mid-turn (prompt received, Stop not yet). Drives the statusline
|
|
4195
4210
|
* working glyph. Additive — older daemons omit it. */
|
|
4196
|
-
working: external_exports.boolean().default(false)
|
|
4211
|
+
working: external_exports.boolean().default(false),
|
|
4212
|
+
/** Unix ms of the last ad rotation (== lastPollMs). Anchors the rotation
|
|
4213
|
+
* countdown bar; the statusline derives fill from (now - rotatedAtMs).
|
|
4214
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
4215
|
+
rotatedAtMs: external_exports.number().int().nonnegative().default(0),
|
|
4216
|
+
/** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
|
|
4217
|
+
* Additive — 0 from older daemons hides the bar. */
|
|
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("")
|
|
4197
4230
|
});
|
|
4198
4231
|
var ImpressionSchema = external_exports.object({
|
|
4199
4232
|
id: external_exports.string(),
|
|
@@ -4222,6 +4255,7 @@ var FocusProbeSchema = external_exports.object({
|
|
|
4222
4255
|
locked: external_exports.boolean(),
|
|
4223
4256
|
screenAwake: external_exports.boolean()
|
|
4224
4257
|
});
|
|
4258
|
+
var ENV_TERMINAL_BUNDLE_IDS = new Set((process.env.ADSINAGENTS_TERMINAL_BUNDLE_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
4225
4259
|
|
|
4226
4260
|
// ../../shared/dist/render.js
|
|
4227
4261
|
function osc8(url, text) {
|
|
@@ -4232,11 +4266,16 @@ var DIM = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
|
4232
4266
|
var BOLD = (s) => `\x1B[1m${s}\x1B[22m`;
|
|
4233
4267
|
var GREEN = (s) => `\x1B[32m${s}\x1B[39m`;
|
|
4234
4268
|
var AMBER = (s) => `\x1B[38;5;215m${s}\x1B[39m`;
|
|
4269
|
+
var FLASH = (s) => `\x1B[1;92m${s}\x1B[22;39m`;
|
|
4235
4270
|
var THEMES = {
|
|
4236
|
-
plain
|
|
4237
|
-
|
|
4238
|
-
|
|
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 }
|
|
4239
4277
|
};
|
|
4278
|
+
var IMPACT_FLASH_MS = 3e3;
|
|
4240
4279
|
var MAX_AD_TEXT = 60;
|
|
4241
4280
|
function renderAdSegment(ad, opts) {
|
|
4242
4281
|
const t = THEMES[opts.theme ?? "plain"];
|
|
@@ -4246,13 +4285,66 @@ function renderAdSegment(ad, opts) {
|
|
|
4246
4285
|
const label = ad.brandName ? `${text} \xB7 ${t.brand(ad.brandName)}` : text;
|
|
4247
4286
|
const clickUrl = `${opts.clickBase}/click/${encodeURIComponent(ad.id)}`;
|
|
4248
4287
|
const linked = opts.supportsOsc8 ? osc8(clickUrl, `${star} ${label} ${arrow}`) : opts.plainLink ? `${star} ${label} \xB7 ${opts.plainLink} ${arrow}` : `${star} ${label} ${arrow}`;
|
|
4249
|
-
return `${t.disclosure("Ad:")} ${linked}
|
|
4288
|
+
return `${t.disclosure("Ad:")} ${linked}`;
|
|
4250
4289
|
}
|
|
4251
4290
|
function renderEarningsSegment(balanceCents, todayCents, theme = "plain") {
|
|
4252
4291
|
const t = THEMES[theme];
|
|
4253
4292
|
const d = (c) => t.money(`$${(Math.max(0, c) / 100).toFixed(2)}`);
|
|
4254
4293
|
return `${d(todayCents)} today \xB7 ${d(balanceCents)}`;
|
|
4255
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
|
+
}
|
|
4339
|
+
var ROTATION_BAR_CELLS = 8;
|
|
4340
|
+
function renderRotationBar(nowMs, rotatedAtMs, rotationMs, theme = "plain") {
|
|
4341
|
+
if (rotationMs <= 0 || rotatedAtMs <= 0)
|
|
4342
|
+
return "";
|
|
4343
|
+
const frac = Math.min(1, Math.max(0, (nowMs - rotatedAtMs) / rotationMs));
|
|
4344
|
+
const filled = Math.min(ROTATION_BAR_CELLS, Math.round(frac * ROTATION_BAR_CELLS));
|
|
4345
|
+
const bar = "\u25B0".repeat(filled) + "\u25B1".repeat(ROTATION_BAR_CELLS - filled);
|
|
4346
|
+
return THEMES[theme ?? "plain"].mark(bar);
|
|
4347
|
+
}
|
|
4256
4348
|
function renderLoadingSegment(theme = "plain") {
|
|
4257
4349
|
const s = "Ad: loading\u2026";
|
|
4258
4350
|
return theme === "plain" ? s : DIM(s);
|
|
@@ -4267,6 +4359,9 @@ function composeStatusLine(priorOutput, ...segments) {
|
|
|
4267
4359
|
return parts.join(" \u2502 ");
|
|
4268
4360
|
}
|
|
4269
4361
|
|
|
4362
|
+
// ../../shared/dist/earnings.js
|
|
4363
|
+
var DAILY_CAP_MILLICENTS = 1e4 * 1e3;
|
|
4364
|
+
|
|
4270
4365
|
// ../../shared/dist/nonce.js
|
|
4271
4366
|
var NONCE_TTL_MS = 30 * 60 * 1e3;
|
|
4272
4367
|
|
|
@@ -4353,6 +4448,7 @@ function main() {
|
|
|
4353
4448
|
const stale = Date.now() - state.updatedAt > STATE_STALE_MS;
|
|
4354
4449
|
const cfg = ConfigSchema.safeParse(readJson(PATHS.config));
|
|
4355
4450
|
const showEarnings = cfg.success && cfg.data.showEarnings;
|
|
4451
|
+
const showPayout = cfg.success && cfg.data.showPayout;
|
|
4356
4452
|
const theme = cfg.success ? cfg.data.theme : "plain";
|
|
4357
4453
|
const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents, theme) : "";
|
|
4358
4454
|
const adParsed = AdSchema.safeParse(readJson(PATHS.currentAd));
|
|
@@ -4369,7 +4465,24 @@ function main() {
|
|
|
4369
4465
|
plainLink: plainClickLink(adParsed.data, serverUrl, clickBase),
|
|
4370
4466
|
theme
|
|
4371
4467
|
});
|
|
4372
|
-
const seg =
|
|
4373
|
-
|
|
4468
|
+
const seg = adSeg;
|
|
4469
|
+
const payoutSeg = showPayout ? renderPayoutSegment(
|
|
4470
|
+
adParsed.data.payoutPerImpMillicents,
|
|
4471
|
+
adParsed.data.payoutPerClickMillicents,
|
|
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"
|
|
4486
|
+
);
|
|
4374
4487
|
}
|
|
4375
4488
|
main();
|