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 +112 -28
- package/bin/cipherwake-chat-hook.js +90 -0
- package/bin/cipherwake-statusline.js +107 -0
- package/bin/pqcheck.js +1613 -38
- package/package.json +4 -2
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
|
+
[](https://www.npmjs.com/package/pqcheck)
|
|
6
|
+
[](https://www.npmjs.com/package/pqcheck)
|
|
7
|
+
[](./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?**
|
|
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
|
-
##
|
|
109
|
+
## Features
|
|
110
|
+
|
|
111
|
+
For the per-release version history see [CHANGELOG.md](./CHANGELOG.md).
|
|
51
112
|
|
|
52
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
159
|
+
# ngrok — ad-hoc, one command
|
|
160
|
+
ngrok http 3000
|
|
161
|
+
--preview https://9b1f-203-0-113-7.ngrok-free.app
|
|
89
162
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 Tunnel — zero-auth quick tunnel
|
|
164
|
+
cloudflared tunnel --url http://localhost:3000
|
|
165
|
+
--preview https://random-words-1234.trycloudflare.com
|
|
166
|
+
```
|
|
96
167
|
|
|
97
|
-
|
|
168
|
+
### Vendor lockfile — `cipherwake.vendors.json`
|
|
98
169
|
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
### Developer habit-loop subcommands
|
|
106
181
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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 (
|
|
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
|
|
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
|
+
);
|