@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.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/agents.d.ts +14 -0
  3. package/dist/commands/agents.js +100 -2
  4. package/dist/commands/card.d.ts +43 -0
  5. package/dist/commands/card.js +153 -102
  6. package/dist/commands/code-config.d.ts +17 -0
  7. package/dist/commands/code-config.js +147 -0
  8. package/dist/commands/code-doctor.d.ts +18 -0
  9. package/dist/commands/code-doctor.js +138 -0
  10. package/dist/commands/code-setup.d.ts +97 -0
  11. package/dist/commands/code-setup.js +330 -0
  12. package/dist/commands/code.d.ts +133 -0
  13. package/dist/commands/code.js +661 -0
  14. package/dist/commands/logs.js +11 -1
  15. package/dist/commands/onboard.d.ts +59 -0
  16. package/dist/commands/onboard.js +395 -0
  17. package/dist/commands/org.d.ts +13 -0
  18. package/dist/commands/org.js +63 -2
  19. package/dist/commands/protection.d.ts +10 -0
  20. package/dist/commands/protection.js +109 -0
  21. package/dist/commands/status.js +5 -0
  22. package/dist/commands/try-me.js +16 -1
  23. package/dist/commands/usage.d.ts +35 -0
  24. package/dist/commands/usage.js +265 -0
  25. package/dist/commands/wrap.d.ts +28 -0
  26. package/dist/commands/wrap.js +331 -0
  27. package/dist/index.js +315 -7
  28. package/dist/lib/agent-config.d.ts +27 -0
  29. package/dist/lib/agent-config.js +86 -0
  30. package/dist/lib/api.d.ts +139 -1
  31. package/dist/lib/api.js +132 -183
  32. package/dist/lib/cli-config.d.ts +33 -0
  33. package/dist/lib/cli-config.js +70 -0
  34. package/dist/lib/code-config.d.ts +78 -0
  35. package/dist/lib/code-config.js +281 -0
  36. package/dist/lib/code.d.ts +154 -0
  37. package/dist/lib/code.js +252 -0
  38. package/dist/lib/config.d.ts +12 -0
  39. package/dist/lib/config.js +55 -3
  40. package/dist/lib/keyed-identity.d.ts +35 -0
  41. package/dist/lib/keyed-identity.js +363 -0
  42. package/dist/lib/protection-drift.d.ts +117 -0
  43. package/dist/lib/protection-drift.js +180 -0
  44. package/dist/lib/skills.js +25 -12
  45. package/dist/lib/version-gate.d.ts +37 -0
  46. package/dist/lib/version-gate.js +84 -0
  47. package/dist/rc-proxy.mjs +341 -0
  48. 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
+ }
@@ -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");
@@ -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 {};