@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,23 @@
1
+ "use strict";
2
+ // tools/meetless-agent/src/lib/identity-envelope.ts
3
+ // Canonical identity-envelope contract for Zone 1 (Active Review). The bash
4
+ // PostToolUse hook computes the same fields; these helpers define the one true
5
+ // shape so the TS reader and the bash writer cannot drift. See
6
+ // notes/20260604-auto-propose-produced-docs-to-kb.md (identity envelope, dedup key).
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.scopeKey = scopeKey;
9
+ exports.dedupIdentity = dedupIdentity;
10
+ // Owner-scoped partition key. Active Review dedup and TTL are evaluated WITHIN a
11
+ // scope, which is what makes cross-owner isolation (test 32) hold even though the
12
+ // dedup tuple below names content, not owner.
13
+ function scopeKey(e) {
14
+ return `${e.workspaceId}|${e.repoRootHash}|${e.ownerUserId}`;
15
+ }
16
+ // Full dedup identity. Spec lists the dedup tuple as
17
+ // workspaceId+repoRootHash+canonicalPath+contentHash+kind; we prefix it with the
18
+ // owner-scoped partition so identical content under two owners (test 32) or two
19
+ // repos (test 5) never collapses. This is the explicit resolution of the spec's
20
+ // dedup-key vs scope-key ambiguity: dedup is partitioned by scope.
21
+ function dedupIdentity(e) {
22
+ return [scopeKey(e), e.canonicalPath, e.contentHash, e.kind].join("|");
23
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ // Shared types + the mechanical-validity classifier for the B5 agent-proxy review
3
+ // commands (`mla kb pending`, `mla kb review`). See
4
+ // notes/20260603-mla-kb-agent-proxy-and-evidence-adoption.md §3 (B5) and P2.
5
+ //
6
+ // The classifier is the enforcement point of the P2 reject-only auto-resolution
7
+ // policy: an automated proxy (`mla kb review --agent`) may auto-REJECT a candidate
8
+ // ONLY when this returns autoRejectable, and may never auto-accept anything. So
9
+ // this gate must be conservative to the point of paranoia: a FALSE POSITIVE here
10
+ // discards a real edge (the exact "poison" the doc warns against), whereas a false
11
+ // negative merely routes the candidate to a human. We therefore implement only the
12
+ // mechanical conditions that are decidable from the candidate row alone with zero
13
+ // ambiguity, and deliberately SKIP the conditions that need server round-trips or
14
+ // the relation-type registry (deleted-doc, duplicate-of-accepted-LIVE-edge,
15
+ // invalid-relation-type, non-LIVE-revision). Those are surfaced to a human instead.
16
+ // The CLI also intentionally does not depend on @meetless/utils, so coupling the
17
+ // gate to RELATION_TYPE_REGISTRY (and risking false rejects on registry drift) is
18
+ // off the table by construction.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.AUTO_REJECT_CONFIDENCE_FLOOR = void 0;
21
+ exports.classifyMechanicalInvalidity = classifyMechanicalInvalidity;
22
+ exports.candidateConsoleUrl = candidateConsoleUrl;
23
+ // Confidence below this floor, combined with no supporting quote, marks a candidate
24
+ // as mechanical noise. Set below the detector's "medium" tier (~0.45) so it only
25
+ // ever fires on the genuinely unsupported tail, never on a borderline-real edge.
26
+ exports.AUTO_REJECT_CONFIDENCE_FLOOR = 0.3;
27
+ function hasSupportingQuote(ev) {
28
+ if (!ev)
29
+ return false;
30
+ const src = typeof ev.sourceQuote === "string" ? ev.sourceQuote.trim() : "";
31
+ const tgt = typeof ev.targetQuote === "string" ? ev.targetQuote.trim() : "";
32
+ return src.length > 0 || tgt.length > 0;
33
+ }
34
+ // Decide whether a candidate is MECHANICALLY invalid (unambiguous noise an agent may
35
+ // auto-reject). Pure; no I/O. See the module header for why this is conservative.
36
+ function classifyMechanicalInvalidity(c) {
37
+ // 1. Self-edge: same artifact on both endpoints. A relation from a doc to itself
38
+ // is structurally meaningless regardless of confidence. (A unary candidate with
39
+ // a null target is NOT a self-edge.)
40
+ if (c.targetArtifactId !== null &&
41
+ c.sourceArtifactId === c.targetArtifactId &&
42
+ (c.targetType === null || c.sourceType === c.targetType)) {
43
+ return {
44
+ autoRejectable: true,
45
+ reasonCode: "self_edge",
46
+ reason: `self-edge: source and target are the same artifact (${c.sourceArtifactId})`,
47
+ };
48
+ }
49
+ // 2. Very-low confidence AND no supporting quote: the detector neither believed it
50
+ // nor anchored it in text. With a quote present we defer to a human (the quote
51
+ // may carry the signal the score missed).
52
+ if (c.confidence < exports.AUTO_REJECT_CONFIDENCE_FLOOR && !hasSupportingQuote(c.evidenceJson)) {
53
+ return {
54
+ autoRejectable: true,
55
+ reasonCode: "low_confidence_no_quote",
56
+ reason: `confidence ${c.confidence.toFixed(2)} below floor ${exports.AUTO_REJECT_CONFIDENCE_FLOOR.toFixed(2)} with no supporting quote`,
57
+ };
58
+ }
59
+ return { autoRejectable: false, reasonCode: null, reason: null };
60
+ }
61
+ // Canonical Console deep link for a relationship candidate. consoleBase must already
62
+ // be trailing-slash-stripped (see getConsoleUrl).
63
+ function candidateConsoleUrl(consoleBase, candidateId) {
64
+ return `${consoleBase}/relationships/${candidateId}`;
65
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KbOwnerCheckError = void 0;
4
+ exports.verifyKbActorIsOwner = verifyKbActorIsOwner;
5
+ const http_1 = require("./http");
6
+ // KB curation §9.3 + §13.14 second bullet: every KB write command MUST refuse
7
+ // to run when the configured `actorUserId` is not a workspace OWNER. The
8
+ // doctor surfaces the same check up front so an operator can fix the config
9
+ // before invoking a write, but the doctor is advisory and an operator can
10
+ // (and does) skip running it. Without a runtime gate, a non-owner config
11
+ // would silently stamp an unauthorized identity onto an outbox event and the
12
+ // audit row would land as if the write were authorized. §9.5 leaves room for
13
+ // a future per-scope `KB_CURATE` permission; v1 is owner-only by design
14
+ // (proposal §11 locked decision Q8) and the gate lives here so all KB write
15
+ // commands share a single source of truth.
16
+ //
17
+ // Implementation parity with doctor.ts (§9.3):
18
+ // - Same endpoint: GET /internal/v1/whoami?workspaceId=<ws>&actorUserId=<id>
19
+ // - Same actorIsOwner resolution: trust the typed boolean when present,
20
+ // fall back to actor.role === "OWNER" so a rollout that ships the CLI
21
+ // before the new server field does not flap.
22
+ // - Same fail-message shape: name the actor, the workspace, and point to
23
+ // the doctor so the operator can re-verify after fixing the config.
24
+ const OWNER_CHECK_TIMEOUT_MS = 6000;
25
+ // The whoami probe is an idempotent GET, so a transient transport failure is
26
+ // safe to retry. Incident (session 2c881e60, 2026-06-14): the per-doc owner
27
+ // check inside `mla kb add` hit a single undici "fetch failed" connection blip
28
+ // right after a heavy ingest, threw with no retry, and the auto-index loop
29
+ // counted the produced doc `failed` with no in-run recovery -> the note was
30
+ // silently orphaned from the KB. A small bounded retry absorbs that blip for
31
+ // EVERY KB write command and the auto-index preflight without weakening the
32
+ // owner-only gate (a deterministic non-owner / 4xx verdict is never retried).
33
+ const OWNER_CHECK_MAX_ATTEMPTS = 3;
34
+ // Linear backoff between attempts. Index 0 is the wait after attempt 1, etc;
35
+ // the last entry is reused if maxAttempts is raised. Kept short: the common
36
+ // failure ("fetch failed" = connection refused/reset) rejects in milliseconds.
37
+ const OWNER_CHECK_BACKOFF_MS = [200, 500];
38
+ class KbOwnerCheckError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "KbOwnerCheckError";
42
+ }
43
+ }
44
+ exports.KbOwnerCheckError = KbOwnerCheckError;
45
+ function resolveActorIsOwner(body) {
46
+ if (!body)
47
+ return false;
48
+ if (typeof body.actorIsOwner === "boolean")
49
+ return body.actorIsOwner;
50
+ return body.actor?.role === "OWNER";
51
+ }
52
+ // A request that never received an HTTP response (DNS, ECONNREFUSED,
53
+ // ECONNRESET, socket hang up, fetch abort/timeout) rejects as a raw
54
+ // TypeError/DOMException with NO `status` (see HttpError doc in http.ts). A 5xx
55
+ // is a server-side transient. Both are worth a bounded retry. A 4xx (incl. a
56
+ // 401 that already survived doFetch's auto-refresh) is a deterministic verdict
57
+ // that a retry cannot change.
58
+ function isTransientHttpError(err) {
59
+ return err.status === undefined || err.status >= 500;
60
+ }
61
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
62
+ async function fetchWhoamiWithRetry(cfg, path, maxAttempts, sleep) {
63
+ for (let attempt = 1;; attempt++) {
64
+ try {
65
+ return await (0, http_1.get)(cfg, path, OWNER_CHECK_TIMEOUT_MS);
66
+ }
67
+ catch (e) {
68
+ const err = e;
69
+ if (attempt >= maxAttempts || !isTransientHttpError(err)) {
70
+ throw new KbOwnerCheckError(`KB owner check failed: could not reach control to verify actor ` +
71
+ `'${cfg.actorUserId}' for workspace '${cfg.workspaceId}'. ` +
72
+ `${err.message.slice(0, 200)}. ` +
73
+ `Run 'mla doctor' to diagnose; KB curation requires OWNER.`);
74
+ }
75
+ const backoff = OWNER_CHECK_BACKOFF_MS[Math.min(attempt - 1, OWNER_CHECK_BACKOFF_MS.length - 1)];
76
+ await sleep(backoff);
77
+ }
78
+ }
79
+ }
80
+ async function verifyKbActorIsOwner(cfg, opts = {}) {
81
+ const path = `/internal/v1/whoami?workspaceId=${encodeURIComponent(cfg.workspaceId)}` +
82
+ `&actorUserId=${encodeURIComponent(cfg.actorUserId)}`;
83
+ const maxAttempts = opts.maxAttempts ?? OWNER_CHECK_MAX_ATTEMPTS;
84
+ const sleep = opts.sleep ?? realSleep;
85
+ const body = await fetchWhoamiWithRetry(cfg, path, maxAttempts, sleep);
86
+ if (!body?.actor) {
87
+ throw new KbOwnerCheckError(`KB owner check failed: actor '${cfg.actorUserId}' is not a member of ` +
88
+ `workspace '${cfg.workspaceId}'. Edit cli-config.json to point at a ` +
89
+ `workspace OWNER, then re-run 'mla doctor' to confirm.`);
90
+ }
91
+ if (!resolveActorIsOwner(body)) {
92
+ const role = body.actor?.role ?? "UNKNOWN";
93
+ throw new KbOwnerCheckError(`KB owner check failed: actor '${cfg.actorUserId}' has role '${role}' ` +
94
+ `in workspace '${cfg.workspaceId}'; KB curation requires OWNER ` +
95
+ `(proposal §9.3 + §13.14). Re-point cli-config.actorUserId at a ` +
96
+ `workspace owner and re-run 'mla doctor'.`);
97
+ }
98
+ }
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ // `mla login` browser-login transport (proposal §6.1-§6.3, T21).
3
+ //
4
+ // Owns the loopback OAuth dance: generate PKCE, stand up a single-shot loopback
5
+ // HTTP listener on 127.0.0.1, open the Console authorize page in the OS browser,
6
+ // wait for the callback (or a 5-minute timeout), then exchange the one-time grant
7
+ // `code` + PKCE `codeVerifier` for a user-token bundle. No new runtime deps: Node
8
+ // built-in `http`, `crypto`, `child_process`, `os` only.
9
+ //
10
+ // Security invariants enforced here:
11
+ // - The exchange POST carries NO Authorization header (proposal §0.01 clause 1 /
12
+ // §4.1): the one-time `code` + PKCE `codeVerifier` in the JSON body ARE the
13
+ // proof-of-possession. Control rejects the call with 400 if any Authorization
14
+ // header is present, so we never send one.
15
+ // - The grant `code`, PKCE `codeVerifier`, and the returned access/refresh
16
+ // tokens are NEVER logged. We log only the authorize URL (which carries the
17
+ // PKCE *challenge*, a sha256 hash, and the `state` CSRF nonce, neither secret)
18
+ // and high-level status lines.
19
+ // - Loopback binds to 127.0.0.1 ONLY (never 0.0.0.0 / localhost) so no other
20
+ // host on the LAN can hit the callback (RFC 8252 §7.3).
21
+ // - `state` is compared constant-time; a mismatch is refused as possible CSRF.
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.generatePkce = generatePkce;
57
+ exports.generateState = generateState;
58
+ exports.consoleUrlFromControl = consoleUrlFromControl;
59
+ exports.openLoopbackServer = openLoopbackServer;
60
+ exports.openBrowser = openBrowser;
61
+ exports.exchangeGrant = exchangeGrant;
62
+ exports.runBrowserLogin = runBrowserLogin;
63
+ const child_process_1 = require("child_process");
64
+ const crypto = __importStar(require("crypto"));
65
+ const http = __importStar(require("http"));
66
+ const os = __importStar(require("os"));
67
+ function generatePkce() {
68
+ const verifier = crypto.randomBytes(32).toString("base64url");
69
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
70
+ return { verifier, challenge };
71
+ }
72
+ function generateState() {
73
+ return crypto.randomBytes(16).toString("base64url");
74
+ }
75
+ // Constant-time string compare. crypto.timingSafeEqual requires equal-length
76
+ // buffers, so a length mismatch short-circuits to false (and never throws).
77
+ function safeEqual(a, b) {
78
+ const ab = Buffer.from(a, "utf8");
79
+ const bb = Buffer.from(b, "utf8");
80
+ if (ab.length !== bb.length)
81
+ return false;
82
+ return crypto.timingSafeEqual(ab, bb);
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Console URL discovery (assumption table row 2). Inferred from the control URL
86
+ // via a small pair table, overridable by the caller (--console-url / cfg /
87
+ // MEETLESS_CONSOLE_URL, resolved in the command layer). Returns null when no
88
+ // pair matches so the caller can fail loud with a "pass --console-url" hint
89
+ // rather than silently guess a wrong origin.
90
+ //
91
+ // NOTE on the local dev port: the proposal's §6.1 example wrote
92
+ // `127.0.0.1:3006 -> http://127.0.0.1:3030`, but the Console dev server actually
93
+ // listens on 3003 (apps/console/package.json `next dev --port ...:-3003`); 3030
94
+ // appears nowhere in the repo. We use the verified 3003 here. The PRODUCTION pair
95
+ // (control.meetless.ai -> app.meetless.ai) matches DEFAULT_CONTROL_URL /
96
+ // DEFAULT_CONSOLE_URL in config.ts, so a fresh `mla login` against the default
97
+ // backend infers the console URL with no flag. Non-default backends (self-hosted
98
+ // or internal) pass --console-url explicitly, or set MEETLESS_CONSOLE_URL /
99
+ // consoleUrl in cli-config.json; we deliberately keep no extra hardcoded pairs.
100
+ // ---------------------------------------------------------------------------
101
+ const CONTROL_CONSOLE_PAIRS = [
102
+ { control: /^https?:\/\/(127\.0\.0\.1|localhost):3006(\/|$)/i, console: "http://127.0.0.1:3003" },
103
+ { control: /^https?:\/\/control\.meetless\.ai(\/|$)/i, console: "https://app.meetless.ai" },
104
+ ];
105
+ function consoleUrlFromControl(controlUrl) {
106
+ const trimmed = controlUrl.trim();
107
+ for (const pair of CONTROL_CONSOLE_PAIRS) {
108
+ if (pair.control.test(trimmed))
109
+ return pair.console;
110
+ }
111
+ return null;
112
+ }
113
+ const CLOSE_TAB_HTML = "<!doctype html><html><head><meta charset=utf-8><title>mla login</title></head>" +
114
+ "<body style=\"font-family:system-ui;margin:3rem;text-align:center\">" +
115
+ "<h2>You can close this tab.</h2>" +
116
+ "<p>Return to your terminal; <code>mla login</code> is finishing up.</p>" +
117
+ "</body></html>";
118
+ function openLoopbackServer(opts) {
119
+ return new Promise((resolveServer, rejectServer) => {
120
+ let settled = false;
121
+ let resolveCb;
122
+ let rejectCb;
123
+ const callbackPromise = new Promise((res, rej) => {
124
+ resolveCb = res;
125
+ rejectCb = rej;
126
+ });
127
+ const server = http.createServer((req, res) => {
128
+ // Parse against a fixed 127.0.0.1 base; req.url is path+query only.
129
+ let parsed;
130
+ try {
131
+ parsed = new URL(req.url ?? "/", "http://127.0.0.1");
132
+ }
133
+ catch {
134
+ res.writeHead(400, { "Content-Type": "text/plain" });
135
+ res.end("Bad request");
136
+ return;
137
+ }
138
+ if (req.method !== "GET" || parsed.pathname !== "/callback") {
139
+ res.writeHead(404, { "Content-Type": "text/plain" });
140
+ res.end("Not found");
141
+ return;
142
+ }
143
+ const gotState = parsed.searchParams.get("state") ?? "";
144
+ const code = parsed.searchParams.get("code") ?? "";
145
+ // Constant-time CSRF check. NEVER log the query string (it carries `code`).
146
+ if (!safeEqual(gotState, opts.state)) {
147
+ res.writeHead(400, { "Content-Type": "text/plain" });
148
+ res.end("State mismatch: possible CSRF; refusing");
149
+ return;
150
+ }
151
+ if (settled) {
152
+ // Single-shot: a second valid callback is a no-op ack.
153
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
154
+ res.end(CLOSE_TAB_HTML);
155
+ return;
156
+ }
157
+ if (!code) {
158
+ settled = true;
159
+ res.writeHead(400, { "Content-Type": "text/plain" });
160
+ res.end("Missing authorization code");
161
+ rejectCb(new Error("Loopback callback arrived without an authorization code."));
162
+ return;
163
+ }
164
+ settled = true;
165
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
166
+ res.end(CLOSE_TAB_HTML);
167
+ resolveCb({ code });
168
+ });
169
+ server.on("error", (err) => {
170
+ if (!settled) {
171
+ settled = true;
172
+ rejectServer(err);
173
+ }
174
+ });
175
+ // port 0 => kernel picks a free port; a caller-supplied port pins it (for
176
+ // SSH `-L` forwarding under --no-browser). host 127.0.0.1 only.
177
+ server.listen(opts.port ?? 0, "127.0.0.1", () => {
178
+ const addr = server.address();
179
+ if (addr === null || typeof addr === "string") {
180
+ server.close();
181
+ rejectServer(new Error("Failed to bind loopback server to 127.0.0.1."));
182
+ return;
183
+ }
184
+ resolveServer({ server, port: addr.port, callbackPromise });
185
+ });
186
+ });
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // Browser launcher (§6.2). No new dependency; platform-appropriate spawn.
190
+ // Returns the launcher's exit code (0 = launched). A non-zero result is a SOFT
191
+ // fallback, not a failure: the caller prints the URL for manual open and keeps
192
+ // the loopback listening.
193
+ // ---------------------------------------------------------------------------
194
+ function spawnOpener(cmd, args) {
195
+ return new Promise((resolve) => {
196
+ try {
197
+ const child = (0, child_process_1.spawn)(cmd, args, { stdio: "ignore" });
198
+ child.on("error", () => resolve(127)); // ENOENT / not installed
199
+ child.on("exit", (code) => resolve(code ?? 0));
200
+ }
201
+ catch {
202
+ resolve(127);
203
+ }
204
+ });
205
+ }
206
+ async function openBrowser(url) {
207
+ const platform = process.platform;
208
+ if (platform === "darwin")
209
+ return spawnOpener("open", [url]);
210
+ if (platform === "win32")
211
+ return spawnOpener("cmd", ["/c", "start", "", url]);
212
+ // Linux / *BSD: try xdg-open, then sensible-browser, then $BROWSER.
213
+ const candidates = ["xdg-open", "sensible-browser"];
214
+ const envBrowser = process.env.BROWSER;
215
+ if (envBrowser && envBrowser.trim().length > 0)
216
+ candidates.push(envBrowser.trim());
217
+ for (const cmd of candidates) {
218
+ const code = await spawnOpener(cmd, [url]);
219
+ if (code === 0)
220
+ return 0;
221
+ }
222
+ return 1;
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Grant exchange (§4.1). Raw fetch with NO Authorization header. We deliberately
226
+ // bypass http.ts's doFetch (which always stamps `Authorization: Bearer`) because
227
+ // control rejects this endpoint with 400 unexpected_authorization_header if any
228
+ // Authorization header is present (§0.01 clause 1).
229
+ // ---------------------------------------------------------------------------
230
+ async function exchangeGrant(controlUrl, code, codeVerifier, timeoutMs = 15000) {
231
+ const url = `${controlUrl.replace(/\/+$/, "")}/internal/v1/auth/cli-login-grants/exchange`;
232
+ const controller = new AbortController();
233
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
234
+ try {
235
+ const res = await fetch(url, {
236
+ method: "POST",
237
+ // Content-Type only; explicitly NO Authorization header.
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ code, codeVerifier }),
240
+ signal: controller.signal,
241
+ });
242
+ const text = await res.text();
243
+ if (!res.ok) {
244
+ // Body may name the failure (e.g. invalid_or_expired); surface it without
245
+ // echoing the request (which holds the code + verifier).
246
+ throw new Error(`Grant exchange failed: HTTP ${res.status}${text ? `: ${text.slice(0, 300)}` : ""}`);
247
+ }
248
+ let parsed;
249
+ try {
250
+ parsed = JSON.parse(text);
251
+ }
252
+ catch {
253
+ throw new Error("Grant exchange returned a non-JSON response.");
254
+ }
255
+ return assertTokenBundle(parsed);
256
+ }
257
+ finally {
258
+ clearTimeout(timer);
259
+ }
260
+ }
261
+ // Minimal structural validation so we never persist a corrupt user-token config.
262
+ function assertTokenBundle(value) {
263
+ const b = value;
264
+ if (!b ||
265
+ typeof b.accessToken !== "string" ||
266
+ !b.accessToken ||
267
+ typeof b.refreshToken !== "string" ||
268
+ !b.refreshToken ||
269
+ typeof b.sessionId !== "string" ||
270
+ !b.user ||
271
+ typeof b.user.id !== "string" ||
272
+ !b.workspace ||
273
+ typeof b.workspace.id !== "string") {
274
+ throw new Error("Grant exchange response was missing required token/identity fields.");
275
+ }
276
+ return b;
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Orchestration: runBrowserLogin (public) / runLoopbackLogin (internal).
280
+ // ---------------------------------------------------------------------------
281
+ function buildAuthUrl(consoleUrl, args) {
282
+ const authUrl = new URL(`${consoleUrl.replace(/\/+$/, "")}/cli/authorize`);
283
+ authUrl.searchParams.set("state", args.state);
284
+ authUrl.searchParams.set("code_challenge", args.challenge);
285
+ authUrl.searchParams.set("code_challenge_method", "S256");
286
+ authUrl.searchParams.set("redirect_uri", `http://127.0.0.1:${args.port}/callback`);
287
+ authUrl.searchParams.set("client_id", "mla");
288
+ authUrl.searchParams.set("machine_hint", os.hostname());
289
+ authUrl.searchParams.set("os", `${os.type()} ${os.release()}`);
290
+ return authUrl;
291
+ }
292
+ // A rejecting timer with a cancel handle so the winning branch of Promise.race
293
+ // can clear it (otherwise the timer keeps the event loop alive for 5 minutes).
294
+ function rejectingTimeout(ms, message) {
295
+ let timer;
296
+ const promise = new Promise((_resolve, reject) => {
297
+ timer = setTimeout(() => reject(new Error(message)), ms);
298
+ });
299
+ return { promise, cancel: () => clearTimeout(timer) };
300
+ }
301
+ const FIVE_MINUTES_MS = 5 * 60 * 1000;
302
+ async function runBrowserLogin(opts) {
303
+ const log = opts.log ?? ((m) => console.log(m));
304
+ const openBrowserFn = opts.openBrowserFn ?? openBrowser;
305
+ const exchangeFn = opts.exchangeFn ?? exchangeGrant;
306
+ const timeoutMs = opts.timeoutMs ?? FIVE_MINUTES_MS;
307
+ const consoleUrl = (opts.consoleUrl && opts.consoleUrl.trim()) || consoleUrlFromControl(opts.controlUrl);
308
+ if (!consoleUrl) {
309
+ throw new Error(`Could not infer the Console URL from control URL "${opts.controlUrl}". ` +
310
+ "Pass --console-url <url> (or set consoleUrl in cli-config.json / MEETLESS_CONSOLE_URL).");
311
+ }
312
+ return runLoopbackLogin(opts.controlUrl, consoleUrl, {
313
+ noBrowser: opts.noBrowser ?? false,
314
+ port: opts.port,
315
+ timeoutMs,
316
+ log,
317
+ openBrowserFn,
318
+ exchangeFn,
319
+ });
320
+ }
321
+ async function runLoopbackLogin(controlUrl, consoleUrl, deps) {
322
+ const { verifier, challenge } = generatePkce();
323
+ const state = generateState();
324
+ const { server, port, callbackPromise } = await openLoopbackServer({ state, port: deps.port });
325
+ const authUrl = buildAuthUrl(consoleUrl, { state, challenge, port }).toString();
326
+ const timer = rejectingTimeout(deps.timeoutMs, "Authorization timed out after 5 minutes.");
327
+ try {
328
+ if (deps.noBrowser) {
329
+ deps.log("Open this URL in a browser on this machine to authorize:");
330
+ deps.log(` ${authUrl}`);
331
+ }
332
+ else {
333
+ deps.log(`Opening browser to ${authUrl}`);
334
+ const exit = await deps.openBrowserFn(authUrl);
335
+ if (exit !== 0) {
336
+ // Soft fallback within the loopback flow (NOT device-code): the browser
337
+ // could not be launched, so print the URL and keep listening.
338
+ deps.log("Could not open a browser automatically. Open this URL manually:");
339
+ deps.log(` ${authUrl}`);
340
+ }
341
+ }
342
+ deps.log("Waiting for authorization (up to 5 minutes)...");
343
+ const { code } = await Promise.race([callbackPromise, timer.promise]);
344
+ // Exchange the one-time code + PKCE verifier for tokens. No Authorization
345
+ // header (§0.01 clause 1). NEVER log `code` or `verifier`.
346
+ const bundle = await deps.exchangeFn(controlUrl, code, verifier);
347
+ return bundle;
348
+ }
349
+ finally {
350
+ timer.cancel();
351
+ server.close();
352
+ }
353
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeControlFetchFromCli = makeControlFetchFromCli;
4
+ exports.makeIntelFetchFromCli = makeIntelFetchFromCli;
5
+ exports.makeIntelAskFromCli = makeIntelAskFromCli;
6
+ const http_1 = require("./http");
7
+ // LLM answer synthesis at /v1/ask routinely runs 15+ seconds (retrieval plus
8
+ // generation), past intelPost's 15s default, which is sized for fast control
9
+ // and intel POSTs. Without its own deadline the AbortController fires mid-flight
10
+ // and the MCP query tool's answer mode returns "This operation was aborted".
11
+ // Give synthesis a generous timeout; override via env for slower models.
12
+ const ASK_SYNTHESIS_TIMEOUT_MS = Number(process.env.MEETLESS_ASK_TIMEOUT_MS) || 60_000;
13
+ const DEFAULT_CONTROL_VERBS = {
14
+ get: http_1.get,
15
+ post: http_1.post,
16
+ patch: http_1.patch,
17
+ };
18
+ const DEFAULT_INTEL_VERBS = {
19
+ get: http_1.intelGet,
20
+ post: http_1.intelPost,
21
+ patch: http_1.intelPatch,
22
+ };
23
+ function parseBody(init) {
24
+ if (!init || init.body === undefined)
25
+ return undefined;
26
+ return JSON.parse(init.body);
27
+ }
28
+ function dispatch(verbs, cfg, pathAndQuery, init) {
29
+ const method = (init?.method ?? "GET").toUpperCase();
30
+ switch (method) {
31
+ case "GET":
32
+ return verbs.get(cfg, pathAndQuery);
33
+ case "POST":
34
+ return verbs.post(cfg, pathAndQuery, parseBody(init));
35
+ case "PATCH":
36
+ return verbs.patch(cfg, pathAndQuery, parseBody(init));
37
+ default:
38
+ throw new Error(`mcp-fetchers: unsupported method ${method}`);
39
+ }
40
+ }
41
+ /**
42
+ * Reactive single-retry refresh: control's verbs auto-refresh internally, but
43
+ * intel's (intelGet/intelPost/intelPatch) do NOT. So for the intel surface we
44
+ * wrap the call: on a 401 in user-token mode, rotate the token once
45
+ * (refreshUserToken mutates cfg.controlToken in place) and retry. Any other
46
+ * error, or a refresh that did not actually rotate ("expired"/"busy"), is
47
+ * rethrown untouched so handlers can still read err.status (e.g. kb 404).
48
+ */
49
+ async function withIntelRefresh(cfg, refresh, fn) {
50
+ try {
51
+ return await fn();
52
+ }
53
+ catch (err) {
54
+ const status = err?.status;
55
+ if (status === 401 && cfg.auth.mode === "user-token") {
56
+ const outcome = await refresh(cfg);
57
+ if (outcome === "refreshed") {
58
+ return await fn();
59
+ }
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+ /** Control fetch closure over http.ts (auto-refreshing) bound to this cfg. */
65
+ function makeControlFetchFromCli(cfg, verbs = DEFAULT_CONTROL_VERBS) {
66
+ return (pathAndQuery, init) => dispatch(verbs, cfg, pathAndQuery, init);
67
+ }
68
+ /** Intel fetch closure over http.ts with a reactive 401 refresh-and-retry. */
69
+ function makeIntelFetchFromCli(cfg, verbs = DEFAULT_INTEL_VERBS, refresh = http_1.refreshUserToken) {
70
+ return (pathAndQuery, init) => withIntelRefresh(cfg, refresh, () => dispatch(verbs, cfg, pathAndQuery, init));
71
+ }
72
+ /**
73
+ * /v1/ask closure, byte-compatible with ask_modes.js makeIntelAsk (same payload
74
+ * keys + defaults: surface "mcp", mode "answer", filters {}, max 8 / min 3,
75
+ * as_of omitted when absent/null), but posting through http.ts (intelPost) so it
76
+ * carries the user-token bearer and inherits the same reactive refresh.
77
+ */
78
+ function makeIntelAskFromCli(cfg, intelPostFn = http_1.intelPost, refresh = http_1.refreshUserToken) {
79
+ return (params) => {
80
+ const payload = {
81
+ workspace_id: params.workspaceId,
82
+ question: params.question,
83
+ surface: params.surface ?? "mcp",
84
+ stream: false,
85
+ language: params.language ?? "en",
86
+ thread_text: params.threadText ?? null,
87
+ mode: params.mode ?? "answer",
88
+ filters: params.filters ?? {},
89
+ max_results: params.maxResults ?? 8,
90
+ min_results: params.minResults ?? 3,
91
+ };
92
+ // Keep the body byte-identical to today when no cutoff is supplied.
93
+ if (params.asOf !== undefined && params.asOf !== null) {
94
+ payload.as_of = params.asOf;
95
+ }
96
+ return withIntelRefresh(cfg, refresh, async () => {
97
+ try {
98
+ return await intelPostFn(cfg, "/v1/ask", payload, ASK_SYNTHESIS_TIMEOUT_MS);
99
+ }
100
+ catch (err) {
101
+ if (isSynthesisTimeout(err)) {
102
+ throw synthesisTimeoutError();
103
+ }
104
+ throw err;
105
+ }
106
+ });
107
+ };
108
+ }
109
+ /**
110
+ * The deadline fired before synthesis returned. undici rejects an aborted fetch
111
+ * with a DOMException named "AbortError" (message "This operation was aborted"),
112
+ * which intelPost rethrows raw. We discriminate on the name, not the message.
113
+ */
114
+ function isSynthesisTimeout(err) {
115
+ return err?.name === "AbortError";
116
+ }
117
+ /**
118
+ * Replaces the cryptic raw "This operation was aborted" with a message that
119
+ * names the failure and tells the operator what to do. Two real causes produce
120
+ * this: intel synthesis genuinely ran long, OR this `mla mcp` server process was
121
+ * spawned before the timeout fix landed and is running stale in-memory code
122
+ * (Node does not hot-reload dist) — so "restart your editor" is a real remedy.
123
+ */
124
+ function synthesisTimeoutError() {
125
+ const secs = Math.round(ASK_SYNTHESIS_TIMEOUT_MS / 1000);
126
+ return new Error(`Meetless answer synthesis timed out after ${secs}s. Intel may be under ` +
127
+ `load, or this MCP server process predates the timeout fix (restart your ` +
128
+ `editor to respawn it). For an immediate result, use mode "search" or the ` +
129
+ `retrieve_knowledge tool (no synthesis), or raise MEETLESS_ASK_TIMEOUT_MS.`);
130
+ }