pqcheck 0.15.3 → 0.16.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/bin/pqcheck.js +293 -18
- package/package.json +1 -1
package/bin/pqcheck.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
})();
|
|
25
25
|
|
|
26
26
|
const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
|
|
27
|
-
const VERSION = "0.
|
|
27
|
+
const VERSION = "0.16.0";
|
|
28
28
|
|
|
29
29
|
// API-key support — paid tiers (Starter $29 / Growth $79 / Scale $199) get
|
|
30
30
|
// per-account monthly quotas instead of the per-IP rate limit. Set via:
|
|
@@ -236,17 +236,18 @@ async function main() {
|
|
|
236
236
|
// the server side.
|
|
237
237
|
const fresh = args.includes("--fresh") || args.includes("--force");
|
|
238
238
|
const aiMode = parseAiMode(args);
|
|
239
|
+
const verbose = isVerboseMode(args); // v0.16.0 — opt-in detailed panel
|
|
239
240
|
|
|
240
241
|
// One-shot scan(s)
|
|
241
242
|
let worstExit = 0;
|
|
242
243
|
for (const domain of domains) {
|
|
243
|
-
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode });
|
|
244
|
+
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode, verbose });
|
|
244
245
|
if (exit > worstExit) worstExit = exit;
|
|
245
246
|
}
|
|
246
247
|
process.exit(worstExit);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode }) {
|
|
250
|
+
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode, verbose }) {
|
|
250
251
|
if (!quiet && format === "text") process.stderr.write(color("dim", `Scanning ${domain}${fresh ? " (forcing fresh)" : ""} ...`));
|
|
251
252
|
let report;
|
|
252
253
|
try {
|
|
@@ -405,17 +406,42 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
405
406
|
];
|
|
406
407
|
}
|
|
407
408
|
|
|
409
|
+
// v0.16.0 high-yield rendering. Old layout was: banner + body + footer.
|
|
410
|
+
// New layout: brand header → decision pulse → alerts (if any) → trust
|
|
411
|
+
// posture → verified-signals summary → (verbose body if --verbose) →
|
|
412
|
+
// AI footer block. The structured footer is always last so downstream
|
|
413
|
+
// agents can parse ship_decision regardless of the verbose flag.
|
|
414
|
+
const highSevFindings = findings.filter((f) => severityRank(f.severity) >= 3);
|
|
415
|
+
const alertCount = highSevFindings.length;
|
|
416
|
+
|
|
408
417
|
console.log("");
|
|
409
|
-
console.log(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
418
|
+
console.log(formatBrandHeader(domain));
|
|
419
|
+
console.log("");
|
|
420
|
+
console.log(formatDecisionPulse({ shipDecision, alertCount, unreachable, diffContext: false }));
|
|
421
|
+
|
|
422
|
+
if (unreachable) {
|
|
423
|
+
// Unreachable path: surface the friendly explanation + next actions
|
|
424
|
+
// immediately (no "verified signals" block — there's nothing to
|
|
425
|
+
// verify if we couldn't reach the site).
|
|
426
|
+
console.log(formatAiBody({ topIssue, whyMatters, nextActions }));
|
|
427
|
+
} else {
|
|
428
|
+
// Alerts above trust posture so the actionable change is first.
|
|
429
|
+
if (alertCount > 0) {
|
|
430
|
+
const alerts = formatAlertsLine(highSevFindings);
|
|
431
|
+
if (alerts) console.log(alerts);
|
|
432
|
+
}
|
|
433
|
+
const tpl = formatTrustPostureLine(report);
|
|
434
|
+
if (tpl) console.log(tpl);
|
|
435
|
+
const vsl = formatVerifiedSignalsLine(report);
|
|
436
|
+
if (vsl) console.log(vsl);
|
|
437
|
+
if (verbose) {
|
|
438
|
+
console.log("");
|
|
439
|
+
console.log(formatHighYieldVerbose(report));
|
|
440
|
+
} else {
|
|
441
|
+
console.log(color("dim", " Run with --verbose to see all verified signals."));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
console.log(formatAiFooterBlock({
|
|
420
446
|
status: shipDecision,
|
|
421
447
|
domain,
|
|
@@ -424,6 +450,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
424
450
|
grade: report.grade || "",
|
|
425
451
|
max_severity: maxSev,
|
|
426
452
|
ship_decision: shipDecision,
|
|
453
|
+
unreachable: unreachable ? "true" : "false",
|
|
427
454
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
428
455
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
429
456
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
@@ -727,6 +754,254 @@ function formatAiFooterBlock(fields) {
|
|
|
727
754
|
return color("dim", lines.join("\n"));
|
|
728
755
|
}
|
|
729
756
|
|
|
757
|
+
// v0.16.0 — high-yield output formatters.
|
|
758
|
+
//
|
|
759
|
+
// Design (per user direction 2026-05-22): every scan should deliver
|
|
760
|
+
// substantive insight, but the default must stay tight (3-5 lines max) so
|
|
761
|
+
// running pqcheck 100 times/month doesn't feel like a mini audit every
|
|
762
|
+
// time. The verbose panel is opt-in via `--verbose`.
|
|
763
|
+
//
|
|
764
|
+
// Three rendering tiers:
|
|
765
|
+
// 1. Status line (1 line): ◆ Cipherwake · X ✓ PASS · DBR 4.7 C · stable 14d
|
|
766
|
+
// 2. Default CLI (3-5 lines): "✓ PASS · no public-surface drift detected"
|
|
767
|
+
// "✓ Trust posture: DBR 4.7 C · top 23% in fintech · stable 14d"
|
|
768
|
+
// "✓ Verified 8 signals · scripts, headers, cookies, cert/SPKI, ..."
|
|
769
|
+
// 3. Verbose (--verbose, 8+ lines): full per-signal breakdown
|
|
770
|
+
// =============================================================================
|
|
771
|
+
|
|
772
|
+
// Plain-English percentile copy. report.sectorRanking.percentile uses
|
|
773
|
+
// "100 = worst, 0 = best". Customer copy inverts to "top N%" framing.
|
|
774
|
+
// No cryptic p-quantiles — `p23` reads like jargon; `top 23%` is universal.
|
|
775
|
+
function formatPercentileCopy(percentile, sectorName) {
|
|
776
|
+
if (typeof percentile !== "number" || !isFinite(percentile)) return null;
|
|
777
|
+
const sector = sectorName || "industry";
|
|
778
|
+
if (percentile <= 10) return `top 10% in ${sector}`;
|
|
779
|
+
if (percentile <= 25) return `top 25% in ${sector}`;
|
|
780
|
+
if (percentile <= 50) return `above median in ${sector}`;
|
|
781
|
+
if (percentile <= 75) return `below median in ${sector}`;
|
|
782
|
+
if (percentile <= 90) return `bottom 25% in ${sector}`;
|
|
783
|
+
return `bottom 10% in ${sector}`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// "stable 14d" / "drifted 2d ago" / "first scan" depending on report data.
|
|
787
|
+
// Reads from report._meta.lastChanged (smartCache populates) — if not
|
|
788
|
+
// available, falls back to "tracked since first scan."
|
|
789
|
+
function formatStabilityCopy(report) {
|
|
790
|
+
const lastChanged = report?._meta?.lastChanged || report?._meta?.lastUpdated;
|
|
791
|
+
if (lastChanged) {
|
|
792
|
+
const ms = Date.now() - new Date(lastChanged).getTime();
|
|
793
|
+
const days = Math.floor(ms / 86400000);
|
|
794
|
+
if (days <= 0) return "drifted today";
|
|
795
|
+
if (days < 7) return `drifted ${days}d ago`;
|
|
796
|
+
return `stable ${days}d`;
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// "✓ Trust posture: DBR 4.7 C · top 23% in fintech · stable 14d"
|
|
802
|
+
// - Only includes sub-segments we have data for. If we can't compute a
|
|
803
|
+
// section, we omit it rather than padding with "—" / "n/a".
|
|
804
|
+
function formatTrustPostureLine(report) {
|
|
805
|
+
const dbr = typeof report?.score === "number" ? report.score.toFixed(1) : null;
|
|
806
|
+
const grade = report?.grade || "";
|
|
807
|
+
if (!dbr) return null;
|
|
808
|
+
const parts = [`DBR ${dbr}${grade ? " " + grade : ""}`];
|
|
809
|
+
const sr = report?.sectorRanking;
|
|
810
|
+
const pctCopy = formatPercentileCopy(sr?.percentile, sr?.sectorLabel || sr?.sectorName || sr?.sector);
|
|
811
|
+
if (pctCopy) parts.push(pctCopy);
|
|
812
|
+
const stability = formatStabilityCopy(report);
|
|
813
|
+
if (stability) parts.push(stability);
|
|
814
|
+
return color("green", "✓ ") + color("bold", "Trust posture: ") + parts.join(color("dim", " · "));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Count verified signals from the scan report. A "signal" is something
|
|
818
|
+
// Cipherwake actively checked AND would surface in a diff if it changed.
|
|
819
|
+
// Returns: { count, categories }.
|
|
820
|
+
function countVerifiedSignals(report) {
|
|
821
|
+
const cats = [];
|
|
822
|
+
// 1. Third-party scripts
|
|
823
|
+
if (report?.publicDeps?.fetched || Array.isArray(report?.publicDeps?.thirdParties)) cats.push("scripts");
|
|
824
|
+
// 2. Security headers (CSP/HSTS/XFO at minimum)
|
|
825
|
+
if (report?.httpHeaders?.reachable) cats.push("headers");
|
|
826
|
+
// 3. Cookies (v0.16.0 — new)
|
|
827
|
+
if (report?.cookies?.reachable) cats.push("cookies");
|
|
828
|
+
// 4. Cert / SPKI
|
|
829
|
+
if (report?.cert || report?._meta?.certSerial) cats.push("cert/SPKI");
|
|
830
|
+
// 5. Source maps (v0.16.0 — new)
|
|
831
|
+
if (report?.sourceMaps?.reachable) cats.push("source maps");
|
|
832
|
+
// 6. TLS posture
|
|
833
|
+
if (report?.publicSurface?.tlsVersion || report?.tlsVersion) cats.push("TLS");
|
|
834
|
+
// 7. Mixed content — not yet a separate probe but counted when we have
|
|
835
|
+
// publicDeps (which exposes any loadedOverHttps:false items).
|
|
836
|
+
if (report?.publicDeps?.fetched) cats.push("mixed-content");
|
|
837
|
+
// 8. Subdomain scale (from CT log scan)
|
|
838
|
+
if (typeof report?.publicSurface?.subdomainCount === "number") cats.push("subdomain scale");
|
|
839
|
+
return { count: cats.length, categories: cats };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Default ("Verified N signals · scripts, headers, ...") is for first scans
|
|
843
|
+
// where there's no baseline to compare. With a baseline + no drift we
|
|
844
|
+
// switch to "Security-relevant surface unchanged" — it directly answers
|
|
845
|
+
// the customer's question ("did anything that matters change?") instead
|
|
846
|
+
// of just naming what we checked. User direction 2026-05-22.
|
|
847
|
+
function formatVerifiedSignalsLine(report, options = {}) {
|
|
848
|
+
const { count, categories } = countVerifiedSignals(report);
|
|
849
|
+
if (count === 0) return null;
|
|
850
|
+
const head = options.diffNoChange === true
|
|
851
|
+
? "Security-relevant surface unchanged"
|
|
852
|
+
: `Verified ${count} signal${count === 1 ? "" : "s"}`;
|
|
853
|
+
return color("green", "✓ ") + color("bold", head) + color("dim", " · " + categories.join(", "));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Default high-yield panel — 3-5 lines max. Used in `pqcheck <domain> --ai`
|
|
857
|
+
// and `pqcheck deploy-check --ai` when no critical findings need to be
|
|
858
|
+
// surfaced prominently (otherwise the alerts go above this block).
|
|
859
|
+
function formatHighYieldDefault(report, { shipDecision, diffContext, diffNoChange } = {}) {
|
|
860
|
+
const lines = [];
|
|
861
|
+
// Header pulse: "✓ PASS · no public-surface drift detected"
|
|
862
|
+
// "⚠ REVIEW · 2 public-surface changes"
|
|
863
|
+
// "⛔ BLOCK · protected path changed"
|
|
864
|
+
if (diffContext) {
|
|
865
|
+
lines.push(diffContext);
|
|
866
|
+
}
|
|
867
|
+
const tpl = formatTrustPostureLine(report);
|
|
868
|
+
if (tpl) lines.push(tpl);
|
|
869
|
+
// diffNoChange flag tells the verified-signals line to switch wording
|
|
870
|
+
// from "Verified N signals" to "Security-relevant surface unchanged" —
|
|
871
|
+
// only set this when there IS a baseline to compare against AND the
|
|
872
|
+
// diff returned zero deltas.
|
|
873
|
+
const vsl = formatVerifiedSignalsLine(report, { diffNoChange: !!diffNoChange });
|
|
874
|
+
if (vsl) lines.push(vsl);
|
|
875
|
+
lines.push(color("dim", " Run with --verbose to see all verified signals."));
|
|
876
|
+
return lines.join("\n");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Verbose panel — opt-in via --verbose. Per-signal bullet breakdown.
|
|
880
|
+
// Used for first-run, troubleshooting, public benchmark/report pages.
|
|
881
|
+
// The caller renders the trust-posture line ABOVE this block, so we
|
|
882
|
+
// don't duplicate it here.
|
|
883
|
+
function formatHighYieldVerbose(report) {
|
|
884
|
+
const lines = [];
|
|
885
|
+
lines.push(color("green", "✓ ") + color("bold", "Verified this deploy:"));
|
|
886
|
+
// Scripts
|
|
887
|
+
if (Array.isArray(report?.publicDeps?.thirdParties)) {
|
|
888
|
+
const hosts = report.publicDeps.thirdParties.slice(0, 6).map(d => d.host || d).filter(Boolean);
|
|
889
|
+
const more = report.publicDeps.thirdParties.length > 6
|
|
890
|
+
? `, +${report.publicDeps.thirdParties.length - 6} more` : "";
|
|
891
|
+
lines.push(color("dim", ` · ${hosts.length} third-party script${hosts.length === 1 ? "" : "s"} intact (${hosts.join(", ")}${more})`));
|
|
892
|
+
}
|
|
893
|
+
// Headers (always-on)
|
|
894
|
+
const hh = report?.httpHeaders;
|
|
895
|
+
if (hh?.reachable) {
|
|
896
|
+
const hdrs = [];
|
|
897
|
+
if (hh.csp?.present) hdrs.push("CSP enforced");
|
|
898
|
+
if (hh.hsts?.present) hdrs.push(`HSTS${hh.hsts.preload ? " preload" : ""}`);
|
|
899
|
+
if (hh.xFrameOptions?.present) hdrs.push(`X-Frame-Options ${hh.xFrameOptions.value || ""}`.trim());
|
|
900
|
+
if (hh.xContentTypeOptions?.present) hdrs.push("X-Content-Type-Options nosniff");
|
|
901
|
+
if (hh.referrerPolicy?.present) hdrs.push(`Referrer-Policy ${hh.referrerPolicy.value || ""}`.trim());
|
|
902
|
+
if (hh.permissionsPolicy?.present) hdrs.push("Permissions-Policy set");
|
|
903
|
+
if (hdrs.length > 0) lines.push(color("dim", ` · ${hdrs.join(" · ")}`));
|
|
904
|
+
}
|
|
905
|
+
// Cert
|
|
906
|
+
if (report?.cert || report?._meta?.certSerial) {
|
|
907
|
+
const cert = report.cert || {};
|
|
908
|
+
const days = cert.notAfter
|
|
909
|
+
? Math.floor((new Date(cert.notAfter).getTime() - Date.now()) / 86400000)
|
|
910
|
+
: null;
|
|
911
|
+
const certLine = days !== null
|
|
912
|
+
? `Cert: ${days}d until expiry${cert.issuer ? " · issued by " + cert.issuer : ""}`
|
|
913
|
+
: `Cert: ${cert.issuer || "issued"}`;
|
|
914
|
+
lines.push(color("dim", ` · ${certLine}`));
|
|
915
|
+
}
|
|
916
|
+
// Cookies (v0.16.0)
|
|
917
|
+
const ck = report?.cookies;
|
|
918
|
+
if (ck?.reachable) {
|
|
919
|
+
if (ck.count === 0) {
|
|
920
|
+
lines.push(color("dim", ` · Cookies: none set`));
|
|
921
|
+
} else {
|
|
922
|
+
const flags = [];
|
|
923
|
+
if (!ck.anyMissingSecure) flags.push("all Secure");
|
|
924
|
+
if (!ck.anyMissingHttpOnly) flags.push("all HttpOnly");
|
|
925
|
+
if (!ck.anyMissingSameSite) flags.push("all SameSite set");
|
|
926
|
+
lines.push(color("dim", ` · Cookies: ${ck.count} set${flags.length ? " · " + flags.join(" + ") : ""}`));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// Source maps (v0.16.0)
|
|
930
|
+
const sm = report?.sourceMaps;
|
|
931
|
+
if (sm?.reachable) {
|
|
932
|
+
lines.push(color("dim", ` · ${sm.exposed ? `Source maps EXPOSED on ${sm.exposedCount} script(s)` : "No source maps exposed"}`));
|
|
933
|
+
}
|
|
934
|
+
// Mixed content
|
|
935
|
+
if (Array.isArray(report?.publicDeps?.thirdParties)) {
|
|
936
|
+
const insecure = report.publicDeps.thirdParties.filter(d => d.loadedOverHttps === false).length;
|
|
937
|
+
lines.push(color("dim", ` · ${insecure === 0 ? "No mixed-content (all third-parties over HTTPS)" : `Mixed content: ${insecure} resource(s) over HTTP`}`));
|
|
938
|
+
}
|
|
939
|
+
return lines.join("\n");
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Helper: pretty-print 1-3 alerts (when ship_decision = review/block) ABOVE
|
|
943
|
+
// the trust-posture line, so the actionable change is the FIRST thing the
|
|
944
|
+
// customer sees. Each alert is one line with an emoji + concise summary.
|
|
945
|
+
function formatAlertsLine(findings, max = 3) {
|
|
946
|
+
if (!Array.isArray(findings) || findings.length === 0) return null;
|
|
947
|
+
const sortedAlerts = [...findings].sort(
|
|
948
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity),
|
|
949
|
+
);
|
|
950
|
+
const lines = [];
|
|
951
|
+
for (const f of sortedAlerts.slice(0, max)) {
|
|
952
|
+
const sev = String(f.severity || "").toLowerCase();
|
|
953
|
+
const sym = sev === "critical" ? "⛔" : sev === "high" ? "⚠" : "ⓘ";
|
|
954
|
+
const c = sev === "critical" ? "red" : sev === "high" ? "yellow" : "dim";
|
|
955
|
+
const title = f.title || f.id || "finding";
|
|
956
|
+
lines.push(color(c, `${sym} ${title}`));
|
|
957
|
+
}
|
|
958
|
+
if (sortedAlerts.length > max) {
|
|
959
|
+
lines.push(color("dim", ` +${sortedAlerts.length - max} more (run with --verbose)`));
|
|
960
|
+
}
|
|
961
|
+
return lines.join("\n");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function isVerboseMode(args) {
|
|
965
|
+
return args.includes("--verbose") || args.includes("--explain");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// One-line "decision pulse" — the prominent first line that tells the customer
|
|
969
|
+
// the answer to "is this deploy safe to announce?" in <80 characters.
|
|
970
|
+
// Wording branches on context:
|
|
971
|
+
// - Diff context (deploy-check / trust-diff / preview-diff): talks about
|
|
972
|
+
// CHANGES vs the baseline ("no public-surface drift detected").
|
|
973
|
+
// - Basic scan (no baseline): talks about FINDINGS from this scan
|
|
974
|
+
// ("no high-severity findings").
|
|
975
|
+
function formatDecisionPulse({ shipDecision, alertCount, unreachable, diffContext }) {
|
|
976
|
+
if (unreachable) {
|
|
977
|
+
return color("red", "⛔ UNREACHABLE") + color("dim", " · scanner couldn't reach the domain");
|
|
978
|
+
}
|
|
979
|
+
const n = alertCount || 0;
|
|
980
|
+
if (shipDecision === "pass") {
|
|
981
|
+
const tail = diffContext
|
|
982
|
+
? "no public-surface drift detected"
|
|
983
|
+
: "no high-severity findings";
|
|
984
|
+
return color("green", "✓ PASS") + color("dim", " · " + tail);
|
|
985
|
+
}
|
|
986
|
+
if (shipDecision === "review") {
|
|
987
|
+
const noun = diffContext ? "public-surface change" : "high-severity finding";
|
|
988
|
+
return color("yellow", "⚠ REVIEW") + color("dim", ` · ${n} ${noun}${n === 1 ? "" : "s"}`);
|
|
989
|
+
}
|
|
990
|
+
if (shipDecision === "block") {
|
|
991
|
+
const noun = diffContext ? "critical change" : "critical finding";
|
|
992
|
+
return color("red", "⛔ BLOCK") + color("dim", ` · ${n} ${noun}${n === 1 ? "" : "s"}`);
|
|
993
|
+
}
|
|
994
|
+
return color("dim", "· no decision");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// "◆ Cipherwake — domain" — the v0.16.0 brand+domain header line. Shorter
|
|
998
|
+
// than the old AI banner so the decision-pulse line below it carries the
|
|
999
|
+
// status. The banner color stays neutral (no decision color) because the
|
|
1000
|
+
// decision-pulse line owns that semantic.
|
|
1001
|
+
function formatBrandHeader(domain) {
|
|
1002
|
+
return color("bold", "◆ Cipherwake") + color("dim", " — ") + color("bold", domain);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
730
1005
|
// Persist last-scan state to ~/.config/cipherwake/last-scan.json.
|
|
731
1006
|
// Feeds the cipherwake-statusline + cipherwake-prompt-hook + cipherwake-chat-hook
|
|
732
1007
|
// scripts so users get persistent ambient state in their AI coder's surfaces.
|
|
@@ -4770,7 +5045,7 @@ esac
|
|
|
4770
5045
|
const CLAUDE_STATUSLINE_CONFIG_SNIPPET = `
|
|
4771
5046
|
"statusLine": {
|
|
4772
5047
|
"type": "command",
|
|
4773
|
-
"command": "npx cipherwake-statusline"
|
|
5048
|
+
"command": "npx --package=pqcheck@latest cipherwake-statusline"
|
|
4774
5049
|
}`;
|
|
4775
5050
|
|
|
4776
5051
|
// R74-confirm SHIP #14-15 (GPT 2026-05-22): network connectivity diagnostic.
|
|
@@ -5107,12 +5382,12 @@ async function runSetupCommand(args) {
|
|
|
5107
5382
|
if (existed && settings.statusLine && typeof settings.statusLine === "object") {
|
|
5108
5383
|
// Already has a statusLine config — don't overwrite.
|
|
5109
5384
|
console.log(color("dim", ` ⊝ ~/.claude/settings.json already has a statusLine entry — leaving alone`));
|
|
5110
|
-
console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx cipherwake-statusline"`));
|
|
5385
|
+
console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
|
|
5111
5386
|
installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
|
|
5112
5387
|
} else {
|
|
5113
5388
|
const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
|
|
5114
5389
|
if (backupPath) console.log(color("dim", ` backup: ${backupPath}`));
|
|
5115
|
-
settings.statusLine = { type: "command", command: "npx cipherwake-statusline" };
|
|
5390
|
+
settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
|
|
5116
5391
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
5117
5392
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
5118
5393
|
console.log(color("green", ` ✓ added statusLine config → ~/.claude/settings.json`));
|
|
@@ -5151,7 +5426,7 @@ async function runSetupCommand(args) {
|
|
|
5151
5426
|
}
|
|
5152
5427
|
bashEntry.hooks = bashEntry.hooks || [];
|
|
5153
5428
|
|
|
5154
|
-
const cipherwakeHookCmd = "npx cipherwake-chat-hook";
|
|
5429
|
+
const cipherwakeHookCmd = "npx --package=pqcheck@latest cipherwake-chat-hook";
|
|
5155
5430
|
const alreadyInstalled = bashEntry.hooks.some(
|
|
5156
5431
|
(h) => h?.type === "command" && typeof h?.command === "string" && h.command.includes("cipherwake-chat-hook"),
|
|
5157
5432
|
);
|
|
@@ -5196,7 +5471,7 @@ async function runSetupCommand(args) {
|
|
|
5196
5471
|
settings.hooks = settings.hooks || {};
|
|
5197
5472
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit || [];
|
|
5198
5473
|
|
|
5199
|
-
const cipherwakeHookCmd = "npx cipherwake-prompt-hook";
|
|
5474
|
+
const cipherwakeHookCmd = "npx --package=pqcheck@latest cipherwake-prompt-hook";
|
|
5200
5475
|
const alreadyInstalled = settings.hooks.UserPromptSubmit.some(
|
|
5201
5476
|
(entry) => Array.isArray(entry?.hooks) && entry.hooks.some(
|
|
5202
5477
|
(h) => h?.type === "command" && typeof h?.command === "string" && h.command.includes("cipherwake-prompt-hook"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coder",
|