@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.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- 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
|
+
}
|