@meetless/mla 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +81 -0
  3. package/dist/build-info.json +9 -0
  4. package/dist/bundles/ask-core.js +396 -0
  5. package/dist/bundles/mcp.js +16592 -0
  6. package/dist/bundles/trace-core.js +263 -0
  7. package/dist/cli.js +828 -0
  8. package/dist/commands/activate.js +781 -0
  9. package/dist/commands/adoption.js +130 -0
  10. package/dist/commands/ask.js +290 -0
  11. package/dist/commands/context.js +114 -0
  12. package/dist/commands/debug.js +313 -0
  13. package/dist/commands/doctor.js +1021 -0
  14. package/dist/commands/enrich.js +427 -0
  15. package/dist/commands/evidence.js +229 -0
  16. package/dist/commands/flush.js +184 -0
  17. package/dist/commands/graph.js +104 -0
  18. package/dist/commands/init.js +272 -0
  19. package/dist/commands/internal-active-review.js +322 -0
  20. package/dist/commands/internal-auto-index.js +188 -0
  21. package/dist/commands/internal-capture-decisions.js +320 -0
  22. package/dist/commands/internal-evidence-correlate.js +239 -0
  23. package/dist/commands/internal-evidence-hooks.js +240 -0
  24. package/dist/commands/internal-evidence-inject.js +231 -0
  25. package/dist/commands/internal-finalize.js +221 -0
  26. package/dist/commands/internal-pretool-observe.js +225 -0
  27. package/dist/commands/internal-refresh.js +136 -0
  28. package/dist/commands/internal-session-nudge.js +120 -0
  29. package/dist/commands/internal-steer-sync.js +117 -0
  30. package/dist/commands/internal-turn-recap.js +140 -0
  31. package/dist/commands/kb.js +375 -0
  32. package/dist/commands/kb_add.js +681 -0
  33. package/dist/commands/kb_forget.js +283 -0
  34. package/dist/commands/kb_move.js +45 -0
  35. package/dist/commands/kb_pending.js +410 -0
  36. package/dist/commands/kb_personal.js +149 -0
  37. package/dist/commands/kb_promote.js +188 -0
  38. package/dist/commands/kb_purge.js +168 -0
  39. package/dist/commands/kb_reingest.js +335 -0
  40. package/dist/commands/kb_retime.js +170 -0
  41. package/dist/commands/kb_review.js +391 -0
  42. package/dist/commands/kb_revision.js +179 -0
  43. package/dist/commands/kb_show.js +385 -0
  44. package/dist/commands/label.js +226 -0
  45. package/dist/commands/login.js +295 -0
  46. package/dist/commands/logout.js +108 -0
  47. package/dist/commands/mcp-supervisor.js +93 -0
  48. package/dist/commands/mcp.js +227 -0
  49. package/dist/commands/queue-prune.js +98 -0
  50. package/dist/commands/review.js +358 -0
  51. package/dist/commands/rewire.js +124 -0
  52. package/dist/commands/rules.js +728 -0
  53. package/dist/commands/scan-context.js +67 -0
  54. package/dist/commands/session.js +347 -0
  55. package/dist/commands/stats.js +479 -0
  56. package/dist/commands/status.js +61 -0
  57. package/dist/commands/summary.js +250 -0
  58. package/dist/commands/turn.js +114 -0
  59. package/dist/commands/uninstall.js +222 -0
  60. package/dist/commands/whoami.js +102 -0
  61. package/dist/commands/workspace.js +130 -0
  62. package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
  63. package/dist/hooks-template/ce0-session-start.sh +49 -0
  64. package/dist/hooks-template/ce0-stop.sh +29 -0
  65. package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
  66. package/dist/hooks-template/common.sh +934 -0
  67. package/dist/hooks-template/event-batch-filter.jq +67 -0
  68. package/dist/hooks-template/flush.sh +503 -0
  69. package/dist/hooks-template/post-tool-use.sh +423 -0
  70. package/dist/hooks-template/pre-tool-use.sh +69 -0
  71. package/dist/hooks-template/session-start.sh +140 -0
  72. package/dist/hooks-template/stop.sh +308 -0
  73. package/dist/hooks-template/user-prompt-submit.sh +1162 -0
  74. package/dist/lib/activation.js +79 -0
  75. package/dist/lib/active-conflict-cache.js +141 -0
  76. package/dist/lib/active-memory.js +59 -0
  77. package/dist/lib/active-review-runner.js +26 -0
  78. package/dist/lib/agent-decision/index.js +25 -0
  79. package/dist/lib/agent-decision/keys.js +49 -0
  80. package/dist/lib/agent-decision/normalize-claude.js +183 -0
  81. package/dist/lib/agent-decision/types.js +21 -0
  82. package/dist/lib/agent-decision/validate.js +216 -0
  83. package/dist/lib/analytics/capture.js +96 -0
  84. package/dist/lib/analytics/command-event.js +267 -0
  85. package/dist/lib/analytics/consent.js +58 -0
  86. package/dist/lib/analytics/coverage-gap.js +96 -0
  87. package/dist/lib/analytics/envelope.js +236 -0
  88. package/dist/lib/analytics/event-id.js +86 -0
  89. package/dist/lib/analytics/evidence.js +150 -0
  90. package/dist/lib/analytics/followthrough.js +194 -0
  91. package/dist/lib/analytics/forwarder.js +109 -0
  92. package/dist/lib/analytics/logs.js +78 -0
  93. package/dist/lib/analytics/metrics.js +78 -0
  94. package/dist/lib/analytics/recorder.js +92 -0
  95. package/dist/lib/analytics/review-analytics.js +75 -0
  96. package/dist/lib/analytics/sequence.js +77 -0
  97. package/dist/lib/analytics/store.js +131 -0
  98. package/dist/lib/analytics/turn-recap.js +279 -0
  99. package/dist/lib/artifact_id.js +108 -0
  100. package/dist/lib/auth-breaker.js +161 -0
  101. package/dist/lib/auto-index.js +112 -0
  102. package/dist/lib/classifier.js +88 -0
  103. package/dist/lib/config.js +298 -0
  104. package/dist/lib/conflict-advisory.js +64 -0
  105. package/dist/lib/debug-bundle.js +520 -0
  106. package/dist/lib/enrichment/ingest.js +301 -0
  107. package/dist/lib/enrichment/plan.js +253 -0
  108. package/dist/lib/enrichment/protocol.js +359 -0
  109. package/dist/lib/enrichment/scout-brief.js +176 -0
  110. package/dist/lib/failure-telemetry.js +444 -0
  111. package/dist/lib/git.js +200 -0
  112. package/dist/lib/governance-cache.js +77 -0
  113. package/dist/lib/governed-path-cache.js +76 -0
  114. package/dist/lib/http.js +677 -0
  115. package/dist/lib/identity-envelope.js +23 -0
  116. package/dist/lib/kb-candidate.js +65 -0
  117. package/dist/lib/kb_acl.js +98 -0
  118. package/dist/lib/login.js +353 -0
  119. package/dist/lib/mcp-fetchers.js +130 -0
  120. package/dist/lib/mcp-restart.js +47 -0
  121. package/dist/lib/observability.js +805 -0
  122. package/dist/lib/open-url.js +33 -0
  123. package/dist/lib/orphan-guard.js +70 -0
  124. package/dist/lib/packaged.js +21 -0
  125. package/dist/lib/reconcile-sessions.js +171 -0
  126. package/dist/lib/redactor.js +89 -0
  127. package/dist/lib/relationship-candidate-query.js +27 -0
  128. package/dist/lib/render.js +611 -0
  129. package/dist/lib/rules/applicability.js +64 -0
  130. package/dist/lib/rules/attest-code-rule-version.js +47 -0
  131. package/dist/lib/rules/attest-notes-location.js +217 -0
  132. package/dist/lib/rules/attest-rule-version.js +69 -0
  133. package/dist/lib/rules/canonical-json.js +97 -0
  134. package/dist/lib/rules/ce0-emit.js +64 -0
  135. package/dist/lib/rules/ce0-evidence.js +281 -0
  136. package/dist/lib/rules/ce0-recall-sample.js +82 -0
  137. package/dist/lib/rules/ce0-rule.js +55 -0
  138. package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
  139. package/dist/lib/rules/ce0-store.js +683 -0
  140. package/dist/lib/rules/ce0-telemetry-project.js +93 -0
  141. package/dist/lib/rules/ce0-telemetry.js +158 -0
  142. package/dist/lib/rules/code-rule-registry.js +17 -0
  143. package/dist/lib/rules/command-match.js +185 -0
  144. package/dist/lib/rules/consult-evidence-binding.js +27 -0
  145. package/dist/lib/rules/consultation-capture-adapter.js +193 -0
  146. package/dist/lib/rules/content-match.js +56 -0
  147. package/dist/lib/rules/deny-admission.js +99 -0
  148. package/dist/lib/rules/durable-observation.js +190 -0
  149. package/dist/lib/rules/enforce-notes-version.js +421 -0
  150. package/dist/lib/rules/evaluation-input-hash.js +126 -0
  151. package/dist/lib/rules/evaluator.js +108 -0
  152. package/dist/lib/rules/inert-rule-families.js +51 -0
  153. package/dist/lib/rules/input-authority-resolver.js +241 -0
  154. package/dist/lib/rules/interception-schema.js +170 -0
  155. package/dist/lib/rules/interception-store.js +267 -0
  156. package/dist/lib/rules/live-input-authority.js +66 -0
  157. package/dist/lib/rules/local-matcher.js +108 -0
  158. package/dist/lib/rules/local-observe.js +79 -0
  159. package/dist/lib/rules/local-rule-version-repo.js +214 -0
  160. package/dist/lib/rules/memory-requirement.js +109 -0
  161. package/dist/lib/rules/notes-observe.js +39 -0
  162. package/dist/lib/rules/notes-path.js +261 -0
  163. package/dist/lib/rules/notes-rule.js +75 -0
  164. package/dist/lib/rules/observe-adapter.js +114 -0
  165. package/dist/lib/rules/observed-rule-hash.js +119 -0
  166. package/dist/lib/rules/prompt-submit-adapter.js +132 -0
  167. package/dist/lib/rules/requirement-subject.js +240 -0
  168. package/dist/lib/rules/rule-activity.js +67 -0
  169. package/dist/lib/rules/rule-version-hash.js +151 -0
  170. package/dist/lib/rules/runtime-scope.js +55 -0
  171. package/dist/lib/rules/stop-adapter.js +116 -0
  172. package/dist/lib/rules/stop-response-snapshot.js +174 -0
  173. package/dist/lib/rules/types.js +10 -0
  174. package/dist/lib/rules/ulid.js +46 -0
  175. package/dist/lib/rules/version-evaluation.js +156 -0
  176. package/dist/lib/scanner/agent-memory.js +99 -0
  177. package/dist/lib/scanner/bootstrap-summary.js +87 -0
  178. package/dist/lib/scanner/cache.js +59 -0
  179. package/dist/lib/scanner/frontmatter.js +42 -0
  180. package/dist/lib/scanner/parse-directives.js +69 -0
  181. package/dist/lib/scanner/parse-structured.js +72 -0
  182. package/dist/lib/scanner/render.js +73 -0
  183. package/dist/lib/scanner/scan.js +132 -0
  184. package/dist/lib/scanner/score.js +38 -0
  185. package/dist/lib/scanner/scout-mission.js +126 -0
  186. package/dist/lib/scanner/types.js +7 -0
  187. package/dist/lib/session-scope.js +195 -0
  188. package/dist/lib/spool.js +355 -0
  189. package/dist/lib/staleness.js +100 -0
  190. package/dist/lib/steer-cache.js +87 -0
  191. package/dist/lib/tagged-reference.js +20 -0
  192. package/dist/lib/temporal.js +109 -0
  193. package/dist/lib/turn-recap-emit.js +67 -0
  194. package/dist/lib/unwire.js +253 -0
  195. package/dist/lib/update-check.js +469 -0
  196. package/dist/lib/update-notifier.js +217 -0
  197. package/dist/lib/upgrade-apply.js +643 -0
  198. package/dist/lib/wire.js +1087 -0
  199. package/dist/lib/workspace.js +96 -0
  200. package/dist/lib/zip.js +154 -0
  201. package/dist/pretool-entry.js +37 -0
  202. package/package.json +75 -0
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AUTH_BREAKER_PATH = void 0;
37
+ exports.fingerprintToken = fingerprintToken;
38
+ exports.tripAuthBreaker = tripAuthBreaker;
39
+ exports.consultAuthBreaker = consultAuthBreaker;
40
+ exports.clearAuthBreaker = clearAuthBreaker;
41
+ const crypto = __importStar(require("crypto"));
42
+ const fs = __importStar(require("fs"));
43
+ const config_1 = require("./config");
44
+ // Dead-auth circuit breaker (incident: a dead `mla login` self-DoSing control).
45
+ //
46
+ // THE PROBLEM. When a user-token session's refresh token is genuinely dead
47
+ // (expired ~30d idle, revoked, or rotated out from under a stale process), every
48
+ // hook-driven `mla` call (heartbeat, steer-sync, flush, _internal refresh) does
49
+ // the same dance: authenticated control call -> 401 -> refresh -> 401 -> surface
50
+ // "login expired". With the editor firing those hooks on every tool use across
51
+ // several long-lived `mla mcp` workers, that became a tight validate+refresh
52
+ // storm against control (measured ~6-8 req/sec on constant hashed keys), i.e. a
53
+ // self-inflicted DoS that no server-side rate limit can cure (the server can only
54
+ // cheapen each rejection, not stop the client from asking).
55
+ //
56
+ // THE CURE. The FIRST process to have its refresh token REJECTED writes a small
57
+ // sentinel here, keyed to a one-way fingerprint of that exact refresh token.
58
+ // Every later user-token control call consults it first (consultAuthBreaker) and
59
+ // fails fast WITHOUT touching control. The flood collapses from "forever" to a
60
+ // bounded burst (one validate+refresh per process until the shared sentinel lands,
61
+ // then silence).
62
+ //
63
+ // WHY FINGERPRINT-KEYED (self-healing). The sentinel records sha256(refreshToken)
64
+ // .slice(0,16), NOT a bare "auth is dead" flag. consult re-reads the ON-DISK
65
+ // config (not the caller's possibly-stale in-memory cfg) and only stays open while
66
+ // the on-disk refresh token still matches the fingerprint. The instant the token
67
+ // changes (an `mla login` writes a fresh pair) the fingerprint no longer matches,
68
+ // consult clears the sentinel and lets the call through. That is what lets a
69
+ // re-login heal the long-lived `mla mcp` workers LIVE: the worker bound its cfg
70
+ // object once at boot, but consult reads disk, sees the new token, and reopens the
71
+ // gate without a restart. Comparing the in-memory token instead would wedge the
72
+ // worker forever (it would never observe the re-login).
73
+ //
74
+ // WHY THIS LEAKS NOTHING. The fingerprint is a one-way sha256 slice; it cannot be
75
+ // reversed to a token, and the file lives right next to cli-config.json (which
76
+ // holds the actual tokens, mode 0600) anyway.
77
+ // Sibling of CFG_PATH (config.ts), so it shares the config's home and is wiped by
78
+ // the same `rm -rf ~/.meetless` an operator already uses to reset.
79
+ exports.AUTH_BREAKER_PATH = `${config_1.HOME}/auth-dead.json`;
80
+ // One-way, non-reversible. Same construction as control's hashToken prefix and
81
+ // http.ts's rate-limit keys, so the value is recognizable in logs without
82
+ // exposing token material.
83
+ function fingerprintToken(token) {
84
+ return crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
85
+ }
86
+ // Trip the breaker for a specific refresh token that control just REJECTED. Only
87
+ // http.ts's refreshUserToken calls this, and only on a true "unauthorized"
88
+ // (401/410) outcome, never on a transient/throttled one, so the gate can never
89
+ // close on a mere rate-limit burst. Best-effort: a write failure just means the
90
+ // next process re-attempts the dance and trips it then.
91
+ function tripAuthBreaker(refreshToken, reason) {
92
+ const sentinel = {
93
+ refreshFingerprint: fingerprintToken(refreshToken),
94
+ deadSince: new Date().toISOString(),
95
+ reason,
96
+ };
97
+ try {
98
+ // Write-then-rename so a concurrent reader never sees a torn file (5 workers
99
+ // + N hooks share this path).
100
+ const tmp = `${exports.AUTH_BREAKER_PATH}.tmp.${process.pid}`;
101
+ fs.writeFileSync(tmp, JSON.stringify(sentinel) + "\n", { mode: 0o600 });
102
+ fs.renameSync(tmp, exports.AUTH_BREAKER_PATH);
103
+ }
104
+ catch {
105
+ // Best-effort sentinel; never let a breaker write break the caller.
106
+ }
107
+ }
108
+ // Returns true iff the breaker is OPEN: a prior refresh was rejected AND the
109
+ // on-disk refresh token still matches that rejection's fingerprint. Fails OPEN
110
+ // (returns true) for nothing: every uncertain path fails CLOSED (returns false,
111
+ // let the call proceed) so a bug here can never wedge a healthy session. Clears a
112
+ // stale sentinel as a side effect when the on-disk credential has moved on.
113
+ function consultAuthBreaker() {
114
+ let raw;
115
+ try {
116
+ raw = fs.readFileSync(exports.AUTH_BREAKER_PATH, "utf8");
117
+ }
118
+ catch {
119
+ return false; // No sentinel (the common, healthy case) -> proceed.
120
+ }
121
+ let sentinel;
122
+ try {
123
+ sentinel = JSON.parse(raw);
124
+ }
125
+ catch {
126
+ return false; // Torn/garbage read (concurrent writer) -> proceed, don't block.
127
+ }
128
+ if (!sentinel || typeof sentinel.refreshFingerprint !== "string") {
129
+ return false;
130
+ }
131
+ // Compare against the ON-DISK token, not the caller's in-memory cfg, so a
132
+ // re-login that rotated the token on disk reopens the gate for live workers.
133
+ let cfg;
134
+ try {
135
+ cfg = (0, config_1.readConfig)();
136
+ }
137
+ catch {
138
+ return false; // Config unreadable -> fail open, never block on our own read.
139
+ }
140
+ if (cfg.auth.mode !== "user-token") {
141
+ // Logged out / downgraded to shared-key: the dead user-token is irrelevant.
142
+ clearAuthBreaker();
143
+ return false;
144
+ }
145
+ if (fingerprintToken(cfg.auth.refreshToken) === sentinel.refreshFingerprint) {
146
+ return true; // Same dead token still on disk -> gate stays shut.
147
+ }
148
+ // The on-disk token changed (a re-login happened): the sentinel is stale.
149
+ clearAuthBreaker();
150
+ return false;
151
+ }
152
+ // Clear the breaker. Called on a successful login/refresh and on the stale-token
153
+ // path above. Idempotent and best-effort.
154
+ function clearAuthBreaker() {
155
+ try {
156
+ fs.unlinkSync(exports.AUTH_BREAKER_PATH);
157
+ }
158
+ catch {
159
+ // Already gone (never tripped, or another process cleared it first).
160
+ }
161
+ }
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.selectIndexTargets = selectIndexTargets;
37
+ exports.buildKbAddArgv = buildKbAddArgv;
38
+ // tools/meetless-agent/src/lib/auto-index.ts
39
+ // Pure selection + argv construction for the Zone 2 auto-index loop. Given the
40
+ // reduced Active Review records for a session, pick the produced docs to index
41
+ // into the owner's Personal KB and build the `mla kb add` argv for each. No I/O;
42
+ // the command layer (internal-auto-index.ts) owns the store read, the on-disk
43
+ // existence check, and the (fail-soft) add invocation.
44
+ // See notes/20260605-mla-auto-index-loop-implementation-plan.md.
45
+ const path = __importStar(require("path"));
46
+ const observability_1 = require("./observability");
47
+ // One target per (repoRootHash, canonicalPath). Only produced_doc records that
48
+ // carry a repoRoot are eligible: tagged_reference docs are user-named, not
49
+ // agent-produced, and a record without a repoRoot predates Phase A and cannot be
50
+ // resolved on disk. reduceActiveMemory yields records most-recent-last, so a later
51
+ // record overwrites an earlier one for the same key -> latest content wins.
52
+ function selectIndexTargets(records) {
53
+ const latest = new Map();
54
+ for (const r of records) {
55
+ if (r.kind !== "produced_doc")
56
+ continue;
57
+ if (!r.repoRoot || r.repoRoot.length === 0)
58
+ continue;
59
+ latest.set(`${r.repoRootHash}|${r.canonicalPath}`, r);
60
+ }
61
+ return Array.from(latest.values()).map((r) => ({
62
+ absPath: path.join(r.repoRoot, r.canonicalPath),
63
+ workspaceId: r.workspaceId,
64
+ canonicalPath: r.canonicalPath,
65
+ contentHash: r.contentHash,
66
+ }));
67
+ }
68
+ // The Zone 2 personal-KB add contract: agent_distilled provenance (ADVISORY echo
69
+ // under the two-axis model; the server derives recorded trust from the capture
70
+ // path), workspace pinned from the record (NOT marker-resolved, since the detached
71
+ // run has cwd=$HOME), and --queue so the add returns after the revision commits
72
+ // without blocking on the async GRAPH_EXTRACT job.
73
+ //
74
+ // NO --posture: commit e7f20756 removed the --posture contract from `mla kb add`
75
+ // (every notes ingest is born reviewOutcome=PENDING; LIVE/SHADOW posture is dead).
76
+ // `mla kb add` now REJECTS --posture as an unknown flag, so emitting it here made
77
+ // every auto-index ingest fail ("Unknown flag: --posture") -- session files were
78
+ // recorded but never ingested or mined for relationships. Keep this argv in lockstep
79
+ // with kb_add.ts's VALUE_FLAGS/BOOLEAN_FLAGS.
80
+ //
81
+ // --reingest-if-active makes this an add-or-UPDATE. Without it, a doc the agent
82
+ // produced once is ACTIVE in the KB, and every later edit re-runs `kb add` over an
83
+ // ACTIVE identity, which the kb add route hard-refuses ("use mla kb reingest"). The
84
+ // loop swallows that exit-2 as a failure, so a re-edited doc silently never accrues
85
+ // a second revision. With the flag, a changed body reingests in place (new revision)
86
+ // and a frontmatter-only change patches; an unchanged doc still no-ops.
87
+ function buildKbAddArgv(t, sessionId) {
88
+ const argv = [
89
+ t.absPath,
90
+ "--mode",
91
+ "file",
92
+ "--provenance",
93
+ "agent_distilled",
94
+ "--workspace",
95
+ t.workspaceId,
96
+ "--queue",
97
+ "--reingest-if-active",
98
+ ];
99
+ // Channel B (sync ingest): carry THIS session's raw Claude UUID through to the
100
+ // intel ingest route as `--agent-session <uuid>` (it rides the kb add HTTP body)
101
+ // so the workspace-authoritative sink composes the Langfuse session exactly once
102
+ // (INV-COMPOSE-ONCE). The value is canonicalized here (trim, uuid-shape, lowercase)
103
+ // but NEVER composed; an absent or malformed session simply omits the flag and the
104
+ // ingest still runs (intel falls back to its own grouping). No env var is read: the
105
+ // detached auto-index process has a bare environment, so the session arrives
106
+ // explicitly on its `--session` wire.
107
+ const agentSession = (0, observability_1.canonicalizeSessionId)(sessionId ?? null);
108
+ if (agentSession) {
109
+ argv.push("--agent-session", agentSession);
110
+ }
111
+ return argv;
112
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * File-path risk classifier.
4
+ *
5
+ * Maps a repository file path to a coarse risk category so an automated code
6
+ * reviewer can state deterministic pre-prompt facts ("this change touches
7
+ * auth", "this is a schema migration") before the LLM reasons about the diff.
8
+ *
9
+ * This is a generic, config-driven utility: it ships a DEFAULT_RULES ruleset
10
+ * built from COMMON repository conventions (a Prisma schema, a `migrations/`
11
+ * dir, an `auth`/`authz` dir, `webhooks`/`integrations`, a `prompts/` dir of
12
+ * YAML, an `outbox`/`handlers` dir, NestJS-style `*.controller.ts` / `*.dto.ts`
13
+ * or an `api/` dir, shell scripts and a `tools/` dir, docs). None of the rules
14
+ * encode any one repo's internal service map; point `classify` at your own
15
+ * ruleset (or extend DEFAULT_RULES) to fit a differently-structured codebase.
16
+ *
17
+ * Ordering is load-bearing. First match wins. Narrow rules come before broad
18
+ * rules (e.g. `*.controller.ts` before a bare `api/` dir).
19
+ *
20
+ * The ruleset is pinned by a VERSIONED fixture (`fixtures/risk-classifier.fixture.json`,
21
+ * `version: 1`) and a drift test (`test/lib/classifier.spec.ts`): every fixture
22
+ * case must classify to its declared category, the fixture's declared taxonomy
23
+ * must equal ALL_CATEGORIES, and every non-`unknown` category must be exercised.
24
+ * Adding or changing a rule therefore forces a matching fixture update (and, on
25
+ * a taxonomy change, a version bump).
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.DEFAULT_RULES = exports.ALL_CATEGORIES = void 0;
29
+ exports.classify = classify;
30
+ exports.classifyMany = classifyMany;
31
+ // Runtime list of every category. The drift test asserts the fixture's declared
32
+ // taxonomy equals this set, so the type and the fixture cannot drift apart.
33
+ exports.ALL_CATEGORIES = [
34
+ "schema_or_migration",
35
+ "auth_or_permission",
36
+ "external_integration",
37
+ "llm_prompt",
38
+ "outbox_or_handler",
39
+ "api_contract",
40
+ "cli_or_tooling",
41
+ "docs",
42
+ "unknown",
43
+ ];
44
+ const _categoryCompletenessCheck = true;
45
+ void _categoryCompletenessCheck;
46
+ // Generic default ruleset. Patterns are prefix-agnostic ((^|\/) anchors a path
47
+ // segment whether it is at the repo root or nested), so the same rule matches
48
+ // `auth/x.ts`, `src/auth/x.ts`, and `packages/api/src/auth/x.ts`.
49
+ exports.DEFAULT_RULES = [
50
+ { pattern: /(^|\/)prisma\/schema\.prisma$/, category: "schema_or_migration" },
51
+ { pattern: /(^|\/)migrations?\//, category: "schema_or_migration" },
52
+ { pattern: /(^|\/)(auth|authz)\//, category: "auth_or_permission" },
53
+ { pattern: /(^|\/)(webhooks?|integrations?)\//, category: "external_integration" },
54
+ { pattern: /(^|\/)prompts\/.*\.ya?ml$/, category: "llm_prompt" },
55
+ { pattern: /(^|\/)(outbox|handlers?)\//, category: "outbox_or_handler" },
56
+ { pattern: /\.(controller|dto)\.ts$/, category: "api_contract" },
57
+ { pattern: /(^|\/)api\//, category: "api_contract" },
58
+ { pattern: /(^|\/)tools\//, category: "cli_or_tooling" },
59
+ { pattern: /\.sh$/, category: "cli_or_tooling" },
60
+ { pattern: /(^|\/)docs\//, category: "docs" },
61
+ { pattern: /\.md$/, category: "docs" },
62
+ ];
63
+ function normalize(path) {
64
+ let p = path.replace(/\\/g, "/");
65
+ while (p.startsWith("./") || p.startsWith(".\\")) {
66
+ p = p.slice(2);
67
+ }
68
+ while (p.startsWith("/")) {
69
+ p = p.slice(1);
70
+ }
71
+ return p;
72
+ }
73
+ function classify(path, rules = exports.DEFAULT_RULES) {
74
+ if (!path)
75
+ return "unknown";
76
+ const normalized = normalize(path);
77
+ for (const rule of rules) {
78
+ if (rule.pattern.test(normalized))
79
+ return rule.category;
80
+ }
81
+ return "unknown";
82
+ }
83
+ function classifyMany(paths, rules = exports.DEFAULT_RULES) {
84
+ const out = {};
85
+ for (const p of paths)
86
+ out[p] = classify(p, rules);
87
+ return out;
88
+ }
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SESSION_GATE_DIR = exports.HOOKS_DIR = exports.QUEUE_DIR = exports.CFG_PATH = exports.HOME = exports.DEFAULT_CONSOLE_URL = exports.DEFAULT_INTEL_URL = exports.DEFAULT_CONTROL_URL = void 0;
37
+ exports.getConsoleUrl = getConsoleUrl;
38
+ exports.telemetryDisabled = telemetryDisabled;
39
+ exports.readConfig = readConfig;
40
+ exports.loadWorkspaceConfig = loadWorkspaceConfig;
41
+ exports.readKbConfig = readKbConfig;
42
+ exports.writeConfig = writeConfig;
43
+ exports.configExists = configExists;
44
+ exports.readUpdateConfig = readUpdateConfig;
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const workspace_1 = require("./workspace");
49
+ // Hosted prod defaults. A freshly-installed `mla` reaches the Meetless
50
+ // production backend out of the box with zero flags. Override to staging or
51
+ // local with the MEETLESS_BACKEND_URL / MEETLESS_INTEL_URL / MEETLESS_CONSOLE_URL
52
+ // env vars (or the matching cli-config.json fields), which always win over these.
53
+ exports.DEFAULT_CONTROL_URL = "https://control.meetless.ai";
54
+ exports.DEFAULT_INTEL_URL = "https://intel.meetless.ai";
55
+ exports.DEFAULT_CONSOLE_URL = "https://app.meetless.ai";
56
+ // Strip trailing slash so callers can always concatenate `${base}/relationships/<id>`
57
+ // without producing a `//` in the URL.
58
+ function getConsoleUrl(cfg) {
59
+ const raw = process.env.MEETLESS_CONSOLE_URL || cfg.consoleUrl || exports.DEFAULT_CONSOLE_URL;
60
+ return raw.replace(/\/+$/, "");
61
+ }
62
+ exports.HOME = process.env.MEETLESS_HOME || path.join(os.homedir(), ".meetless");
63
+ exports.CFG_PATH = path.join(exports.HOME, "cli-config.json");
64
+ exports.QUEUE_DIR = path.join(exports.HOME, "queue");
65
+ exports.HOOKS_DIR = path.join(exports.HOME, "hooks");
66
+ // Per-session OFF sentinels (`<sid>.off`) written by `mla mute` (removed by
67
+ // `mla unmute`) and read by meetless_session_disabled in common.sh. This is the
68
+ // per-session capture lifecycle, distinct from the `.meetless.json` workspace
69
+ // binding that `mla activate` / `mla deactivate` manage. Must match
70
+ // SESSION_GATE_DIR in common.sh.
71
+ exports.SESSION_GATE_DIR = path.join(exports.HOME, "session-gate");
72
+ // The master telemetry kill switch. The single source of truth for "no telemetry
73
+ // of any kind leaves (or, for the local deadletter, is even recorded on) this
74
+ // machine." It lives here in low-level config (not in observability.ts) so the
75
+ // trace plane, the analytics-consent gate, the debug command, AND the
76
+ // failure-telemetry deadletter can all share it without an import cycle.
77
+ // observability.ts re-exports it for back-compat. MEETLESS_TELEMETRY in
78
+ // {off,0,false,no} hard-disables; a truthy MEETLESS_NO_TELEMETRY does the same.
79
+ // An unset MEETLESS_TELEMETRY is NOT disabled here: per-plane opt-IN (analytics
80
+ // forwarding) is enforced at the forwarding sites, not by this hard switch.
81
+ function telemetryDisabled(env = process.env) {
82
+ const t = (env.MEETLESS_TELEMETRY || "").trim().toLowerCase();
83
+ if (t === "off" || t === "0" || t === "false" || t === "no")
84
+ return true;
85
+ const no = (env.MEETLESS_NO_TELEMETRY || "").trim().toLowerCase();
86
+ if (no && no !== "0" && no !== "false" && no !== "no")
87
+ return true;
88
+ return false;
89
+ }
90
+ function asString(value) {
91
+ return typeof value === "string" && value.length > 0 ? value : undefined;
92
+ }
93
+ // Normalize the optional on-disk `update` object into a UpdateConfig, or
94
+ // undefined when absent/empty so OnDiskConfig omits it (no defaults written to
95
+ // disk). autoApply is strictly boolean-true to opt in; anything else stays
96
+ // nag-only. channel falls back to "stable". Returns undefined when neither field
97
+ // is meaningfully set, so a bare `{}` does not get persisted back.
98
+ function normalizeUpdate(value) {
99
+ if (!value || typeof value !== "object")
100
+ return undefined;
101
+ const o = value;
102
+ const channel = asString(o.channel);
103
+ const autoApply = o.autoApply === true;
104
+ if (!autoApply && !channel)
105
+ return undefined;
106
+ return { autoApply, channel: channel ?? "stable" };
107
+ }
108
+ // Normalize a parsed user-token-shaped object (either `auth: {mode:'user-token'}`
109
+ // or a legacy expanded top-level) into the CliAuth user-token variant. Throws if
110
+ // the access token is absent: a user-token config with no bearer is corrupt and
111
+ // must fail loud, not silently degrade.
112
+ function normalizeUserToken(src) {
113
+ const accessToken = asString(src.accessToken);
114
+ if (!accessToken) {
115
+ throw new Error(`cli-config.json at ${exports.CFG_PATH} has auth.mode 'user-token' but no access token. ` +
116
+ "The login is corrupt. Run `mla logout` then `mla login`, or " +
117
+ "`mla init --control-token <T>` to fall back to shared-key.");
118
+ }
119
+ const user = src.user ?? {};
120
+ return {
121
+ mode: "user-token",
122
+ accessToken,
123
+ refreshToken: asString(src.refreshToken) ?? "",
124
+ accessExpiresAt: asString(src.accessExpiresAt) ?? "",
125
+ refreshExpiresAt: asString(src.refreshExpiresAt) ?? "",
126
+ sessionId: asString(src.sessionId) ?? "",
127
+ user: {
128
+ id: asString(user.id) ?? "",
129
+ displayName: asString(user.displayName) ?? "",
130
+ email: typeof user.email === "string" ? user.email : null,
131
+ role: asString(user.role) ?? "",
132
+ },
133
+ };
134
+ }
135
+ // The §6.4 compat shim: collapse the three accepted on-disk shapes into one
136
+ // CliAuth. (1) new nested `auth`; (2) legacy shared-key (top-level controlToken,
137
+ // no refresh); (3) legacy expanded user-token (top-level controlToken + refresh
138
+ // + authMode==='user-token'); absent everything => 'none'.
139
+ function normalizeAuthFromDisk(cfg) {
140
+ // (1) New nested shape wins when present and well-formed.
141
+ if (cfg.auth !== null && typeof cfg.auth === "object") {
142
+ const a = cfg.auth;
143
+ if (a.mode === "none")
144
+ return { mode: "none" };
145
+ if (a.mode === "shared-key") {
146
+ const accessToken = asString(a.accessToken);
147
+ if (accessToken)
148
+ return { mode: "shared-key", accessToken };
149
+ // shared-key with no token is meaningless; treat as logged out.
150
+ return { mode: "none" };
151
+ }
152
+ if (a.mode === "user-token") {
153
+ return normalizeUserToken(a);
154
+ }
155
+ // Unknown mode: fail loud rather than guess a credential path.
156
+ throw new Error(`cli-config.json at ${exports.CFG_PATH} has an unrecognized auth.mode. ` +
157
+ "Run `mla logout` then `mla login`, or `mla init --control-token <T>`.");
158
+ }
159
+ // (2)/(3) Legacy top-level controlToken.
160
+ const legacyToken = asString(cfg.controlToken);
161
+ if (legacyToken) {
162
+ if (asString(cfg.refreshToken) && cfg.authMode === "user-token") {
163
+ return normalizeUserToken({ ...cfg, accessToken: legacyToken });
164
+ }
165
+ return { mode: "shared-key", accessToken: legacyToken };
166
+ }
167
+ // Nothing on disk => terminal logged-out state.
168
+ return { mode: "none" };
169
+ }
170
+ function readConfig() {
171
+ if (!fs.existsSync(exports.CFG_PATH)) {
172
+ throw new Error(`cli-config.json not found at ${exports.CFG_PATH}. Run 'mla init' first.`);
173
+ }
174
+ const raw = fs.readFileSync(exports.CFG_PATH, "utf8");
175
+ const cfg = JSON.parse(raw);
176
+ // Non-credential env aliases select WHICH control plane, not WHO you are, so
177
+ // they are honored in every auth mode (CI / containers). MEETLESS_INTEL_ROOT
178
+ // is handled separately in the intel-root resolver.
179
+ const controlUrl = process.env.MEETLESS_BACKEND_URL || cfg.controlUrl || exports.DEFAULT_CONTROL_URL;
180
+ const intelUrl = process.env.MEETLESS_INTEL_URL || cfg.intelUrl || exports.DEFAULT_INTEL_URL;
181
+ const diskAuth = normalizeAuthFromDisk(cfg);
182
+ // §0.01 clause 4 / Finding H: under an on-disk user-token, MEETLESS_CONTROL_TOKEN
183
+ // is REJECTED loudly, never silently honored. Falling back to shared-key here
184
+ // would mis-attribute the operator's audited actions to INTERNAL_API_KEY.
185
+ const envSharedKey = process.env.MEETLESS_CONTROL_TOKEN;
186
+ let auth;
187
+ if (diskAuth.mode === "user-token" && envSharedKey) {
188
+ throw new Error(`MEETLESS_CONTROL_TOKEN is set but you are logged in as ${diskAuth.user.displayName}.\n` +
189
+ "Unset it (`unset MEETLESS_CONTROL_TOKEN`) or run `mla logout` first.");
190
+ }
191
+ else if (envSharedKey) {
192
+ // Env-driven shared key overrides a shared-key / none on-disk state (the
193
+ // documented CI / container path). It never silently overrides user-token
194
+ // (handled above).
195
+ auth = { mode: "shared-key", accessToken: envSharedKey };
196
+ }
197
+ else {
198
+ auth = diskAuth;
199
+ }
200
+ // workspaceId is intentionally NOT resolved here (folder = workspace, T1.1);
201
+ // loadWorkspaceConfig / readKbConfig add it from the `.meetless.json` marker.
202
+ // controlUrl/intelUrl always resolve (hosted prod default is the final
203
+ // fallback above), so there is no missing-config branch to guard here.
204
+ // actorUserId: pinned to auth.user.id under user-token (P3, so the actor
205
+ // header and session identity can never disagree); otherwise the preserved
206
+ // top-level value (migrated across the rewrite, Finding G).
207
+ const actorUserId = auth.mode === "user-token" ? auth.user.id : asString(cfg.actorUserId);
208
+ // Derived controlToken: the bearer for shared-key / user-token; empty for
209
+ // 'none' (a none-mode request fails fast at the http layer, §6.5).
210
+ const controlToken = auth.mode === "none" ? "" : auth.accessToken;
211
+ return {
212
+ controlUrl,
213
+ controlToken,
214
+ intelUrl,
215
+ mlaPath: cfg.mlaPath || "",
216
+ intelRoot: cfg.intelRoot,
217
+ consoleUrl: cfg.consoleUrl,
218
+ actorUserId,
219
+ update: normalizeUpdate(cfg.update),
220
+ auth,
221
+ };
222
+ }
223
+ // Load the machine config AND resolve the active workspace from the nearest
224
+ // `.meetless.json` marker (folder = workspace, T1.1). This is the single entry
225
+ // point every workspace-scoped command uses instead of readConfig(): it threads
226
+ // the marker-resolved id onto cfg.workspaceId so existing `cfg.workspaceId`
227
+ // call sites keep working, now sourced from the marker rather than cli-config.
228
+ //
229
+ // `override` is the admin `--workspace <id>` escape hatch (KB ops against
230
+ // another workspace): when provided and non-empty it short-circuits marker
231
+ // resolution, so the command never throws NotActivatedError just because the
232
+ // operator is acting cross-workspace from an unbound directory. When absent,
233
+ // resolveWorkspaceId() walks up from cwd and throws a clean "not activated"
234
+ // error if no marker is found.
235
+ function loadWorkspaceConfig(override) {
236
+ const cfg = readConfig();
237
+ const workspaceId = (override || "").trim() || (0, workspace_1.resolveWorkspaceId)();
238
+ return { ...cfg, workspaceId };
239
+ }
240
+ // KB curation loader: resolves the workspace from the marker (via
241
+ // loadWorkspaceConfig, honoring the optional `--workspace` admin override) AND
242
+ // enforces an actorUserId. `workspaceId` is therefore marker-sourced just like
243
+ // every other workspace-scoped command; cli-config is never consulted for it.
244
+ function readKbConfig(override) {
245
+ const cfg = loadWorkspaceConfig(override);
246
+ const actor = (cfg.actorUserId || "").trim();
247
+ if (!actor) {
248
+ throw new Error(`cli-config.json is missing required field 'actorUserId'. ` +
249
+ `KB curation commands stamp this onto every outbox event so the ` +
250
+ `audit trail records who acted. Re-run 'mla init --actor <id>' ` +
251
+ `or edit ${exports.CFG_PATH} directly to add it.`);
252
+ }
253
+ return { ...cfg, actorUserId: actor };
254
+ }
255
+ function writeConfig(cfg) {
256
+ fs.mkdirSync(exports.HOME, { recursive: true });
257
+ fs.mkdirSync(exports.QUEUE_DIR, { recursive: true });
258
+ fs.mkdirSync(exports.HOOKS_DIR, { recursive: true });
259
+ // P3: under a user session the actor is the authenticated user, full stop, so
260
+ // it can never drift from auth.user.id even if a caller passed a stale value.
261
+ const actorUserId = cfg.auth.mode === "user-token" ? cfg.auth.user.id : cfg.actorUserId;
262
+ // Serialize ONLY the canonical fields. `controlToken` is intentionally dropped:
263
+ // it is a read-time projection of auth.accessToken, persisting it would create
264
+ // two sources of truth that can silently diverge on the next login/refresh.
265
+ const onDisk = {
266
+ controlUrl: cfg.controlUrl,
267
+ ...(cfg.intelUrl ? { intelUrl: cfg.intelUrl } : {}),
268
+ ...(cfg.workspaceId ? { workspaceId: cfg.workspaceId } : {}),
269
+ mlaPath: cfg.mlaPath,
270
+ ...(cfg.intelRoot ? { intelRoot: cfg.intelRoot } : {}),
271
+ ...(cfg.consoleUrl ? { consoleUrl: cfg.consoleUrl } : {}),
272
+ ...(actorUserId ? { actorUserId } : {}),
273
+ ...(cfg.update ? { update: cfg.update } : {}),
274
+ auth: cfg.auth,
275
+ };
276
+ fs.writeFileSync(exports.CFG_PATH, JSON.stringify(onDisk, null, 2) + "\n", { mode: 0o600 });
277
+ }
278
+ function configExists() {
279
+ return fs.existsSync(exports.CFG_PATH);
280
+ }
281
+ // Throw-free reader for the self-upgrade hot path. The upgrade machinery (the
282
+ // apply-on-launch promote step and `mla upgrade`) can run BEFORE `mla init`
283
+ // exists, so it must never throw the "run mla init first" error readConfig
284
+ // raises on a missing file. Returns nag-only defaults (autoApply false, channel
285
+ // "stable") on a missing/corrupt/empty config and only enables autoApply when
286
+ // the on-disk `update.autoApply` is exactly true.
287
+ function readUpdateConfig() {
288
+ const fallback = { autoApply: false, channel: "stable" };
289
+ try {
290
+ if (!fs.existsSync(exports.CFG_PATH))
291
+ return fallback;
292
+ const raw = JSON.parse(fs.readFileSync(exports.CFG_PATH, "utf8"));
293
+ return normalizeUpdate(raw.update) ?? fallback;
294
+ }
295
+ catch {
296
+ return fallback;
297
+ }
298
+ }