election-vibe 0.1.0 → 0.1.1
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/README.md +3 -1
- package/dist/cli.js +33 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Unofficial results for the US general election of November 3, 2026, in your term
|
|
|
9
9
|
You need Node 22 or later.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm i -g election-vibe # installs the `elections` command
|
|
12
|
+
npm i -g election-vibe # installs the `elections` command (also as `election-vibe`)
|
|
13
13
|
elections collect # the collector: leave it running in its own terminal
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -310,6 +310,8 @@ To remove it all, in this order (the plugin lives inside the package's folder, s
|
|
|
310
310
|
|
|
311
311
|
**Which Claude Code version?** The plugin was tested with Claude Code 2.1.316. To update: update the package, restart the collector, then `/reload-plugins`. To remove: the plugin before the package (see above).
|
|
312
312
|
|
|
313
|
+
**Does it work behind a corporate proxy?** It connects to the sources directly and does not use a proxy (`HTTPS_PROXY` is not read). On a network that inspects HTTPS or blocks direct connections, the reads fail, and `elections sources` says why for each source (for example "the connection's certificate is not trusted").
|
|
314
|
+
|
|
313
315
|
**How do I pick the display time zone?** `--tz <IANA zone>` on any command, or `ELECTIONS_TZ`. The default is your computer's zone.
|
|
314
316
|
|
|
315
317
|
**How much disk does it use?** A few megabytes for the snapshots, plus one log line per collector cycle.
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { appendFileSync } from "fs";
|
|
4
|
+
import { appendFileSync, realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
5
6
|
|
|
6
7
|
// src/store.ts
|
|
7
8
|
import { randomBytes } from "crypto";
|
|
@@ -215,7 +216,7 @@ var REHEARSAL_SENTENCE = {
|
|
|
215
216
|
|
|
216
217
|
// src/http.ts
|
|
217
218
|
import { gunzipSync } from "zlib";
|
|
218
|
-
var USER_AGENT = `ElectionsCompanion/${"0.1.
|
|
219
|
+
var USER_AGENT = `ElectionsCompanion/${"0.1.1"} (unofficial results reader; local, no telemetry)`;
|
|
219
220
|
function retryAfterMs(header, nowMs) {
|
|
220
221
|
if (!header) return void 0;
|
|
221
222
|
const t = header.trim();
|
|
@@ -295,6 +296,9 @@ async function fetchDocument(url, opts) {
|
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
if (res.status === 401 || res.status === 403) return { kind: "denied", status: res.status, fetchedAt: stamp() };
|
|
299
|
+
if (res.status === 407) {
|
|
300
|
+
return { kind: "client-error", status: 407, fetchedAt: stamp(), detail: "HTTP 407: a proxy asks for credentials; this tool connects directly and does not use a proxy" };
|
|
301
|
+
}
|
|
298
302
|
if (res.status >= 500) return { kind: "server-error", status: res.status, fetchedAt: stamp() };
|
|
299
303
|
if (res.status !== 200) return { kind: "client-error", status: res.status, fetchedAt: stamp() };
|
|
300
304
|
let body = opts.method === "HEAD" ? Buffer.alloc(0) : await readBounded2(res, maxBytes);
|
|
@@ -319,11 +323,23 @@ async function fetchDocument(url, opts) {
|
|
|
319
323
|
} catch (err) {
|
|
320
324
|
if (err instanceof BodyTooLargeError) return { kind: "too-large", status: err.status, fetchedAt: stamp(), detail: "captured body over bound" };
|
|
321
325
|
const aborted = err.name === "AbortError";
|
|
322
|
-
return { kind: aborted ? "timeout" : "network", fetchedAt: stamp(), detail: aborted ? void 0 :
|
|
326
|
+
return { kind: aborted ? "timeout" : "network", fetchedAt: stamp(), detail: aborted ? void 0 : networkWords(err) };
|
|
323
327
|
} finally {
|
|
324
328
|
clearTimeout(timer);
|
|
325
329
|
}
|
|
326
330
|
}
|
|
331
|
+
function networkWords(err) {
|
|
332
|
+
const raw = err?.cause?.code;
|
|
333
|
+
const code = typeof raw === "string" && /^[A-Z][A-Z0-9_]{1,39}$/.test(raw) ? raw : "";
|
|
334
|
+
if (/^(SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_(GET_ISSUER_CERT(_LOCALLY)?|VERIFY_LEAF_SIGNATURE)|CERT_[A-Z_]+|ERR_TLS_[A-Z_]+)$/.test(code)) {
|
|
335
|
+
return `the connection's certificate is not trusted (${code}): a network that inspects HTTPS, such as a corporate proxy, causes this; this tool connects directly`;
|
|
336
|
+
}
|
|
337
|
+
if (/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET)$/.test(code)) {
|
|
338
|
+
return `the connection was refused or cut off (${code}): no network, or a firewall or proxy that blocks direct connections`;
|
|
339
|
+
}
|
|
340
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `the source's name could not be resolved (${code}): no network, or a network that blocks the name`;
|
|
341
|
+
return code ? `the connection failed (${code})` : "the connection failed";
|
|
342
|
+
}
|
|
327
343
|
function jsonOf(body) {
|
|
328
344
|
try {
|
|
329
345
|
return JSON.parse(body.toString("utf8"));
|
|
@@ -46269,7 +46285,7 @@ function storeOf(p) {
|
|
|
46269
46285
|
}
|
|
46270
46286
|
async function main(argv, io) {
|
|
46271
46287
|
if (argv[0] === "--version" || argv[0] === "version") {
|
|
46272
|
-
io.out("0.1.
|
|
46288
|
+
io.out("0.1.1");
|
|
46273
46289
|
return 0;
|
|
46274
46290
|
}
|
|
46275
46291
|
const p = parseArgs(argv);
|
|
@@ -46476,6 +46492,8 @@ ${renderRace(v, { nowMs, zone, width, color, partisan, pollsClose })}`);
|
|
|
46476
46492
|
disabled: cfg.disabledSources.includes(s.id),
|
|
46477
46493
|
approved: approvals[s.id] ?? null,
|
|
46478
46494
|
lastOutcome: st?.lastOutcome ?? null,
|
|
46495
|
+
// Why the last read failed, in words (a blocked network names what blocks it); null after a success.
|
|
46496
|
+
lastOutcomeDetail: st?.lastOutcome && st.lastOutcome !== "ok" && st.lastOutcome !== "not-modified" && st.lastOutcomeDetail ? humanLabel(st.lastOutcomeDetail, 300) : null,
|
|
46479
46497
|
lastSuccessAt: st?.lastSuccessAt ?? null,
|
|
46480
46498
|
cooldownUntil: st?.cooldownUntil ?? null,
|
|
46481
46499
|
deniedAt: st?.deniedAt ?? null,
|
|
@@ -46492,7 +46510,7 @@ ${renderRace(v, { nowMs, zone, width, color, partisan, pollsClose })}`);
|
|
|
46492
46510
|
const lines = rows.map(
|
|
46493
46511
|
(r) => [
|
|
46494
46512
|
`${r.name} (${r.id}) \xB7 ledger ${r.ledger} \xB7 admission ${r.admission}${r.disabled ? " \xB7 DISABLED locally" : ""}`,
|
|
46495
|
-
` last: ${r.lastOutcome ?? "never read"}${r.lastSuccessAt ? ` \xB7 last success ${formatAge(r.lastSuccessAt, nowMs)} ago` : ""}${r.cooldownUntil && Date.parse(r.cooldownUntil) > nowMs ? ` \xB7 cooling down until ${formatInstant(r.cooldownUntil, zone)}` : ""}${r.deniedAt ? " \xB7 ACCESS DENIED: stopped pending diagnosis" : ""}`,
|
|
46513
|
+
` last: ${r.lastOutcome ?? "never read"}${r.lastOutcomeDetail ? `: ${r.lastOutcomeDetail}` : ""}${r.lastSuccessAt ? ` \xB7 last success ${formatAge(r.lastSuccessAt, nowMs)} ago` : ""}${r.cooldownUntil && Date.parse(r.cooldownUntil) > nowMs ? ` \xB7 cooling down until ${formatInstant(r.cooldownUntil, zone)}` : ""}${r.deniedAt ? " \xB7 ACCESS DENIED: stopped pending diagnosis" : ""}`,
|
|
46496
46514
|
` revision: ${r.lastRevision ? `${localizeTimes(r.lastRevision.label, zone)} (hash ${r.lastRevision.hash})` : "none"} \xB7 production: ${r.approved ? `approved from ${approvalPoint(r.id, r.approved.revisionOrder, zone)}, at ${formatInstant(r.approved.approvedAt, zone)}` : r.admission === "operator" ? "awaiting production approval" : "admitted by source tag after poll close"}`,
|
|
46497
46515
|
` requests this hour: ${r.requestsThisHour} \xB7 ${r.officialLink ? `official results: ${r.officialLink}` : `not an official source (aggregator): ${r.site}`}`
|
|
46498
46516
|
].join("\n")
|
|
@@ -46720,8 +46738,15 @@ function approvalPoint(sourceId, order, zone) {
|
|
|
46720
46738
|
const src = sourceById(sourceId);
|
|
46721
46739
|
return src && orderIsTime(src) ? `the report of ${formatInstant(new Date(order).toISOString(), zone)}` : `revision ${order}`;
|
|
46722
46740
|
}
|
|
46723
|
-
|
|
46724
|
-
if (
|
|
46741
|
+
function isEntryPath(launched, moduleUrl) {
|
|
46742
|
+
if (!launched) return false;
|
|
46743
|
+
try {
|
|
46744
|
+
return realpathSync(launched) === realpathSync(fileURLToPath(moduleUrl));
|
|
46745
|
+
} catch {
|
|
46746
|
+
return false;
|
|
46747
|
+
}
|
|
46748
|
+
}
|
|
46749
|
+
if (isEntryPath(process.argv[1], import.meta.url)) {
|
|
46725
46750
|
main(process.argv.slice(2), {
|
|
46726
46751
|
out: (s) => process.stdout.write(`${s}
|
|
46727
46752
|
`),
|
|
@@ -46751,6 +46776,7 @@ function exitWhenFlushed(code) {
|
|
|
46751
46776
|
process.stderr.write("", done);
|
|
46752
46777
|
}
|
|
46753
46778
|
export {
|
|
46779
|
+
isEntryPath,
|
|
46754
46780
|
main,
|
|
46755
46781
|
parseArgs
|
|
46756
46782
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "election-vibe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "US election night, Nov 3, 2026, in your terminal and in Claude Code: unofficial results from official free sources and civicAPI, kept on your machine. Never calls a race. Unofficial results. Not an election authority. Not affiliated with any government, campaign, or party.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"election",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"node": ">=22"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
-
"elections": "./dist/cli.js"
|
|
22
|
+
"elections": "./dist/cli.js",
|
|
23
|
+
"election-vibe": "./dist/cli.js"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"dist",
|