@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,226 @@
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.parseLabelArgs = parseLabelArgs;
37
+ exports.runLabel = runLabel;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ // `mla label` -- the A3 operator-label affordance (notes/20260603-mla-kb-agent
42
+ // -proxy-and-evidence-adoption.md §3, §7.2). A lightweight way for the operator
43
+ // to mark a handful of enrichments useful / noisy / harmful / prevented-a
44
+ // -mistake. It writes the reserved `operator_label` block back into a trace line
45
+ // in ~/.meetless/logs/ask-traces.jsonl. Low volume, high signal: this is the
46
+ // ground-truth anchor the composite needs before any weight tuning, and the
47
+ // `harmful` flag is the exact field the A5 carry-forward hook reads to suppress
48
+ // a re-surface, so a `--harmful` label here closes that loop.
49
+ //
50
+ // mla label [<trace_id>] [--useful] [--noisy] [--harmful]
51
+ // [--prevented-mistake] [--note <text>]
52
+ //
53
+ // With no <trace_id> it labels the LATEST trace in the current session, scoping
54
+ // to CLAUDE_CODE_SESSION_ID exactly like `mla summary` / `mla review`. The
55
+ // parent Claude Code shell exports that var, so the operator labels "the
56
+ // enrichment I just saw" without copying its id off the prompt. Pass an explicit
57
+ // trace_id to label any past trace from outside a session.
58
+ //
59
+ // This is the WRITE side of the block that `mla summary` reads and tallies; it
60
+ // is deliberately a standalone command, not a revival of the removed `mla
61
+ // traces` tree (that subtree was dropped 2026-05-31, see summary.ts header).
62
+ // Paths resolve lazily from MEETLESS_HOME (same fallback as lib/config + summary)
63
+ // so the short-lived CLI picks up the operator's env and tests can point at a
64
+ // temp dir.
65
+ function logDir() {
66
+ return path.join(process.env.MEETLESS_HOME || path.join(os.homedir(), ".meetless"), "logs");
67
+ }
68
+ function tracesFile() {
69
+ return path.join(logDir(), "ask-traces.jsonl");
70
+ }
71
+ function readLines() {
72
+ if (!fs.existsSync(tracesFile()))
73
+ return [];
74
+ return fs
75
+ .readFileSync(tracesFile(), "utf8")
76
+ .split("\n")
77
+ .filter((l) => l.trim().length > 0);
78
+ }
79
+ function parse(line) {
80
+ try {
81
+ const o = JSON.parse(line);
82
+ return o && typeof o === "object" ? o : null;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ function parseLabelArgs(argv) {
89
+ let traceId = null;
90
+ const patch = {};
91
+ let any = false;
92
+ for (let i = 0; i < argv.length; i++) {
93
+ const a = argv[i];
94
+ if (a === "--useful") {
95
+ patch.useful = true;
96
+ any = true;
97
+ }
98
+ else if (a === "--noisy") {
99
+ patch.noisy = true;
100
+ any = true;
101
+ }
102
+ else if (a === "--harmful") {
103
+ patch.harmful = true;
104
+ any = true;
105
+ }
106
+ else if (a === "--prevented-mistake") {
107
+ patch.prevented_mistake = true;
108
+ any = true;
109
+ }
110
+ else if (a === "--note") {
111
+ const v = argv[++i];
112
+ if (v === undefined)
113
+ throw new Error("--note requires a value");
114
+ patch.notes = v;
115
+ any = true;
116
+ }
117
+ else if (a.startsWith("-")) {
118
+ throw new Error(`Unknown flag for \`mla label\`: ${a}`);
119
+ }
120
+ else if (traceId === null) {
121
+ traceId = a;
122
+ }
123
+ else {
124
+ throw new Error(`Unexpected extra argument: ${a}`);
125
+ }
126
+ }
127
+ if (!any) {
128
+ throw new Error("Provide at least one of --useful / --noisy / --harmful / --prevented-mistake / --note.");
129
+ }
130
+ return { traceId, patch };
131
+ }
132
+ // Compact, deterministic render of the merged label state for the confirmation
133
+ // line. Shows the FULL resulting block (not just this patch) so the operator
134
+ // sees the cumulative verdict after a merge.
135
+ function renderLabel(l) {
136
+ const parts = [];
137
+ if (l.useful === true)
138
+ parts.push("useful");
139
+ if (l.noisy === true)
140
+ parts.push("noisy");
141
+ if (l.harmful === true)
142
+ parts.push("harmful");
143
+ if (l.prevented_mistake === true)
144
+ parts.push("prevented-mistake");
145
+ if (typeof l.notes === "string" && l.notes.length > 0)
146
+ parts.push(`note="${l.notes}"`);
147
+ return parts.length ? parts.join(", ") : "(no flags set)";
148
+ }
149
+ function runLabel(argv) {
150
+ let args;
151
+ try {
152
+ args = parseLabelArgs(argv);
153
+ }
154
+ catch (e) {
155
+ console.error(e.message);
156
+ return 2;
157
+ }
158
+ const lines = readLines();
159
+ if (lines.length === 0) {
160
+ console.error(`No traces found at ${tracesFile()}.`);
161
+ return 1;
162
+ }
163
+ // Resolve which line index(es) to label, plus a label for the confirmation.
164
+ let targetIdxs;
165
+ let what;
166
+ if (args.traceId) {
167
+ // Explicit trace_id: rewrite every matching line. trace_id is a unique join
168
+ // key, but if a line were ever duplicated we label them all so the read side
169
+ // can never see a stale copy.
170
+ targetIdxs = lines.flatMap((line, i) => (parse(line)?.trace_id === args.traceId ? [i] : []));
171
+ if (targetIdxs.length === 0) {
172
+ console.error(`Trace not found: ${args.traceId}`);
173
+ return 1;
174
+ }
175
+ what = args.traceId;
176
+ }
177
+ else {
178
+ // Default selector: the latest trace in the current session.
179
+ const session = (process.env.CLAUDE_CODE_SESSION_ID || "").trim();
180
+ if (!session) {
181
+ console.error("No <trace_id> given and no current session (CLAUDE_CODE_SESSION_ID unset). " +
182
+ "Pass an explicit trace_id, or run `mla label` inside a Claude Code session.");
183
+ return 2;
184
+ }
185
+ let lastIdx = -1;
186
+ let lastTid = "";
187
+ lines.forEach((line, i) => {
188
+ const t = parse(line);
189
+ if (t && t.session_id === session) {
190
+ lastIdx = i;
191
+ lastTid = t.trace_id ?? "";
192
+ }
193
+ });
194
+ if (lastIdx < 0) {
195
+ console.error(`No traces for the current session (${session}) at ${tracesFile()}. ` +
196
+ "Pass an explicit trace_id to label a trace from another session.");
197
+ return 1;
198
+ }
199
+ targetIdxs = [lastIdx];
200
+ what = lastTid || `(latest in ${session})`;
201
+ }
202
+ const targets = new Set(targetIdxs);
203
+ let merged = {};
204
+ const rewritten = lines.map((line, i) => {
205
+ if (!targets.has(i))
206
+ return line;
207
+ const t = parse(line);
208
+ if (!t)
209
+ return line; // defensive: selection only picks parseable lines.
210
+ merged = { ...(t.operator_label ?? {}), ...args.patch };
211
+ return JSON.stringify({ ...t, operator_label: merged });
212
+ });
213
+ // Atomic replace: write a sibling temp file, then rename over the target.
214
+ // Node has no native advisory lock and we deliberately do NOT shell out to
215
+ // flock(1) for this; labeling is a single-operator action that happens
216
+ // BETWEEN prompts (never during a hook write), so the lost-append window if
217
+ // the hook appends a brand-new trace between our read and rename is
218
+ // negligible, and the temp+rename guarantees any concurrent reader always
219
+ // sees a complete, consistent file.
220
+ const tmp = path.join(logDir(), `.ask-traces.${process.pid}.tmp`);
221
+ fs.writeFileSync(tmp, rewritten.join("\n") + "\n");
222
+ fs.renameSync(tmp, tracesFile());
223
+ const n = targetIdxs.length;
224
+ console.log(`Labeled ${what}: ${renderLabel(merged)} (${n} line${n === 1 ? "" : "s"}).`);
225
+ return 0;
226
+ }
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseLoginArgs = parseLoginArgs;
4
+ exports.runLogin = runLogin;
5
+ const config_1 = require("../lib/config");
6
+ const http_1 = require("../lib/http");
7
+ const auth_breaker_1 = require("../lib/auth-breaker");
8
+ const login_1 = require("../lib/login");
9
+ const wire_1 = require("../lib/wire");
10
+ // Default liveness probe: GET /internal/v1/auth/me through the control `get`
11
+ // helper, which already does the §6.5 refresh-on-401 dance. A genuinely live
12
+ // session (or one the access token silently refreshes for) resolves; a session
13
+ // whose refresh token is dead server-side rejects with HttpError.status === 401.
14
+ async function defaultVerifySession(cfg) {
15
+ await (0, http_1.get)(cfg, "/internal/v1/auth/me");
16
+ }
17
+ // `mla login` (proposal §6.6, T24).
18
+ //
19
+ // Browser-based user login over the loopback OAuth + PKCE flow (the transport
20
+ // lives in lib/login.ts, T21). This command is the thin policy layer on top:
21
+ // - refuses to run before `mla init` (no cli-config.json to write into);
22
+ // - is a no-op when already logged in with a comfortably-fresh refresh token;
23
+ // - resolves the Console URL by an explicit precedence ladder;
24
+ // - validates the --no-browser / --port pairing;
25
+ // - on success REPLACES auth.* in cli-config.json with the user-token shape
26
+ // (the prior shared-key value is NOT preserved, §6.6); `mla logout` is the
27
+ // only path back to shared-key, via a fresh `mla init --control-token`.
28
+ //
29
+ // SECURITY: this command never logs the access token, refresh token, grant code,
30
+ // or PKCE verifier. It prints only identity (display name / email / workspace)
31
+ // and the (non-secret) expiry timestamps.
32
+ // `mla login` is a no-op when the live user-token's refresh window still has
33
+ // more than this much runway. Below it (or any other mode), we re-run the flow.
34
+ // Mirrors §6.6: "more than 24h remaining".
35
+ const REFRESH_FRESH_THRESHOLD_MS = 24 * 60 * 60 * 1000;
36
+ // Strict argv parsing, mirroring `mla init`'s VALUE_FLAGS/BOOLEAN_FLAGS shape
37
+ // (init.ts): a value flag must be followed by a non-flag value; unknown flags
38
+ // and positionals throw. `mla login` takes no positionals.
39
+ const VALUE_FLAGS = new Set(["--console-url", "--port"]);
40
+ // --force skips every no-op/self-heal short-circuit and runs the browser flow
41
+ // unconditionally (escape hatch for "just re-mint my tokens").
42
+ const BOOLEAN_FLAGS = new Set(["--no-browser", "--force"]);
43
+ function parseLoginArgs(argv) {
44
+ const out = {};
45
+ for (let i = 0; i < argv.length; i++) {
46
+ const a = argv[i];
47
+ if (VALUE_FLAGS.has(a)) {
48
+ const v = argv[i + 1];
49
+ if (v === undefined) {
50
+ throw new Error(`Missing value for ${a}`);
51
+ }
52
+ if (v.startsWith("--") || v.startsWith("-")) {
53
+ throw new Error(`Missing value for ${a} (got the next flag ${v} instead)`);
54
+ }
55
+ if (a === "--console-url") {
56
+ out.consoleUrl = v;
57
+ }
58
+ else if (a === "--port") {
59
+ const port = Number(v);
60
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
61
+ throw new Error(`Invalid --port value "${v}": expected an integer in 1..65535.`);
62
+ }
63
+ out.port = port;
64
+ }
65
+ i += 1;
66
+ continue;
67
+ }
68
+ if (BOOLEAN_FLAGS.has(a)) {
69
+ if (a === "--no-browser")
70
+ out.noBrowser = true;
71
+ else if (a === "--force")
72
+ out.force = true;
73
+ continue;
74
+ }
75
+ if (a.startsWith("--") || a.startsWith("-")) {
76
+ throw new Error(`Unknown flag: ${a}. Supported flags: ${[...VALUE_FLAGS, ...BOOLEAN_FLAGS].sort().join(", ")}`);
77
+ }
78
+ throw new Error(`Unexpected positional argument: ${a}. \`mla login\` takes no positional arguments.`);
79
+ }
80
+ return out;
81
+ }
82
+ // Resolve the Console origin by the §6.6 precedence ladder, stopping at the first
83
+ // defined value: --console-url > MEETLESS_CONSOLE_URL > raw cfg.consoleUrl. When
84
+ // all three are absent this returns undefined and runBrowserLogin infers the
85
+ // origin from the control URL via its pair table (failing loud if no pair
86
+ // matches). We deliberately read the RAW `cfg.consoleUrl`, NOT getConsoleUrl():
87
+ // the latter defaults to localhost:3000 and would mask the pair-table inference
88
+ // (and silently point login at the wrong origin for a prod control URL).
89
+ function resolveConsoleOverride(flags, cfg) {
90
+ const fromFlag = flags.consoleUrl?.trim();
91
+ if (fromFlag)
92
+ return fromFlag;
93
+ const fromEnv = process.env.MEETLESS_CONSOLE_URL?.trim();
94
+ if (fromEnv)
95
+ return fromEnv;
96
+ const fromCfg = cfg.consoleUrl?.trim();
97
+ if (fromCfg)
98
+ return fromCfg;
99
+ return undefined;
100
+ }
101
+ // Map control's exchange bundle into the on-disk user-token credential. Only the
102
+ // four display fields of `user` survive: role here is display-only (§4.6); every
103
+ // authorization decision re-reads the live WorkspaceUser.role server-side.
104
+ function bundleToUserTokenAuth(bundle) {
105
+ return {
106
+ mode: "user-token",
107
+ accessToken: bundle.accessToken,
108
+ refreshToken: bundle.refreshToken,
109
+ accessExpiresAt: bundle.accessExpiresAt,
110
+ refreshExpiresAt: bundle.refreshExpiresAt,
111
+ sessionId: bundle.sessionId,
112
+ user: {
113
+ id: bundle.user.id,
114
+ displayName: bundle.user.displayName,
115
+ email: bundle.user.email,
116
+ role: bundle.user.role,
117
+ },
118
+ };
119
+ }
120
+ // Best-effort humanizer for "how much runway is left" on an ISO expiry. Returns
121
+ // null for an unparseable/empty timestamp so callers can degrade gracefully.
122
+ function formatRemaining(iso) {
123
+ const ms = Date.parse(iso) - Date.now();
124
+ if (Number.isNaN(ms))
125
+ return null;
126
+ if (ms <= 0)
127
+ return "expired";
128
+ const hours = Math.floor(ms / (60 * 60 * 1000));
129
+ if (hours < 48)
130
+ return `in ~${hours}h`;
131
+ return `in ~${Math.floor(hours / 24)}d`;
132
+ }
133
+ // The shared "you're still logged in" message. Pulled out so the fast path, the
134
+ // verify-confirmed path, and the offline-fallback path all print identically.
135
+ function printAlreadyLoggedIn(auth) {
136
+ const who = auth.user.displayName || auth.user.id;
137
+ const email = auth.user.email ? ` <${auth.user.email}>` : "";
138
+ const runway = formatRemaining(auth.refreshExpiresAt);
139
+ console.log(`Already logged in as ${who}${email}.`);
140
+ console.log(` Session expires ${runway ?? "soon"} (run \`mla logout && mla login\` to re-login).`);
141
+ }
142
+ async function runLogin(argv, deps = {}) {
143
+ const verifySession = deps.verifySession ?? defaultVerifySession;
144
+ const browserLogin = deps.browserLogin ?? login_1.runBrowserLogin;
145
+ let flags;
146
+ try {
147
+ flags = parseLoginArgs(argv);
148
+ }
149
+ catch (e) {
150
+ console.error(e.message);
151
+ return 2;
152
+ }
153
+ // --no-browser needs a fixed loopback port: the browser runs on a different
154
+ // machine (SSH), so the operator forwards `ssh -L <port>:127.0.0.1:<port>`
155
+ // ahead of time and the redirect_uri must target that known port (§6.6). With
156
+ // a browser on this machine, port 0 (kernel-assigned) is correct.
157
+ if (flags.noBrowser && flags.port === undefined) {
158
+ console.error("--port <n> is required with --no-browser: the loopback redirect must " +
159
+ "target a port you have forwarded (e.g. `ssh -L 8765:127.0.0.1:8765`).");
160
+ return 2;
161
+ }
162
+ // `mla login` writes INTO cli-config.json. When none exists yet (a fresh
163
+ // install that goes straight to `mla login`, which is the documented flow on
164
+ // the install page), bootstrap a minimal MACHINE config pointing at the hosted
165
+ // prod backend, then carry on -- so login works with zero extra steps instead
166
+ // of dead-ending on "run `mla init` first".
167
+ //
168
+ // Multi-repo safety: this is HOME-level (one cli-config.json per MEETLESS_HOME,
169
+ // shared by every repo on the machine, the long-standing model) and writes NO
170
+ // per-folder workspace binding. A user with several repos still binds each one
171
+ // to its own workspace through its `.meetless.json` marker (`mla activate`);
172
+ // login never reads or writes that, so it is correct from any directory. The
173
+ // bootstrap is idempotent: it fires only when the config is absent, so a second
174
+ // `mla login` from another repo just reads the existing config.
175
+ //
176
+ // It deliberately does NOT wire capture hooks or the MCP server (that is
177
+ // `mla init`'s runWire job, §6.6) -- it only creates the config the browser
178
+ // login writes tokens into. A non-default backend (dogfood/staging/self-host)
179
+ // is still pinned with `mla init --control-url ...`; MEETLESS_BACKEND_URL /
180
+ // MEETLESS_INTEL_URL continue to override these defaults at read time
181
+ // (readConfig), so a one-off `MEETLESS_BACKEND_URL=... mla login` still works.
182
+ if (!(0, config_1.configExists)()) {
183
+ (0, config_1.writeConfig)({
184
+ controlUrl: config_1.DEFAULT_CONTROL_URL,
185
+ controlToken: "", // auth.mode 'none': no bearer until the login below
186
+ intelUrl: config_1.DEFAULT_INTEL_URL,
187
+ mlaPath: (0, wire_1.resolveMlaPath)(),
188
+ auth: { mode: "none" },
189
+ });
190
+ console.log(`No cli-config.json found; created ${config_1.CFG_PATH} for the hosted backend ` +
191
+ `(${config_1.DEFAULT_CONTROL_URL}).`);
192
+ console.log("Tip: run `mla init` to wire capture hooks and the Meetless MCP server " +
193
+ "into your coding agent.");
194
+ }
195
+ let cfg;
196
+ try {
197
+ cfg = (0, config_1.readConfig)();
198
+ }
199
+ catch (e) {
200
+ // readConfig throws loudly on a corrupt config or the Gate-4 env conflict
201
+ // (user-token on disk + MEETLESS_CONTROL_TOKEN set). Surface it verbatim.
202
+ console.error(e.message);
203
+ return 1;
204
+ }
205
+ // No-op when already logged in with a comfortably-fresh refresh token. Forcing
206
+ // a re-login is `mla login --force` (or `mla logout && mla login`). A corrupt/
207
+ // empty refreshExpiresAt parses to NaN, so this guard safely falls through to a
208
+ // real login rather than mis-treating a broken session as fresh.
209
+ //
210
+ // T29 self-heal: NO local timestamp is proof the session is alive. The on-disk
211
+ // refresh token can be dead server-side (rotated/revoked) while refreshExpiresAt
212
+ // still reads ~Nd out, AND the access JWT can sit well inside its 24h TTL while
213
+ // the session it belongs to was revoked (e.g. a control-dev reseed). The original
214
+ // T29 trusted a still-live access token and fast-no-op'd without probing, so An
215
+ // hit the exact closed loop it was meant to kill: every hook 401'd telling him to
216
+ // "run `mla login`", and `mla login` answered "already logged in" all day. So we
217
+ // NEVER short-circuit on a local timestamp alone:
218
+ // - refresh window locally-fresh -> ALWAYS PROBE control (GET /auth/me, which
219
+ // refreshes transparently). Live -> no-op. Rejected (401/403) -> the session
220
+ // is dead server-side: fall through to a real browser login (self-heal).
221
+ // Unreachable (network error, no .status) -> keep the cached session rather
222
+ // than force a doomed flow on someone merely offline.
223
+ // - refresh window locally-expired -> no probe; re-auth is required regardless,
224
+ // so drop straight through to a browser login.
225
+ // - --force always re-authenticates and skips the probe entirely.
226
+ if (cfg.auth.mode === "user-token" && !flags.force) {
227
+ const auth = cfg.auth;
228
+ const refreshRemainingMs = Date.parse(auth.refreshExpiresAt) - Date.now();
229
+ const refreshLocallyFresh = !Number.isNaN(refreshRemainingMs) && refreshRemainingMs > REFRESH_FRESH_THRESHOLD_MS;
230
+ if (refreshLocallyFresh) {
231
+ // Verify against control before ever declaring "already logged in". login is a
232
+ // rare, interactive command, so one GET /auth/me on the happy path is a non-issue
233
+ // next to the all-day dead loop a blind no-op can hide.
234
+ try {
235
+ await verifySession(cfg);
236
+ printAlreadyLoggedIn(auth);
237
+ return 0;
238
+ }
239
+ catch (e) {
240
+ const status = e.status;
241
+ if (status === 401 || status === 403) {
242
+ // Session is dead server-side. Self-heal: drop through to a real browser
243
+ // login instead of the old no-op dead end.
244
+ console.log("Cached session is no longer valid server-side. Re-authenticating...");
245
+ }
246
+ else {
247
+ // No HTTP status -> never reached the server (control down / offline).
248
+ // Keep the cached session; do not open a browser flow that cannot reach
249
+ // control anyway.
250
+ printAlreadyLoggedIn(auth);
251
+ console.log(" (could not verify with control; keeping cached session for now.)");
252
+ return 0;
253
+ }
254
+ }
255
+ }
256
+ }
257
+ const consoleUrl = resolveConsoleOverride(flags, cfg);
258
+ let bundle;
259
+ try {
260
+ bundle = await browserLogin({
261
+ controlUrl: cfg.controlUrl,
262
+ consoleUrl,
263
+ noBrowser: flags.noBrowser ?? false,
264
+ port: flags.port,
265
+ });
266
+ }
267
+ catch (e) {
268
+ // runBrowserLogin already keeps tokens/codes out of its messages. Print the
269
+ // message (timeout, CSRF refusal, exchange failure, missing console URL).
270
+ console.error(e.message);
271
+ return 1;
272
+ }
273
+ const auth = bundleToUserTokenAuth(bundle);
274
+ // REPLACE auth.* outright: no shared-key preservation (§6.6). actorUserId and
275
+ // controlToken are re-derived by writeConfig/readConfig from the new auth.
276
+ (0, config_1.writeConfig)({
277
+ ...cfg,
278
+ auth,
279
+ controlToken: auth.accessToken,
280
+ actorUserId: auth.user.id,
281
+ });
282
+ // A fresh login retires any dead-auth circuit breaker proactively. consult also
283
+ // self-clears on the fingerprint mismatch, but clearing here reopens the gate
284
+ // for live `mla mcp` workers the instant the new token lands on disk.
285
+ (0, auth_breaker_1.clearAuthBreaker)();
286
+ const email = bundle.user.email ? ` <${bundle.user.email}>` : "";
287
+ const accessRunway = formatRemaining(bundle.accessExpiresAt);
288
+ const refreshRunway = formatRemaining(bundle.refreshExpiresAt);
289
+ console.log(`Logged in as ${bundle.user.displayName}${email}.`);
290
+ console.log(` Workspace: ${bundle.workspace.name} (${bundle.workspace.slug})`);
291
+ console.log(` Role: ${bundle.user.role}`);
292
+ console.log(` Access token expires ${accessRunway ?? "soon"}; refresh token ${refreshRunway ?? "soon"}.`);
293
+ console.log("Next: mla whoami");
294
+ return 0;
295
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.revokeCliSession = revokeCliSession;
4
+ exports.runLogout = runLogout;
5
+ const config_1 = require("../lib/config");
6
+ // Raw fetch to the guardless revoke route. Deliberately bypasses http.ts's
7
+ // doFetch (which always stamps an Authorization header): the refresh token in the
8
+ // body is the proof, and we send no bearer because the access token may be dead.
9
+ // Never throws: a network failure becomes a non-cleared result so the caller can
10
+ // still clear local state.
11
+ async function revokeCliSession(controlUrl, sessionId, refreshToken, timeoutMs = 10000) {
12
+ const url = `${controlUrl.replace(/\/+$/, "")}/internal/v1/auth/sessions/revoke`;
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15
+ try {
16
+ const res = await fetch(url, {
17
+ method: "POST",
18
+ // Content-Type only; NO Authorization header (body proof-of-possession).
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ sessionId, refreshToken }),
21
+ signal: controller.signal,
22
+ });
23
+ if (res.ok)
24
+ return { serverCleared: true, detail: "session revoked" };
25
+ if (res.status === 401 || res.status === 410) {
26
+ return {
27
+ serverCleared: true,
28
+ detail: "session was already revoked server-side",
29
+ };
30
+ }
31
+ return { serverCleared: false, detail: `control returned HTTP ${res.status}` };
32
+ }
33
+ catch (e) {
34
+ // Timeout / DNS / connection refused: do not block the local clear.
35
+ return {
36
+ serverCleared: false,
37
+ detail: `control unreachable (${e.name})`,
38
+ };
39
+ }
40
+ finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ async function runLogout(argv, deps = {}) {
45
+ if (argv.length > 0) {
46
+ console.error(`\`mla logout\` takes no arguments (got: ${argv.join(" ")}). ` +
47
+ "There is no --all flag; revoke other sessions from the Console.");
48
+ return 2;
49
+ }
50
+ const log = deps.log ?? ((m) => console.log(m));
51
+ const revokeFn = deps.revokeFn ?? revokeCliSession;
52
+ if (!(0, config_1.configExists)()) {
53
+ // Nothing to log out of, and nothing to write. Idempotent success.
54
+ log("Not logged in (no cli-config.json). Nothing to do.");
55
+ return 0;
56
+ }
57
+ let cfg;
58
+ try {
59
+ cfg = (0, config_1.readConfig)();
60
+ }
61
+ catch (e) {
62
+ // A corrupt/conflicting config can't be safely rewritten here; surface it.
63
+ console.error(e.message);
64
+ return 1;
65
+ }
66
+ if (cfg.auth.mode !== "user-token") {
67
+ // shared-key or none: there is no user session to revoke. We deliberately do
68
+ // NOT touch a shared-key config (logout is not "downgrade my shared key");
69
+ // the operator manages that via `mla init`.
70
+ if (cfg.auth.mode === "shared-key") {
71
+ log("Logged in with a shared key, not a user session; nothing to revoke.");
72
+ log("To remove it, edit cli-config.json or re-run `mla init`.");
73
+ }
74
+ else {
75
+ log("Already logged out (auth.mode: none).");
76
+ }
77
+ return 0;
78
+ }
79
+ const { sessionId, refreshToken } = cfg.auth;
80
+ const who = cfg.auth.user.displayName || cfg.auth.user.id;
81
+ // Best-effort server revoke. Missing sessionId/refreshToken (corrupt session)
82
+ // skips the network call and goes straight to local clear.
83
+ if (sessionId && refreshToken) {
84
+ const result = await revokeFn(cfg.controlUrl, sessionId, refreshToken);
85
+ if (result.serverCleared) {
86
+ log(`Revoked: ${result.detail}.`);
87
+ }
88
+ else {
89
+ log(`Local logout complete, but ${result.detail}.`);
90
+ log("The session may still be active server-side until it expires.");
91
+ }
92
+ }
93
+ else {
94
+ log("Local session was incomplete; clearing it without a server revoke.");
95
+ }
96
+ // Clear auth.* to the terminal `none` state. NEVER restore shared-key (§6.6).
97
+ // Top-level controlUrl/intelUrl/mlaPath/etc. survive so the next
98
+ // `mla init --control-token` or `mla login` runs cleanly. writeConfig
99
+ // re-derives controlToken ("") and drops actorUserId under none.
100
+ (0, config_1.writeConfig)({
101
+ ...cfg,
102
+ auth: { mode: "none" },
103
+ controlToken: "",
104
+ actorUserId: undefined,
105
+ });
106
+ log(`Logged out ${who}. Run \`mla login\` to log back in.`);
107
+ return 0;
108
+ }