@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,677 @@
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.DEFAULT_INTEL_URL = void 0;
37
+ exports.buildRequestHeaders = buildRequestHeaders;
38
+ exports.refreshUserToken = refreshUserToken;
39
+ exports.get = get;
40
+ exports.post = post;
41
+ exports.patch = patch;
42
+ exports.buildIntelHeaders = buildIntelHeaders;
43
+ exports.intelGet = intelGet;
44
+ exports.intelPost = intelPost;
45
+ exports.intelPatch = intelPatch;
46
+ exports.ping = ping;
47
+ const fs = __importStar(require("fs"));
48
+ const config_1 = require("./config");
49
+ const auth_breaker_1 = require("./auth-breaker");
50
+ const observability_1 = require("./observability");
51
+ function buildError(status, body, method, url) {
52
+ const e = new Error(`${method} ${url} -> HTTP ${status}: ${body.slice(0, 500)}`);
53
+ e.status = status;
54
+ e.body = body;
55
+ return e;
56
+ }
57
+ // Wedge v6 Epoch 28: Build per-request headers. Content-Type is set ONLY when
58
+ // there is a body. Sending `Content-Type: application/json` on a body-less GET
59
+ // is HTTP-semantically wrong (RFC 7231 §3.1.1.5) AND a documented platform
60
+ // trap: Express's `body-parser` json() middleware on certain Node versions
61
+ // silently 400s a body-less request that advertises a JSON content type. The
62
+ // failure mode is invisible (no body in the 400 response) and the CLI's
63
+ // HttpError surfaces "HTTP 400: " with no diagnostic. Past production breakage
64
+ // is recorded in CLAUDE.md "Hard-Won Platform Lessons" -> macOS/Node.js.
65
+ //
66
+ // T1.4 (folder = workspace): when an actor is supplied (cli-config.actorUserId)
67
+ // it is stamped as X-Meetless-Actor on EVERY control request. The membership
68
+ // guard (INV-AUTH-1) needs the caller identity to resolve a WorkspaceUser; the
69
+ // header is harmless on reads and load-bearing on agent-review writes (it also
70
+ // covers agent-traces, which never carry an actor in the body). A blank/
71
+ // whitespace-only actor is treated as absent so the config-less `mla init` path
72
+ // stays header-free.
73
+ function buildRequestHeaders(token, hasBody, actorUserId) {
74
+ const h = {
75
+ Authorization: `Bearer ${token}`,
76
+ };
77
+ if (hasBody) {
78
+ h["Content-Type"] = "application/json";
79
+ }
80
+ // Stamp the run's trace_id on every outbound request so Sentry tags, intel's
81
+ // Langfuse traces, and any server-side scope share a single id. CLI never
82
+ // reads X-Trace-ID off the response; the run's id is immutable.
83
+ const traceId = (0, observability_1.getRunTraceId)();
84
+ if (traceId) {
85
+ h["X-Trace-ID"] = traceId;
86
+ }
87
+ if (actorUserId && actorUserId.trim().length > 0) {
88
+ h["X-Meetless-Actor"] = actorUserId;
89
+ }
90
+ return h;
91
+ }
92
+ // P2.1 / P2.2: span helper. Child spans wrap every outbound HTTP call so the
93
+ // Langfuse trace renders one span per `mlaFetch` with route, http.status, and
94
+ // latency_ms attributes. plane is "intel" or "control"; route is derived from
95
+ // the URL path via routeNameFromPath so id-shaped segments roll up cleanly.
96
+ // Returns null when no tracer is registered (mla init / config-less paths),
97
+ // so the http layer no-ops cheaply.
98
+ function startHttpSpan(plane, method, path) {
99
+ const tracer = (0, observability_1.getRunTracer)();
100
+ if (!tracer)
101
+ return { handle: null, startMs: Date.now() };
102
+ const handle = tracer.startSpan({
103
+ name: `${plane}.${(0, observability_1.routeNameFromPath)(path)}`,
104
+ });
105
+ handle.setAttribute("http.method", method);
106
+ handle.setAttribute("route", path);
107
+ return { handle, startMs: Date.now() };
108
+ }
109
+ function endHttpSpan(ctx, outcome) {
110
+ const { handle, startMs } = ctx;
111
+ if (!handle)
112
+ return;
113
+ const latencyMs = Date.now() - startMs;
114
+ handle.setAttribute("latency_ms", latencyMs);
115
+ if (outcome.kind === "ok") {
116
+ handle.setAttribute("http.status", outcome.status);
117
+ handle.end({ status: "ok", output: { status: outcome.status, latency_ms: latencyMs } });
118
+ return;
119
+ }
120
+ if (outcome.kind === "http_error") {
121
+ handle.setAttribute("http.status", outcome.status);
122
+ handle.end({
123
+ status: "error",
124
+ output: { status: outcome.status, latency_ms: latencyMs },
125
+ });
126
+ return;
127
+ }
128
+ handle.end({ status: "error", error: outcome.error, output: { latency_ms: latencyMs } });
129
+ }
130
+ // Single-shot control request: exactly one fetch, no auth-mode policy, no retry.
131
+ // doFetch (below) wraps this with the none-mode fail-fast and the user-token
132
+ // auto-refresh dance (§6.5). Splitting them keeps the refresh retry a clean
133
+ // "call doFetchOnce again with the rotated token" rather than re-entrant.
134
+ async function doFetchOnce(cfg, method, path, body, timeoutMs = 10000) {
135
+ const url = `${cfg.controlUrl}${path}`;
136
+ const controller = new AbortController();
137
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
138
+ const hasBody = body !== undefined && body !== null;
139
+ const span = startHttpSpan("control", method, path);
140
+ try {
141
+ let res;
142
+ try {
143
+ res = await fetch(url, {
144
+ method,
145
+ headers: buildRequestHeaders(cfg.controlToken, hasBody, cfg.actorUserId),
146
+ body: hasBody ? JSON.stringify(body) : undefined,
147
+ signal: controller.signal,
148
+ });
149
+ }
150
+ catch (err) {
151
+ endHttpSpan(span, { kind: "network_error", error: err });
152
+ throw err;
153
+ }
154
+ const text = await res.text();
155
+ if (!res.ok) {
156
+ endHttpSpan(span, { kind: "http_error", status: res.status });
157
+ throw buildError(res.status, text, method, url);
158
+ }
159
+ endHttpSpan(span, { kind: "ok", status: res.status });
160
+ if (!text)
161
+ return {};
162
+ try {
163
+ return JSON.parse(text);
164
+ }
165
+ catch {
166
+ return { raw: text };
167
+ }
168
+ }
169
+ finally {
170
+ clearTimeout(timer);
171
+ }
172
+ }
173
+ // Control request with the §6.4/§6.5 auth policy layered on top of doFetchOnce:
174
+ // - `mode: 'none'` -> fail fast with "not logged in" (Blocking 3), unless the
175
+ // caller is an unauthenticated probe (doctor health).
176
+ // - `mode: 'user-token'` + 401 -> transparently refresh the access token once
177
+ // (concurrency-safe, §6.5 clause 7) and retry the original request ONCE. A
178
+ // second 401 becomes `auth_expired`.
179
+ // - `mode: 'shared-key'` -> a 401 propagates directly (the operator rotated the
180
+ // shared key out of band; they must re-run `mla init --control-token <NEW>`).
181
+ async function doFetch(cfg, method, path, body, timeoutMs = 10000, opts = {}) {
182
+ if (!opts.allowUnauthenticated && cfg.auth.mode === "none") {
183
+ throw notLoggedInError();
184
+ }
185
+ // Dead-auth circuit breaker. Once control has REJECTED this exact on-disk
186
+ // refresh token (tripAuthBreaker, below), fail fast WITHOUT touching control so
187
+ // a dead session's hooks (heartbeat, steer-sync, flush) stop self-DoSing it
188
+ // with a validate+refresh storm. consultAuthBreaker re-reads disk and self-
189
+ // clears the moment the token changes (an `mla login`), so a re-login heals even
190
+ // the long-lived `mla mcp` workers live. Skipped for unauthenticated probes and
191
+ // for shared-key (no refresh token to be rejected).
192
+ if (!opts.allowUnauthenticated &&
193
+ cfg.auth.mode === "user-token" &&
194
+ (0, auth_breaker_1.consultAuthBreaker)()) {
195
+ throw authExpiredError();
196
+ }
197
+ try {
198
+ return await doFetchOnce(cfg, method, path, body, timeoutMs);
199
+ }
200
+ catch (e) {
201
+ const err = e;
202
+ // Auto-refresh applies ONLY to a user-token 401. Unauthenticated probes,
203
+ // shared-key, none, network errors, and non-401 statuses all propagate.
204
+ if (opts.allowUnauthenticated ||
205
+ err.status !== 401 ||
206
+ cfg.auth.mode !== "user-token") {
207
+ throw e;
208
+ }
209
+ const outcome = await refreshUserToken(cfg);
210
+ if (outcome === "busy") {
211
+ throw refreshBusyError();
212
+ }
213
+ if (outcome === "expired") {
214
+ throw authExpiredError();
215
+ }
216
+ // "refreshed": cfg now carries the rotated token. Retry exactly once.
217
+ try {
218
+ const out = await doFetchOnce(cfg, method, path, body, timeoutMs);
219
+ // A call that succeeds after a refresh proves auth recovered; ensure no
220
+ // dead-auth sentinel lingers (belt-and-suspenders with consult's self-clear).
221
+ (0, auth_breaker_1.clearAuthBreaker)();
222
+ return out;
223
+ }
224
+ catch (e2) {
225
+ const err2 = e2;
226
+ if (err2.status === 401) {
227
+ throw authExpiredError();
228
+ }
229
+ throw e2;
230
+ }
231
+ }
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // Auth-policy errors (§6.4, §6.5). All carry an empty `body` and a
235
+ // human-readable message that NEVER contains a token.
236
+ // ---------------------------------------------------------------------------
237
+ function notLoggedInError() {
238
+ const e = new Error("Not logged in. Run `mla login` (or `mla init --control-token <T>`).");
239
+ e.body = "";
240
+ return e;
241
+ }
242
+ function authExpiredError() {
243
+ // §6.5: invisible until the refresh token itself expires (~30 days idle).
244
+ const e = new Error("Your CLI login expired. Run `mla login`.");
245
+ e.status = 401;
246
+ e.body = "";
247
+ return e;
248
+ }
249
+ function refreshBusyError() {
250
+ const e = new Error("Another mla process is refreshing the login. Retry in a moment.");
251
+ e.body = "";
252
+ return e;
253
+ }
254
+ // Fail-fast guard for the intel plane (which always needs a real bearer; intel
255
+ // validates it via control, §7). Mirrors doFetch's none-mode reject. Intel does
256
+ // NOT auto-refresh in v1: refresh is scoped to control's doFetch (§6.5). A
257
+ // user-token whose access token expired refreshes on its next control call; the
258
+ // rotated token (cfg mutated in place) is then used by any later intel call in
259
+ // the same run.
260
+ function assertIntelAuthed(cfg) {
261
+ if (cfg.auth.mode === "none") {
262
+ throw notLoggedInError();
263
+ }
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // Concurrency-safe access-token refresh (§6.5, §0.01 clause 7).
267
+ //
268
+ // Two `mla` processes (e.g. the detached auto-index loop and an interactive
269
+ // `mla review`) can 401 on the SAME on-disk refresh token at the same instant.
270
+ // Refresh tokens are single-use (the server rotates on every call, §9), so a
271
+ // naive double-refresh would let one rotation win and tear the other's session
272
+ // down with a spurious "login expired". The lock + re-read makes the loser adopt
273
+ // the winner's freshly-rotated token instead of POSTing a now-dead one.
274
+ // ---------------------------------------------------------------------------
275
+ // Sidecar advisory lock, NOT the config file itself: a crashed holder can never
276
+ // corrupt cli-config.json, and a stale lock is safe to steal.
277
+ const LOCK_PATH = `${config_1.CFG_PATH}.lock`;
278
+ // Cap the wait for an interactive command (§6.5 clause 1: "e.g. 5s"). On expiry
279
+ // we surface "retry" rather than hang.
280
+ const LOCK_WAIT_CAP_MS = 5000;
281
+ const LOCK_POLL_MS = 75;
282
+ // A lock older than this is treated as abandoned (holder crashed) and stolen.
283
+ // Comfortably above the refresh HTTP timeout so we never steal a live refresh.
284
+ const LOCK_STALE_MS = 30000;
285
+ const REFRESH_TIMEOUT_MS = 10000;
286
+ // Treat an access token expiring within this window as already-expired, so we
287
+ // never adopt a token that would die mid-request.
288
+ const ACCESS_SKEW_MS = 5000;
289
+ function sleep(ms) {
290
+ return new Promise((resolve) => setTimeout(resolve, ms));
291
+ }
292
+ // Acquire the exclusive sidecar lock. Returns the open fd, or null if the cap
293
+ // elapsed while another process held it (caller maps null -> "busy"/"retry").
294
+ async function acquireRefreshLock() {
295
+ const deadline = Date.now() + LOCK_WAIT_CAP_MS;
296
+ for (;;) {
297
+ try {
298
+ // `wx`: create-and-fail-if-exists is the atomic test-and-set.
299
+ const fd = fs.openSync(LOCK_PATH, "wx");
300
+ try {
301
+ fs.writeSync(fd, `${process.pid} ${Date.now()}\n`);
302
+ }
303
+ catch {
304
+ // Best effort: the lock IS the file's existence, not its content.
305
+ }
306
+ return fd;
307
+ }
308
+ catch (e) {
309
+ if (e.code !== "EEXIST")
310
+ throw e;
311
+ // Held. Steal only a clearly-abandoned (stale) lock, then retry.
312
+ try {
313
+ const age = Date.now() - fs.statSync(LOCK_PATH).mtimeMs;
314
+ if (age > LOCK_STALE_MS) {
315
+ fs.unlinkSync(LOCK_PATH);
316
+ continue;
317
+ }
318
+ }
319
+ catch {
320
+ // Holder released between EEXIST and stat: just retry the create.
321
+ }
322
+ if (Date.now() >= deadline)
323
+ return null;
324
+ await sleep(LOCK_POLL_MS);
325
+ }
326
+ }
327
+ }
328
+ function releaseRefreshLock(fd) {
329
+ if (fd === null)
330
+ return;
331
+ try {
332
+ fs.closeSync(fd);
333
+ }
334
+ catch {
335
+ /* ignore */
336
+ }
337
+ try {
338
+ fs.unlinkSync(LOCK_PATH);
339
+ }
340
+ catch {
341
+ // A stale-steal by another process may have already removed it.
342
+ }
343
+ }
344
+ function accessTokenStillFresh(accessExpiresAt) {
345
+ const ms = Date.parse(accessExpiresAt) - Date.now();
346
+ return !Number.isNaN(ms) && ms > ACCESS_SKEW_MS;
347
+ }
348
+ // Adopt a (re-read or freshly-rotated) user-token into the caller's in-memory
349
+ // cfg so the retry, and the rest of this run, use it without re-reading disk.
350
+ function adoptAuth(cfg, auth) {
351
+ cfg.auth = auth;
352
+ cfg.controlToken = auth.accessToken;
353
+ cfg.actorUserId = auth.user.id;
354
+ }
355
+ // POST the refresh token (body proof-of-possession; NO Authorization header,
356
+ // the access token is dead). The refresh token is NEVER logged. Returns the
357
+ // wire body, or a sentinel: "unauthorized" (refresh token itself is dead ->
358
+ // re-login) vs "transient" (network/5xx -> retry, session untouched).
359
+ async function callRefresh(controlUrl, refreshToken) {
360
+ const url = `${controlUrl.replace(/\/+$/, "")}/internal/v1/auth/token/refresh`;
361
+ const controller = new AbortController();
362
+ const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
363
+ try {
364
+ let res;
365
+ try {
366
+ res = await fetch(url, {
367
+ method: "POST",
368
+ headers: { "Content-Type": "application/json" },
369
+ body: JSON.stringify({ refreshToken }),
370
+ signal: controller.signal,
371
+ });
372
+ }
373
+ catch {
374
+ return "transient"; // timeout / DNS / connection refused
375
+ }
376
+ if (res.status === 401 || res.status === 410)
377
+ return "unauthorized";
378
+ if (!res.ok)
379
+ return "transient"; // 5xx etc: server broken, not session dead
380
+ const text = await res.text();
381
+ try {
382
+ return JSON.parse(text);
383
+ }
384
+ catch {
385
+ return "transient";
386
+ }
387
+ }
388
+ finally {
389
+ clearTimeout(timer);
390
+ }
391
+ }
392
+ // The lock + re-read + (maybe) rotate critical section (§6.5 clauses 1-3). The
393
+ // lock is held ONLY here, never across the original API request. Releases on
394
+ // every exit path via finally.
395
+ //
396
+ // Exported (Part 3) so `mla _internal refresh` can trigger the SAME
397
+ // concurrency-safe refresh the in-process auto-refresh uses. The hook-triggered
398
+ // caller is byte-identical to the doFetch caller from the config file's view: it
399
+ // shares the sidecar lock, single-flight re-read, and atomic writeConfig. Bash
400
+ // performs no token crypto, persistence, or refresh HTTP of its own.
401
+ async function refreshUserToken(cfg) {
402
+ // Defensive: doFetch only calls this for user-token, but guard anyway.
403
+ if (cfg.auth.mode !== "user-token")
404
+ return "expired";
405
+ const fd = await acquireRefreshLock();
406
+ if (fd === null)
407
+ return "busy"; // another process is mid-refresh; tell operator to retry
408
+ try {
409
+ // Clause 2: re-read AFTER the lock. Another process may have rotated while
410
+ // we waited.
411
+ let fresh;
412
+ try {
413
+ fresh = (0, config_1.readConfig)();
414
+ }
415
+ catch {
416
+ // Config became unreadable (corrupt, or the Gate-4 env conflict appeared
417
+ // mid-run). We cannot safely refresh; the operator must re-login / fix env.
418
+ return "expired";
419
+ }
420
+ if (fresh.auth.mode !== "user-token") {
421
+ // Another process logged out or downgraded the config underneath us.
422
+ return "expired";
423
+ }
424
+ // Already rotated by another process: adopt it, NO network call (this is the
425
+ // case that prevents the double-rotation race).
426
+ if (accessTokenStillFresh(fresh.auth.accessExpiresAt)) {
427
+ adoptAuth(cfg, fresh.auth);
428
+ return "refreshed";
429
+ }
430
+ // Clause 3: still expired -> rotate against control.
431
+ const rotated = await callRefresh(fresh.controlUrl, fresh.auth.refreshToken);
432
+ if (rotated === "transient") {
433
+ // Do NOT tear the session down on a transient outage: the on-disk refresh
434
+ // token is untouched and still valid. Surface as "retry".
435
+ return "busy";
436
+ }
437
+ if (rotated === "unauthorized") {
438
+ // The refresh token itself was REJECTED (401/410): the session is genuinely
439
+ // dead, not throttled. Trip the breaker keyed to THIS token so every later
440
+ // call (this process and the other hooks/workers sharing the config) fails
441
+ // fast instead of re-hammering control. A transient/throttled outcome maps to
442
+ // "transient"->"busy" above and never reaches here, so a rate-limit burst
443
+ // (the server's new 429) can never trip the breaker.
444
+ (0, auth_breaker_1.tripAuthBreaker)(fresh.auth.refreshToken, "refresh_rejected");
445
+ return "expired";
446
+ }
447
+ if (rotated.accessToken === null) {
448
+ // RaceRecoveryResult: the server saw a benign race and minted no new pair.
449
+ // Re-read once: the winning process's rotation may now be on disk. NEVER
450
+ // writeConfig the null tokens.
451
+ let after;
452
+ try {
453
+ after = (0, config_1.readConfig)();
454
+ }
455
+ catch {
456
+ return "expired";
457
+ }
458
+ if (after.auth.mode === "user-token" &&
459
+ accessTokenStillFresh(after.auth.accessExpiresAt)) {
460
+ adoptAuth(cfg, after.auth);
461
+ return "refreshed";
462
+ }
463
+ return "expired";
464
+ }
465
+ // Normal rotation. Refresh does not change identity; only the tokens rotate,
466
+ // so preserve user + sessionId (fall back to the wire sessionId if present).
467
+ if (!rotated.refreshToken ||
468
+ !rotated.accessExpiresAt ||
469
+ !rotated.refreshExpiresAt) {
470
+ // Malformed success body: treat as transient rather than persist a partial
471
+ // credential.
472
+ return "busy";
473
+ }
474
+ const newAuth = {
475
+ mode: "user-token",
476
+ accessToken: rotated.accessToken,
477
+ refreshToken: rotated.refreshToken,
478
+ accessExpiresAt: rotated.accessExpiresAt,
479
+ refreshExpiresAt: rotated.refreshExpiresAt,
480
+ sessionId: rotated.sessionId ?? fresh.auth.sessionId,
481
+ user: fresh.auth.user,
482
+ };
483
+ (0, config_1.writeConfig)({
484
+ ...fresh,
485
+ auth: newAuth,
486
+ controlToken: newAuth.accessToken,
487
+ actorUserId: newAuth.user.id,
488
+ });
489
+ adoptAuth(cfg, newAuth);
490
+ return "refreshed";
491
+ }
492
+ finally {
493
+ releaseRefreshLock(fd);
494
+ }
495
+ }
496
+ async function get(cfg, path, timeoutMs) {
497
+ return (await doFetch(cfg, "GET", path, undefined, timeoutMs));
498
+ }
499
+ async function post(cfg, path, body, timeoutMs) {
500
+ return (await doFetch(cfg, "POST", path, body, timeoutMs));
501
+ }
502
+ async function patch(cfg, path, body, timeoutMs) {
503
+ return (await doFetch(cfg, "PATCH", path, body, timeoutMs));
504
+ }
505
+ // Intel reads (KB inspector, T18). Intel is a SEPARATE base URL from control
506
+ // (cfg.intelUrl, default 127.0.0.1:8100) but accepts the same bearer the hook
507
+ // uses for /v1/intercept + /v1/ask: cfg.controlToken IS intel's INTERNAL_API_KEY
508
+ // in the dogfood config (see user-prompt-submit.sh INTEL_TOKEN). Keeping the
509
+ // token source identical to the hook avoids a second secret in cli-config.json.
510
+ exports.DEFAULT_INTEL_URL = "http://127.0.0.1:8100";
511
+ // Build per-request headers for intel calls. Mirrors buildRequestHeaders for
512
+ // control: stamp X-Trace-ID when a run-local id exists so intel adopts it as
513
+ // the Langfuse trace id (intel/app/core/context.py:55). hasBody gates
514
+ // Content-Type to avoid the Express bodyParser silent-400 trap on GET.
515
+ function buildIntelHeaders(token, hasBody) {
516
+ const h = {
517
+ Authorization: `Bearer ${token}`,
518
+ };
519
+ if (hasBody) {
520
+ h["Content-Type"] = "application/json";
521
+ }
522
+ const traceId = (0, observability_1.getRunTraceId)();
523
+ if (traceId) {
524
+ h["X-Trace-ID"] = traceId;
525
+ }
526
+ // X-Agent-Session-ID carries the raw canonical Claude UUID (Channel A). Intel
527
+ // stores it verbatim on RequestContext and composes the workspace-namespaced
528
+ // Langfuse session exactly once at its telemetry sink, so the CLI never sends
529
+ // the composed value. Stamped only when a run-local session id exists; absent
530
+ // means "no agent session" (console fallback at intel), and the value is
531
+ // already canonicalized so it cannot inject a header.
532
+ const sessionId = (0, observability_1.getRunSessionId)();
533
+ if (sessionId) {
534
+ h["X-Agent-Session-ID"] = sessionId;
535
+ }
536
+ return h;
537
+ }
538
+ async function intelGet(cfg, path, timeoutMs = 10000) {
539
+ assertIntelAuthed(cfg);
540
+ const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
541
+ const url = `${base}${path}`;
542
+ const controller = new AbortController();
543
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
544
+ const span = startHttpSpan("intel", "GET", path);
545
+ try {
546
+ let res;
547
+ try {
548
+ res = await fetch(url, {
549
+ method: "GET",
550
+ headers: buildIntelHeaders(cfg.controlToken, false),
551
+ signal: controller.signal,
552
+ });
553
+ }
554
+ catch (err) {
555
+ endHttpSpan(span, { kind: "network_error", error: err });
556
+ throw err;
557
+ }
558
+ (0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
559
+ const text = await res.text();
560
+ if (!res.ok) {
561
+ endHttpSpan(span, { kind: "http_error", status: res.status });
562
+ throw buildError(res.status, text, "GET", url);
563
+ }
564
+ endHttpSpan(span, { kind: "ok", status: res.status });
565
+ if (!text)
566
+ return {};
567
+ try {
568
+ return JSON.parse(text);
569
+ }
570
+ catch {
571
+ return { raw: text };
572
+ }
573
+ }
574
+ finally {
575
+ clearTimeout(timer);
576
+ }
577
+ }
578
+ async function intelPost(cfg, path, body, timeoutMs = 15000) {
579
+ assertIntelAuthed(cfg);
580
+ const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
581
+ const url = `${base}${path}`;
582
+ const controller = new AbortController();
583
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
584
+ const span = startHttpSpan("intel", "POST", path);
585
+ try {
586
+ let res;
587
+ try {
588
+ res = await fetch(url, {
589
+ method: "POST",
590
+ headers: buildIntelHeaders(cfg.controlToken, true),
591
+ body: JSON.stringify(body),
592
+ signal: controller.signal,
593
+ });
594
+ }
595
+ catch (err) {
596
+ endHttpSpan(span, { kind: "network_error", error: err });
597
+ throw err;
598
+ }
599
+ (0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
600
+ const text = await res.text();
601
+ if (!res.ok) {
602
+ endHttpSpan(span, { kind: "http_error", status: res.status });
603
+ throw buildError(res.status, text, "POST", url);
604
+ }
605
+ endHttpSpan(span, { kind: "ok", status: res.status });
606
+ if (!text)
607
+ return {};
608
+ try {
609
+ return JSON.parse(text);
610
+ }
611
+ catch {
612
+ return { raw: text };
613
+ }
614
+ }
615
+ finally {
616
+ clearTimeout(timer);
617
+ }
618
+ }
619
+ // Intel writes via PATCH (KB posture flip, `mla kb promote`). Mirrors intelPost
620
+ // exactly: same intel base URL, same buildIntelHeaders(controlToken, true), same
621
+ // span plane, same error handling, same JSON parse. Only the HTTP method differs.
622
+ async function intelPatch(cfg, path, body, timeoutMs = 15000) {
623
+ assertIntelAuthed(cfg);
624
+ const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
625
+ const url = `${base}${path}`;
626
+ const controller = new AbortController();
627
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
628
+ const span = startHttpSpan("intel", "PATCH", path);
629
+ try {
630
+ let res;
631
+ try {
632
+ res = await fetch(url, {
633
+ method: "PATCH",
634
+ headers: buildIntelHeaders(cfg.controlToken, true),
635
+ body: JSON.stringify(body),
636
+ signal: controller.signal,
637
+ });
638
+ }
639
+ catch (err) {
640
+ endHttpSpan(span, { kind: "network_error", error: err });
641
+ throw err;
642
+ }
643
+ (0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
644
+ const text = await res.text();
645
+ if (!res.ok) {
646
+ endHttpSpan(span, { kind: "http_error", status: res.status });
647
+ throw buildError(res.status, text, "PATCH", url);
648
+ }
649
+ endHttpSpan(span, { kind: "ok", status: res.status });
650
+ if (!text)
651
+ return {};
652
+ try {
653
+ return JSON.parse(text);
654
+ }
655
+ catch {
656
+ return { raw: text };
657
+ }
658
+ }
659
+ finally {
660
+ clearTimeout(timer);
661
+ }
662
+ }
663
+ async function ping(cfg, path) {
664
+ try {
665
+ // allowUnauthenticated: doctor pings the UNAUTHENTICATED /internal/v1/health
666
+ // route to prove connectivity. A `mode: 'none'` config must not fail-fast
667
+ // here (there is genuinely no session, but the probe still answers), and the
668
+ // probe has no token to auto-refresh. Goes through doFetch directly to pass
669
+ // the bypass that `get` cannot express.
670
+ await doFetch(cfg, "GET", path, undefined, 5000, { allowUnauthenticated: true });
671
+ return { ok: true };
672
+ }
673
+ catch (e) {
674
+ const err = e;
675
+ return { ok: false, status: err.status, error: err.message };
676
+ }
677
+ }