@mnemom/mnemom 0.17.0-next.2 → 0.17.0-next.3

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.
@@ -11,8 +11,22 @@
11
11
  * live), Remote-Control capability (node + openssl + the --remote-control flag),
12
12
  * and claude.ai login (informational — Remote Control needs it).
13
13
  */
14
+ type Mark = "ok" | "warn" | "miss" | "info";
14
15
  export interface DoctorOptions {
15
16
  cli?: string;
16
17
  }
17
18
  /** Run the preflight and print the checklist. Sets process.exitCode on failure. */
18
19
  export declare function codeDoctorCommand(options?: DoctorOptions): Promise<void>;
20
+ export interface CodeAccessProbe {
21
+ mark: Mark;
22
+ text: string;
23
+ hint?: string;
24
+ }
25
+ /**
26
+ * The invited-cohort gate, as the doctor reports it. Signed out → info (we can't
27
+ * check without a session); enabled → ok with the reason; a determined
28
+ * `code_not_enabled` → MISS with the api's hint (a launch WOULD be blocked);
29
+ * anything inconclusive → info, never a MISS (the launch preamble fails open too).
30
+ */
31
+ export declare function probeCodeAccess(): Promise<CodeAccessProbe>;
32
+ export {};
@@ -16,6 +16,10 @@ import { existsSync } from "node:fs";
16
16
  import { anthropicKeySource, checkRcCapability, resolveCliBin, } from "./code.js";
17
17
  import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, } from "../lib/code.js";
18
18
  import { CODE_CONFIG_PATH } from "../lib/code-config.js";
19
+ import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
20
+ import { isLoggedIn } from "../lib/auth.js";
21
+ import { getActiveOrg } from "../lib/cli-config.js";
22
+ import { isCodeNotEnabled, sourceLabel } from "./code-invite.js";
19
23
  const MARK = { ok: "[ ok ]", warn: "[warn]", miss: "[MISS]", info: "[info]" };
20
24
  const out = (line = "") => console.log(line);
21
25
  /** Run the preflight and print the checklist. Sets process.exitCode on failure. */
@@ -78,6 +82,9 @@ export async function codeDoctorCommand(options = {}) {
78
82
  add("info", "claude.ai login: could not determine");
79
83
  }
80
84
  }
85
+ // ── Mnemom Code access (invited cohort gate) ──
86
+ const gate = await probeCodeAccess();
87
+ add(gate.mark, gate.text, gate.hint);
81
88
  // ── render ──
82
89
  const door = resolveAnthropicDoor(env);
83
90
  out("mnemom code doctor");
@@ -136,3 +143,36 @@ function claudeLoggedIn(bin) {
136
143
  return null;
137
144
  }
138
145
  }
