@brozarti/spincome 0.1.2 → 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/dist/ad.js +7 -2
- package/dist/animate.js +44 -0
- package/dist/display.js +46 -16
- package/dist/hook.js +10 -7
- package/dist/session.js +41 -0
- package/package.json +1 -1
- package/src/ad.ts +8 -3
- package/src/animate.ts +49 -0
- package/src/display.ts +53 -17
- package/src/hook.ts +14 -10
- package/src/session.ts +42 -0
package/dist/ad.js
CHANGED
|
@@ -27,7 +27,7 @@ async function fetchAd(context) {
|
|
|
27
27
|
}
|
|
28
28
|
async function recordImpression(adId, developerKey, actualCpmCents, context) {
|
|
29
29
|
try {
|
|
30
|
-
await fetch(`${config_js_1.API_BASE}/ads/impression`, {
|
|
30
|
+
const res = await fetch(`${config_js_1.API_BASE}/ads/impression`, {
|
|
31
31
|
method: "POST",
|
|
32
32
|
headers: {
|
|
33
33
|
"Content-Type": "application/json",
|
|
@@ -36,8 +36,13 @@ async function recordImpression(adId, developerKey, actualCpmCents, context) {
|
|
|
36
36
|
body: JSON.stringify({ adId, actualCpmCents, ...context }),
|
|
37
37
|
signal: AbortSignal.timeout(3000),
|
|
38
38
|
});
|
|
39
|
+
if (res.ok) {
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
return data.earnedCents ?? 0;
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
catch {
|
|
41
|
-
//
|
|
45
|
+
// ignore
|
|
42
46
|
}
|
|
47
|
+
return 0;
|
|
43
48
|
}
|
package/dist/animate.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.animateWhileLoading = animateWhileLoading;
|
|
4
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
5
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
6
|
+
const CLEAR_LINE = "\x1b[2K\r";
|
|
7
|
+
const UP_ONE = "\x1b[1A";
|
|
8
|
+
const GREEN = "\x1b[32m";
|
|
9
|
+
const BRIGHT_GREEN = "\x1b[92m";
|
|
10
|
+
const DIM = "\x1b[2m";
|
|
11
|
+
const BOLD = "\x1b[1m";
|
|
12
|
+
const R = "\x1b[0m";
|
|
13
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
14
|
+
const COIN = ["◐", "◓", "◑", "◒"];
|
|
15
|
+
const LOGO = `${BOLD}${GREEN}$${R}${BOLD} spincome${R}`;
|
|
16
|
+
async function animateWhileLoading(work) {
|
|
17
|
+
process.stdout.write(HIDE_CURSOR);
|
|
18
|
+
let frame = 0;
|
|
19
|
+
let dots = 0;
|
|
20
|
+
let done = false;
|
|
21
|
+
let result;
|
|
22
|
+
const tick = setInterval(() => {
|
|
23
|
+
if (done)
|
|
24
|
+
return;
|
|
25
|
+
const spinner = BRIGHT_GREEN + SPINNER[frame % SPINNER.length] + R;
|
|
26
|
+
const coin = GREEN + COIN[frame % COIN.length] + R;
|
|
27
|
+
dots = (dots + 1) % 4;
|
|
28
|
+
const dotStr = DIM + ".".repeat(dots).padEnd(3) + R;
|
|
29
|
+
process.stdout.write(CLEAR_LINE +
|
|
30
|
+
` ${spinner} ${LOGO} ${coin} ${DIM}earning${R}${dotStr}`);
|
|
31
|
+
frame++;
|
|
32
|
+
}, 80);
|
|
33
|
+
try {
|
|
34
|
+
result = await work;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
done = true;
|
|
38
|
+
clearInterval(tick);
|
|
39
|
+
process.stdout.write(CLEAR_LINE);
|
|
40
|
+
process.stdout.write(UP_ONE + CLEAR_LINE);
|
|
41
|
+
process.stdout.write(SHOW_CURSOR);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
package/dist/display.js
CHANGED
|
@@ -1,28 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderAd = renderAd;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const R = "\x1b[0m";
|
|
5
|
+
const B = "\x1b[1m";
|
|
6
6
|
const DIM = "\x1b[2m";
|
|
7
|
+
const GREEN = "\x1b[32m";
|
|
8
|
+
const BRIGHT_GREEN = "\x1b[92m";
|
|
7
9
|
const CYAN = "\x1b[36m";
|
|
8
10
|
const YELLOW = "\x1b[33m";
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const WHITE = "\x1b[97m";
|
|
12
|
+
const BG = "\x1b[48;5;234m";
|
|
13
|
+
const W = 62;
|
|
14
|
+
function pad(text, visibleLen) {
|
|
15
|
+
const spaces = Math.max(0, W - 2 - visibleLen);
|
|
16
|
+
return ` ${text}${" ".repeat(spaces)} `;
|
|
13
17
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
function box(content, visibleLen) {
|
|
19
|
+
return `${BG}${pad(content, visibleLen)}${R}`;
|
|
20
|
+
}
|
|
21
|
+
function ruler() {
|
|
22
|
+
return `${DIM}${"─".repeat(W)}${R}`;
|
|
23
|
+
}
|
|
24
|
+
function renderAd(ad, earnedCents, sessionCents, context) {
|
|
25
|
+
const earnedDollars = (earnedCents / 100).toFixed(4);
|
|
26
|
+
const sessionDollars = (sessionCents / 100).toFixed(4);
|
|
27
|
+
const contextTag = context?.fileExt
|
|
28
|
+
? ` · ${context.fileExt.toUpperCase()} dev`
|
|
29
|
+
: context?.toolName
|
|
30
|
+
? ` · ${context.toolName}`
|
|
31
|
+
: "";
|
|
32
|
+
const sponsorLine = `${DIM}Sponsored · ${ad.advertiser}${contextTag}${R}`;
|
|
33
|
+
const headlineLine = `${B}${WHITE}${ad.headline}${R}`;
|
|
34
|
+
const bodyLine = `${DIM}${ad.body}${R}`;
|
|
35
|
+
const ctaLine = `${CYAN}${ad.cta}${R} ${DIM}${ad.clickUrl}${R}`;
|
|
36
|
+
const earnLine = `${BRIGHT_GREEN}+$${earnedDollars} earned${R} ${DIM}session total: ${GREEN}$${sessionDollars}${R}`;
|
|
37
|
+
const footerLine = `${DIM}spincome · /disable to opt out${R}`;
|
|
38
|
+
const sponsorLen = `Sponsored · ${ad.advertiser}${contextTag}`.length;
|
|
39
|
+
const headlineLen = ad.headline.length;
|
|
40
|
+
const bodyLen = ad.body.length;
|
|
41
|
+
const ctaLen = `${ad.cta} ${ad.clickUrl}`.length;
|
|
42
|
+
const earnLen = `+$${earnedDollars} earned session total: $${sessionDollars}`.length;
|
|
43
|
+
const footerLen = `spincome · /disable to opt out`.length;
|
|
17
44
|
return [
|
|
18
45
|
"",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
ruler(),
|
|
47
|
+
box(sponsorLine, sponsorLen),
|
|
48
|
+
box(headlineLine, headlineLen),
|
|
49
|
+
box(bodyLine, bodyLen),
|
|
50
|
+
box("", 0),
|
|
51
|
+
box(ctaLine, ctaLen),
|
|
52
|
+
box("", 0),
|
|
53
|
+
box(earnLine, earnLen),
|
|
54
|
+
box(footerLine, footerLen),
|
|
55
|
+
ruler(),
|
|
26
56
|
"",
|
|
27
57
|
].join("\n");
|
|
28
58
|
}
|
package/dist/hook.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
// Entry point for the Claude Code PostToolUse hook.
|
|
4
|
-
// Claude Code pipes the hook payload as JSON on stdin.
|
|
5
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
5
|
};
|
|
@@ -9,14 +7,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
7
|
const ad_js_1 = require("./ad.js");
|
|
10
8
|
const display_js_1 = require("./display.js");
|
|
11
9
|
const config_js_1 = require("./config.js");
|
|
10
|
+
const session_js_1 = require("./session.js");
|
|
11
|
+
const animate_js_1 = require("./animate.js");
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
function extractContext(payload) {
|
|
14
14
|
const toolName = payload.tool_name ?? undefined;
|
|
15
|
-
// Extract file extension from any path-like field in the tool input
|
|
16
15
|
const filePath = payload.tool_input?.file_path ??
|
|
17
16
|
payload.tool_input?.path ??
|
|
18
17
|
undefined;
|
|
19
|
-
const fileExt = filePath
|
|
18
|
+
const fileExt = filePath
|
|
19
|
+
? path_1.default.extname(filePath).replace(".", "").toLowerCase() || undefined
|
|
20
|
+
: undefined;
|
|
20
21
|
return { toolName, fileExt };
|
|
21
22
|
}
|
|
22
23
|
async function main() {
|
|
@@ -31,13 +32,15 @@ async function main() {
|
|
|
31
32
|
payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
|
-
//
|
|
35
|
+
// empty or malformed stdin
|
|
35
36
|
}
|
|
36
37
|
const context = extractContext(payload);
|
|
38
|
+
// Fetch ad and record impression concurrently, animate while waiting
|
|
37
39
|
const ad = await (0, ad_js_1.fetchAd)(context);
|
|
38
40
|
if (!ad)
|
|
39
41
|
process.exit(0);
|
|
40
|
-
|
|
41
|
-
(0,
|
|
42
|
+
const earnedCents = await (0, animate_js_1.animateWhileLoading)((0, ad_js_1.recordImpression)(ad.id, config.developerKey, ad.actualCpmCents, context));
|
|
43
|
+
const session = (0, session_js_1.addToSession)(earnedCents);
|
|
44
|
+
process.stdout.write((0, display_js_1.renderAd)(ad, earnedCents, session.totalCents, context));
|
|
42
45
|
}
|
|
43
46
|
main().catch(() => process.exit(0));
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addToSession = addToSession;
|
|
7
|
+
exports.getSessionTotal = getSessionTotal;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const SESSION_PATH = path_1.default.join(os_1.default.homedir(), ".spincome", "session.json");
|
|
12
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours = one coding session
|
|
13
|
+
function readSession() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs_1.default.readFileSync(SESSION_PATH, "utf8");
|
|
16
|
+
const s = JSON.parse(raw);
|
|
17
|
+
if (Date.now() - s.startedAt > SESSION_TTL_MS)
|
|
18
|
+
return null;
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeSession(s) {
|
|
26
|
+
const dir = path_1.default.dirname(SESSION_PATH);
|
|
27
|
+
if (!fs_1.default.existsSync(dir))
|
|
28
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
29
|
+
fs_1.default.writeFileSync(SESSION_PATH, JSON.stringify(s));
|
|
30
|
+
}
|
|
31
|
+
function addToSession(earnedCents) {
|
|
32
|
+
const existing = readSession();
|
|
33
|
+
const session = existing
|
|
34
|
+
? { ...existing, totalCents: existing.totalCents + earnedCents, impressions: existing.impressions + 1 }
|
|
35
|
+
: { startedAt: Date.now(), totalCents: earnedCents, impressions: 1 };
|
|
36
|
+
writeSession(session);
|
|
37
|
+
return session;
|
|
38
|
+
}
|
|
39
|
+
function getSessionTotal() {
|
|
40
|
+
return readSession()?.totalCents ?? 0;
|
|
41
|
+
}
|
package/package.json
CHANGED
package/src/ad.ts
CHANGED
|
@@ -41,9 +41,9 @@ export async function recordImpression(
|
|
|
41
41
|
developerKey: string,
|
|
42
42
|
actualCpmCents: number,
|
|
43
43
|
context: AdContext
|
|
44
|
-
): Promise<
|
|
44
|
+
): Promise<number> {
|
|
45
45
|
try {
|
|
46
|
-
await fetch(`${API_BASE}/ads/impression`, {
|
|
46
|
+
const res = await fetch(`${API_BASE}/ads/impression`, {
|
|
47
47
|
method: "POST",
|
|
48
48
|
headers: {
|
|
49
49
|
"Content-Type": "application/json",
|
|
@@ -52,7 +52,12 @@ export async function recordImpression(
|
|
|
52
52
|
body: JSON.stringify({ adId, actualCpmCents, ...context }),
|
|
53
53
|
signal: AbortSignal.timeout(3000),
|
|
54
54
|
});
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
const data = await res.json() as { earnedCents?: number };
|
|
57
|
+
return data.earnedCents ?? 0;
|
|
58
|
+
}
|
|
55
59
|
} catch {
|
|
56
|
-
//
|
|
60
|
+
// ignore
|
|
57
61
|
}
|
|
62
|
+
return 0;
|
|
58
63
|
}
|
package/src/animate.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
2
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
3
|
+
const CLEAR_LINE = "\x1b[2K\r";
|
|
4
|
+
const UP_ONE = "\x1b[1A";
|
|
5
|
+
|
|
6
|
+
const GREEN = "\x1b[32m";
|
|
7
|
+
const BRIGHT_GREEN = "\x1b[92m";
|
|
8
|
+
const DIM = "\x1b[2m";
|
|
9
|
+
const BOLD = "\x1b[1m";
|
|
10
|
+
const R = "\x1b[0m";
|
|
11
|
+
|
|
12
|
+
const SPINNER = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
|
|
13
|
+
const COIN = ["◐","◓","◑","◒"];
|
|
14
|
+
|
|
15
|
+
const LOGO = `${BOLD}${GREEN}$${R}${BOLD} spincome${R}`;
|
|
16
|
+
|
|
17
|
+
export async function animateWhileLoading<T>(work: Promise<T>): Promise<T> {
|
|
18
|
+
process.stdout.write(HIDE_CURSOR);
|
|
19
|
+
|
|
20
|
+
let frame = 0;
|
|
21
|
+
let dots = 0;
|
|
22
|
+
let done = false;
|
|
23
|
+
let result: T;
|
|
24
|
+
|
|
25
|
+
const tick = setInterval(() => {
|
|
26
|
+
if (done) return;
|
|
27
|
+
const spinner = BRIGHT_GREEN + SPINNER[frame % SPINNER.length] + R;
|
|
28
|
+
const coin = GREEN + COIN[frame % COIN.length] + R;
|
|
29
|
+
dots = (dots + 1) % 4;
|
|
30
|
+
const dotStr = DIM + ".".repeat(dots).padEnd(3) + R;
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
CLEAR_LINE +
|
|
33
|
+
` ${spinner} ${LOGO} ${coin} ${DIM}earning${R}${dotStr}`
|
|
34
|
+
);
|
|
35
|
+
frame++;
|
|
36
|
+
}, 80);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
result = await work;
|
|
40
|
+
} finally {
|
|
41
|
+
done = true;
|
|
42
|
+
clearInterval(tick);
|
|
43
|
+
process.stdout.write(CLEAR_LINE);
|
|
44
|
+
process.stdout.write(UP_ONE + CLEAR_LINE);
|
|
45
|
+
process.stdout.write(SHOW_CURSOR);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result!;
|
|
49
|
+
}
|
package/src/display.ts
CHANGED
|
@@ -1,30 +1,66 @@
|
|
|
1
1
|
import type { Ad } from "./ad.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const R = "\x1b[0m";
|
|
4
|
+
const B = "\x1b[1m";
|
|
5
5
|
const DIM = "\x1b[2m";
|
|
6
|
-
const
|
|
6
|
+
const GREEN = "\x1b[32m";
|
|
7
|
+
const BRIGHT_GREEN = "\x1b[92m";
|
|
8
|
+
const CYAN = "\x1b[36m";
|
|
7
9
|
const YELLOW = "\x1b[33m";
|
|
8
|
-
const
|
|
10
|
+
const WHITE = "\x1b[97m";
|
|
11
|
+
const BG = "\x1b[48;5;234m";
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const W = 62;
|
|
14
|
+
|
|
15
|
+
function pad(text: string, visibleLen: number): string {
|
|
16
|
+
const spaces = Math.max(0, W - 2 - visibleLen);
|
|
17
|
+
return ` ${text}${" ".repeat(spaces)} `;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function box(content: string, visibleLen: number): string {
|
|
21
|
+
return `${BG}${pad(content, visibleLen)}${R}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ruler(): string {
|
|
25
|
+
return `${DIM}${"─".repeat(W)}${R}`;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
export function renderAd(ad: Ad): string {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
28
|
+
export function renderAd(ad: Ad, earnedCents: number, sessionCents: number, context?: { toolName?: string; fileExt?: string }): string {
|
|
29
|
+
const earnedDollars = (earnedCents / 100).toFixed(4);
|
|
30
|
+
const sessionDollars = (sessionCents / 100).toFixed(4);
|
|
31
|
+
|
|
32
|
+
const contextTag = context?.fileExt
|
|
33
|
+
? ` · ${context.fileExt.toUpperCase()} dev`
|
|
34
|
+
: context?.toolName
|
|
35
|
+
? ` · ${context.toolName}`
|
|
36
|
+
: "";
|
|
37
|
+
|
|
38
|
+
const sponsorLine = `${DIM}Sponsored · ${ad.advertiser}${contextTag}${R}`;
|
|
39
|
+
const headlineLine = `${B}${WHITE}${ad.headline}${R}`;
|
|
40
|
+
const bodyLine = `${DIM}${ad.body}${R}`;
|
|
41
|
+
const ctaLine = `${CYAN}${ad.cta}${R} ${DIM}${ad.clickUrl}${R}`;
|
|
42
|
+
const earnLine = `${BRIGHT_GREEN}+$${earnedDollars} earned${R} ${DIM}session total: ${GREEN}$${sessionDollars}${R}`;
|
|
43
|
+
const footerLine = `${DIM}spincome · /disable to opt out${R}`;
|
|
44
|
+
|
|
45
|
+
const sponsorLen = `Sponsored · ${ad.advertiser}${contextTag}`.length;
|
|
46
|
+
const headlineLen = ad.headline.length;
|
|
47
|
+
const bodyLen = ad.body.length;
|
|
48
|
+
const ctaLen = `${ad.cta} ${ad.clickUrl}`.length;
|
|
49
|
+
const earnLen = `+$${earnedDollars} earned session total: $${sessionDollars}`.length;
|
|
50
|
+
const footerLen = `spincome · /disable to opt out`.length;
|
|
18
51
|
|
|
19
52
|
return [
|
|
20
53
|
"",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
ruler(),
|
|
55
|
+
box(sponsorLine, sponsorLen),
|
|
56
|
+
box(headlineLine, headlineLen),
|
|
57
|
+
box(bodyLine, bodyLen),
|
|
58
|
+
box("", 0),
|
|
59
|
+
box(ctaLine, ctaLen),
|
|
60
|
+
box("", 0),
|
|
61
|
+
box(earnLine, earnLen),
|
|
62
|
+
box(footerLine, footerLen),
|
|
63
|
+
ruler(),
|
|
28
64
|
"",
|
|
29
65
|
].join("\n");
|
|
30
66
|
}
|
package/src/hook.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Entry point for the Claude Code PostToolUse hook.
|
|
3
|
-
// Claude Code pipes the hook payload as JSON on stdin.
|
|
4
2
|
|
|
5
3
|
import { fetchAd, recordImpression, type AdContext } from "./ad.js";
|
|
6
4
|
import { renderAd } from "./display.js";
|
|
7
5
|
import { readConfig } from "./config.js";
|
|
6
|
+
import { addToSession } from "./session.js";
|
|
7
|
+
import { animateWhileLoading } from "./animate.js";
|
|
8
8
|
import path from "path";
|
|
9
9
|
|
|
10
10
|
interface HookPayload {
|
|
@@ -18,15 +18,13 @@ interface HookPayload {
|
|
|
18
18
|
|
|
19
19
|
function extractContext(payload: HookPayload): AdContext {
|
|
20
20
|
const toolName = payload.tool_name ?? undefined;
|
|
21
|
-
|
|
22
|
-
// Extract file extension from any path-like field in the tool input
|
|
23
21
|
const filePath =
|
|
24
22
|
payload.tool_input?.file_path ??
|
|
25
23
|
payload.tool_input?.path ??
|
|
26
24
|
undefined;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const fileExt = filePath
|
|
26
|
+
? path.extname(filePath).replace(".", "").toLowerCase() || undefined
|
|
27
|
+
: undefined;
|
|
30
28
|
return { toolName, fileExt };
|
|
31
29
|
}
|
|
32
30
|
|
|
@@ -41,15 +39,21 @@ async function main() {
|
|
|
41
39
|
try {
|
|
42
40
|
payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
43
41
|
} catch {
|
|
44
|
-
//
|
|
42
|
+
// empty or malformed stdin
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
const context = extractContext(payload);
|
|
46
|
+
|
|
47
|
+
// Fetch ad and record impression concurrently, animate while waiting
|
|
48
48
|
const ad = await fetchAd(context);
|
|
49
49
|
if (!ad) process.exit(0);
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const earnedCents = await animateWhileLoading(
|
|
52
|
+
recordImpression(ad.id, config.developerKey, ad.actualCpmCents, context)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const session = addToSession(earnedCents);
|
|
56
|
+
process.stdout.write(renderAd(ad, earnedCents, session.totalCents, context));
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
main().catch(() => process.exit(0));
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
const SESSION_PATH = path.join(os.homedir(), ".spincome", "session.json");
|
|
6
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours = one coding session
|
|
7
|
+
|
|
8
|
+
interface Session {
|
|
9
|
+
startedAt: number;
|
|
10
|
+
totalCents: number;
|
|
11
|
+
impressions: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readSession(): Session | null {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(SESSION_PATH, "utf8");
|
|
17
|
+
const s = JSON.parse(raw) as Session;
|
|
18
|
+
if (Date.now() - s.startedAt > SESSION_TTL_MS) return null;
|
|
19
|
+
return s;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeSession(s: Session): void {
|
|
26
|
+
const dir = path.dirname(SESSION_PATH);
|
|
27
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(s));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function addToSession(earnedCents: number): Session {
|
|
32
|
+
const existing = readSession();
|
|
33
|
+
const session: Session = existing
|
|
34
|
+
? { ...existing, totalCents: existing.totalCents + earnedCents, impressions: existing.impressions + 1 }
|
|
35
|
+
: { startedAt: Date.now(), totalCents: earnedCents, impressions: 1 };
|
|
36
|
+
writeSession(session);
|
|
37
|
+
return session;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getSessionTotal(): number {
|
|
41
|
+
return readSession()?.totalCents ?? 0;
|
|
42
|
+
}
|