@mnemom/mnemom 0.17.33 → 0.17.34-next.2
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/dist/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +26 -0
- package/dist/commands/agent.js +228 -53
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +232 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -0
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +144 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +4 -3
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom scan`: a read-only local scan of GitHub PR metadata. Runs entirely on this machine; nothing is
|
|
3
|
+
* uploaded in this build. The share-level prompt gates what a later upload step may send, and the chosen
|
|
4
|
+
* payload is printed so the user sees exactly what would leave. Pure logic is exported so each reject
|
|
5
|
+
* arm is unit-testable without Commander (same pattern as commands/usage.ts).
|
|
6
|
+
*/
|
|
7
|
+
import { isValidRepoSlug, resolveCredential, } from "../lib/scan/credential.js";
|
|
8
|
+
import { failureText } from "../lib/scan/failure.js";
|
|
9
|
+
import { runLocalScan } from "../lib/scan/scan.js";
|
|
10
|
+
import { askInput, askSelect } from "../lib/prompt.js";
|
|
11
|
+
import { isSpendTiersEnabled, NOT_ENABLED_MSG, parseMonthlySpendUsd, selectSpend, SPEND_INVALID_MSG, SPEND_NO_TERMINAL_MSG, } from "../lib/scan/spend/spend-tiers.js";
|
|
12
|
+
import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, } from "../lib/scan/spend/anthropic-usage.js";
|
|
13
|
+
import { getAuthInfo } from "../lib/auth.js";
|
|
14
|
+
import { listMyOrgs } from "../lib/api.js";
|
|
15
|
+
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
16
|
+
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
17
|
+
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
18
|
+
import { buildSharePayload, parseShareLevel, promptShareLevel, } from "../lib/scan/share.js";
|
|
19
|
+
/** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
|
|
20
|
+
export function parseRepoFlags(raw) {
|
|
21
|
+
const out = (raw ?? [])
|
|
22
|
+
.flatMap((r) => r.split(","))
|
|
23
|
+
.map((r) => r.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
return out.length > 0 && out.every(isValidRepoSlug) ? out : null;
|
|
26
|
+
}
|
|
27
|
+
const EMAIL = /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,63}$/;
|
|
28
|
+
export const isEmail = (v) => typeof v === "string" && v.length <= 320 && EMAIL.test(v.trim());
|
|
29
|
+
/** The runner's email (mnemom login, else asked) and the CTO's (flag, else "are you the CTO?"). */
|
|
30
|
+
export async function resolveRecipients(o) {
|
|
31
|
+
if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
|
|
32
|
+
throw new Error("--cto-email must be a valid email address");
|
|
33
|
+
let runner = o.runnerEmail ?? getAuthInfo()?.email ?? "";
|
|
34
|
+
if (!isEmail(runner))
|
|
35
|
+
runner = (await askInput("Your email (the report is sent to you):")).trim();
|
|
36
|
+
if (!isEmail(runner))
|
|
37
|
+
throw new Error("a valid runner email is required");
|
|
38
|
+
runner = runner.trim();
|
|
39
|
+
if (o.ctoEmail !== undefined)
|
|
40
|
+
return { runner_email: runner, cto_email: o.ctoEmail.trim() };
|
|
41
|
+
const isCto = await askSelect("Are you the CTO?", ["Yes, I am the CTO", "No, someone else is"]);
|
|
42
|
+
if (isCto === null)
|
|
43
|
+
throw new Error("--cto-email is required when you are not the CTO");
|
|
44
|
+
if (isCto.startsWith("Yes"))
|
|
45
|
+
return { runner_email: runner, cto_email: runner };
|
|
46
|
+
const cto = (await askInput("CTO email (the report is sent to them too):")).trim();
|
|
47
|
+
if (!isEmail(cto))
|
|
48
|
+
throw new Error("--cto-email is required when you are not the CTO");
|
|
49
|
+
return { runner_email: runner, cto_email: cto };
|
|
50
|
+
}
|
|
51
|
+
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
52
|
+
export async function resolveSelfReportedSpend(o) {
|
|
53
|
+
if (o.monthlySpend === undefined && !o.noAnthropicUsage)
|
|
54
|
+
return null;
|
|
55
|
+
if (!isSpendTiersEnabled(o.env))
|
|
56
|
+
throw new Error(NOT_ENABLED_MSG);
|
|
57
|
+
let text = o.monthlySpend;
|
|
58
|
+
if (text === undefined) {
|
|
59
|
+
if (!(o.isTty ?? process.stdin.isTTY))
|
|
60
|
+
throw new Error(SPEND_NO_TERMINAL_MSG);
|
|
61
|
+
text = await askInput("Approximate monthly AI spend in USD (blank to skip; kept in memory only):");
|
|
62
|
+
if (text.trim() === "")
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const n = parseMonthlySpendUsd(text);
|
|
66
|
+
if (n === null)
|
|
67
|
+
throw new Error(SPEND_INVALID_MSG);
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
71
|
+
export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
72
|
+
/**
|
|
73
|
+
* Tier 1: read measured usage with the CTO's Anthropic admin key, locally. The key comes from
|
|
74
|
+
* ANTHROPIC_ADMIN_KEY or a masked prompt (--anthropic-admin-key; never a value on argv), lives only in this
|
|
75
|
+
* call, and goes only to api.anthropic.com. Returns null for "no key" and for every failure, after printing a
|
|
76
|
+
* fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
|
|
77
|
+
* back to tier 2, then tier 3.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveMeasuredUsage(o, period) {
|
|
80
|
+
if (o.anthropicAdminKey && !isSpendTiersEnabled(o.env))
|
|
81
|
+
throw new Error(NOT_ENABLED_MSG);
|
|
82
|
+
if (!isSpendTiersEnabled(o.env) || o.noAnthropicUsage)
|
|
83
|
+
return null;
|
|
84
|
+
// An explicit typed figure is the user's choice of tier 2: an ambient env key must not override it.
|
|
85
|
+
if (o.monthlySpend !== undefined && !o.anthropicAdminKey)
|
|
86
|
+
return null;
|
|
87
|
+
let key = (o.env ?? process.env)[ADMIN_KEY_ENV];
|
|
88
|
+
if (o.anthropicAdminKey) {
|
|
89
|
+
if (!(o.isTty ?? process.stdin.isTTY))
|
|
90
|
+
throw new Error(NO_ADMIN_KEY_TERMINAL_MSG);
|
|
91
|
+
key = await askInput("Anthropic admin key (kept in memory only, sent only to Anthropic):", true);
|
|
92
|
+
}
|
|
93
|
+
if (key === undefined || key.trim() === "")
|
|
94
|
+
return null;
|
|
95
|
+
if (!isAdminKeyShape(key)) {
|
|
96
|
+
console.log(usageFailureText("key_invalid"));
|
|
97
|
+
console.log(TIER1_FALLBACK_NOTE);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await fetchMeasuredUsage(key, period, o.anthropicDeps);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.log(usageFailureText(e instanceof AnthropicUsageError ? e.reason : "upstream"));
|
|
105
|
+
console.log(TIER1_FALLBACK_NOTE);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
|
|
111
|
+
* needs an org. Inference uses the signed-in user's own memberships (GET /v1/orgs, personal org
|
|
112
|
+
* excluded): exactly one org is used; none, several, or a failed read stops the scan loudly BEFORE any
|
|
113
|
+
* scan work, naming the fix. It never guesses between orgs and never skips the email.
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveScanOrg(o) {
|
|
116
|
+
if (o.org !== undefined) {
|
|
117
|
+
if (!isOrgId(o.org))
|
|
118
|
+
throw new Error(INVALID_ORG_MSG);
|
|
119
|
+
return o.org;
|
|
120
|
+
}
|
|
121
|
+
let orgs;
|
|
122
|
+
try {
|
|
123
|
+
orgs = (await (o.listOrgs ?? listMyOrgs)()).filter((x) => !x.is_personal);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new Error("Could not look up your org to email the report to. Sign in with `mnemom login`, or pass --org <org_id>.");
|
|
127
|
+
}
|
|
128
|
+
if (orgs.length === 1 && isOrgId(orgs[0].org_id))
|
|
129
|
+
return orgs[0].org_id;
|
|
130
|
+
if (orgs.length === 0)
|
|
131
|
+
throw new Error("You belong to no organization, so the report has nowhere to be emailed from. Pass --org <org_id>.");
|
|
132
|
+
throw new Error(`You belong to ${orgs.length} organizations; pass --org <org_id> (${orgs.map((x) => x.org_id).join(", ")}).`);
|
|
133
|
+
}
|
|
134
|
+
export const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
|
|
135
|
+
/**
|
|
136
|
+
* The Record read for the preview: null when the flag gate is off (no read, no output, as before).
|
|
137
|
+
* With the gate on and no --org it is a note, not a guess. 404 becomes a note; 401/403 throw
|
|
138
|
+
* RecordAuthError (fail closed); every other failure becomes a fixed note. Nothing is stored.
|
|
139
|
+
*/
|
|
140
|
+
export async function resolveRecordOutcome(o) {
|
|
141
|
+
if (!isSpendTiersEnabled(o.env))
|
|
142
|
+
return null;
|
|
143
|
+
if (o.org === undefined)
|
|
144
|
+
return "no_org";
|
|
145
|
+
if (!isOrgId(o.org))
|
|
146
|
+
throw new Error(INVALID_ORG_MSG);
|
|
147
|
+
return fetchRecordTotals(o.org, o.recordDeps);
|
|
148
|
+
}
|
|
149
|
+
export async function scanCommand(o) {
|
|
150
|
+
if (o.share !== undefined && parseShareLevel(o.share) === null) {
|
|
151
|
+
throw new Error("--share must be one of: summary, full, none");
|
|
152
|
+
}
|
|
153
|
+
if (o.repos.length === 0)
|
|
154
|
+
throw new Error("--repo owner/name is required (repeatable)");
|
|
155
|
+
// The report is emailed on every scan, so resolve the org before any scan work; failure is loud.
|
|
156
|
+
const emailOrg = await resolveScanOrg(o);
|
|
157
|
+
if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
|
|
158
|
+
throw new Error("--cto-email must be a valid email address");
|
|
159
|
+
const selfReported = await resolveSelfReportedSpend(o);
|
|
160
|
+
const now = o.now ?? Date.now;
|
|
161
|
+
const end = new Date(now()).toISOString().slice(0, 10);
|
|
162
|
+
const period = o.period ?? { start: end, end };
|
|
163
|
+
const measured = await resolveMeasuredUsage(o, period);
|
|
164
|
+
const credential = o.credential ?? (await resolveCredential().catch(() => null));
|
|
165
|
+
if (!credential)
|
|
166
|
+
throw new Error(failureText("credential"));
|
|
167
|
+
// Before the local scan: a refused credential fails closed without spending the GitHub reads.
|
|
168
|
+
const record = await resolveRecordOutcome(o);
|
|
169
|
+
const outcome = await runLocalScan({ credential, now, ...o.deps }, { repos: o.repos, period });
|
|
170
|
+
if (outcome.status === "withheld")
|
|
171
|
+
throw new Error(`Scan withheld: ${failureText(outcome.reason)}`);
|
|
172
|
+
const { result } = outcome;
|
|
173
|
+
console.log("Local scan (read-only)");
|
|
174
|
+
console.log(`PRs examined: ${result.totals.examined} of ${result.totals.prs} merged (${result.coverage.windowStart} to ${result.coverage.windowEnd})`);
|
|
175
|
+
console.log(`Coverage: ${result.coverage.complete ? "complete" : `partial${result.coverage.stoppedBy ? ` (stopped by ${result.coverage.stoppedBy})` : ""}`}`);
|
|
176
|
+
// Flag off: the line as before. Flag on: the tier chosen, or no line at all when evidence is thin.
|
|
177
|
+
const tiers = isSpendTiersEnabled(o.env);
|
|
178
|
+
const spend = tiers
|
|
179
|
+
? selectSpend({
|
|
180
|
+
measuredUsd: measured?.usd ?? o.anthropicUsageUsd,
|
|
181
|
+
over200kShare: measured ? measured.over200kShare : o.anthropicOver200kShare,
|
|
182
|
+
selfReportedUsd: selfReported,
|
|
183
|
+
estimate: result.spend,
|
|
184
|
+
})
|
|
185
|
+
: null;
|
|
186
|
+
const spendLine = spend ? spend.line : result.spend.line;
|
|
187
|
+
if (spendLine !== null)
|
|
188
|
+
console.log(spendLine);
|
|
189
|
+
if (record === "no_org")
|
|
190
|
+
console.log(RECORD_NO_ORG_NOTE);
|
|
191
|
+
else if (record !== null)
|
|
192
|
+
for (const line of renderRecordSection(record))
|
|
193
|
+
console.log(line);
|
|
194
|
+
const level = parseShareLevel(o.share) ?? (await promptShareLevel());
|
|
195
|
+
// Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
|
|
196
|
+
// Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
|
|
197
|
+
const emailFailure = await emailReport(o, emailOrg, result, level, period, spend);
|
|
198
|
+
if (level === "none") {
|
|
199
|
+
console.log("Share level: nothing. No share payload is built; Mnemom keeps nothing (the report email is only sent to you).");
|
|
200
|
+
if (emailFailure)
|
|
201
|
+
throw new ReportEmailError(emailFailure);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const payload = buildSharePayload(level, result, await resolveRecipients(o), spend ? (spend.shareable ? spend.line : null) : undefined);
|
|
205
|
+
console.log(`Share level: ${level}. Upload is not wired in this build; this is what would be sent:`);
|
|
206
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
207
|
+
if (emailFailure)
|
|
208
|
+
throw new ReportEmailError(emailFailure);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Emails the report. Loud on every failure: prints the reason name, never the server's words, and
|
|
212
|
+
* returns it so the caller exits non-zero after the local report is shown.
|
|
213
|
+
*/
|
|
214
|
+
async function emailReport(o, org, result, level, period, spend) {
|
|
215
|
+
const fail = (f) => {
|
|
216
|
+
console.log(failureMessage(f));
|
|
217
|
+
return f;
|
|
218
|
+
};
|
|
219
|
+
const report = buildEmailReport(result, {
|
|
220
|
+
orgId: org,
|
|
221
|
+
period,
|
|
222
|
+
now: o.now ?? Date.now,
|
|
223
|
+
sendSpendRange: spend ? spend.shareable && spend.tier === 3 : true,
|
|
224
|
+
});
|
|
225
|
+
const body = buildEmailBody(report, level, result.coverage.complete, o.ctoEmail?.trim());
|
|
226
|
+
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
|
227
|
+
if (outcome.status === "failed")
|
|
228
|
+
return fail(outcome.reason);
|
|
229
|
+
if (outcome.status === "unavailable") {
|
|
230
|
+
// Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
|
|
231
|
+
console.log(UNAVAILABLE_MSG);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
console.log(outcome.status === "dry_run"
|
|
235
|
+
? DRY_RUN_MSG
|
|
236
|
+
: `Report emailed to you (${outcome.recipients} recipient${outcome.recipients === 1 ? "" : "s"}).`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type LockResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
fd: number;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "held" | "error";
|
|
7
|
+
detail: string;
|
|
8
|
+
};
|
|
9
|
+
/** The helper commands, tried in order. Each locks fd 3 and exits 0 (taken), 3 (held), else error. */
|
|
10
|
+
export declare const FLOCK_HELPERS: readonly (readonly string[])[];
|
|
11
|
+
export interface FlockDeps {
|
|
12
|
+
openSync: (p: string, flags: string, mode: number) => number;
|
|
13
|
+
closeSync: (fd: number) => void;
|
|
14
|
+
mkdirSync: (p: string, opts: {
|
|
15
|
+
recursive: true;
|
|
16
|
+
mode: number;
|
|
17
|
+
}) => unknown;
|
|
18
|
+
run: (argv: readonly string[], fd: number) => number | null;
|
|
19
|
+
exists: (p: string) => boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const defaultFlockDeps: FlockDeps;
|
|
22
|
+
/**
|
|
23
|
+
* Take an exclusive non-blocking flock on `lockPath` (created 0600 in a 0700 directory). On success
|
|
24
|
+
* the returned fd must stay open for the life of the lock; closing it (or exiting) releases it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function tryLockFile(lockPath: string, deps?: FlockDeps): LockResult;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// flock(2) LOCK_EX|LOCK_NB for the durable seat proxy's per-conversation lock (S1 proxyLockPath,
|
|
2
|
+
// BACKBONE amendment 51, C13-R4).
|
|
3
|
+
//
|
|
4
|
+
// Node has no flock binding, so the lock is taken by a short-lived helper on an fd this process
|
|
5
|
+
// opened and passes down: flock locks belong to the open file description, so the lock stays held
|
|
6
|
+
// by this process's fd after the helper exits, is visible to `lsof -p <this pid>`, and is released
|
|
7
|
+
// when this process exits for any reason (kill -9 included). The helper is the system perl
|
|
8
|
+
// (Fcntl), present on every macOS; python3 is the fallback.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
/** The helper commands, tried in order. Each locks fd 3 and exits 0 (taken), 3 (held), else error. */
|
|
13
|
+
export const FLOCK_HELPERS = [
|
|
14
|
+
[
|
|
15
|
+
"/usr/bin/perl",
|
|
16
|
+
"-MFcntl=:flock",
|
|
17
|
+
"-e",
|
|
18
|
+
'open(my $fh, "+<&=", 3) or exit 9; flock($fh, LOCK_EX|LOCK_NB) ? exit 0 : exit 3',
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"/usr/bin/python3",
|
|
22
|
+
"-I",
|
|
23
|
+
"-c",
|
|
24
|
+
"import fcntl,sys\ntry:\n fcntl.flock(3, fcntl.LOCK_EX|fcntl.LOCK_NB)\nexcept BlockingIOError:\n sys.exit(3)\nexcept Exception:\n sys.exit(9)",
|
|
25
|
+
],
|
|
26
|
+
];
|
|
27
|
+
export const defaultFlockDeps = {
|
|
28
|
+
openSync: (p, flags, mode) => fs.openSync(p, flags, mode),
|
|
29
|
+
closeSync: (fd) => fs.closeSync(fd),
|
|
30
|
+
mkdirSync: (p, opts) => fs.mkdirSync(p, opts),
|
|
31
|
+
run: (argv, fd) => spawnSync(argv[0], argv.slice(1), {
|
|
32
|
+
stdio: ["ignore", "ignore", "ignore", fd],
|
|
33
|
+
timeout: 10_000,
|
|
34
|
+
}).status,
|
|
35
|
+
exists: (p) => fs.existsSync(p),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Take an exclusive non-blocking flock on `lockPath` (created 0600 in a 0700 directory). On success
|
|
39
|
+
* the returned fd must stay open for the life of the lock; closing it (or exiting) releases it.
|
|
40
|
+
*/
|
|
41
|
+
export function tryLockFile(lockPath, deps = defaultFlockDeps) {
|
|
42
|
+
let fd;
|
|
43
|
+
try {
|
|
44
|
+
deps.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
45
|
+
fd = deps.openSync(lockPath, "a", 0o600);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return { ok: false, reason: "error", detail: e.message };
|
|
49
|
+
}
|
|
50
|
+
// The fd is handed back only when the lock is taken; every other way out (held, no helper, a
|
|
51
|
+
// helper or the existence probe throwing) closes it here.
|
|
52
|
+
let taken = false;
|
|
53
|
+
try {
|
|
54
|
+
for (const helper of FLOCK_HELPERS) {
|
|
55
|
+
if (!deps.exists(helper[0]))
|
|
56
|
+
continue;
|
|
57
|
+
const status = deps.run(helper, fd);
|
|
58
|
+
if (status === 0) {
|
|
59
|
+
taken = true;
|
|
60
|
+
return { ok: true, fd };
|
|
61
|
+
}
|
|
62
|
+
if (status === 3)
|
|
63
|
+
return { ok: false, reason: "held", detail: "lock held" };
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, reason: "error", detail: "no flock helper could take the lock" };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return { ok: false, reason: "error", detail: e.message };
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (!taken) {
|
|
72
|
+
try {
|
|
73
|
+
deps.closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* already closed */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -83,6 +83,11 @@ export interface LauncherArgvSplit {
|
|
|
83
83
|
launcherArgv: string[];
|
|
84
84
|
passthrough: string[];
|
|
85
85
|
yoloInjected: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The launcher's interactive Remote Control prefix (`--remote-control <name> [--name <name>]`,
|
|
88
|
+
* agent.ts launch): named as a dropped --remote-control, its session name never a positional.
|
|
89
|
+
*/
|
|
90
|
+
rcInjected: boolean;
|
|
86
91
|
/** The --session-id the launcher pinned, if any. */
|
|
87
92
|
sessionId: string | null;
|
|
88
93
|
/**
|
|
@@ -100,7 +105,8 @@ export interface LauncherArgvSplit {
|
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
102
107
|
* Split an original claude argv (after argv[0]) the way `mnemom agent` built it (buildCliArgs):
|
|
103
|
-
* [--model v] [--effort v] [yolo] [--session-id id]
|
|
108
|
+
* [--remote-control n [--name n]] [--model v] [--effort v] [yolo] [--session-id id]
|
|
109
|
+
* [--plugin-dir dir] <passthrough>
|
|
104
110
|
* [--strict-mcp-config] [--mcp-config <v>]. `passthroughHint` (the tokens after the parent
|
|
105
111
|
* launcher's `--`, read from its own argv) pins the split exactly when it occurs where the
|
|
106
112
|
* layout allows; an empty hint (the parent was read and had no `--`) makes the shape read exact;
|
|
@@ -159,15 +159,21 @@ function isSubsequenceAt(hay, needle, at) {
|
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
161
|
* Split an original claude argv (after argv[0]) the way `mnemom agent` built it (buildCliArgs):
|
|
162
|
-
* [--model v] [--effort v] [yolo] [--session-id id]
|
|
162
|
+
* [--remote-control n [--name n]] [--model v] [--effort v] [yolo] [--session-id id]
|
|
163
|
+
* [--plugin-dir dir] <passthrough>
|
|
163
164
|
* [--strict-mcp-config] [--mcp-config <v>]. `passthroughHint` (the tokens after the parent
|
|
164
165
|
* launcher's `--`, read from its own argv) pins the split exactly when it occurs where the
|
|
165
166
|
* layout allows; an empty hint (the parent was read and had no `--`) makes the shape read exact;
|
|
166
167
|
* without a hint (null or absent) the prefix and suffix are read by shape and reported ambiguous.
|
|
167
168
|
*/
|
|
168
169
|
export function splitLauncherArgv(argv, passthroughHint) {
|
|
169
|
-
//
|
|
170
|
+
// The RC launch puts its own `--remote-control <name> --name <name>` first.
|
|
170
171
|
let i = 0;
|
|
172
|
+
const rcName = argv[1];
|
|
173
|
+
const rcInjected = argv[0] === REMOTE_CONTROL_FLAG && rcName !== undefined && readToken(rcName).flag === null;
|
|
174
|
+
if (rcInjected)
|
|
175
|
+
i = argv[2] === "--name" && argv[3] === rcName ? 4 : 2;
|
|
176
|
+
// Prefix: launcher-injected flags in their fixed order.
|
|
171
177
|
const prefix = {};
|
|
172
178
|
for (const [flag, arity] of LAUNCHER_PREFIX) {
|
|
173
179
|
if (argv[i] !== flag)
|
|
@@ -230,6 +236,7 @@ export function splitLauncherArgv(argv, passthroughHint) {
|
|
|
230
236
|
launcherArgv,
|
|
231
237
|
passthrough,
|
|
232
238
|
yoloInjected: prefix[YOLO_FLAG] === true,
|
|
239
|
+
rcInjected,
|
|
233
240
|
sessionId: typeof prefix["--session-id"] === "string" ? prefix["--session-id"] : null,
|
|
234
241
|
exact,
|
|
235
242
|
ambiguous,
|
|
@@ -245,7 +252,7 @@ export function runningSourceInput(originalArgv, cwd, opts = {}) {
|
|
|
245
252
|
userArgv: [...split.ambiguous, ...split.passthrough],
|
|
246
253
|
yoloInjected: split.yoloInjected,
|
|
247
254
|
// Names are deduplicated, so a --remote-control the user also passed is named once.
|
|
248
|
-
remoteControl: opts.remoteControl === true,
|
|
255
|
+
remoteControl: opts.remoteControl === true || split.rcInjected,
|
|
249
256
|
cwd,
|
|
250
257
|
};
|
|
251
258
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ExperimentalPrefs } from "../lib/experimental.js";
|
|
2
|
+
/** True when argv (after the node + script pair) names the seat proxy. */
|
|
3
|
+
export declare function isSeatProxyArgv(argv: readonly string[]): boolean;
|
|
4
|
+
export interface SeatProxyEntryDeps {
|
|
5
|
+
stored: () => ExperimentalPrefs | undefined;
|
|
6
|
+
stderr: (line: string) => void;
|
|
7
|
+
exit: (code: number) => void;
|
|
8
|
+
run: (argv: readonly string[]) => Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** Gate, then run the proxy with the arguments after `seat-proxy`. */
|
|
11
|
+
export declare function runSeatProxyEntry(argv: readonly string[], deps: SeatProxyEntryDeps): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Entry dispatch for the hidden `mnemom agent host seat-proxy` (C3). It bypasses commander (the
|
|
2
|
+
// `agent` launcher's options would reject the proxy's flags, as with `agent slack`), the update
|
|
3
|
+
// hook and the experimental migration, so the proxy's stdout carries only its READY line.
|
|
4
|
+
//
|
|
5
|
+
// Gate (BACKBONE Flags/CLI): the durable experimental feature, read from config.json only, like the
|
|
6
|
+
// host daemon that spawns the proxy (resolveExperimental with an empty env; MNEMOM_EXPERIMENTAL is
|
|
7
|
+
// never read here). Off: exit 2 with the gate message, no HTTP, no file under HOME, no process.
|
|
8
|
+
import { gatedMessage, resolveExperimental } from "../lib/experimental.js";
|
|
9
|
+
/** True when argv (after the node + script pair) names the seat proxy. */
|
|
10
|
+
export function isSeatProxyArgv(argv) {
|
|
11
|
+
return argv[0] === "agent" && argv[1] === "host" && argv[2] === "seat-proxy";
|
|
12
|
+
}
|
|
13
|
+
/** Gate, then run the proxy with the arguments after `seat-proxy`. */
|
|
14
|
+
export function runSeatProxyEntry(argv, deps) {
|
|
15
|
+
const on = resolveExperimental({ stored: deps.stored(), env: {} }).enabled.includes("durable");
|
|
16
|
+
if (!on) {
|
|
17
|
+
deps.stderr(`${gatedMessage("`mnemom agent host`", "durable")}\n`);
|
|
18
|
+
deps.exit(2);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
deps.run(argv.slice(3)).catch(() => {
|
|
22
|
+
deps.stderr("mnemom agent host seat-proxy: internal error\n");
|
|
23
|
+
deps.exit(1);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import { TurnOrigins } from "../turn-origin.mjs";
|
|
4
|
+
import { type LockResult } from "./flock.js";
|
|
5
|
+
import { type SeatIdentity, type HelloRelay } from "./seat-socket.js";
|
|
6
|
+
export declare const EXIT_CONFIG = 2;
|
|
7
|
+
export declare const EXIT_LOCK_HELD = 3;
|
|
8
|
+
export declare const EXIT_KEY_DEAD = 4;
|
|
9
|
+
/**
|
|
10
|
+
* The drop reasons and exit reasons this proxy logs (the C13 test hooks name them). These are local
|
|
11
|
+
* literals, not S1 values: S1 freezes no drop-reason or exit-reason enum (gap
|
|
12
|
+
* c3-seat-proxy-enums-not-in-s1), so other pieces cannot import them yet.
|
|
13
|
+
*/
|
|
14
|
+
export type DropReason = "arm_missing" | "arm_unparseable" | "unattended_hold_off" | "unattended_fence_off" | "turn_wait_timeout" | "pre_first_prompt" | "no_session";
|
|
15
|
+
export type ExitReason = "fenced_exit" | "revoked_exit" | "ended_exit";
|
|
16
|
+
/** The most a dead-key-status (401/409/410) body is buffered to; a longer one is dropped, never truncated. */
|
|
17
|
+
export declare const DEAD_BODY_CAP: number;
|
|
18
|
+
/** The largest request body forwarded (the Messages API request limit); a larger one gets a local 413. */
|
|
19
|
+
export declare const REQUEST_BODY_CAP: number;
|
|
20
|
+
/**
|
|
21
|
+
* Client headers that never pass: hop-by-hop, proxy-authorization and every x-mnemom-* (the proxy
|
|
22
|
+
* sets the Mnemom ones from the lease). The provider credential (authorization, x-api-key) passes:
|
|
23
|
+
* the seat carries it exactly as `mnemom agent` does (dogfood B6 ruling, superseding C13-R2/R8).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isStrippedClientHeader(name: string): boolean;
|
|
26
|
+
export interface SeatProxyArgs {
|
|
27
|
+
conversation: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
door: URL;
|
|
30
|
+
cwd: string;
|
|
31
|
+
seat: SeatIdentity | null;
|
|
32
|
+
helloRelay: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse and validate the argv after `seat-proxy`. Errors name the option, never echo a value.
|
|
36
|
+
* `isDir` and `env` are injected so the check is pure.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseSeatProxyArgs(argv: readonly string[], env: Record<string, string | undefined>, isDir: (p: string) => boolean): {
|
|
39
|
+
ok: true;
|
|
40
|
+
args: SeatProxyArgs;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
error: string;
|
|
44
|
+
};
|
|
45
|
+
/** The first stdin line (CR stripped), or "" on EOF / timeout. Only the first line is ever read. */
|
|
46
|
+
export declare function readKeyLine(stream: NodeJS.ReadableStream, timeoutMs?: number): Promise<string>;
|
|
47
|
+
/** The arm verdict for one door response: forward, or the drop reason. */
|
|
48
|
+
export declare function armVerdict(pathname: string, rawHeaders: readonly string[]): {
|
|
49
|
+
forward: true;
|
|
50
|
+
} | {
|
|
51
|
+
forward: false;
|
|
52
|
+
reason: DropReason;
|
|
53
|
+
};
|
|
54
|
+
/** The dead-key signal in a door response (C13-R7), read from the JSON body's error.type. */
|
|
55
|
+
export declare function deadKeyReason(status: number, body: string): ExitReason | null;
|
|
56
|
+
/** Whether a request path is origin-form and free of dot segments (raw or percent-encoded). */
|
|
57
|
+
export declare function isCleanOriginForm(url: string): boolean;
|
|
58
|
+
export type Route = "messages" | "count_tokens" | "hello" | "telemetry" | "refused";
|
|
59
|
+
/** Where a request goes: the two model POSTs are forwarded; startup paths are answered locally. */
|
|
60
|
+
export declare function routeOf(method: string, pathname: string): Route;
|
|
61
|
+
export interface SeatProxyDeps {
|
|
62
|
+
log: (line: string) => void;
|
|
63
|
+
exit: (code: number) => void;
|
|
64
|
+
turns: TurnOrigins;
|
|
65
|
+
requestId?: () => string;
|
|
66
|
+
/** Overrides REQUEST_BODY_CAP (tests). */
|
|
67
|
+
requestBodyCap?: number;
|
|
68
|
+
/** Overrides DEAD_BODY_CAP (tests). */
|
|
69
|
+
deadBodyCap?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SeatProxyState {
|
|
72
|
+
dead: ExitReason | null;
|
|
73
|
+
}
|
|
74
|
+
/** The request handler of a seat proxy for `cfg` with lease key `key`. */
|
|
75
|
+
export declare function createSeatProxyHandler(cfg: SeatProxyArgs, key: string, deps: SeatProxyDeps, state?: SeatProxyState): (req: http.IncomingMessage, res: http.ServerResponse) => void;
|
|
76
|
+
export interface SeatProxyMainDeps {
|
|
77
|
+
env: Record<string, string | undefined>;
|
|
78
|
+
stdin: NodeJS.ReadableStream;
|
|
79
|
+
stdout: (line: string) => void;
|
|
80
|
+
stderr: (line: string) => void;
|
|
81
|
+
exit: (code: number) => void;
|
|
82
|
+
home: string;
|
|
83
|
+
isDir: (p: string) => boolean;
|
|
84
|
+
lock: (lockPath: string) => LockResult;
|
|
85
|
+
relayFor: (socketPath: string) => HelloRelay;
|
|
86
|
+
keyTimeoutMs?: number;
|
|
87
|
+
/** Loopback listen addresses (tests override them to exercise a failed bind). */
|
|
88
|
+
hostV4?: string;
|
|
89
|
+
hostV6?: string;
|
|
90
|
+
}
|
|
91
|
+
export declare function defaultMainDeps(): SeatProxyMainDeps;
|
|
92
|
+
/**
|
|
93
|
+
* The 'connect' listener for a seat proxy's loopback servers: a CONNECT (a tunnel request, never
|
|
94
|
+
* origin-form) is refused with a 400 and the socket closed. Without a listener Node would drop the
|
|
95
|
+
* socket silently; this makes the refusal explicit and logged.
|
|
96
|
+
*/
|
|
97
|
+
export declare function refuseConnect(log: (line: string) => void): (req: http.IncomingMessage, socket: Duplex) => void;
|
|
98
|
+
export interface RunningSeatProxy {
|
|
99
|
+
port: number;
|
|
100
|
+
socketPath: string;
|
|
101
|
+
close: () => Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `mnemom agent host seat-proxy …`: validate, read the key, take the conversation lock, listen,
|
|
105
|
+
* print READY. Resolves with the running proxy, or with an exit code it already passed to `exit`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function seatProxyMain(argv: readonly string[], deps?: SeatProxyMainDeps): Promise<RunningSeatProxy | number>;
|