146
+ /**
147
+ * The invited-cohort gate, as the doctor reports it. Signed out → info (we can't
148
+ * check without a session); enabled → ok with the reason; a determined
149
+ * `code_not_enabled` → MISS with the api's hint (a launch WOULD be blocked);
150
+ * anything inconclusive → info, never a MISS (the launch preamble fails open too).
151
+ */
152
+ export async function probeCodeAccess() {
153
+ if (!(await isLoggedIn())) {
154
+ return {
155
+ mark: "info",
156
+ text: "Mnemom Code access: sign in to check",
157
+ hint: "run `mnemom login`; @mnemom.ai accounts are enabled by default, others need an invitation (mnemom code --invite <token>)",
158
+ };
159
+ }
160
+ const org = getActiveOrg();
161
+ const orgLabel = org ? `${org.name} (${org.slug})` : "your personal org";
162
+ try {
163
+ const ent = await getCodeEntitlement(org?.org_id);
164
+ return { mark: "ok", text: `Mnemom Code access: enabled for ${orgLabel} (${sourceLabel(ent.source)})` };
165
+ }
166
+ catch (err) {
167
+ if (isCodeNotEnabled(err)) {
168
+ const hint = err.details?.hint;
169
+ return {
170
+ mark: "miss",
171
+ text: `Mnemom Code access: not enabled for ${orgLabel}`,
172
+ hint: typeof hint === "string" && hint ? hint : "redeem an invitation: mnemom code --invite <token>",
173
+ };
174
+ }
175
+ const detail = err instanceof MnemomApiError || err instanceof Error ? err.message : String(err);
176
+ return { mark: "info", text: `Mnemom Code access: could not determine (${detail})` };
177
+ }
178
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Mnemom Code — the invited-cohort gate + `mnemom code --invite <token>`.
3
+ *
4
+ * `mnemom code` is open by default to Mnemom's own people (@mnemom.ai) and to
5
+ * customers who redeemed an invitation. The api decides
6
+ * (`GET /v1/code/entitlement`); this module is the CLI side of that:
7
+ *
8
+ * - `assertCodeEnabled(orgId, orgLabel)` runs in the launch preamble (right
9
+ * after sign-in, before the µ-balance check). A determined
10
+ * `code_not_enabled` BLOCKS with the api's own hint; anything inconclusive
11
+ * (network, 5xx, a membership 403) fails OPEN — the gate is a friendly
12
+ * pre-check, the api/gateway remain the real enforcement.
13
+ * - `codeInviteCommand(token)` redeems a `pinv_…` invitation
14
+ * (`POST /v1/invites/accept`): signs the customer in first (creating a free
15
+ * account in the same flow if needed), unlocks Mnemom Code for them, and
16
+ * reports whether the starter µ landed. Backs both `mnemom code --invite
17
+ * <token> [scenario]` and the `mnemom code invite <token>` sub-verb.
18
+ *
19
+ * All narration goes to stderr (the `mnemom code` convention — stdout is for
20
+ * the dry-run plan only). Copy says "µ", never "MU".
21
+ */
22
+ import { MnemomApiError, type AcceptProductInviteResult, type CodeEntitlement } from "../lib/api.js";
23
+ /** Thrown when the org is determined NOT enabled. The guidance is already printed
24
+ * when this is thrown — callers exit quietly (same contract as MuInsufficientError). */
25
+ export declare class CodeNotEnabledError extends Error {
26
+ readonly orgLabel: string;
27
+ constructor(orgLabel: string);
28
+ }
29
+ /** `pinv_` + base64url — the product-invite token shape the api mints. */
30
+ export declare const INVITE_TOKEN_RE: RegExp;
31
+ export declare function looksLikeInviteToken(value: unknown): value is string;
32
+ /** Drop control characters (ANSI escapes included) from an api-supplied string before
33
+ * it reaches the terminal — defense in depth; the value is first-party but printed raw. */
34
+ export declare function sanitizeTerminalText(s: string): string;
35
+ /** Human label for the gate's `source`. */
36
+ export declare function sourceLabel(source: CodeEntitlement["source"]): string;
37
+ /** The api's `code_not_enabled` 403 — the ONE case that blocks. */
38
+ export declare function isCodeNotEnabled(err: unknown): err is MnemomApiError;
39
+ /**
40
+ * Enforce the Mnemom Code gate for the org the launch targets. Blocks (prints
41
+ * guidance + throws CodeNotEnabledError) ONLY on a determined `code_not_enabled`;
42
+ * every other failure is inconclusive and fails open with a dim note.
43
+ */
44
+ export declare function assertCodeEnabled(orgId: string, orgLabel: string): Promise<void>;
45
+ /** Ensures a Mnemom session exists (injected so tests never touch the real login flow). */
46
+ export type EnsureSession = (opts: {
47
+ interactive?: boolean;
48
+ }) => Promise<boolean>;
49
+ /** Map an accept failure to the sentence the customer needs. Null = not ours to explain. */
50
+ export declare function explainAcceptError(err: unknown): string | null;
51
+ /** Print the outcome of a successful accept. */
52
+ export declare function printAccepted(res: AcceptProductInviteResult): void;
53
+ /**
54
+ * `mnemom code --invite <token>` / `mnemom code invite <token>`: sign in, redeem,
55
+ * report. Throws with a clear message on a bad token, no session, or an accept
56
+ * failure the customer can act on; rethrows anything else.
57
+ */
58
+ export declare function codeInviteCommand(token: string | undefined, opts?: {
59
+ interactive?: boolean;
60
+ ensureSession?: EnsureSession;
61
+ }): Promise<AcceptProductInviteResult>;
62
+ export declare const NO_SCENARIO_MESSAGE = "mnemom code: a <scenario> slug is required (e.g. `mnemom code onboarding`) \u2014 or redeem an invitation with `mnemom code --invite <token>`.";
63
+ export interface CodeInvocation {
64
+ /** The positional `[scenario]` (may be a reserved sub-verb such as "invite"). */
65
+ scenario?: string;
66
+ /** Everything after the scenario. */
67
+ passthrough: string[];
68
+ /** `--invite <token>`. */
69
+ inviteFlag?: string;
70
+ }
71
+ export interface CodeInvocationDeps {
72
+ redeem: (token: string | undefined) => Promise<unknown>;
73
+ launch: (scenario: string) => Promise<void>;
74
+ }
75
+ /**
76
+ * How the `code` command's positional + `--invite` compose — kept OUT of
77
+ * index.ts so it is unit-testable:
78
+ * - `mnemom code invite <token>` → redeem, done (sub-verb; "invite" is reserved)
79
+ * - `mnemom code --invite <token>` → redeem, done
80
+ * - `mnemom code --invite <token> <scen>` → redeem, then launch
81
+ * - `mnemom code <scen>` → launch
82
+ * - `mnemom code` → error with guidance
83
+ */
84
+ export declare function runCodeInvocation(input: CodeInvocation, deps: CodeInvocationDeps): Promise<void>;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Mnemom Code — the invited-cohort gate + `mnemom code --invite <token>`.
3
+ *
4
+ * `mnemom code` is open by default to Mnemom's own people (@mnemom.ai) and to
5
+ * customers who redeemed an invitation. The api decides
6
+ * (`GET /v1/code/entitlement`); this module is the CLI side of that:
7
+ *
8
+ * - `assertCodeEnabled(orgId, orgLabel)` runs in the launch preamble (right
9
+ * after sign-in, before the µ-balance check). A determined
10
+ * `code_not_enabled` BLOCKS with the api's own hint; anything inconclusive
11
+ * (network, 5xx, a membership 403) fails OPEN — the gate is a friendly
12
+ * pre-check, the api/gateway remain the real enforcement.
13
+ * - `codeInviteCommand(token)` redeems a `pinv_…` invitation
14
+ * (`POST /v1/invites/accept`): signs the customer in first (creating a free
15
+ * account in the same flow if needed), unlocks Mnemom Code for them, and
16
+ * reports whether the starter µ landed. Backs both `mnemom code --invite
17
+ * <token> [scenario]` and the `mnemom code invite <token>` sub-verb.
18
+ *
19
+ * All narration goes to stderr (the `mnemom code` convention — stdout is for
20
+ * the dry-run plan only). Copy says "µ", never "MU".
21
+ */
22
+ import { acceptProductInvite, CODE_NOT_ENABLED, getCodeEntitlement, MnemomApiError, } from "../lib/api.js";
23
+ import { fmt } from "../lib/format.js";
24
+ import { isInteractive } from "../lib/prompt.js";
25
+ const say = (line = "") => console.error(line);
26
+ /** Thrown when the org is determined NOT enabled. The guidance is already printed
27
+ * when this is thrown — callers exit quietly (same contract as MuInsufficientError). */
28
+ export class CodeNotEnabledError extends Error {
29
+ orgLabel;
30
+ constructor(orgLabel) {
31
+ super("Mnemom Code is not enabled for this organization");
32
+ this.orgLabel = orgLabel;
33
+ this.name = "CodeNotEnabledError";
34
+ }
35
+ }
36
+ /** `pinv_` + base64url — the product-invite token shape the api mints. */
37
+ export const INVITE_TOKEN_RE = /^pinv_[A-Za-z0-9_-]{8,}$/;
38
+ export function looksLikeInviteToken(value) {
39
+ return typeof value === "string" && INVITE_TOKEN_RE.test(value.trim());
40
+ }
41
+ const FALLBACK_HINT = "Ask your Mnemom contact for a Mnemom Code invitation, then run: mnemom code --invite <token>";
42
+ /** Drop control characters (ANSI escapes included) from an api-supplied string before
43
+ * it reaches the terminal — defense in depth; the value is first-party but printed raw. */
44
+ export function sanitizeTerminalText(s) {
45
+ // eslint-disable-next-line no-control-regex
46
+ return s.replace(/[\u0000-\u001f\u007f]/g, "");
47
+ }
48
+ /** Human label for the gate's `source`. */
49
+ export function sourceLabel(source) {
50
+ switch (source) {
51
+ case "internal_cohort":
52
+ return "internal cohort";
53
+ case "staff":
54
+ return "Mnemom staff";
55
+ case "invitation":
56
+ return "invitation";
57
+ default:
58
+ return String(source);
59
+ }
60
+ }
61
+ /** The api's `code_not_enabled` 403 — the ONE case that blocks. */
62
+ export function isCodeNotEnabled(err) {
63
+ return (err instanceof MnemomApiError && err.effectiveStatus === 403 && err.code === CODE_NOT_ENABLED);
64
+ }
65
+ function hintFrom(err) {
66
+ const d = err.details;
67
+ return typeof d?.hint === "string" && d.hint.trim()
68
+ ? sanitizeTerminalText(d.hint)
69
+ : FALLBACK_HINT;
70
+ }
71
+ function printNotEnabled(orgLabel, hint) {
72
+ say();
73
+ say(fmt.warn(`Mnemom Code isn't enabled for ${orgLabel} yet.`));
74
+ say(fmt.dim(" mnemom code is rolling out to an invited cohort. If you have an invitation token"));
75
+ say(fmt.dim(" (pinv_…), redeem it and you're in:"));
76
+ say();
77
+ say(" mnemom code --invite <token>");
78
+ say();
79
+ say(fmt.dim(` ${hint}`));
80
+ }
81
+ /**
82
+ * Enforce the Mnemom Code gate for the org the launch targets. Blocks (prints
83
+ * guidance + throws CodeNotEnabledError) ONLY on a determined `code_not_enabled`;
84
+ * every other failure is inconclusive and fails open with a dim note.
85
+ */
86
+ export async function assertCodeEnabled(orgId, orgLabel) {
87
+ try {
88
+ await getCodeEntitlement(orgId);
89
+ }
90
+ catch (err) {
91
+ if (isCodeNotEnabled(err)) {
92
+ printNotEnabled(orgLabel, hintFrom(err));
93
+ throw new CodeNotEnabledError(orgLabel);
94
+ }
95
+ say(fmt.dim(` (Could not verify Mnemom Code access — ${err instanceof Error ? err.message : String(err)}. Continuing.)`));
96
+ }
97
+ }
98
+ async function defaultEnsureSession(opts) {
99
+ // Lazy import: code-setup imports assertCodeEnabled from THIS module, so a static
100
+ // import here would be a cycle.
101
+ const { ensureMnemomSession } = await import("./code-setup.js");
102
+ return ensureMnemomSession(opts);
103
+ }
104
+ /** Map an accept failure to the sentence the customer needs. Null = not ours to explain. */
105
+ export function explainAcceptError(err) {
106
+ if (!(err instanceof MnemomApiError))
107
+ return null;
108
+ switch (err.effectiveStatus) {
109
+ case 404:
110
+ return "No invitation found for that token — check it was copied completely.";
111
+ case 410:
112
+ return "This invitation has expired or was revoked — ask your Mnemom contact for a new one.";
113
+ case 403:
114
+ return "This invitation was issued to a different account — sign in with the email it was sent to (mnemom logout, then mnemom login).";
115
+ case 409:
116
+ return "The invitation changed while redeeming — run the command again.";
117
+ default:
118
+ return null;
119
+ }
120
+ }
121
+ /** Print the outcome of a successful accept. */
122
+ export function printAccepted(res) {
123
+ if (res.already_accepted) {
124
+ say(fmt.success("This invitation was already accepted — Mnemom Code is unlocked for you."));
125
+ }
126
+ else {
127
+ say(fmt.success("Mnemom Code is unlocked for your account."));
128
+ }
129
+ if (res.grant === "fired") {
130
+ say(fmt.dim(" Your starter µ has been credited."));
131
+ }
132
+ else if (res.grant === "deferred") {
133
+ say(fmt.dim(" Your starter µ credit is pending — you can start now; if it hasn't landed shortly, contact support@mnemom.ai."));
134
+ }
135
+ say(fmt.dim(" Start a governed session: mnemom code <scenario>"));
136
+ }
137
+ /**
138
+ * `mnemom code --invite <token>` / `mnemom code invite <token>`: sign in, redeem,
139
+ * report. Throws with a clear message on a bad token, no session, or an accept
140
+ * failure the customer can act on; rethrows anything else.
141
+ */
142
+ export async function codeInviteCommand(token, opts = {}) {
143
+ const trimmed = (token ?? "").trim();
144
+ if (!looksLikeInviteToken(trimmed)) {
145
+ throw new Error("mnemom code --invite: expected a Mnemom Code invitation token (pinv_…) — copy it from your invitation email.");
146
+ }
147
+ const interactive = opts.interactive ?? isInteractive();
148
+ const ensureSession = opts.ensureSession ?? defaultEnsureSession;
149
+ const haveSession = await ensureSession({ interactive });
150
+ if (!haveSession) {
151
+ throw new Error("mnemom code --invite: redeeming needs a Mnemom account.\n" +
152
+ " Sign in: mnemom login\n" +
153
+ " Then run the same --invite command again.");
154
+ }
155
+ say(fmt.dim(" Redeeming your Mnemom Code invitation…"));
156
+ let result;
157
+ try {
158
+ result = await acceptProductInvite(trimmed);
159
+ }
160
+ catch (err) {
161
+ const explained = explainAcceptError(err);
162
+ if (explained)
163
+ throw new Error(`mnemom code --invite: ${explained}`, { cause: err });
164
+ throw err;
165
+ }
166
+ printAccepted(result);
167
+ return result;
168
+ }
169
+ // ── `mnemom code` entry dispatch ─────────────────────────────────────────────
170
+ export const NO_SCENARIO_MESSAGE = "mnemom code: a <scenario> slug is required (e.g. `mnemom code onboarding`) — or redeem an invitation with `mnemom code --invite <token>`.";
171
+ /**
172
+ * How the `code` command's positional + `--invite` compose — kept OUT of
173
+ * index.ts so it is unit-testable:
174
+ * - `mnemom code invite <token>` → redeem, done (sub-verb; "invite" is reserved)
175
+ * - `mnemom code --invite <token>` → redeem, done
176
+ * - `mnemom code --invite <token> <scen>` → redeem, then launch
177
+ * - `mnemom code <scen>` → launch
178
+ * - `mnemom code` → error with guidance
179
+ */
180
+ export async function runCodeInvocation(input, deps) {
181
+ if (input.scenario === "invite") {
182
+ await deps.redeem(input.passthrough[0]);
183
+ return;
184
+ }
185
+ if (input.inviteFlag) {
186
+ await deps.redeem(input.inviteFlag);
187
+ if (!input.scenario)
188
+ return;
189
+ }
190
+ if (!input.scenario)
191
+ throw new Error(NO_SCENARIO_MESSAGE);
192
+ await deps.launch(input.scenario);
193
+ }
@@ -66,6 +66,12 @@ export declare class MuInsufficientError extends Error {
66
66
  readonly orgLabel: string;
67
67
  constructor(orgLabel: string);
68
68
  }
69
+ /**
70
+ * The preamble errors whose guidance is ALREADY printed when thrown — callers
71
+ * exit 1 quietly instead of re-printing them as "Error: …". One predicate so the
72
+ * two catch sites (`mnemom code` and `mnemom code setup`) cannot drift.
73
+ */
74
+ export declare function isQuietPreambleExit(err: unknown): boolean;
69
75
  /**
70
76
  * Ensure the customer has a Mnemom session, the way `mnemom try-me` does: if
71
77
  * they are not signed in, offer a one-click browser sign-in (device-flow
@@ -34,6 +34,7 @@ import { configKeySpec, loadCodeConfig, writeCodeConfig } from "../lib/code-conf
34
34
  import { deriveHashProof } from "./agents.js";
35
35
  import { birthThroughGateway } from "./wrap.js";
36
36
  import { resolveAnthropicKey } from "./code.js";
37
+ import { assertCodeEnabled, CodeNotEnabledError } from "./code-invite.js";
37
38
  const say = (line = "") => console.error(line);
38
39
  /**
39
40
  * The default governed-agent slug for the current customer: `code-<handle>`,
@@ -99,13 +100,59 @@ export async function ensureGovernedAgent(opts) {
99
100
  const agentId = await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey);
100
101
  const hashProof = deriveHashProof(opts.anthropicKey, slug);
101
102
  await claimAgent(agentId, { hashProof, orgId: org?.org_id });
102
- await putAlignmentCard(agentId, JSON.stringify(buildCodeAlignmentCard(agentId)), "application/json");
103
- await putProtectionCard(agentId, JSON.stringify(buildCodeProtectionCard(agentId)), "application/json");
104
- say(fmt.success(`mnemom code: provisioned + claimed '${slug}' (${agentId}) in ${orgLabel} — observe posture published (goal contracts nudge).`));
103
+ say(fmt.success(`mnemom code: claimed '${slug}' (${agentId}) in ${orgLabel}.`));
104
+ // Publish the observe-only cards. A card write that can't complete (propagation
105
+ // right after the claim, or a missing cards:write scope) does NOT abort the
106
+ // provision — the agent is claimed and governed on whatever landed; we warn how
107
+ // to finish it. So `provisioned` reflects the claim, not a perfect card set.
108
+ await publishDefaultCards(agentId);
105
109
  // 6. Remember the slug so future launches skip straight to usable.
106
110
  persistAgentSlug(slug);
107
111
  return { slug, provisioned: true };
108
112
  }
113
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
114
+ /**
115
+ * Write one card with a brief retry to absorb the org-membership propagation that
116
+ * can transiently 401/403/409 a write right after a claim (the same window
117
+ * `mnemom try-me` guards). A write that still fails returns ok:false rather than
118
+ * throwing, so provisioning degrades gracefully.
119
+ */
120
+ async function tryPublishCard(name, write) {
121
+ for (let attempt = 0; attempt < 2; attempt++) {
122
+ try {
123
+ await write();
124
+ return { name, ok: true };
125
+ }
126
+ catch (err) {
127
+ const status = err instanceof MnemomApiError ? err.status : undefined;
128
+ const maybeTransient = status === undefined || status === 401 || status === 403 || status === 409;
129
+ if (attempt === 0 && maybeTransient) {
130
+ await sleep(1500); // let the claim's org-membership propagate, then retry once
131
+ continue;
132
+ }
133
+ return { name, ok: false, error: err instanceof Error ? err.message : String(err) };
134
+ }
135
+ }
136
+ return { name, ok: false, error: "unknown error" };
137
+ }
138
+ /** Publish both default observe-only cards; never throws — warns on a card that can't be written. */
139
+ async function publishDefaultCards(agentId) {
140
+ const align = await tryPublishCard("alignment", () => putAlignmentCard(agentId, JSON.stringify(buildCodeAlignmentCard(agentId)), "application/json"));
141
+ const prot = await tryPublishCard("protection", () => putProtectionCard(agentId, JSON.stringify(buildCodeProtectionCard(agentId)), "application/json"));
142
+ if (align.ok && prot.ok) {
143
+ say(fmt.success("mnemom code: observe posture published (alignment + protection observe; goal contracts nudge)."));
144
+ return;
145
+ }
146
+ if (align.ok)
147
+ say(fmt.success("mnemom code: alignment card published (observe)."));
148
+ for (const c of [align, prot]) {
149
+ if (c.ok)
150
+ continue;
151
+ const publishCmd = c.name === "alignment" ? "mnemom card publish" : "mnemom protection publish";
152
+ say(fmt.warn(`mnemom code: couldn't publish the ${c.name} card — ${c.error}`));
153
+ say(fmt.dim(` The agent is claimed and running; finish it later with \`${publishCmd}\`.`));
154
+ }
155
+ }
109
156
  /** `mnemom code setup [--agent <slug>]` — the explicit provisioning entry point. */
110
157
  export async function codeSetupCommand(opts = {}) {
111
158
  const gatewayHost = resolveGatewayHost(process.env);
@@ -114,8 +161,8 @@ export async function codeSetupCommand(opts = {}) {
114
161
  await ensureSessionAndBalance({});
115
162
  }
116
163
  catch (err) {
117
- if (err instanceof MuInsufficientError)
118
- process.exit(1);
164
+ if (isQuietPreambleExit(err))
165
+ process.exit(1); // guidance already printed
119
166
  throw err;
120
167
  }
121
168
  const anthropicKey = await resolveAnthropicKey(process.env, { allowPrompt: true });
@@ -154,6 +201,14 @@ export class MuInsufficientError extends Error {
154
201
  this.name = "MuInsufficientError";
155
202
  }
156
203
  }
204
+ /**
205
+ * The preamble errors whose guidance is ALREADY printed when thrown — callers
206
+ * exit 1 quietly instead of re-printing them as "Error: …". One predicate so the
207
+ * two catch sites (`mnemom code` and `mnemom code setup`) cannot drift.
208
+ */
209
+ export function isQuietPreambleExit(err) {
210
+ return err instanceof MuInsufficientError || err instanceof CodeNotEnabledError;
211
+ }
157
212
  /** Best-effort detection of a headless/SSH box where a browser can't open. */
158
213
  function headlessEnv() {
159
214
  const e = process.env;
@@ -274,6 +329,9 @@ export async function ensureSessionAndBalance(opts) {
274
329
  const active = getActiveOrg();
275
330
  const orgId = active?.org_id ?? (await getMyPersonalOrg()).org_id;
276
331
  const orgLabel = active ? `${active.name} (${active.slug})` : "your personal org";
332
+ // Invited-cohort gate FIRST: an org that is not enabled for Mnemom Code gets
333
+ // the invite guidance, not a top-up message (the µ check would be moot).
334
+ await assertCodeEnabled(orgId, orgLabel);
277
335
  await assertMuBalance(orgId, orgLabel);
278
336
  return { orgId, orgLabel };
279
337
  }
@@ -34,7 +34,7 @@ import { MNEMOM_DIR } from "../lib/config.js";
34
34
  import { configKeySpec, loadCodeConfig, writeCodeConfig } from "../lib/code-config.js";
35
35
  import { fmt } from "../lib/format.js";
36
36
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
37
- import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, MuInsufficientError, } from "./code-setup.js";
37
+ import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, } from "./code-setup.js";
38
38
  import { assembleContract, buildCustomHeaders, isProdGateway, launchPreferenceExplicit, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, sanitizeAgentName, } from "../lib/code.js";
