@mnemom/mnemom 0.16.1 → 0.17.0-next.0
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/README.md +1 -0
- package/dist/commands/agents.d.ts +14 -0
- package/dist/commands/agents.js +100 -2
- package/dist/commands/card.d.ts +43 -0
- package/dist/commands/card.js +153 -102
- package/dist/commands/code-config.d.ts +17 -0
- package/dist/commands/code-config.js +147 -0
- package/dist/commands/code-doctor.d.ts +18 -0
- package/dist/commands/code-doctor.js +138 -0
- package/dist/commands/code-setup.d.ts +97 -0
- package/dist/commands/code-setup.js +330 -0
- package/dist/commands/code.d.ts +133 -0
- package/dist/commands/code.js +661 -0
- package/dist/commands/logs.js +11 -1
- package/dist/commands/onboard.d.ts +59 -0
- package/dist/commands/onboard.js +395 -0
- package/dist/commands/org.d.ts +13 -0
- package/dist/commands/org.js +63 -2
- package/dist/commands/protection.d.ts +10 -0
- package/dist/commands/protection.js +109 -0
- package/dist/commands/status.js +5 -0
- package/dist/commands/try-me.js +16 -1
- package/dist/commands/usage.d.ts +35 -0
- package/dist/commands/usage.js +265 -0
- package/dist/commands/wrap.d.ts +28 -0
- package/dist/commands/wrap.js +331 -0
- package/dist/index.js +315 -7
- package/dist/lib/agent-config.d.ts +27 -0
- package/dist/lib/agent-config.js +86 -0
- package/dist/lib/api.d.ts +139 -1
- package/dist/lib/api.js +132 -183
- package/dist/lib/cli-config.d.ts +33 -0
- package/dist/lib/cli-config.js +70 -0
- package/dist/lib/code-config.d.ts +78 -0
- package/dist/lib/code-config.js +281 -0
- package/dist/lib/code.d.ts +154 -0
- package/dist/lib/code.js +252 -0
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.js +55 -3
- package/dist/lib/keyed-identity.d.ts +35 -0
- package/dist/lib/keyed-identity.js +363 -0
- package/dist/lib/protection-drift.d.ts +117 -0
- package/dist/lib/protection-drift.js +180 -0
- package/dist/lib/skills.js +25 -12
- package/dist/lib/version-gate.d.ts +37 -0
- package/dist/lib/version-gate.js +84 -0
- package/dist/rc-proxy.mjs +341 -0
- package/package.json +9 -7
|
@@ -7,6 +7,7 @@ import { PROTECTION_CARD_MAX_BYTES, getProtectionCard, putProtectionCard, resolv
|
|
|
7
7
|
import { requireAuth } from "../lib/auth.js";
|
|
8
8
|
import { fmt } from "../lib/format.js";
|
|
9
9
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
10
|
+
import { compareProtectionCards, formatDriftReport, isDrift } from "../lib/protection-drift.js";
|
|
10
11
|
const PROTECTION_MODES = ["off", "observe", "nudge", "enforce"];
|
|
11
12
|
const SURFACE_KEYS = ["incoming", "outgoing", "tool_calls", "tool_responses"];
|
|
12
13
|
// Mirrors mnemom-api/src/composition/validate.ts OP_SEVERITIES (MNE-833).
|
|
@@ -822,3 +823,111 @@ export async function protectionEditCommand(agentName, options = {}) {
|
|
|
822
823
|
process.exit(1);
|
|
823
824
|
}
|
|
824
825
|
}
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// Drift check — committed snapshot vs live canonical card
|
|
828
|
+
//
|
|
829
|
+
// Closes the gap that the live posture had no committed representation: an
|
|
830
|
+
// out-of-band SQL write to the canonical row (or a recompose that reverts one)
|
|
831
|
+
// changed what every turn is screened against, with no diff and no alert. This
|
|
832
|
+
// command is the alert. See `cards/README.md` for the full mechanism and for
|
|
833
|
+
// why the repo file is a SNAPSHOT rather than the source of truth.
|
|
834
|
+
// ============================================================================
|
|
835
|
+
/**
|
|
836
|
+
* Compare a committed protection-card snapshot against the live CANONICAL
|
|
837
|
+
* (composed) card. Read-only: never writes, never publishes.
|
|
838
|
+
*
|
|
839
|
+
* Exit 0 = no drift, exit 1 = drift or error, so a scheduled job can gate on it.
|
|
840
|
+
*/
|
|
841
|
+
export async function protectionDriftCommand(file, agentName, opts = {}) {
|
|
842
|
+
const filePath = path.resolve(file);
|
|
843
|
+
if (!fs.existsSync(filePath)) {
|
|
844
|
+
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
let parsed;
|
|
848
|
+
try {
|
|
849
|
+
parsed = parseProtectionFile(filePath);
|
|
850
|
+
}
|
|
851
|
+
catch (e) {
|
|
852
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
853
|
+
console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
// Prefer the agent_id the snapshot itself declares — the snapshot is per-agent
|
|
857
|
+
// and pointing it at a different agent would compare two unrelated cards.
|
|
858
|
+
const declaredAgentId = typeof parsed.parsed.agent_id === "string" ? parsed.parsed.agent_id : undefined;
|
|
859
|
+
let agentId;
|
|
860
|
+
if (agentName) {
|
|
861
|
+
try {
|
|
862
|
+
agentId = await resolveAgentId(agentName);
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
// Resolution needs auth + a network round trip. Report it as a drift-check
|
|
866
|
+
// failure (exit 1) rather than letting a raw stack reach a scheduled job.
|
|
867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
868
|
+
console.log("\n" + fmt.error(`Could not resolve agent "${agentName}": ${message}`) + "\n");
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
agentId = declaredAgentId;
|
|
874
|
+
}
|
|
875
|
+
if (!agentId) {
|
|
876
|
+
console.log("\n" +
|
|
877
|
+
fmt.error("No agent to compare against: the snapshot declares no agent_id and no --agent was given.") +
|
|
878
|
+
"\n");
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
if (declaredAgentId && agentId !== declaredAgentId) {
|
|
882
|
+
console.log("\n" +
|
|
883
|
+
fmt.error(`Refusing to compare: --agent resolves to ${agentId} but the snapshot declares ${declaredAgentId}.`) +
|
|
884
|
+
"\n");
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
let live;
|
|
888
|
+
try {
|
|
889
|
+
const { body, contentType } = await getProtectionCard(agentId, "json");
|
|
890
|
+
if (!body) {
|
|
891
|
+
console.log("\n" +
|
|
892
|
+
fmt.error(`No live protection card for ${agentId}, but a committed snapshot exists at ${filePath}.`) +
|
|
893
|
+
"\n");
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const decoded = contentType.includes("yaml") ? yaml.load(body) : JSON.parse(body);
|
|
897
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
898
|
+
throw new Error("live card did not decode to an object");
|
|
899
|
+
}
|
|
900
|
+
live = decoded;
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
904
|
+
console.log("\n" + fmt.error(`Failed to fetch live protection card: ${message}`) + "\n");
|
|
905
|
+
process.exit(1);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const result = compareProtectionCards(parsed.parsed, live);
|
|
909
|
+
const drifted = isDrift(result, { strict: opts.strict });
|
|
910
|
+
if (opts.json) {
|
|
911
|
+
console.log(JSON.stringify({ agent_id: agentId, snapshot: filePath, strict: opts.strict === true, drifted, ...result }, null, 2));
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
console.log(fmt.header("Protection Card Drift"));
|
|
915
|
+
console.log();
|
|
916
|
+
console.log(fmt.label(" Snapshot:", ` ${filePath}`));
|
|
917
|
+
console.log(fmt.label(" Agent:", ` ${agentId}`));
|
|
918
|
+
console.log(fmt.label(" Compared against:", " live canonical (composed) card"));
|
|
919
|
+
console.log();
|
|
920
|
+
console.log(formatDriftReport(result, { strict: opts.strict }));
|
|
921
|
+
console.log();
|
|
922
|
+
if (drifted) {
|
|
923
|
+
console.log(fmt.error("DRIFT: the live posture differs from the committed snapshot.") +
|
|
924
|
+
"\n\n Reconcile deliberately — see cards/README.md. Do not publish the snapshot\n" +
|
|
925
|
+
" to 'fix' the diff without first confirming what the sources compose to.\n");
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
console.log(fmt.success("No drift.") + "\n");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (drifted)
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -188,6 +188,11 @@ async function showTraceSummary(agentId) {
|
|
|
188
188
|
if (integrity.violation_count > 0) {
|
|
189
189
|
console.log(`Violations: ${integrity.violation_count}`);
|
|
190
190
|
}
|
|
191
|
+
// MNE-596: logged/visible but NOT subtracted from Verified and NOT part
|
|
192
|
+
// of Violations above — a correctly policy-refused attack, not a real one.
|
|
193
|
+
if (integrity.bounded_refusal_count && integrity.bounded_refusal_count > 0) {
|
|
194
|
+
console.log(`Bounded Refusals: ${integrity.bounded_refusal_count} (policy-enforced, not counted)`);
|
|
195
|
+
}
|
|
191
196
|
}
|
|
192
197
|
else {
|
|
193
198
|
console.log("Integrity: No data yet");
|
package/dist/commands/try-me.js
CHANGED
|
@@ -28,8 +28,9 @@ import { putAlignmentCard, putProtectionCard, listOrgAgents, MnemomApiError } fr
|
|
|
28
28
|
import { resolveAuth, loginWithBrowser, loginWithDeviceFlow } from "../lib/auth.js";
|
|
29
29
|
import { openBrowser } from "../lib/oauth.js";
|
|
30
30
|
import { askSelect, askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
31
|
-
import { getApiUrl } from "../lib/config.js";
|
|
31
|
+
import { getApiUrl, setApiUrlOverride } from "../lib/config.js";
|
|
32
32
|
import { fmt } from "../lib/format.js";
|
|
33
|
+
import { getAgentName, mergeAgentConfig } from "../lib/agent-config.js";
|
|
33
34
|
const POLL_INTERVAL_MS = 3000;
|
|
34
35
|
/**
|
|
35
36
|
* Entry point for `mnemom try-me <token>`. Throws on fatal misconfiguration
|
|
@@ -58,6 +59,12 @@ export async function tryMeCommand(token, options = {}) {
|
|
|
58
59
|
throw new Error(`'${token}' doesn't look like a try-me token (expected e.g. tryme_…). ` +
|
|
59
60
|
"Copy the token from your Dojo /try-me invite.");
|
|
60
61
|
}
|
|
62
|
+
// Point EVERY API surface for this run at the ring the operator chose via
|
|
63
|
+
// `--api` — including OAuth discovery + the one-click claim login, which
|
|
64
|
+
// otherwise fall back to getApiUrl()'s prod default and leak the sign-in to
|
|
65
|
+
// prod even though resolve/birth ran on the ring.
|
|
66
|
+
if (options.api)
|
|
67
|
+
setApiUrlOverride(options.api);
|
|
61
68
|
// ── State: resolve ────────────────────────────────────────────────────────
|
|
62
69
|
say(fmt.header("Mnemom Dojo — try-me"));
|
|
63
70
|
say();
|
|
@@ -114,6 +121,8 @@ export async function tryMeCommand(token, options = {}) {
|
|
|
114
121
|
result.claimed = true;
|
|
115
122
|
result.steps.push({ step: "claim", status: "ok", detail: agentId });
|
|
116
123
|
say(fmt.success("Claimed — you now own this agent."));
|
|
124
|
+
// Persist identity so subsequent skill invocations skip the identity prompts.
|
|
125
|
+
mergeAgentConfig({ agent_id: agentId, agent_name: name, gateway_url: manifest.gateway.endpoint });
|
|
117
126
|
// The session context the card writes share: ensureSession + writeCardWithRetry
|
|
118
127
|
// both need it (the latter re-triggers ensureSession on a persistent auth 403).
|
|
119
128
|
const sessionCtx = { nonInteractive, autoOpen, agentId, say };
|
|
@@ -156,6 +165,12 @@ export async function tryMeCommand(token, options = {}) {
|
|
|
156
165
|
async function pickName(manifest, options, skipPrompt, say) {
|
|
157
166
|
if (options.name && options.name.trim())
|
|
158
167
|
return options.name.trim();
|
|
168
|
+
// On repeat invocations, use the saved name so the identity prompt is skipped.
|
|
169
|
+
const savedName = getAgentName();
|
|
170
|
+
if (savedName) {
|
|
171
|
+
say(fmt.dim(`Using saved agent name "${savedName}" (override with --name).`));
|
|
172
|
+
return savedName;
|
|
173
|
+
}
|
|
159
174
|
const opts = manifest.handoff.name_options ?? [];
|
|
160
175
|
const fallback = opts[0] ?? "mnemom-dojo-agent";
|
|
161
176
|
if (skipPrompt) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type UsageDays } from "../lib/api.js";
|
|
2
|
+
export interface UsageCommandOptions {
|
|
3
|
+
org: string;
|
|
4
|
+
days?: number;
|
|
5
|
+
person?: string;
|
|
6
|
+
provider?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
limit?: number;
|
|
9
|
+
cursor?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse a numeric CLI flag, warning and falling back to the caller's default
|
|
14
|
+
* on anything that is not a whole number of at least 1.
|
|
15
|
+
*
|
|
16
|
+
* Exported (rather than closed over inside the command registration) so the
|
|
17
|
+
* reject-and-warn arm is directly testable — it is the only branch here with
|
|
18
|
+
* observable behaviour, and reaching it through Commander would mean driving
|
|
19
|
+
* the whole CLI just to assert one stderr line.
|
|
20
|
+
*
|
|
21
|
+
* Strict on purpose. `parseInt("2abc", 10)` returns `2`, so an `isNaN` guard
|
|
22
|
+
* accepts partial-numeric input and silently acts on a value the user did not
|
|
23
|
+
* type; `--limit -1` is likewise nonsense that the server would have to
|
|
24
|
+
* reject. A whole-digits test rejects both up front.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseNumericFlag(flag: string, raw?: string): number | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Validate `--days` against the window enum the endpoint actually accepts.
|
|
29
|
+
*
|
|
30
|
+
* The server 400s on anything outside {7, 30, 90}. Rejecting locally turns a
|
|
31
|
+
* round-trip and a raw API error into an immediate message that names the
|
|
32
|
+
* allowed values.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseDaysFlag(raw?: string): UsageDays | undefined;
|
|
35
|
+
export declare function usageCommand(opts: UsageCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { getOrgUsage, MnemomApiError, USAGE_ALLOWED_DAYS, } from "../lib/api.js";
|
|
2
|
+
import { requireAuth } from "../lib/auth.js";
|
|
3
|
+
import { fmt } from "../lib/format.js";
|
|
4
|
+
/**
|
|
5
|
+
* Parse a numeric CLI flag, warning and falling back to the caller's default
|
|
6
|
+
* on anything that is not a whole number of at least 1.
|
|
7
|
+
*
|
|
8
|
+
* Exported (rather than closed over inside the command registration) so the
|
|
9
|
+
* reject-and-warn arm is directly testable — it is the only branch here with
|
|
10
|
+
* observable behaviour, and reaching it through Commander would mean driving
|
|
11
|
+
* the whole CLI just to assert one stderr line.
|
|
12
|
+
*
|
|
13
|
+
* Strict on purpose. `parseInt("2abc", 10)` returns `2`, so an `isNaN` guard
|
|
14
|
+
* accepts partial-numeric input and silently acts on a value the user did not
|
|
15
|
+
* type; `--limit -1` is likewise nonsense that the server would have to
|
|
16
|
+
* reject. A whole-digits test rejects both up front.
|
|
17
|
+
*/
|
|
18
|
+
export function parseNumericFlag(flag, raw) {
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!/^\d+$/.test(raw.trim()) || Number(raw.trim()) < 1) {
|
|
22
|
+
// Echo the offending value back, but stripped: it is user-supplied and
|
|
23
|
+
// lands unescaped in a terminal, so control characters (ANSI cursor/colour
|
|
24
|
+
// sequences) are removed and the length is capped before it is printed.
|
|
25
|
+
const safe = raw.replace(/[^\x20-\x7e]/g, "?").slice(0, 32);
|
|
26
|
+
console.error(`Warning: ${flag} must be a whole number of 1 or more; ignoring '${safe}' and using default.`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return Number(raw.trim());
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate `--days` against the window enum the endpoint actually accepts.
|
|
33
|
+
*
|
|
34
|
+
* The server 400s on anything outside {7, 30, 90}. Rejecting locally turns a
|
|
35
|
+
* round-trip and a raw API error into an immediate message that names the
|
|
36
|
+
* allowed values.
|
|
37
|
+
*/
|
|
38
|
+
export function parseDaysFlag(raw) {
|
|
39
|
+
if (raw === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const n = parseNumericFlag("--days", raw);
|
|
42
|
+
if (n === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (!USAGE_ALLOWED_DAYS.includes(n)) {
|
|
45
|
+
console.error(`Warning: --days must be one of ${USAGE_ALLOWED_DAYS.join(", ")}; ` +
|
|
46
|
+
`ignoring '${n}' and using default.`);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
const HEADERS = {
|
|
52
|
+
person: "Person",
|
|
53
|
+
provider: "Provider",
|
|
54
|
+
model: "Model",
|
|
55
|
+
tokensIn: "Tokens in",
|
|
56
|
+
tokensOut: "Tokens out",
|
|
57
|
+
requests: "Requests",
|
|
58
|
+
};
|
|
59
|
+
// Control and bidi/invisible characters are stripped from every API-sourced string
|
|
60
|
+
// before it reaches the terminal. A crafted display name, email or error body
|
|
61
|
+
// ("\x1b[2J\x1b[H" to clear the screen, a right-to-left override to reorder a row)
|
|
62
|
+
// would otherwise let table content repaint or scramble an operator's terminal.
|
|
63
|
+
//
|
|
64
|
+
// Only the dangerous characters go: the C0/C1 control block, zero-width
|
|
65
|
+
// space/non-joiner, the LRM/RLM marks, the bidi embedding+override block, the
|
|
66
|
+
// directional isolates, and the BOM/zero-width no-break space. Printable
|
|
67
|
+
// non-ASCII is PRESERVED — a real name like "José" or "田中" must render as
|
|
68
|
+
// itself — which is why this is deliberately NOT the ASCII-only strip
|
|
69
|
+
// `parseNumericFlag` applies to flag input (there the value is supposed to be
|
|
70
|
+
// digits, so collapsing everything else is the right call). U+200D ZWJ is left
|
|
71
|
+
// alone so emoji sequences in a display name do not shatter into components.
|
|
72
|
+
//
|
|
73
|
+
// The control block is matched as `\p{Cc}` rather than an explicit
|
|
74
|
+
// `\u0000-\u001f\u007f-\u009f` range. The two are exactly equivalent (verified
|
|
75
|
+
// across all 1.1M code points), but the range form puts control characters in
|
|
76
|
+
// the regex literal, which `no-control-regex` blocks — and silencing that rule
|
|
77
|
+
// here would silence it for any accidental control character added later.
|
|
78
|
+
const UNSAFE_TERMINAL_CHARS = /[\p{Cc}\u200b\u200c\u200e\u200f\u2028-\u202e\u2066-\u2069\ufeff]/gu;
|
|
79
|
+
/** Replace terminal-control characters with `?` so tampering is visible rather than silent. */
|
|
80
|
+
function sanitizeForTerminal(s) {
|
|
81
|
+
return s.replace(UNSAFE_TERMINAL_CHARS, "?");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The endpoint is hidden behind USAGE_ATTRIBUTION_API_ENABLED, and when that
|
|
85
|
+
* flag is off it answers **404, not 403** — deliberately, so a disabled feature
|
|
86
|
+
* looks exactly like a route that was never deployed. A genuinely unknown org,
|
|
87
|
+
* and a caller the org is not visible to, also produce 404.
|
|
88
|
+
*
|
|
89
|
+
* So 404 is irreducibly ambiguous and this message must not pick one cause. The
|
|
90
|
+
* first version of this command labelled it "Org not found or not accessible",
|
|
91
|
+
* which reads as a mistyped org id when the overwhelmingly common cause is
|
|
92
|
+
* simply that the flag is off in that environment.
|
|
93
|
+
*/
|
|
94
|
+
const NOT_AVAILABLE_MSG = "Usage attribution is not available for this org. It may not be enabled in " +
|
|
95
|
+
"this environment yet, or the org may not exist or not be visible to you. " +
|
|
96
|
+
"Contact your Mnemom account team to enable it.";
|
|
97
|
+
export async function usageCommand(opts) {
|
|
98
|
+
await requireAuth();
|
|
99
|
+
let data;
|
|
100
|
+
try {
|
|
101
|
+
data = await getOrgUsage(opts.org, {
|
|
102
|
+
days: opts.days,
|
|
103
|
+
personId: opts.person,
|
|
104
|
+
provider: opts.provider,
|
|
105
|
+
model: opts.model,
|
|
106
|
+
limit: opts.limit,
|
|
107
|
+
cursor: opts.cursor,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof MnemomApiError) {
|
|
112
|
+
if (err.effectiveStatus === 404) {
|
|
113
|
+
console.error(fmt.error(NOT_AVAILABLE_MSG));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
// 403 stays distinct from the not-available path above: it is a real role
|
|
117
|
+
// deny (member/viewer), and telling that caller "not enabled" would send
|
|
118
|
+
// them to the account team for something their own org admin can grant.
|
|
119
|
+
if (err.effectiveStatus === 403) {
|
|
120
|
+
console.error(fmt.error("You do not have permission to view usage for this org " +
|
|
121
|
+
"(requires owner, admin, or auditor)."));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
// Same sanitize as the identity column below: an error body is API-sourced
|
|
125
|
+
// and lands unescaped in a terminal. One rule for every API-sourced string.
|
|
126
|
+
console.error(fmt.error(sanitizeForTerminal(err.message)));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
console.error(fmt.error(sanitizeForTerminal(msg)));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
if (opts.json) {
|
|
134
|
+
console.log(JSON.stringify(data, null, 2));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Rendered as UTC calendar days. The window is `from` inclusive / `to`
|
|
138
|
+
// exclusive, so the last day a reader should expect to see is the day BEFORE
|
|
139
|
+
// `to` — printing `to` raw invites "why is the last day empty?".
|
|
140
|
+
const windowLabel = `${utcDay(data.from)} to ${utcDay(dayBefore(data.to))} (UTC)`;
|
|
141
|
+
console.log(fmt.header(`mnemom usage — ${windowLabel}`));
|
|
142
|
+
console.log();
|
|
143
|
+
console.log(fmt.label("Org:", sanitizeForTerminal(opts.org)));
|
|
144
|
+
console.log(fmt.label("Window:", windowLabel));
|
|
145
|
+
if (data.collection_started_at) {
|
|
146
|
+
console.log(fmt.label("Reporting since:", utcDay(data.collection_started_at)));
|
|
147
|
+
}
|
|
148
|
+
if (data.data.length === 0) {
|
|
149
|
+
console.log();
|
|
150
|
+
// Deliberately distinguishable from the not-available path above: this is a
|
|
151
|
+
// successful, authorized read that found nothing, not a hidden failure.
|
|
152
|
+
console.log(fmt.dim("No consumption recorded for this window."));
|
|
153
|
+
console.log();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(fmt.section("Consumption by person"));
|
|
158
|
+
console.log();
|
|
159
|
+
// The numeric columns size themselves to their widest value rather than
|
|
160
|
+
// taking a fixed width. A 64-bit total runs to 20 digits (max uint64 is
|
|
161
|
+
// 18446744073709551615) and would overflow any narrower column, ragging
|
|
162
|
+
// every column to its right. padLeft deliberately does NOT truncate — a
|
|
163
|
+
// clipped token count is wrong data, not just untidy layout — so the column
|
|
164
|
+
// grows instead. `data.data` is non-empty here (the zero-row case returned
|
|
165
|
+
// above), so each spread always has an argument.
|
|
166
|
+
const rows = data.data;
|
|
167
|
+
const colW = {
|
|
168
|
+
person: 30,
|
|
169
|
+
provider: Math.max(HEADERS.provider.length, ...rows.map((r) => label(r.provider).length)),
|
|
170
|
+
model: Math.max(HEADERS.model.length, ...rows.map((r) => label(r.model).length)),
|
|
171
|
+
tokensIn: Math.max(HEADERS.tokensIn.length, ...rows.map((r) => r.tokens_in.length)),
|
|
172
|
+
tokensOut: Math.max(HEADERS.tokensOut.length, ...rows.map((r) => r.tokens_out.length)),
|
|
173
|
+
requests: Math.max(HEADERS.requests.length, ...rows.map((r) => r.request_count.length)),
|
|
174
|
+
};
|
|
175
|
+
console.log(fmt.dim([
|
|
176
|
+
padRight(HEADERS.person, colW.person),
|
|
177
|
+
padRight(HEADERS.provider, colW.provider),
|
|
178
|
+
padRight(HEADERS.model, colW.model),
|
|
179
|
+
padLeft(HEADERS.tokensIn, colW.tokensIn),
|
|
180
|
+
padLeft(HEADERS.tokensOut, colW.tokensOut),
|
|
181
|
+
padLeft(HEADERS.requests, colW.requests),
|
|
182
|
+
].join(" ")));
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
console.log(formatRow(row, colW));
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
// Coverage is reported from the server's pre-pagination totals, never counted
|
|
188
|
+
// from the rows on screen: totals cover the complete filtered result, so a
|
|
189
|
+
// per-page count would understate consumption on every page but the last.
|
|
190
|
+
const totals = data.totals;
|
|
191
|
+
console.log(fmt.dim(`Totals — tokens in ${totals.tokens_in}, out ${totals.tokens_out}, ` +
|
|
192
|
+
`requests ${totals.request_count}.`));
|
|
193
|
+
console.log(fmt.dim(`Attributed to a person: ${totals.attributed_request_count} of ` +
|
|
194
|
+
`${totals.request_count} requests (${formatRate(totals.attribution_rate)}). ` +
|
|
195
|
+
`Unattributed: ${totals.unattributed_request_count}.`));
|
|
196
|
+
if (data.next_cursor) {
|
|
197
|
+
console.log();
|
|
198
|
+
// The cursor is opaque and API-sourced, so it gets the same treatment as
|
|
199
|
+
// every other API-sourced string here — no exception for "it's just an id".
|
|
200
|
+
// An unsanitized cursor is the worst of the lot: this line invites the user
|
|
201
|
+
// to copy it back onto their command line, so a bidi override inside it
|
|
202
|
+
// could make the pasted command read as something other than what it runs.
|
|
203
|
+
console.log(fmt.dim(`More rows available. Use --cursor '${sanitizeForTerminal(data.next_cursor)}' to continue.`));
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
207
|
+
/** Render the attribution rate as a percentage without inventing precision. */
|
|
208
|
+
function formatRate(rate) {
|
|
209
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
210
|
+
}
|
|
211
|
+
/** ISO timestamp to `YYYY-MM-DD`, keeping the server's UTC framing. */
|
|
212
|
+
function utcDay(iso) {
|
|
213
|
+
return sanitizeForTerminal(iso).slice(0, 10);
|
|
214
|
+
}
|
|
215
|
+
/** One UTC day before an ISO timestamp, so an exclusive bound reads inclusively. */
|
|
216
|
+
function dayBefore(iso) {
|
|
217
|
+
const t = Date.parse(iso);
|
|
218
|
+
// A malformed bound is echoed as-is rather than becoming "Invalid Date": the
|
|
219
|
+
// window label is disclosure copy, and it must never be the thing that fails.
|
|
220
|
+
if (Number.isNaN(t))
|
|
221
|
+
return iso;
|
|
222
|
+
return new Date(t - 24 * 60 * 60 * 1000).toISOString();
|
|
223
|
+
}
|
|
224
|
+
/** Blank provider/model values still occupy a column; the server sends "Unknown". */
|
|
225
|
+
function label(value) {
|
|
226
|
+
return sanitizeForTerminal(value || "Unknown");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The person column. The server has already resolved the display label and
|
|
230
|
+
* guarantees a raw user UUID is never it — former or deleted members come back
|
|
231
|
+
* as "Former or deleted member". So this prefers `display_name`, falls back to
|
|
232
|
+
* `email`, and must NOT fall back to `user_id`, which would leak the UUID the
|
|
233
|
+
* API deliberately withholds. `user_id` is null on the unattributed row, which
|
|
234
|
+
* the server labels for us; the literal here is only a last-resort guard.
|
|
235
|
+
*/
|
|
236
|
+
function personLabel(row) {
|
|
237
|
+
return sanitizeForTerminal(row.display_name || row.email || "Unattributed");
|
|
238
|
+
}
|
|
239
|
+
function formatRow(row, colW) {
|
|
240
|
+
// Sanitized before padding, not after: the identity is an API-sourced display
|
|
241
|
+
// string (a platform user's own email/name), and it is printed straight to a
|
|
242
|
+
// terminal. Stripping first also keeps the column width honest — a control
|
|
243
|
+
// sequence occupies string length but no screen columns, so padding an
|
|
244
|
+
// unsanitized value would rag the table even without any attack.
|
|
245
|
+
return [
|
|
246
|
+
padRight(personLabel(row), colW.person),
|
|
247
|
+
padRight(label(row.provider), colW.provider),
|
|
248
|
+
padRight(label(row.model), colW.model),
|
|
249
|
+
padLeft(row.tokens_in, colW.tokensIn),
|
|
250
|
+
padLeft(row.tokens_out, colW.tokensOut),
|
|
251
|
+
padLeft(row.request_count, colW.requests),
|
|
252
|
+
].join(" ");
|
|
253
|
+
}
|
|
254
|
+
// `> width`, not `>= width`: a value whose length equals the column width fits
|
|
255
|
+
// exactly, and truncating it would destroy its last character for no gain. Both
|
|
256
|
+
// arms return a `width`-wide string, so the table stayed aligned either way —
|
|
257
|
+
// the bug was invisible in the layout and only ever lost data.
|
|
258
|
+
function padRight(s, width) {
|
|
259
|
+
return s.length > width ? s.slice(0, width - 1) + "…" : s.padEnd(width);
|
|
260
|
+
}
|
|
261
|
+
// Never truncates: callers pass numeric totals, and a clipped total would
|
|
262
|
+
// misreport consumption. Columns are sized to fit instead (see colW above).
|
|
263
|
+
function padLeft(s, width) {
|
|
264
|
+
return s.length >= width ? s : s.padStart(width);
|
|
265
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom wrap` — instrument an existing production agent through the Mnemom gateway (MNE-935, A4).
|
|
3
|
+
*
|
|
4
|
+
* Asks the developer for their provider + framework + agent name, makes a birth
|
|
5
|
+
* call to the gateway to provision the agent identity, seeds starter alignment +
|
|
6
|
+
* protection cards, and emits a drop-in code example. No reasoning change to the
|
|
7
|
+
* host agent.
|
|
8
|
+
*
|
|
9
|
+
* AUTH MODEL: Two credentials are involved:
|
|
10
|
+
* - Mnemom auth (MNEMOM_API_KEY or `mnemom login` JWT) — used for card writes
|
|
11
|
+
* - Provider API key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY) — used
|
|
12
|
+
* for the one-time gateway birth call; this is the same key the user's agent
|
|
13
|
+
* already holds, so the birth requires no new credential
|
|
14
|
+
*
|
|
15
|
+
* See SKILL-RUNNER-CONTRACT.md §2–§4 for the lifecycle + output contract.
|
|
16
|
+
*/
|
|
17
|
+
type Provider = "anthropic" | "openai" | "gemini";
|
|
18
|
+
export interface WrapOptions {
|
|
19
|
+
json?: boolean;
|
|
20
|
+
yes?: boolean;
|
|
21
|
+
provider?: string;
|
|
22
|
+
framework?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
providerKey?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function wrapCommand(options?: WrapOptions): Promise<void>;
|
|
27
|
+
export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string): Promise<string>;
|
|
28
|
+
export {};
|