@fusengine/harness 0.1.34 → 0.1.35

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/dist/cli/bin.mjs CHANGED
@@ -3,13 +3,85 @@ import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
3
3
  import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
4
4
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CUL70W0k.mjs";
5
5
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
6
- import { t as handleHook } from "../handle-DMHjTY1E.mjs";
6
+ import { Et as claudeHome, Tt as todayUtc, t as handleHook } from "../handle-CYlA6TeF.mjs";
7
+ import { join } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ //#region src/changelog/fetch.ts
11
+ /**
12
+ * Changelog scanner — ports the changelog-watcher plugin's `fetch-changelog`
13
+ * into the harness (exposed as the `harness changelog` CLI verb). Fetches the
14
+ * official Claude Code changelog, detects how many versions are new since the
15
+ * last check, persists per-day state, and returns a JSON summary. Dual-runtime:
16
+ * global `fetch` + `node:fs` (works under Node 20+ and Bun, no imports needed).
17
+ */
18
+ const CHANGELOG_URL = "https://code.claude.com/docs/en/changelog.md";
19
+ /**
20
+ * Parse up to 10 semver versions from the changelog, newest first. Matches the
21
+ * current docs format (`<Update label="X.Y.Z" …>` MDX blocks) AND the legacy
22
+ * markdown headers (`## vX.Y.Z` / `## X.Y.Z`) so it survives a format rollback.
23
+ */
24
+ function parseVersions(md) {
25
+ return [...md.matchAll(/<Update\s+label="v?(\d+\.\d+\.\d+)"|^##\s+v?(\d+\.\d+\.\d+)/gm)].map((m) => m[1] ?? m[2] ?? "").filter(Boolean).slice(0, 10);
26
+ }
27
+ /** Count versions newer than `lastKnown` (stops at the first match). */
28
+ function countNew(versions, lastKnown) {
29
+ if (!lastKnown) return 0;
30
+ let n = 0;
31
+ for (const v of versions) {
32
+ if (v === lastKnown) break;
33
+ n++;
34
+ }
35
+ return n;
36
+ }
37
+ /** Read the saved `last_version` for today's state file ("" when absent/corrupt). */
38
+ function lastKnownVersion(stateFile) {
39
+ if (!existsSync(stateFile)) return "";
40
+ try {
41
+ return JSON.parse(readFileSync(stateFile, "utf8")).last_version ?? "";
42
+ } catch {
43
+ return "";
44
+ }
45
+ }
46
+ /**
47
+ * Fetch + parse the changelog, diff against the saved state, persist, and return
48
+ * the scan summary. Throws on network failure (the CLI maps it to exit 1).
49
+ * @param now - Clock (ms).
50
+ * @param home - Home dir.
51
+ */
52
+ async function scanChangelog(now = Date.now(), home = homedir()) {
53
+ const res = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(1e4) });
54
+ if (!res.ok) throw new Error(`changelog fetch failed: ${res.status}`);
55
+ const versions = parseVersions(await res.text());
56
+ const latest = versions[0] ?? "";
57
+ const dir = join(claudeHome(home), "logs", "00-changelog");
58
+ const today = todayUtc(now);
59
+ const stateFile = join(dir, `${today}-state.json`);
60
+ const lastKnown = lastKnownVersion(stateFile);
61
+ const newCount = countNew(versions, lastKnown);
62
+ try {
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(stateFile, JSON.stringify({
65
+ last_version: latest,
66
+ previous: lastKnown,
67
+ new_versions: newCount,
68
+ checked: today
69
+ }, null, 2));
70
+ } catch {}
71
+ return {
72
+ latest,
73
+ new_since_last_check: newCount,
74
+ recent_versions: versions
75
+ };
76
+ }
77
+ //#endregion
7
78
  //#region src/cli/bin.ts
8
79
  /**
9
80
  * harness — CLI for @fusengine/harness.
10
81
  * harness check cli-mode: check staged files (pre-commit), exit non-zero on a violation
11
82
  * harness init [id] write the wiring file for a harness (defaults to the detected one)
12
83
  * harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
84
+ * harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
13
85
  */