39
39
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/code.json, 0600. */
40
40
  const CODE_STORE = join(MNEMOM_DIR, "code.json");
@@ -132,8 +132,8 @@ export async function codeCommand(scenario, options = {}) {
132
132
  await ensureSessionAndBalance({ interactive: isInteractive() });
133
133
  }
134
134
  catch (err) {
135
- if (err instanceof MuInsufficientError)
136
- process.exit(1); // top-up message already printed
135
+ if (isQuietPreambleExit(err))
136
+ process.exit(1); // guidance already printed
137
137
  throw err;
138
138
  }
139
139
  // Now that a session exists, a defaulted slug can use the real login handle
@@ -287,6 +287,11 @@ export function resolveCliBin(cliOpt, env) {
287
287
  throw new Error(`mnemom code: --cli path '${requested}' not found.`);
288
288
  }
289
289
  for (const dir of (env.PATH || "").split(":")) {
290
+ // Skip ephemeral cmux CLI shims: they only work inside a cmux session and,
291
+ // when spawned standalone, fall back to `exec claude` on a shim-stripped PATH
292
+ // and fail with "claude not found in PATH". They're never a durable target.
293
+ if (dir.includes("cmux-cli-shims"))
294
+ continue;
290
295
  const cand = join(dir || ".", requested);
291
296
  if (existsSync(cand))
292
297
  return { bin: cand, name };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { codeCommand } from "./commands/code.js";
24
24
  import { codeConfigCommand } from "./commands/code-config.js";
25
25
  import { codeDoctorCommand } from "./commands/code-doctor.js";
26
26
  import { codeSetupCommand } from "./commands/code-setup.js";
27
+ import { codeInviteCommand, runCodeInvocation } from "./commands/code-invite.js";
27
28
  import { applyConfigToEnv, loadCodeConfig } from "./lib/code-config.js";
28
29
  import { wrapCommand } from "./commands/wrap.js";
29
30
  import { onboardCommand } from "./commands/onboard.js";
@@ -99,9 +100,9 @@ const collectRepeatable = (value, previous) => {
99
100
  return previous;
100
101
  };
101
102
  program
102
- .command("code <scenario> [passthrough...]")
103
+ .command("code [scenario] [passthrough...]")
103
104
  .description("Launch a governed coding agent through the Mnemom gateway (governed identity + optional sealed contract). " +
104
- "Sub-verbs: `mnemom code setup` provisions your governed agent; `mnemom code doctor` preflights prerequisites; `mnemom code config …` manages persistent settings (~/.mnemom/code.toml).")
105
+ "Sub-verbs: `mnemom code setup` provisions your governed agent; `mnemom code doctor` preflights prerequisites; `mnemom code config …` manages persistent settings (~/.mnemom/code.toml); `mnemom code invite <token>` redeems a Mnemom Code invitation.")
105
106
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
106
107
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
107
108
  .option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
