@mnemom/mnemom 0.17.33 → 0.17.34-next.1
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/commands/agent-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +15 -0
- package/dist/commands/agent.js +83 -7
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/index.js +35 -0
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -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 +136 -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/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
|
|
3
3
|
* the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
|
|
4
|
-
* card may say. No word lists live here. The package is a
|
|
4
|
+
* card may say. No word lists live here. The package is a runtime dependency pinned to the aip
|
|
5
5
|
* release tarball and is loaded lazily, so the default CLI path never touches it; if it is
|
|
6
6
|
* absent at runtime the share command fails closed with a clear message.
|
|
7
7
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
|
|
3
3
|
* the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
|
|
4
|
-
* card may say. No word lists live here. The package is a
|
|
4
|
+
* card may say. No word lists live here. The package is a runtime dependency pinned to the aip
|
|
5
5
|
* release tarball and is loaded lazily, so the default CLI path never touches it; if it is
|
|
6
6
|
* absent at runtime the share command fails closed with a clear message.
|
|
7
7
|
*/
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* line, or the proxy's stdin under RC). It is never logged, printed, or passed as
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
|
+
import { type ChildProcess } from "node:child_process";
|
|
29
|
+
import { type SessionKeyDeps, type SessionKeyMintArgs, type SessionKeyRenewal } from "../lib/session-key.js";
|
|
28
30
|
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
31
|
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
30
32
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
@@ -447,6 +449,19 @@ export declare function rcProxyUrl(port: string, proxyCredential: string): strin
|
|
|
447
449
|
* selectLaunchAgent). A truncated/garbled `mnm-…` throws instead of becoming a name.
|
|
448
450
|
*/
|
|
449
451
|
export declare function resolveLaunchPin(options: Pick<AgentOptions, "agent" | "agentId">): string | undefined;
|
|
452
|
+
/** The longest line the proxy sends the launcher (`remint <key id>`) is far shorter than this. */
|
|
453
|
+
export declare const RC_CONTROL_LINE_MAX = 1024;
|
|
454
|
+
/**
|
|
455
|
+
* Renew the Remote Control proxy's session key over its control pipe (fd 3; rc-proxy.mjs SESSION
|
|
456
|
+
* KEY RENEWAL): a `key <id> <expires> <key>` line before each expiry, a `remint <id>` line from the
|
|
457
|
+
* proxy when the gateway rejects its key, answered with a new key or `key-failed`. Renewal events
|
|
458
|
+
* go to the proxy log (the terminal belongs to Claude Code by then), as key ids and expiry times.
|
|
459
|
+
*/
|
|
460
|
+
export declare function startRcSessionKeyRenewal(proxy: Pick<ChildProcess, "stdio">, proxyLog: string, initial: {
|
|
461
|
+
keyId: string;
|
|
462
|
+
expiresAt: string;
|
|
463
|
+
mintArgs: SessionKeyMintArgs;
|
|
464
|
+
}, makeDeps?: (say: (line: string) => void) => SessionKeyDeps): SessionKeyRenewal | undefined;
|
|
450
465
|
/**
|
|
451
466
|
* Locates the `handoff` MCP server (src/mcp/handoff-server.mjs in dev, dist/mcp/handoff-server.mjs
|
|
452
467
|
* once built — a raw, uncompiled `.mjs` file that `copy-assets.mjs` copies verbatim, exactly like
|
package/dist/commands/agent.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
29
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
30
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
31
31
|
import { tmpdir, homedir, hostname } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
@@ -39,7 +39,7 @@ import { ALLOW_AUTOCOMPACT_OFF_ENV, dropAutocompactOffEnv, hasOwnSettingsFlag, m
|
|
|
39
39
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
40
40
|
import { createApiKey } from "../lib/api.js";
|
|
41
41
|
import { recordOrgFor } from "../lib/record-org.js";
|
|
42
|
-
import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, } from "../lib/session-key.js";
|
|
42
|
+
import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, startSessionKeyRenewal, } from "../lib/session-key.js";
|
|
43
43
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
44
44
|
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
45
45
|
import { isLoggedIn } from "../lib/auth.js";
|
|
@@ -612,6 +612,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
612
612
|
// org is the key owner's personal org, where the agent does not exist, so the API answers 404
|
|
613
613
|
// and the launch would fail closed (door1-path run 40400479127). Same read as Record's.
|
|
614
614
|
let sessionKeyId;
|
|
615
|
+
// What a Remote Control launch needs to re-mint before the 24h expiry (startSessionKeyRenewal).
|
|
616
|
+
let sessionKeyRenewal;
|
|
615
617
|
// One read of the served agent's org serves the mint and Record; each says why in its own
|
|
616
618
|
// words when the read fails and the account org is used instead.
|
|
617
619
|
let servedOrg;
|
|
@@ -632,13 +634,15 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
632
634
|
say(fmt.warn(`mnemom agent: could not read ${servedAgentId}'s org (${o.fallback}); minting the session key under ${account.orgId}.`));
|
|
633
635
|
mintOrgId = o.orgId;
|
|
634
636
|
}
|
|
635
|
-
const
|
|
637
|
+
const mintArgs = {
|
|
636
638
|
agentId: servedAgentId,
|
|
637
639
|
orgId: mintOrgId,
|
|
638
640
|
sessionId: cc.id ?? undefined,
|
|
639
|
-
}
|
|
641
|
+
};
|
|
642
|
+
const minted = await mintSessionGatewayKey(defaultSessionKeyDeps(say), mintArgs);
|
|
640
643
|
mnemomKey = minted.key;
|
|
641
644
|
sessionKeyId = minted.keyId;
|
|
645
|
+
sessionKeyRenewal = { expiresAt: minted.expiresAt, mintArgs };
|
|
642
646
|
}
|
|
643
647
|
// ── 1M context window by default ── the model this launch runs on gets `[1m]` when it has no
|
|
644
648
|
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
@@ -828,6 +832,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
828
832
|
yolo,
|
|
829
833
|
ccSessionId,
|
|
830
834
|
sessionKeyId,
|
|
835
|
+
sessionKeyRenewal,
|
|
831
836
|
});
|
|
832
837
|
}
|
|
833
838
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1688,13 +1693,28 @@ function launchRemoteControl(ctx) {
|
|
|
1688
1693
|
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1689
1694
|
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1690
1695
|
};
|
|
1696
|
+
// The minted key expires after 24h and an RC seat outlives that: renew it over a control pipe
|
|
1697
|
+
// (fd 3) — the new key never touches argv, env or disk. Only its id and expiry ride in the env.
|
|
1698
|
+
const renewing = Boolean(ctx.sessionKeyId && ctx.sessionKeyRenewal && ctx.mnemomKey);
|
|
1699
|
+
if (renewing) {
|
|
1700
|
+
proxyEnv.MNEMOM_AGENT_RC_CONTROL_FD = "3";
|
|
1701
|
+
proxyEnv.MNEMOM_AGENT_RC_MNEMOM_KEY_ID = ctx.sessionKeyId;
|
|
1702
|
+
proxyEnv.MNEMOM_AGENT_RC_MNEMOM_KEY_EXPIRES_AT = ctx.sessionKeyRenewal?.expiresAt;
|
|
1703
|
+
}
|
|
1691
1704
|
// MNE-8245 defense-in-depth: the proxy gets the session's (minted) Mnemom key on stdin only
|
|
1692
1705
|
// and reads none of the owner's own Mnemom credential vars, so they never ride into its env.
|
|
1693
1706
|
stripOwnerCredentialEnv(proxyEnv);
|
|
1694
1707
|
const proxy = spawn("node", [proxyScript], {
|
|
1695
1708
|
env: proxyEnv,
|
|
1696
|
-
stdio: ["pipe", "pipe", "inherit"],
|
|
1709
|
+
stdio: ["pipe", "pipe", "inherit", renewing ? "pipe" : "ignore"],
|
|
1697
1710
|
});
|
|
1711
|
+
const renewal = renewing
|
|
1712
|
+
? startRcSessionKeyRenewal(proxy, proxyLog, {
|
|
1713
|
+
keyId: ctx.sessionKeyId,
|
|
1714
|
+
expiresAt: ctx.sessionKeyRenewal?.expiresAt,
|
|
1715
|
+
mintArgs: ctx.sessionKeyRenewal?.mintArgs,
|
|
1716
|
+
})
|
|
1717
|
+
: undefined;
|
|
1698
1718
|
let stopped = false;
|
|
1699
1719
|
const stopProxyAndTempDir = () => {
|
|
1700
1720
|
if (!proxy.killed) {
|
|
@@ -1721,9 +1741,12 @@ function launchRemoteControl(ctx) {
|
|
|
1721
1741
|
if (stopped)
|
|
1722
1742
|
return;
|
|
1723
1743
|
stopped = true;
|
|
1744
|
+
renewal?.stop();
|
|
1724
1745
|
stopProxyAndTempDir();
|
|
1725
|
-
|
|
1726
|
-
|
|
1746
|
+
// After a renewal the proxy holds a newer key than the one minted at launch: revoke that one.
|
|
1747
|
+
const keyId = renewal?.currentKeyId() ?? ctx.sessionKeyId;
|
|
1748
|
+
if (keyId) {
|
|
1749
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), keyId);
|
|
1727
1750
|
}
|
|
1728
1751
|
};
|
|
1729
1752
|
// Last-resort SYNC fallback only — Node's "exit" handlers cannot await, so this branch never
|
|
@@ -1736,6 +1759,7 @@ function launchRemoteControl(ctx) {
|
|
|
1736
1759
|
if (stopped)
|
|
1737
1760
|
return;
|
|
1738
1761
|
stopped = true;
|
|
1762
|
+
renewal?.stop();
|
|
1739
1763
|
stopProxyAndTempDir();
|
|
1740
1764
|
});
|
|
1741
1765
|
// Secrets on stdin — four lines (anthropic, mnemom, openai, proxy credential). An api-key launch
|
|
@@ -1793,6 +1817,58 @@ function launchRemoteControl(ctx) {
|
|
|
1793
1817
|
});
|
|
1794
1818
|
tryStart();
|
|
1795
1819
|
}
|
|
1820
|
+
/** The longest line the proxy sends the launcher (`remint <key id>`) is far shorter than this. */
|
|
1821
|
+
export const RC_CONTROL_LINE_MAX = 1024;
|
|
1822
|
+
/**
|
|
1823
|
+
* Renew the Remote Control proxy's session key over its control pipe (fd 3; rc-proxy.mjs SESSION
|
|
1824
|
+
* KEY RENEWAL): a `key <id> <expires> <key>` line before each expiry, a `remint <id>` line from the
|
|
1825
|
+
* proxy when the gateway rejects its key, answered with a new key or `key-failed`. Renewal events
|
|
1826
|
+
* go to the proxy log (the terminal belongs to Claude Code by then), as key ids and expiry times.
|
|
1827
|
+
*/
|
|
1828
|
+
export function startRcSessionKeyRenewal(proxy, proxyLog, initial, makeDeps = defaultSessionKeyDeps) {
|
|
1829
|
+
const control = proxy.stdio?.[3];
|
|
1830
|
+
if (!control)
|
|
1831
|
+
return undefined;
|
|
1832
|
+
const logLine = (line) => {
|
|
1833
|
+
try {
|
|
1834
|
+
appendFileSync(proxyLog, `${new Date().toISOString()} launcher: ${line}\n`);
|
|
1835
|
+
}
|
|
1836
|
+
catch {
|
|
1837
|
+
/* best effort: the log dir may be gone at exit */
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
control.on("error", () => {
|
|
1841
|
+
/* the proxy exited: the next delivery fails and is logged */
|
|
1842
|
+
});
|
|
1843
|
+
const renewal = startSessionKeyRenewal({
|
|
1844
|
+
deps: makeDeps(logLine),
|
|
1845
|
+
mintArgs: initial.mintArgs,
|
|
1846
|
+
initial: { keyId: initial.keyId, expiresAt: initial.expiresAt },
|
|
1847
|
+
deliver: (minted) => {
|
|
1848
|
+
if (control.destroyed || !control.writable)
|
|
1849
|
+
throw new Error("the proxy's control pipe is closed");
|
|
1850
|
+
control.write(`key ${minted.keyId} ${minted.expiresAt} ${minted.key}\n`);
|
|
1851
|
+
},
|
|
1852
|
+
refuse: (reason) => {
|
|
1853
|
+
if (!control.destroyed && control.writable)
|
|
1854
|
+
control.write(`key-failed ${reason}\n`);
|
|
1855
|
+
},
|
|
1856
|
+
});
|
|
1857
|
+
let buf = "";
|
|
1858
|
+
control.on("data", (chunk) => {
|
|
1859
|
+
buf += chunk.toString("utf8");
|
|
1860
|
+
let i;
|
|
1861
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
1862
|
+
const [cmd, keyId] = buf.slice(0, i).split(" ");
|
|
1863
|
+
buf = buf.slice(i + 1);
|
|
1864
|
+
if (cmd === "remint")
|
|
1865
|
+
void renewal.renew("rejected", keyId);
|
|
1866
|
+
}
|
|
1867
|
+
if (buf.length > RC_CONTROL_LINE_MAX)
|
|
1868
|
+
buf = ""; // a runaway line, never a real request
|
|
1869
|
+
});
|
|
1870
|
+
return renewal;
|
|
1871
|
+
}
|
|
1796
1872
|
function spawnRcCli(ctx) {
|
|
1797
1873
|
// Session name for the claude.ai session list: "<agent>-<scenario>", unless
|
|
1798
1874
|
// the caller passed their own name after `--` (`--name x`, `--name=x`, `-n x`; lifted out here).
|
|
@@ -15,7 +15,7 @@ const OP_SEVERITIES = ["low", "medium", "high", "critical"];
|
|
|
15
15
|
// Per ADR-037 Decision 4: deny public LLM endpoints + public DNS providers,
|
|
16
16
|
// and the any-host CIDRs, at write time.
|
|
17
17
|
// T8-4 (2026-05-19) extended the corpus per
|
|
18
|
-
// `safe-house-hardening/
|
|
18
|
+
// `safe-house-hardening/audits/deny-list-completeness.md`: added emerging
|
|
19
19
|
// LLM providers (Replicate, Together, Anyscale, Fireworks, xAI,
|
|
20
20
|
// Perplexity, DeepSeek) and three reserved IP ranges (link-local v4/v6
|
|
21
21
|
// + multicast v4).
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { type GitHubCredential } from "../lib/scan/credential.js";
|
|
8
|
+
import { type ScanDeps } from "../lib/scan/scan.js";
|
|
9
|
+
import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
|
|
10
|
+
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
11
|
+
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
12
|
+
import { type ReportRecipients } from "../lib/scan/share.js";
|
|
13
|
+
export interface ScanCommandOptions {
|
|
14
|
+
repos: string[];
|
|
15
|
+
period?: {
|
|
16
|
+
start: string;
|
|
17
|
+
end: string;
|
|
18
|
+
};
|
|
19
|
+
share?: string;
|
|
20
|
+
/** Report recipient. Required unless the runner is the CTO (asked when omitted). */
|
|
21
|
+
ctoEmail?: string;
|
|
22
|
+
/** Tier 2: the CTO's own monthly figure, as typed. Needs MNEMOM_ORG_SCAN_MODE=true. */
|
|
23
|
+
monthlySpend?: string;
|
|
24
|
+
/** Tier 2 without a figure on the command line: ask for one on the terminal. */
|
|
25
|
+
noAnthropicUsage?: boolean;
|
|
26
|
+
/** Tier 1: ask for the Anthropic admin key on the terminal (masked). Otherwise ANTHROPIC_ADMIN_KEY is used if set. */
|
|
27
|
+
anthropicAdminKey?: boolean;
|
|
28
|
+
/** Tier 1 result, when the caller already has it (test seam; set from the Anthropic reads otherwise). */
|
|
29
|
+
anthropicUsageUsd?: number;
|
|
30
|
+
anthropicOver200kShare?: number | null;
|
|
31
|
+
/** Test seam: the fetch behind the two Anthropic reads. */
|
|
32
|
+
anthropicDeps?: AnthropicUsageDeps;
|
|
33
|
+
/**
|
|
34
|
+
* The org the report is emailed for; inferred from the signed-in user's single non-personal org when
|
|
35
|
+
* omitted (see resolveScanOrg). The Record totals preview (MNEMOM_ORG_SCAN_MODE=true) reads ONLY an
|
|
36
|
+
* org passed explicitly here, never an inferred one: a guess could read the wrong org.
|
|
37
|
+
*/
|
|
38
|
+
org?: string;
|
|
39
|
+
/** Test seam: the Record read. */
|
|
40
|
+
recordDeps?: RecordDeps;
|
|
41
|
+
/** Test seam: the report-email POST. */
|
|
42
|
+
emailDeps?: ReportEmailDeps;
|
|
43
|
+
/** Test seam: the signed-in user's orgs, used to infer --org when it is omitted. */
|
|
44
|
+
listOrgs?: () => Promise<Array<{
|
|
45
|
+
org_id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
is_personal?: boolean;
|
|
48
|
+
}>>;
|
|
49
|
+
/** Test seams. */
|
|
50
|
+
isTty?: boolean;
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
runnerEmail?: string;
|
|
53
|
+
credential?: GitHubCredential;
|
|
54
|
+
deps?: Partial<ScanDeps>;
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
/** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
|
|
58
|
+
export declare function parseRepoFlags(raw: string[] | undefined): string[] | null;
|
|
59
|
+
export declare const isEmail: (v: unknown) => v is string;
|
|
60
|
+
/** The runner's email (mnemom login, else asked) and the CTO's (flag, else "are you the CTO?"). */
|
|
61
|
+
export declare function resolveRecipients(o: {
|
|
62
|
+
runnerEmail?: string;
|
|
63
|
+
ctoEmail?: string;
|
|
64
|
+
}): Promise<ReportRecipients>;
|
|
65
|
+
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
66
|
+
export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
|
|
67
|
+
export declare const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
68
|
+
export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
69
|
+
/**
|
|
70
|
+
* Tier 1: read measured usage with the CTO's Anthropic admin key, locally. The key comes from
|
|
71
|
+
* ANTHROPIC_ADMIN_KEY or a masked prompt (--anthropic-admin-key; never a value on argv), lives only in this
|
|
72
|
+
* call, and goes only to api.anthropic.com. Returns null for "no key" and for every failure, after printing a
|
|
73
|
+
* fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
|
|
74
|
+
* back to tier 2, then tier 3.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveMeasuredUsage(o: ScanCommandOptions, period: {
|
|
77
|
+
start: string;
|
|
78
|
+
end: string;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
usd: number;
|
|
81
|
+
over200kShare: number | null;
|
|
82
|
+
} | null>;
|
|
83
|
+
/**
|
|
84
|
+
* --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
|
|
85
|
+
* needs an org. Inference uses the signed-in user's own memberships (GET /v1/orgs, personal org
|
|
86
|
+
* excluded): exactly one org is used; none, several, or a failed read stops the scan loudly BEFORE any
|
|
87
|
+
* scan work, naming the fix. It never guesses between orgs and never skips the email.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveScanOrg(o: ScanCommandOptions): Promise<string>;
|
|
90
|
+
export declare const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
|
|
91
|
+
/**
|
|
92
|
+
* The Record read for the preview: null when the flag gate is off (no read, no output, as before).
|
|
93
|
+
* With the gate on and no --org it is a note, not a guess. 404 becomes a note; 401/403 throw
|
|
94
|
+
* RecordAuthError (fail closed); every other failure becomes a fixed note. Nothing is stored.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveRecordOutcome(o: ScanCommandOptions): Promise<RecordOutcome | "no_org" | null>;
|
|
97
|
+
export declare function scanCommand(o: ScanCommandOptions): Promise<void>;
|
|
@@ -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
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ import { wrapCommand } from "./commands/wrap.js";
|
|
|
49
49
|
import { onboardCommand } from "./commands/onboard.js";
|
|
50
50
|
import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
|
|
51
51
|
import { usageCommand, parseNumericFlag, parseDaysFlag } from "./commands/usage.js";
|
|
52
|
+
import { scanCommand, parseRepoFlags } from "./commands/scan.js";
|
|
52
53
|
import { updateCommand } from "./commands/update.js";
|
|
53
54
|
import { runAction } from "./lib/update/action.js";
|
|
54
55
|
import { configListCommand, configGetCommand, configSetCommand, configUnsetCommand, } from "./commands/config.js";
|
|
@@ -2196,6 +2197,40 @@ program
|
|
|
2196
2197
|
process.exit(1);
|
|
2197
2198
|
}
|
|
2198
2199
|
});
|
|
2200
|
+
program
|
|
2201
|
+
.command("scan")
|
|
2202
|
+
.description("Read-only local scan of GitHub PR metadata; the report is emailed to you every time (counts only, no repo names) and Mnemom keeps nothing at share level none")
|
|
2203
|
+
.option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated)")
|
|
2204
|
+
.option("--start <YYYY-MM-DD>", "Period start (default: today)")
|
|
2205
|
+
.option("--end <YYYY-MM-DD>", "Period end (default: today)")
|
|
2206
|
+
.option("--share <level>", "summary | full | none (default: ask)")
|
|
2207
|
+
.option("--cto-email <email>", "CTO who receives the report (required unless you are the CTO; asked when omitted)")
|
|
2208
|
+
.option("--monthly-spend <usd>", "Your own approximate monthly AI spend (self-reported; never sent or saved; needs MNEMOM_ORG_SCAN_MODE=true)")
|
|
2209
|
+
.option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com (needs MNEMOM_ORG_SCAN_MODE=true)")
|
|
2210
|
+
.option("--no-anthropic-usage", "Decline Anthropic usage and be asked for a monthly figure (needs MNEMOM_ORG_SCAN_MODE=true)")
|
|
2211
|
+
.option("--org <org_id>", "Your org id; the report is emailed to you for this org (default: your only organization, else the scan stops and asks). Also selects the Record totals preview (needs MNEMOM_ORG_SCAN_MODE=true)")
|
|
2212
|
+
.action(async (options) => {
|
|
2213
|
+
try {
|
|
2214
|
+
const repos = parseRepoFlags(options.repo);
|
|
2215
|
+
if (!repos)
|
|
2216
|
+
throw new Error("--repo owner/name is required (repeatable)");
|
|
2217
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2218
|
+
await scanCommand({
|
|
2219
|
+
repos,
|
|
2220
|
+
period: { start: options.start ?? options.end ?? today, end: options.end ?? today },
|
|
2221
|
+
share: options.share,
|
|
2222
|
+
ctoEmail: options.ctoEmail,
|
|
2223
|
+
monthlySpend: options.monthlySpend,
|
|
2224
|
+
noAnthropicUsage: options.anthropicUsage === false,
|
|
2225
|
+
anthropicAdminKey: options.anthropicAdminKey === true,
|
|
2226
|
+
org: options.org,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
catch (error) {
|
|
2230
|
+
console.error("Error:", error instanceof Error ? error.message : "scan failed");
|
|
2231
|
+
process.exit(1);
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2199
2234
|
// ============================================================================
|
|
2200
2235
|
// update + config — CLI auto-update (channel latest|next, mode auto|notify|off)
|
|
2201
2236
|
//
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -450,6 +450,19 @@ export interface RecordTotals {
|
|
|
450
450
|
row_count: number;
|
|
451
451
|
}
|
|
452
452
|
export declare function getRecordTotals(orgId: string, lineageKey: string, signal?: AbortSignal): Promise<RecordTotals>;
|
|
453
|
+
/** The window form of the totals read (contract i2-totals-window v1.2), the fields the scan preview uses. */
|
|
454
|
+
export interface RecordWindowTotals {
|
|
455
|
+
model_cost_mmu: string;
|
|
456
|
+
mu_charged_mmu: string | null;
|
|
457
|
+
row_count: number;
|
|
458
|
+
as_of: string | null;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* GET /v1/orgs/{org_id}/record/totals?from=&to= — the org's total over a window (at most 30 days,
|
|
462
|
+
* `to` not in the future). Returns the raw body: the caller shape-checks it. A 404 means the org is
|
|
463
|
+
* not in the Record cohort.
|
|
464
|
+
*/
|
|
465
|
+
export declare function getRecordWindowTotals(orgId: string, from: string, to: string, signal?: AbortSignal): Promise<unknown>;
|
|
453
466
|
/**
|
|
454
467
|
* Report one commit to Record's commit index (Doors W1.8, lib/commit-report.ts):
|
|
455
468
|
* `POST /v1/orgs/{org}/record/commits`, body RecordCommit/v1. Returns the HTTP
|
package/dist/lib/api.js
CHANGED
|
@@ -427,6 +427,14 @@ export async function getMe() {
|
|
|
427
427
|
export async function getRecordTotals(orgId, lineageKey, signal) {
|
|
428
428
|
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/record/totals?lineage_key=${encodeURIComponent(lineageKey)}`, signal ? { signal } : {}, { notFoundLabel: "Record totals", fallback: "Could not read record totals" });
|
|
429
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* GET /v1/orgs/{org_id}/record/totals?from=&to= — the org's total over a window (at most 30 days,
|
|
432
|
+
* `to` not in the future). Returns the raw body: the caller shape-checks it. A 404 means the org is
|
|
433
|
+
* not in the Record cohort.
|
|
434
|
+
*/
|
|
435
|
+
export async function getRecordWindowTotals(orgId, from, to, signal) {
|
|
436
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/record/totals?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, signal ? { signal } : {}, { notFoundLabel: "Record totals", fallback: "Could not read record totals" });
|
|
437
|
+
}
|
|
430
438
|
/**
|
|
431
439
|
* Report one commit to Record's commit index (Doors W1.8, lib/commit-report.ts):
|
|
432
440
|
* `POST /v1/orgs/{org}/record/commits`, body RecordCommit/v1. Returns the HTTP
|