14
86
  async function readStdin() {
15
87
  const chunks = [];
@@ -56,7 +128,17 @@ if (cmd === "hook") {
56
128
  const written = files.map((f) => writeInitFile(process.cwd(), f));
57
129
  process.stdout.write(`harness: wired ${id} -> ${written.join(", ")}\n`);
58
130
  process.exit(0);
59
- } else {
131
+ } else if (cmd === "changelog") try {
132
+ process.stdout.write(JSON.stringify(await scanChangelog()) + "\n");
133
+ process.exit(0);
134
+ } catch (e) {
135
+ process.stdout.write(JSON.stringify({
136
+ status: "error",
137
+ message: e instanceof Error ? e.message : "changelog fetch failed"
138
+ }) + "\n");
139
+ process.exit(1);
140
+ }
141
+ else {
60
142
  const files = stagedFiles();
61
143
  if (files.length === 0) process.exit(0);
62
144
  const violations = checkStaged(files, stagedContent);
@@ -16,6 +16,88 @@ import { homedir } from "node:os";
16
16
  import { createHash } from "node:crypto";
17
17
  import { mkdir, rmdir } from "node:fs/promises";
18
18
  import { execFileSync } from "node:child_process";
19
+ //#region src/runtime/home-state.ts
20
+ /** Home `~/.claude` dir (single source for every home-based hook path). */
21
+ function claudeHome(home = homedir()) {
22
+ return join(home, ".claude");
23
+ }
24
+ /** `~/.claude/fusengine-cache` base dir for legacy session/cache state. */
25
+ function fusengineCache(home = homedir()) {
26
+ return join(claudeHome(home), "fusengine-cache");
27
+ }
28
+ /** `~/.claude/fusengine-cache/sessions` — per-session JSON state dir. */
29
+ function sessionsDir(home = homedir()) {
30
+ return join(fusengineCache(home), "sessions");
31
+ }
32
+ const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
33
+ /** Validate a session id (1-128 url-safe chars); null when invalid. */
34
+ function sanitizeSessionId(sid) {
35
+ const s = String(sid ?? "").trim();
36
+ return SID_RE.test(s) ? s : null;
37
+ }
38
+ /** Unified per-session state file path: `sessions/session-<sid>.json`. */
39
+ function sessionStatePath(sid, home = homedir()) {
40
+ return join(sessionsDir(home), `session-${sid}.json`);
41
+ }
42
+ /** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
43
+ function loadSessionState(sid, home = homedir()) {
44
+ const path = sessionStatePath(sid, home);
45
+ try {
46
+ if (!existsSync(path)) return {};
47
+ const data = JSON.parse(readFileSync(path, "utf-8"));
48
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+ /** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
54
+ function saveSessionState(sid, state, home = homedir()) {
55
+ mkdirSync(sessionsDir(home), {
56
+ recursive: true,
57
+ mode: 448
58
+ });
59
+ atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
60
+ }
61
+ //#endregion
62
+ //#region src/runtime/lifecycle/security/skill-state.ts
63
+ /**
64
+ * Shared security-tracker state: per-UTC-day JSON under
65
+ * `~/.claude/logs/00-security`. Ports the state helpers of
66
+ * `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
67
+ */
68
+ /** `~/.claude/logs/00-security` state directory. */
69
+ function securityStateDir(home = homedir()) {
70
+ return join(claudeHome(home), "logs", "00-security");
71
+ }
72
+ /** Current UTC date as `YYYY-MM-DD`. */
73
+ function todayUtc(now = Date.now()) {
74
+ return new Date(now).toISOString().slice(0, 10);
75
+ }
76
+ /** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
77
+ function isoUtc(now = Date.now()) {
78
+ return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
79
+ }
80
+ /** Today's security-state file path. */
81
+ function securityStatePath(now = Date.now(), home = homedir()) {
82
+ return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
83
+ }
84
+ /** Load today's security state, or `{}` when missing/corrupt. */
85
+ function loadSecurityState(now = Date.now(), home = homedir()) {
86
+ const path = securityStatePath(now, home);
87
+ try {
88
+ if (!existsSync(path)) return {};
89
+ const data = JSON.parse(readFileSync(path, "utf-8"));
90
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+ /** Persist today's security state (indent 2, no trailing newline). */
96
+ function saveSecurityState(state, now = Date.now(), home = homedir()) {
97
+ mkdirSync(securityStateDir(home), { recursive: true });
98
+ writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
99
+ }
100
+ //#endregion
19
101
  //#region src/runtime/activity.ts
20
102
  /** Min response length (chars) for a lead agent call to count as `sufficient`. */
21
103
  const AGENT_QUALITY_MIN = 500;
@@ -671,49 +753,6 @@ function taskContext(cwd) {
671
753
  return ctx ? contextResponse("PreToolUse", ctx) : "";
672
754
  }
673
755
  //#endregion
674
- //#region src/runtime/home-state.ts
675
- /** Home `~/.claude` dir (single source for every home-based hook path). */
676
- function claudeHome(home = homedir()) {
677
- return join(home, ".claude");
678
- }
679
- /** `~/.claude/fusengine-cache` base dir for legacy session/cache state. */
680
- function fusengineCache(home = homedir()) {
681
- return join(claudeHome(home), "fusengine-cache");
682
- }
683
- /** `~/.claude/fusengine-cache/sessions` — per-session JSON state dir. */
684
- function sessionsDir(home = homedir()) {
685
- return join(fusengineCache(home), "sessions");
686
- }
687
- const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
688
- /** Validate a session id (1-128 url-safe chars); null when invalid. */
689
- function sanitizeSessionId(sid) {
690
- const s = String(sid ?? "").trim();
691
- return SID_RE.test(s) ? s : null;
692
- }
693
- /** Unified per-session state file path: `sessions/session-<sid>.json`. */
694
- function sessionStatePath(sid, home = homedir()) {
695
- return join(sessionsDir(home), `session-${sid}.json`);
696
- }
697
- /** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
698
- function loadSessionState(sid, home = homedir()) {
699
- const path = sessionStatePath(sid, home);
700
- try {
701
- if (!existsSync(path)) return {};
702
- const data = JSON.parse(readFileSync(path, "utf-8"));
703
- return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
704
- } catch {
705
- return {};
706
- }
707
- }
708
- /** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
709
- function saveSessionState(sid, state, home = homedir()) {
710
- mkdirSync(sessionsDir(home), {
711
- recursive: true,
712
- mode: 448
713
- });
714
- atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
715
- }
716
- //#endregion
717
756
  //#region src/runtime/dev-context.ts
718
757
  /** Run a git subcommand in `cwd`, returning trimmed stdout or "" on error. */
719
758
  function git(cwd, args) {
@@ -3017,45 +3056,6 @@ function trackEnrichment(filePath) {
3017
3056
  } catch {}
3018
3057
  }
3019
3058
  //#endregion
3020
- //#region src/runtime/lifecycle/security/skill-state.ts
3021
- /**
3022
- * Shared security-tracker state: per-UTC-day JSON under
3023
- * `~/.claude/logs/00-security`. Ports the state helpers of
3024
- * `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
3025
- */
3026
- /** `~/.claude/logs/00-security` state directory. */
3027
- function securityStateDir(home = homedir()) {
3028
- return join(claudeHome(home), "logs", "00-security");
3029
- }
3030
- /** Current UTC date as `YYYY-MM-DD`. */
3031
- function todayUtc(now = Date.now()) {
3032
- return new Date(now).toISOString().slice(0, 10);
3033
- }
3034
- /** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
3035
- function isoUtc(now = Date.now()) {
3036
- return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
3037
- }
3038
- /** Today's security-state file path. */
3039
- function securityStatePath(now = Date.now(), home = homedir()) {
3040
- return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
3041
- }
3042
- /** Load today's security state, or `{}` when missing/corrupt. */
3043
- function loadSecurityState(now = Date.now(), home = homedir()) {
3044
- const path = securityStatePath(now, home);
3045
- try {
3046
- if (!existsSync(path)) return {};
3047
- const data = JSON.parse(readFileSync(path, "utf-8"));
3048
- return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
3049
- } catch {
3050
- return {};
3051
- }
3052
- }
3053
- /** Persist today's security state (indent 2, no trailing newline). */
3054
- function saveSecurityState(state, now = Date.now(), home = homedir()) {
3055
- mkdirSync(securityStateDir(home), { recursive: true });
3056
- writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
3057
- }
3058
- //#endregion
3059
3059
  //#region src/runtime/lifecycle/security/track-skill-read.ts
3060
3060
  /**
3061
3061
  * Security skill-read tracker (PostToolUse Read). Ports `track-skill-read.py`:
@@ -3903,4 +3903,4 @@ async function handleHook(id, payload, opts) {
3903
3903
  });
3904
3904
  }
3905
3905
  //#endregion
3906
- export { detectSolidProfile as $, dispatchLessons as A, mcpPreIntercept as At, mergeLines as B, securityStateDir as C, defaultStateDir as Ct, dispatchLifecycle as D, MCP_TTL_MS as Dt, trackEnrichment as E, normalizeEvent as Et, writePluginMap as F, trackSessionChanges as G, getFileDesc as H, generateProjectMap as I, saveApexState as J, validateRulesLoaded as K, isProject as L, lessonsStateFileFor as M, activityFor as Mt, cartoSessionStart as N, aipilotPostToolUse as O, isMcpTool as Ot, generateEcosystemMap as P, subagentCacheContext as Q, writeTree as R, saveSecurityState as S, recordActivity as St, todayUtc as T, trackFile as Tt, listChildren as U, countFiles as V, postEditTypescript as W, validateTeammateOutput as X, logToolFailure as Y, trackAgentMemory as Z, trackWatchResearch as _, sessionStatePath as _t, TRIVIAL_BUDGET as a, pruneEmptyDirs as at, isoUtc as b, taskContext as bt, detectDuplication as c, trimLogFile as ct, lifecycleStdout as d, projectContext as dt, solidDetectStart as et, postEditContext as f, claudeHome as ft, postTrackingSideEffects as g, saveSessionState as gt, securityAdvisory as h, sanitizeSessionId as ht, REQUIRED_AGENTS as i, sessionStartCore as it, lessonsFileFor as j, queryOf as jt, dispatchAipilot as k, mcpPostStore as kt, dryGate as l, devContext as lt, seoPostToolUseResponse as m, loadSessionState as mt, handlePre as n, readRules as nt, gate as o, purgeTtlTree as ot, seoPostToolUse as p, fusengineCache as pt, cleanupSession as q, DEFAULT_WINDOW_MS as r, runSessionStartCleanups as rt, preCommitGate as s, removeOldFiles as st, handleHook as t, injectRules as tt, extractSymbols as u, gitContext as ut, trackMcpResearch as v, sessionsDir as vt, securityStatePath as w, projectHash$1 as wt, loadSecurityState as x, respond as xt, trackSkillRead as y, promptSubmitContext as yt, loadEnriched as z };
3906
+ export { pruneEmptyDirs as $, generateProjectMap as A, saveSessionState as At, validateRulesLoaded as B, dispatchAipilot as C, securityStateDir as Ct, cartoSessionStart as D, fusengineCache as Dt, lessonsStateFileFor as E, claudeHome as Et, countFiles as F, trackAgentMemory as G, saveApexState as H, getFileDesc as I, solidDetectStart as J, subagentCacheContext as K, listChildren as L, writeTree as M, sessionsDir as Mt, loadEnriched as N, generateEcosystemMap as O, loadSessionState as Ot, mergeLines as P, sessionStartCore as Q, postEditTypescript as R, aipilotPostToolUse as S, saveSecurityState as St, lessonsFileFor as T, todayUtc as Tt, logToolFailure as U, cleanupSession as V, validateTeammateOutput as W, readRules as X, injectRules as Y, runSessionStartCleanups as Z, trackWatchResearch as _, mcpPreIntercept as _t, TRIVIAL_BUDGET as a, projectContext as at, trackEnrichment as b, isoUtc as bt, detectDuplication as c, respond as ct, lifecycleStdout as d, projectHash$1 as dt, purgeTtlTree as et, postEditContext as f, trackFile as ft, postTrackingSideEffects as g, mcpPostStore as gt, securityAdvisory as h, isMcpTool as ht, REQUIRED_AGENTS as i, gitContext as it, isProject as j, sessionStatePath as jt, writePluginMap as k, sanitizeSessionId as kt, dryGate as l, recordActivity as lt, seoPostToolUseResponse as m, MCP_TTL_MS as mt, handlePre as n, trimLogFile as nt, gate as o, promptSubmitContext as ot, seoPostToolUse as p, normalizeEvent as pt, detectSolidProfile as q, DEFAULT_WINDOW_MS as r, devContext as rt, preCommitGate as s, taskContext as st, handleHook as t, removeOldFiles as tt, extractSymbols as u, defaultStateDir as ut, trackMcpResearch as v, queryOf as vt, dispatchLessons as w, securityStatePath as wt, dispatchLifecycle as x, loadSecurityState as xt, trackSkillRead as y, activityFor as yt, trackSessionChanges as z };
@@ -1,5 +1,5 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
- import { $ as detectSolidProfile, A as dispatchLessons, At as mcpPreIntercept, B as mergeLines, C as securityStateDir, Ct as defaultStateDir, D as dispatchLifecycle, Dt as MCP_TTL_MS, E as trackEnrichment, Et as normalizeEvent, F as writePluginMap, G as trackSessionChanges, H as getFileDesc, I as generateProjectMap, J as saveApexState, K as validateRulesLoaded, L as isProject, M as lessonsStateFileFor, Mt as activityFor, N as cartoSessionStart, O as aipilotPostToolUse, Ot as isMcpTool, P as generateEcosystemMap, Q as subagentCacheContext, R as writeTree, S as saveSecurityState, St as recordActivity, T as todayUtc, Tt as trackFile, U as listChildren, V as countFiles, W as postEditTypescript, X as validateTeammateOutput, Y as logToolFailure, Z as trackAgentMemory, _ as trackWatchResearch, _t as sessionStatePath, a as TRIVIAL_BUDGET, at as pruneEmptyDirs, b as isoUtc, bt as taskContext, c as detectDuplication, ct as trimLogFile, d as lifecycleStdout, dt as projectContext, et as solidDetectStart, f as postEditContext, ft as claudeHome, g as postTrackingSideEffects, gt as saveSessionState, h as securityAdvisory, ht as sanitizeSessionId, i as REQUIRED_AGENTS, it as sessionStartCore, j as lessonsFileFor, jt as queryOf, k as dispatchAipilot, kt as mcpPostStore, l as dryGate, lt as devContext, m as seoPostToolUseResponse, mt as loadSessionState, n as handlePre, nt as readRules, o as gate, ot as purgeTtlTree, p as seoPostToolUse, pt as fusengineCache, q as cleanupSession, r as DEFAULT_WINDOW_MS, rt as runSessionStartCleanups, s as preCommitGate, st as removeOldFiles, t as handleHook, tt as injectRules, u as extractSymbols, ut as gitContext, v as trackMcpResearch, vt as sessionsDir, w as securityStatePath, wt as projectHash, x as loadSecurityState, xt as respond, y as trackSkillRead, yt as promptSubmitContext, z as loadEnriched } from "../handle-DMHjTY1E.mjs";
2
+ import { $ as pruneEmptyDirs, A as generateProjectMap, At as saveSessionState, B as validateRulesLoaded, C as dispatchAipilot, Ct as securityStateDir, D as cartoSessionStart, Dt as fusengineCache, E as lessonsStateFileFor, Et as claudeHome, F as countFiles, G as trackAgentMemory, H as saveApexState, I as getFileDesc, J as solidDetectStart, K as subagentCacheContext, L as listChildren, M as writeTree, Mt as sessionsDir, N as loadEnriched, O as generateEcosystemMap, Ot as loadSessionState, P as mergeLines, Q as sessionStartCore, R as postEditTypescript, S as aipilotPostToolUse, St as saveSecurityState, T as lessonsFileFor, Tt as todayUtc, U as logToolFailure, V as cleanupSession, W as validateTeammateOutput, X as readRules, Y as injectRules, Z as runSessionStartCleanups, _ as trackWatchResearch, _t as mcpPreIntercept, a as TRIVIAL_BUDGET, at as projectContext, b as trackEnrichment, bt as isoUtc, c as detectDuplication, ct as respond, d as lifecycleStdout, dt as projectHash, et as purgeTtlTree, f as postEditContext, ft as trackFile, g as postTrackingSideEffects, gt as mcpPostStore, h as securityAdvisory, ht as isMcpTool, i as REQUIRED_AGENTS, it as gitContext, j as isProject, jt as sessionStatePath, k as writePluginMap, kt as sanitizeSessionId, l as dryGate, lt as recordActivity, m as seoPostToolUseResponse, mt as MCP_TTL_MS, n as handlePre, nt as trimLogFile, o as gate, ot as promptSubmitContext, p as seoPostToolUse, pt as normalizeEvent, q as detectSolidProfile, r as DEFAULT_WINDOW_MS, rt as devContext, s as preCommitGate, st as taskContext, t as handleHook, tt as removeOldFiles, u as extractSymbols, ut as defaultStateDir, v as trackMcpResearch, vt as queryOf, w as dispatchLessons, wt as securityStatePath, x as dispatchLifecycle, xt as loadSecurityState, y as trackSkillRead, yt as activityFor, z as trackSessionChanges } from "../handle-CYlA6TeF.mjs";
3
3
  //#region src/runtime/storage.ts
4
4
  /**
5
5
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",