@@ -124,6 +125,7 @@ program
124
125
  "max",
125
126
  ]))
126
127
  .option("--trim-mcp", "Launch the CLI with no MCP servers (--strict-mcp-config + empty --mcp-config)")
128
+ .option("--invite <token>", "Redeem a Mnemom Code invitation (pinv_…): unlocks Mnemom Code for you + credits your starter µ; launches too when a <scenario> is given")
127
129
  .option("--setup", "Provision + claim the governed agent before launch (non-interactive opt-in)")
128
130
  .option("--no-setup", "Skip governed-agent provisioning entirely for this launch")
129
131
  .option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
@@ -131,6 +133,17 @@ program
131
133
  .option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
132
134
  .action(async (scenario, passthrough, opts) => {
133
135
  try {
136
+ // Invitation redemption — `mnemom code invite <token>` (sub-verb) or
137
+ // `mnemom code --invite <token> [scenario]` — composes with the launch in
138
+ // runCodeInvocation (commands/code-invite.ts, unit-tested); everything below
139
+ // is the launch half, running only when a real scenario was given.
140
+ await runCodeInvocation({ scenario, passthrough, inviteFlag: opts.invite }, { redeem: (token) => codeInviteCommand(token), launch: (scenario) => launchCode(scenario) });
141
+ }
142
+ catch (error) {
143
+ console.error("Error:", error instanceof Error ? error.message : error);
144
+ process.exit(1);
145
+ }
146
+ async function launchCode(scenario) {
134
147
  // Sub-verb: `mnemom code config …` manages persistent settings and never
135
148
  // launches. (A scenario literally named "config" is reserved for this.)
136
149
  if (scenario === "config") {
@@ -191,10 +204,6 @@ program
191
204
  dryRun: opts.dryRun,
192
205
  });
193
206
  }
194
- catch (error) {
195
- console.error("Error:", error instanceof Error ? error.message : error);
196
- process.exit(1);
197
- }
198
207
  });
