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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +33 -7
  3. 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.0"} (unofficial results reader; local, no telemetry)`;
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 : String(err.message ?? err).slice(0, 120) };
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.0");
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
- var isEntry = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/elections") || process.argv[1]?.endsWith("cli.js");
46724
- if (isEntry) {
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.0",
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",