pqcheck 0.14.1 → 0.15.0

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > **Decryption Blast Radius scanner** — find out how much of your data unlocks when quantum decryption arrives.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/pqcheck.svg?style=flat-square&color=06b6d4)](https://www.npmjs.com/package/pqcheck)
6
+ [![npm downloads](https://img.shields.io/npm/dm/pqcheck.svg?style=flat-square&color=06b6d4)](https://www.npmjs.com/package/pqcheck)
7
+ [![license](https://img.shields.io/npm/l/pqcheck.svg?style=flat-square&color=06b6d4)](./LICENSE)
8
+
5
9
  ```bash
6
10
  npx pqcheck stripe.com
7
11
  ```
@@ -12,6 +16,61 @@ The same scanner that powers [cipherwake.io](https://cipherwake.io), the browser
12
16
 
13
17
  ---
14
18
 
19
+ ## What it does
20
+
21
+ | Command | What it gives you |
22
+ |---|---|
23
+ | `npx pqcheck <domain>` | One-shot DBR scan + grade. The original surface. |
24
+ | `npx pqcheck trust-diff <domain>` | Compare today's public trust posture vs a baseline (last-week, last-month, or a saved CI baseline). For CI gates + release checklists. |
25
+ | `npx pqcheck preview-diff --preview <URL> --production <URL>` | Compare a Vercel/Netlify preview deployment URL to production. Surfaces new third-party scripts, header regressions, and DBR score drops *inside the PR*, before merge. |
26
+ | `npx pqcheck vendors export/check/sync <domain>` | Vendor lockfile (`cipherwake.vendors.json`) + CI gate that exits non-zero when a new third-party origin appears. Like `package-lock.json` for vendor scripts. |
27
+ | `npx pqcheck onboard <domain>` | One command: scan → scaffold the GitHub Action → capture a vendor lockfile → set a baseline → commit + push. Zero copy-paste from docs. |
28
+ | **`npx pqcheck guard --domain <D> -- <cmd>`** 🆕 | **Deploy guard wrapper.** Wraps any deploy command. Runs `deploy-check` first; conditionally runs `<cmd>` based on `ship_decision`. Modes: `--gate-mode balanced` (default) / `advisory` / `strict`. ONE command instead of two — the strongest single artifact for AI-coder workflows because the AI never has to remember to chain check + deploy. |
29
+ | **`npx pqcheck protocol install`** 🆕 | **Opt-in installer** for the AI Coder Protocol — appends the pre-deploy verification rule to your `CLAUDE.md` / `.cursorrules` / `.aider.conf.yml` with explicit consent (Rule 17). One upfront question (auto / manual / no). Never silent writes. |
30
+ | **`npx pqcheck setup --auto --domain <D>`** 🆕 | **One-command full setup for every AI coder.** Installs (idempotently): GitHub Action workflow, AI Coder Protocol across all detected rules files (Claude / Cursor / Copilot / Aider / Windsurf / Continue / Cline / AGENTS.md) using fenced markers (`<!-- CIPHERWAKE_AI_CODER_PROTOCOL_START/END -->`), git pre-push hook, Claude Code statusLine + chat-hook (PostToolUse Bash). Skip flags available. Backups taken before any `~/.claude/settings.json` write. Audit trail at `~/.config/cipherwake/install-prefs.json`; install manifest at `~/.config/cipherwake/install-manifest.json`. |
31
+ | **`npx pqcheck setup --plan --domain <D>`** 🆕 | **Dry-run mode.** Prints every file change `--auto` would make (target paths + operation type: create / append-markered / deep-merge / backup-first) without writing anything. Run this first when you're not sure what `--auto` will touch. |
32
+ | **`npx pqcheck debug-network`** 🆕 | **Connectivity diagnostic.** Probes cipherwake.io API, homepage, crt.sh upstream, and the direct Vercel URL (bypassing Cloudflare). Reports HTTP status + timing per hop. Use when "scan hung" / "command not found" / corporate proxy issues come up — surfaces the actual broken hop with an actionable cause list. |
33
+ | **`--ai` flag** (any of the above) | **AI Coder Mode** (0.15.0). Three-layer output (banner / body / structured `CIPHERWAKE_AI_GUARD_RESULT` block) tuned for Claude Code / Cursor / Aider / Zed. Includes a `ship_decision=pass\|review\|block` field your AI coworker parses to decide whether to announce the deploy, ask you, or revert. See [/methodology/ai-coder-mode](https://cipherwake.io/methodology/ai-coder-mode). |
34
+
35
+ Free tier covers all of the above within 100 Trust Diff calls/month per repo via OIDC. **Founder Pro** ($19.99/mo, locked while subscription active) raises that to 5,000 calls/month + unlocks custom thresholds, vendor lockfile, CI fail rules, and 5 watched domains. Single-domain scans (`npx pqcheck <domain>`) are anonymous + rate-limited per IP — no account or key needed. `npx pqcheck deploy-check <domain> --ai` also works fully anonymously for first-deploy gating.
36
+
37
+ ### AI Coder Mode in 30 seconds
38
+
39
+ ```bash
40
+ npx pqcheck cipherwake.io --ai
41
+ ```
42
+
43
+ Output:
44
+
45
+ ```
46
+ ◆ cipherwake · scan · ⚠ REVIEW · cipherwake.io · DBR 4.1 C · HIGH · ship_decision=review
47
+
48
+ Top finding:
49
+ [HIGH] ECDHE-only — quantum-vulnerable key exchange
50
+
51
+ Why it matters:
52
+ Forward-secret against classical attackers. Shor's algorithm decrypts
53
+ recorded handshakes once a CRQC exists.
54
+
55
+ Recommended next action:
56
+ Review finding above and decide if it was intentional.
57
+ View full report: https://cipherwake.io/r/cipherwake.io
58
+ Re-scan with --fresh after fix: npx pqcheck cipherwake.io --fresh --ai
59
+
60
+ CIPHERWAKE_AI_GUARD_RESULT
61
+ status=review
62
+ domain=cipherwake.io
63
+ ship_decision=review
64
+ max_severity=high
65
+ top_issue=tls.ecdhe_only_quantum_vulnerable
66
+ advisory_only=true
67
+ END_CIPHERWAKE_AI_GUARD_RESULT
68
+ ```
69
+
70
+ The structured block is what your AI coworker (Claude / Cursor / Aider / Zed) parses to decide whether to announce the deploy, ask you, or revert. Exit code in `--ai` mode reflects `ship_decision`: `0` pass · `1` review · `2` block.
71
+
72
+ ---
73
+
15
74
  ## Get started in 60 seconds
16
75
 
17
76
  Wire Cipherwake into your CI so every PR gets a Trust Diff comment when your domain's public trust posture changes.
@@ -38,7 +97,7 @@ git push
38
97
 
39
98
  That's it. The scaffolded workflow includes `permissions: id-token: write`, so the runner mints a signed OIDC token on each run and Cipherwake meters per repo — no secret to manage. Open a PR and Cipherwake comments inline when cert / SPKI / HSTS / CSP / DMARC / vendor scripts drift since your baseline.
40
99
 
41
- **Need higher limits?** Paid tiers (Starter $29/mo · Growth $79/mo · Scale $199/mo) lift the per-repo quota to 1,000 / 10,000 / 50,000 calls/month. Generate an API key at [/account#api-keys](https://cipherwake.io/account#api-keys), then add it as the repo secret `CIPHERWAKE_API_KEY`. The Action uses the secret when present and falls back to OIDC when not — no code change needed to upgrade.
100
+ **Need higher limits?** **Founder Pro ($19.99/mo)** lifts the per-repo quota to 5,000 calls/month and unlocks custom thresholds, the approved-vendor allowlist, vendor lockfile, CI fail rules, and 5 watched domains. Generate an API key at [/account#api-keys](https://cipherwake.io/account#api-keys), then add it as the repo secret `CIPHERWAKE_API_KEY`. The Action uses the secret when present and falls back to OIDC when not — no code change needed to upgrade. *Founder pricing is locked while your subscription remains active.*
42
101
 
43
102
  **Want more?**
44
103
  - Pre-commit hook: `npx pqcheck deploy-check <domain>` before every deploy
@@ -47,9 +106,21 @@ That's it. The scaffolded workflow includes `permissions: id-token: write`, so t
47
106
 
48
107
  ---
49
108
 
50
- ## What's new in 0.14.0
109
+ ## Features
110
+
111
+ For the per-release version history see [CHANGELOG.md](./CHANGELOG.md).
51
112
 
52
- **Preview Deploy Trust Diff (R67-locked 2026-05-19).** Compare a preview deployment URL against production and surface application-surface changes (new third-party scripts, header regressions, DBR score drops) inside the PR review — before merge. The stickiest dev-workflow feature per Cipherwake's design ranking, with a dedicated SSRF-pinned scan path that keeps preview-URL hostnames out of our moat tables (feature-branch names stay private).
113
+ ### Trust Diff CI gate for posture regressions
114
+
115
+ ```bash
116
+ npx pqcheck trust-diff mycompany.com --baseline last-week --fail-on high
117
+ ```
118
+
119
+ Compares today's public trust posture against a configured baseline (`last-week`, `last-month`, or a saved per-branch baseline). Surfaces cert / SPKI / HSTS / CSP / DMARC / vendor-script drift since the baseline and gates the PR by severity. SARIF output uploads to GitHub Code Scanning. Pair with the [GitHub Action](https://github.com/cipherwakelabs/pqcheck/tree/main/action) `mode: trust-diff` for one-line CI integration.
120
+
121
+ Exit codes: `0` pass · `1` warn · `2` fail · `3` error. Free tier (100 calls/repo/mo via GitHub Actions OIDC, no API key required) silently downgrades fail → report; **Founder Pro** honors `--fail-on` for real CI gating.
122
+
123
+ ### Preview Trust Diff — PR-time URL-vs-URL comparison
53
124
 
54
125
  ```bash
55
126
  npx pqcheck preview-diff \
@@ -57,6 +128,8 @@ npx pqcheck preview-diff \
57
128
  --production https://example.com
58
129
  ```
59
130
 
131
+ Compares a preview-deployment URL to a production URL and surfaces application-surface changes (new third-party scripts, header regressions, DBR score drops) *inside the PR review, before merge*. SSRF-pinned scan path keeps preview-URL hostnames out of Cipherwake's moat tables — feature-branch names stay private.
132
+
60
133
  Sample output:
61
134
 
62
135
  ```
@@ -75,42 +148,48 @@ Sample output:
75
148
  Tier: free · policy: report
76
149
  ```
77
150
 
78
- Flags: `--preview <URL>` · `--production <URL>` · `--compare-transport` (opt in TLS/cert/SPKI in verdict) · `--fail-on <severity>` (default `high`; pass `none` for report-only) · `--format pretty|json`.
79
-
80
- Exit codes match `trust-diff`: `0` pass · `1` warn · `2` fail · `3` error. Free tier silently downgrades fail → report and notes the upgrade hook in the response; Starter+ honors `fail-on` for real CI gating.
151
+ Flags: `--preview <URL>` · `--production <URL>` · `--compare-transport` · `--fail-on <severity>` (default `high`; `none` for report-only) · `--format pretty|json`. CSP weakening detection diffs `script-src` / `default-src` / `object-src` / `frame-ancestors` / `base-uri` / `style-src` for newly-permissive tokens (`*`, `'unsafe-inline'`, `'unsafe-eval'`, `data:`, `blob:`).
81
152
 
82
- Auth: `CIPHERWAKE_API_KEY` env (or the GitHub Action which fetches OIDC automatically no key needed for Free).
153
+ **Diffing a local dev build against prod?** Cipherwake runs the comparison server-side, so `--preview http://localhost:3000` is rejected (we'd be reaching for *our* loopback, not yours). Expose your dev build via a public tunnel:
83
154
 
84
- Also: CSP weakening detection now diffs `script-src` / `default-src` / `object-src` / `frame-ancestors` / `base-uri` / `style-src` directives for newly-permissive tokens (`*`, `'unsafe-inline'`, `'unsafe-eval'`, `data:`, `blob:`).
85
-
86
- ## What's new in 0.12.0
155
+ ```bash
156
+ # Vercel/Netlify preview deploys — automatic per PR, free, the design target
157
+ --preview https://feature-x-abc123.vercel.app
87
158
 
88
- **Developer habit-loop bundle (locked 2026-05-16).** Five new subcommands that put Cipherwake where developers already work: PRs, CI, release notes, vendor allowlists. Free tier covers all of them within the 100 Trust Diff calls/month per repo quota.
159
+ # ngrok ad-hoc, one command
160
+ ngrok http 3000
161
+ --preview https://9b1f-203-0-113-7.ngrok-free.app
89
162
 
90
- - `pqcheck init`interactive scaffold for `.github/workflows/cipherwake.yml`. Prompts for domain, fail-on severity, baseline. No copy-paste from docs required.
91
- - `pqcheck deploy-check <domain>` — pre-deploy Trust Diff gate with deploy-friendly framing. Uses last-scan as default baseline. Same exit semantics as `trust-diff`.
92
- - `pqcheck release-checklist [domain]` — markdown checklist for release notes. Offline, no API call.
93
- - `pqcheck vendors export <domain>` — write `cipherwake.vendors.json` from currently observed third-party origins. Like `package-lock.json` for vendor scripts.
94
- - `pqcheck vendors check <domain>` — CI gate; exits **4** when new origins appear that aren't in the lockfile.
95
- - `pqcheck vendors sync <domain>` — Starter+ only; pulls your dashboard-managed approved-vendor allowlist into the lockfile.
163
+ # Cloudflare Tunnelzero-auth quick tunnel
164
+ cloudflared tunnel --url http://localhost:3000
165
+ --preview https://random-words-1234.trycloudflare.com
166
+ ```
96
167
 
97
- Plus: the GitHub Action v3.1 now posts a **sticky PR comment** with Trust Diff results when `comment-on-pr: true` is set, and `/r/<domain>` has a "Copy as GitHub issue" button on every finding.
168
+ ### Vendor lockfile `cipherwake.vendors.json`
98
169
 
99
- ## What's new in 0.11.0
170
+ Like `package-lock.json`, but for the third-party scripts that load on your domain. Capture currently observed vendor origins, commit the lockfile, and CI fails when a PR introduces a new vendor.
100
171
 
101
- **Trust Diff subcommand** — `npx pqcheck trust-diff <domain>` calls `/api/trust-diff` and gates CI on regression severity vs a configured baseline. SARIF output uploads to GitHub's Code Scanning. Pair with `cipherwakelabs/pqcheck@v3` action `mode: trust-diff` for one-line CI integration.
172
+ ```bash
173
+ npx pqcheck vendors export mycompany.com # write cipherwake.vendors.json
174
+ npx pqcheck vendors check mycompany.com # CI gate; exit 4 on new origins
175
+ npx pqcheck vendors sync mycompany.com # Founder Pro — pull dashboard allowlist
176
+ ```
102
177
 
103
- ## What's new in 0.7.9
178
+ `pqcheck deps` also surfaces a one-line site-wide **CSP verdict** above the supply-chain table (`✗ No CSP enforcement` / `⚠ CSP is permissive` / `✓ Strict CSP enforced`) and friendly vendor labels (`New Relic · errors` / `Cloudflare · cdn` / `Adobe Fonts · fonts`) instead of raw hostnames. Same data shape ships on `/r/<domain>` and in the browser extension.
104
179
 
105
- **CSP verdict + vendor labels on `pqcheck deps`.** The supply-chain table now shows a friendly vendor label (`New Relic · errors` / `Cloudflare · cdn` / `Adobe Fonts · fonts`) per host instead of raw `bam.nr-data.net`-style hostnames, plus a one-line site-wide CSP verdict above the table (`✗ No CSP enforcement` / `⚠ CSP is permissive` / `✓ Strict CSP enforced`). Same data shape ships on `/r/<domain>` and in the browser extension — cross-surface parity for the supply-chain story. See [CHANGELOG.md](./CHANGELOG.md).
180
+ ### Developer habit-loop subcommands
106
181
 
107
- ## What's new in 0.7.8
182
+ ```bash
183
+ npx pqcheck init # interactive scaffold for .github/workflows/cipherwake.yml
184
+ npx pqcheck deploy-check # pre-deploy gate (Trust Diff alias, last-scan baseline)
185
+ npx pqcheck release-checklist # markdown trust checklist for release notes (offline)
186
+ ```
108
187
 
109
- **Supply-chain change detection in CI** `pqcheck deps <domain> --baseline file.json` compares the current third-party host list to a stored baseline. New hosts since the last accepted state are flagged `*NEW*` in the pretty table and `"isNew": true` in JSON. Add `--fail-on-new` to exit `4` if anything new appeared — the Polyfill.io-style CI gate that fails PRs introducing third-party scripts until you deliberately accept them with `--write-baseline`. Each row also shows an `SRI` column (on/off/n/a) so you can see which scripts allow silent vendor-side content swaps. See [CHANGELOG.md](./CHANGELOG.md).
188
+ The GitHub Action posts a **sticky PR comment** with results when `comment-on-pr: true` is set on `pull_request` events. Comment auto-edits on subsequent pushes no spam.
110
189
 
111
190
  ---
112
191
 
113
- ## What it does
192
+ ## How DBR scoring works
114
193
 
115
194
  `pqcheck` scans any HTTPS domain and computes its **Decryption Blast Radius score** — the first continuous metric for harvest-now-decrypt-later (HNDL) risk. Every other TLS scanner answers "is post-quantum cryptography enabled?" with yes/no. `pqcheck` answers the question that actually matters: *if an adversary harvests this traffic today and decrypts it in 2035, how much past + future data unlocks?*
116
195
 
@@ -144,8 +223,14 @@ npx pqcheck init Interactive scaffold for .github/w
144
223
  npx pqcheck release-checklist [domain] Pre-release trust checklist (markdown, offline)
145
224
  npx pqcheck vendors export <domain> Write cipherwake.vendors.json from observed third-party scripts
146
225
  npx pqcheck vendors check <domain> CI gate; exit 4 on new origins not in lockfile
147
- npx pqcheck vendors sync <domain> Pull dashboard allowlist into lockfile (Starter+, needs API key)
226
+ npx pqcheck vendors sync <domain> Pull dashboard allowlist into lockfile (Founder Pro, needs API key)
148
227
  npx pqcheck watch <domain> Add domain to your watched list (needs CIPHERWAKE_API_KEY)
228
+ npx pqcheck guard --domain <D> -- <cmd> AI Coder Mode (0.15.0) — wrap any deploy command with a Trust Diff gate
229
+ npx pqcheck deploy-check <D> --ai AI Coder Mode (0.15.0) — frictionless first-deploy, anonymous, emits CIPHERWAKE_AI_GUARD_RESULT block
230
+ npx pqcheck setup --auto --domain <D> AI Coder Mode (0.15.0) — one-command install across CLAUDE.md/.cursorrules/.github + git pre-push hook + statusline
231
+ npx pqcheck setup --plan --domain <D> AI Coder Mode (0.15.0) — dry-run: print every file change before --auto writes anything
232
+ npx pqcheck protocol install --auto AI Coder Mode (0.15.0) — append AI Coder Protocol to detected rules files (idempotent, fenced markers)
233
+ npx pqcheck debug-network Probe upstream connectivity (cipherwake.io / crt.sh / Vercel) — for "scan hung" diagnosis
149
234
  ```
150
235
 
151
236
  ### Multi-domain
@@ -285,9 +370,8 @@ This CLI is one of four ways to consume the [Decryption Blast Radius API](https:
285
370
  | Surface | Where |
286
371
  |---|---|
287
372
  | **CLI** (this package) | `npx pqcheck` |
288
- | **Browser extension** | Chrome Web Store / Firefox AMO / Edge — toolbar badge per tab + dependency analysis |
373
+ | **Browser extension** | [Chrome Web Store](https://chromewebstore.google.com/) — toolbar badge per tab + dependency analysis. Runs on any Chromium-based browser (Edge, Brave, Arc) via sideload. |
289
374
  | **GitHub Action** | [`cipherwakelabs/pqcheck/action@main`](https://github.com/cipherwakelabs/pqcheck/tree/main/action) — PR comments, SARIF upload, lockfile generation |
290
- | **Slack `/pqcheck`** | [Install on workspace](https://cipherwake.io/install-slack) |
291
375
  | **Web** | [cipherwake.io](https://cipherwake.io) — share-friendly URLs at `/r/<domain>` |
292
376
 
293
377
  ## Public API
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // cipherwake-chat-hook — Claude Code PostToolUse hook
4
+ // =============================================================================
5
+ // Reads stdin (tool event JSON from Claude Code), checks if the tool was a
6
+ // pqcheck-related Bash command, reads the latest scan state, emits a
7
+ // systemMessage to Claude Code chat.
8
+ //
9
+ // Wire it up by adding to ~/.claude/settings.json:
10
+ //
11
+ // "hooks": {
12
+ // "PostToolUse": [{
13
+ // "matcher": "Bash",
14
+ // "hooks": [{
15
+ // "type": "command",
16
+ // "command": "npx cipherwake-chat-hook"
17
+ // }]
18
+ // }]
19
+ // }
20
+ //
21
+ // `pqcheck setup --auto` does this for you (idempotently, merging with any
22
+ // existing hook configs per CLAUDE.md Rule 17).
23
+ //
24
+ // Behavior:
25
+ // * Only emits a message if the tool was Bash + the command invoked pqcheck
26
+ // * Only emits if last-scan.json was updated within the last 60s (i.e. this
27
+ // pqcheck invocation actually changed state) — avoids spamming chat for
28
+ // stale state
29
+ // * Single line output for status-bar-style readability
30
+ // =============================================================================
31
+
32
+ import { readFileSync } from "node:fs";
33
+ import { join } from "node:path";
34
+ import { homedir } from "node:os";
35
+
36
+ // Hooks receive event JSON on stdin. If missing / malformed, exit silently.
37
+ let toolEvent;
38
+ try {
39
+ toolEvent = JSON.parse(readFileSync(0, "utf8"));
40
+ } catch {
41
+ process.exit(0);
42
+ }
43
+
44
+ // Only react to Bash tool uses that invoked pqcheck or cipherwake-statusline.
45
+ if (toolEvent?.tool_name !== "Bash") {
46
+ process.exit(0);
47
+ }
48
+ const command = String(toolEvent.tool_input?.command || "");
49
+ const isPqcheck =
50
+ /\bpqcheck\b/.test(command) ||
51
+ /\bcipherwake-statusline\b/.test(command);
52
+ if (!isPqcheck) {
53
+ process.exit(0);
54
+ }
55
+
56
+ // Read the last-scan state. If missing, exit silently.
57
+ let state;
58
+ try {
59
+ state = JSON.parse(
60
+ readFileSync(join(homedir(), ".config", "cipherwake", "last-scan.json"), "utf8"),
61
+ );
62
+ } catch {
63
+ process.exit(0);
64
+ }
65
+
66
+ // Only emit if the state was updated recently (<60s). Otherwise we'd narrate
67
+ // stale state on every unrelated Bash command, which would be obnoxious.
68
+ const writtenAt = new Date(state.written_at).getTime();
69
+ if (!Number.isFinite(writtenAt)) process.exit(0);
70
+ if (Date.now() - writtenAt > 60_000) process.exit(0);
71
+
72
+ const sd = state.ship_decision || "—";
73
+ const emoji = sd === "pass" ? "✓" : sd === "block" ? "✗" : "⚠";
74
+
75
+ const parts = [`◆ Cipherwake: ${emoji} ${state.domain} ship_decision=${sd}`];
76
+ if (typeof state.score === "number") parts.push(`DBR ${state.score.toFixed(1)}${state.grade ? " " + state.grade : ""}`);
77
+ if (state.max_severity && state.max_severity !== "none") parts.push(String(state.max_severity).toUpperCase());
78
+ if (state.top_issue && state.top_issue !== "none") parts.push(`top: ${state.top_issue}`);
79
+
80
+ const message = parts.join(" · ");
81
+
82
+ // Output JSON to stdout — Claude Code reads the `systemMessage` field and
83
+ // displays it to the user in the chat scrollback.
84
+ process.stdout.write(
85
+ JSON.stringify({
86
+ systemMessage: message,
87
+ suppressOutput: true,
88
+ }),
89
+ );
90
+ process.exit(0);
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // cipherwake-statusline — reads ~/.config/cipherwake/last-scan.json and outputs
4
+ // a single-line summary for AI-coder status surfaces.
5
+ // =============================================================================
6
+ // Designed for Claude Code's `statusLine` setting (a config-level hook that
7
+ // runs any shell command and renders its stdout in the persistent status line).
8
+ // One-line install:
9
+ //
10
+ // add to ~/.claude/settings.json:
11
+ // { "statusLine": { "type": "command", "command": "npx cipherwake-statusline" } }
12
+ //
13
+ // Cipherwake never modifies your settings.json — paste the line yourself per
14
+ // CLAUDE.md Rule 17 (consolidated consent for any change outside our own
15
+ // config dir).
16
+ //
17
+ // The script is dependency-free + fast (<50ms on cold start) because Claude
18
+ // Code calls it on every turn. Reads a single file, formats one line, exits.
19
+ // =============================================================================
20
+
21
+ import { readFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { homedir } from "node:os";
24
+
25
+ const STATE_FILE = join(homedir(), ".config", "cipherwake", "last-scan.json");
26
+ const STALE_THRESHOLD_HOURS = 24;
27
+
28
+ const C = {
29
+ reset: "\x1b[0m",
30
+ bold: "\x1b[1m",
31
+ dim: "\x1b[2m",
32
+ green: "\x1b[32m",
33
+ yellow: "\x1b[33m",
34
+ red: "\x1b[31m",
35
+ cyan: "\x1b[36m",
36
+ };
37
+
38
+ function formatAge(iso) {
39
+ if (!iso) return "—";
40
+ const ms = Date.now() - new Date(iso).getTime();
41
+ if (ms < 0) return "just now";
42
+ if (ms < 60_000) return "just now";
43
+ const min = Math.floor(ms / 60_000);
44
+ if (min < 60) return `${min}m ago`;
45
+ const hr = Math.floor(min / 60);
46
+ if (hr < 24) return `${hr}h ago`;
47
+ const d = Math.floor(hr / 24);
48
+ return `${d}d ago`;
49
+ }
50
+
51
+ function ageHours(iso) {
52
+ if (!iso) return Infinity;
53
+ return (Date.now() - new Date(iso).getTime()) / (60 * 60 * 1000);
54
+ }
55
+
56
+ // --no-color / NO_COLOR support per https://no-color.org
57
+ const noColor = process.argv.includes("--no-color") || process.env.NO_COLOR;
58
+ function c(color, str) {
59
+ if (noColor) return str;
60
+ return `${color}${str}${C.reset}`;
61
+ }
62
+
63
+ let state;
64
+ try {
65
+ state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
66
+ } catch {
67
+ // No scan yet — render an onboarding hint so first-time users learn the
68
+ // command. Use the dim color so it doesn't dominate the status line.
69
+ process.stdout.write(
70
+ c(C.dim, "◆ cipherwake · no scan yet · ") + c(C.cyan, "npx pqcheck <domain> --ai")
71
+ );
72
+ process.exit(0);
73
+ }
74
+
75
+ const { domain, score, grade, ship_decision, written_at, max_severity, kind } = state;
76
+ const age = ageHours(written_at);
77
+
78
+ if (age > STALE_THRESHOLD_HOURS) {
79
+ const stalePart = c(C.dim, `◆ ${domain || "cipherwake"} · stale (${formatAge(written_at)})`);
80
+ const hintPart = c(C.cyan, `npx pqcheck ${domain || "<domain>"} --ai`);
81
+ process.stdout.write(`${stalePart} · ${hintPart}`);
82
+ process.exit(0);
83
+ }
84
+
85
+ const symbolByDecision = { pass: "✓", review: "⚠", block: "✗" };
86
+ const colorByDecision = { pass: C.green, review: C.yellow, block: C.red };
87
+ const symbol = symbolByDecision[ship_decision] || "·";
88
+ const cdec = colorByDecision[ship_decision] || C.dim;
89
+
90
+ const dbrStr = typeof score === "number" ? `DBR ${score.toFixed(1)}` : "";
91
+ const gradeStr = grade ? grade : "";
92
+ const sevStr = max_severity && max_severity !== "none"
93
+ ? `· ${String(max_severity).toUpperCase()}`
94
+ : "";
95
+ const kindStr = kind && kind !== "scan" ? `· ${kind}` : "";
96
+
97
+ // Final layout (color-coded; ANSI stripped under --no-color / NO_COLOR=1):
98
+ // ◆ <domain> ✓|⚠|✗ ship_decision · DBR X.X grade · SEVERITY · kind · age
99
+ process.stdout.write(
100
+ c(cdec, "◆") +
101
+ " " +
102
+ c(C.bold, domain || "cipherwake") +
103
+ " " +
104
+ c(cdec, `${symbol} ${(ship_decision || "—").toUpperCase()}`) +
105
+ " " +
106
+ c(C.dim, `· ${dbrStr}${gradeStr ? " " + gradeStr : ""} ${sevStr} ${kindStr} · ${formatAge(written_at)}`)
107
+ );