199
208
  // ── wrap (MNE-935, A4) — instrument an existing agent through the gateway ────
200
209
  //
package/dist/lib/api.d.ts CHANGED
@@ -1182,4 +1182,31 @@ export interface OrgUsageQuery {
1182
1182
  * the user "org not found" on what is most often just the flag being off.
1183
1183
  */
1184
1184
  export declare function getOrgUsage(orgId: string, opts?: OrgUsageQuery): Promise<OrgUsageResponse>;
1185
+ /** `GET /v1/code/entitlement` 200 body — why Mnemom Code is enabled for the org. */
1186
+ export interface CodeEntitlement {
1187
+ org_id: string;
1188
+ enabled: true;
1189
+ source: "internal_cohort" | "staff" | "invitation";
1190
+ }
1191
+ /** The machine-readable code on the gate's 403 when the org is not enabled. */
1192
+ export declare const CODE_NOT_ENABLED = "code_not_enabled";
1193
+ /**
1194
+ * Is Mnemom Code enabled for `orgId` (default: the caller's primary org)?
1195
+ * Resolves on 200; throws MnemomApiError with `.code === "code_not_enabled"`
1196
+ * (and `.details.hint`) when the org is not enabled, or a plain MnemomApiError
1197
+ * for membership/transport failures.
1198
+ */
1199
+ export declare function getCodeEntitlement(orgId?: string): Promise<CodeEntitlement>;
1200
+ /** `POST /v1/invites/accept` 200 body. Never carries an amount — only whether the
1201
+ * starter grant fired or was deferred for a later backfill. */
1202
+ export interface AcceptProductInviteResult {
1203
+ accepted: boolean;
1204
+ already_accepted?: boolean;
1205
+ account_type: "user" | "org";
1206
+ account_id: string;
1207
+ products: string[];
1208
+ grant?: "fired" | "deferred";
1209
+ }
1210
+ /** Redeem a `pinv_…` product invite (a Mnemom Code invitation) for the signed-in user. */
1211
+ export declare function acceptProductInvite(token: string): Promise<AcceptProductInviteResult>;
1185
1212
  export {};
package/dist/lib/api.js CHANGED
@@ -1480,3 +1480,50 @@ export async function getOrgUsage(orgId, opts = {}) {
1480
1480
  const qs = params.toString() ? `?${params.toString()}` : "";
1481
1481
  return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/usage/by-person${qs}`, {}, { fallback: "Failed to fetch usage" });
1482
1482
  }
1483
+ // ─── Mnemom Code — feature gate + invitation redemption ──────────────────────
1484
+ //
1485
+ // `mnemom code` is rolling out to an invited cohort. Two api calls back that:
1486
+ // the per-org gate (`GET /v1/code/entitlement`) the launch preamble checks, and
1487
+ // the product-invite accept (`POST /v1/invites/accept`) behind
1488
+ // `mnemom code --invite <token>`. Both go through `codeFetch` rather than
1489
+ // `gFetch`: gFetch flattens EVERY 403 into "Permission denied (need org admin /
1490
+ // membership)", but the gate's 403 carries a machine-readable
1491
+ // `code_not_enabled` + a hint the command layer must print verbatim, and the
1492
+ // accept route's 403/404/409/410 each mean something specific to the user.
1493
+ // So non-OK responses here are parsed with readApiError (code + details kept)
1494
+ // and only the 401 keeps gFetch's canonical not-authenticated message.
1495
+ async function codeFetch(path, init = {}, fallback) {
1496
+ const url = validateUrl(`${API_BASE}${path}`);
1497
+ const response = await fetchWithAuthRetry(url, async () => ({
1498
+ method: init.method ?? "GET",
1499
+ headers: {
1500
+ ...(await authHeaders()),
1501
+ Accept: "application/json",
1502
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
1503
+ ...(init.headers ?? {}),
1504
+ },
1505
+ body: init.body,
1506
+ }));
1507
+ if (!response.ok) {
1508
+ if (response.status === 401)
1509
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1510
+ throw await readApiError(response, fallback);
1511
+ }
1512
+ return (await response.json());
1513
+ }
1514
+ /** The machine-readable code on the gate's 403 when the org is not enabled. */
1515
+ export const CODE_NOT_ENABLED = "code_not_enabled";
1516
+ /**
1517
+ * Is Mnemom Code enabled for `orgId` (default: the caller's primary org)?
1518
+ * Resolves on 200; throws MnemomApiError with `.code === "code_not_enabled"`
1519
+ * (and `.details.hint`) when the org is not enabled, or a plain MnemomApiError
1520
+ * for membership/transport failures.
1521
+ */
1522
+ export async function getCodeEntitlement(orgId) {
1523
+ const q = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
1524
+ return codeFetch(`/v1/code/entitlement${q}`, {}, "Could not check Mnemom Code access");
1525
+ }
1526
+ /** Redeem a `pinv_…` product invite (a Mnemom Code invitation) for the signed-in user. */
1527
+ export async function acceptProductInvite(token) {
1528
+ return codeFetch("/v1/invites/accept", { method: "POST", body: JSON.stringify({ token }) }, "Could not redeem the invitation");
1529
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.0-next.2",
3
+ "version": "0.17.0-next.3",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {