@mnemom/mnemom 0.17.3-next.5 → 0.17.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.
@@ -13,8 +13,11 @@
13
13
  * Settable keys are the single source of truth in lib/code-config.ts CONFIG_KEYS.
14
14
  * The secret Anthropic key is NOT a config key — it lives in ~/.mnemom/launcher.json.
15
15
  */
16
+ import { type ExperimentalId } from "../lib/experimental.js";
16
17
  /** Entry point for `mnemom agent config <verb> [args...]`. Throws on misuse. */
17
- export declare function agentConfigCommand(argv: string[]): Promise<void>;
18
+ export declare function agentConfigCommand(argv: string[], opts?: {
19
+ enabled?: readonly ExperimentalId[];
20
+ }): Promise<void>;
18
21
  /**
19
22
  * `$VISUAL` / `$EDITOR` is a program plus optional plain flags (`code --wait`, `emacsclient -t`),
20
23
  * never shell syntax: the value is split on whitespace and every token must be a bare
@@ -16,15 +16,17 @@
16
16
  import { spawnSync } from "node:child_process";
17
17
  import { existsSync } from "node:fs";
18
18
  import { LAUNCHER_CONFIG_PATH, CONFIG_KEYS, configKeySpec, loadLauncherConfig, writeLauncherConfig, } from "../lib/launcher-config.js";
19
+ import { loadExperimentalState } from "../lib/cli-config.js";
20
+ import { configKeyMark } from "../lib/experimental.js";
19
21
  const out = (line = "") => console.log(line);
20
22
  const err = (line = "") => console.error(line);
21
23
  /** Entry point for `mnemom agent config <verb> [args...]`. Throws on misuse. */
22
- export async function agentConfigCommand(argv) {
24
+ export async function agentConfigCommand(argv, opts = {}) {
23
25
  const [verb, ...rest] = argv;
24
26
  switch (verb) {
25
27
  case undefined:
26
28
  case "list":
27
- return listConfig();
29
+ return listConfig(opts.enabled ?? loadExperimentalState().enabled);
28
30
  case "path":
29
31
  out(LAUNCHER_CONFIG_PATH);
30
32
  return;
@@ -40,7 +42,8 @@ export async function agentConfigCommand(argv) {
40
42
  throw new Error(`mnemom agent config: unknown verb '${verb}'. Use list | get | set | unset | edit | path.`);
41
43
  }
42
44
  }
43
- function listConfig() {
45
+ /** Settings of experimental features that are off are marked `(experimental, off)`. */
46
+ function listConfig(enabled) {
44
47
  const cfg = loadLauncherConfig();
45
48
  const rows = [];
46
49
  for (const spec of CONFIG_KEYS) {
@@ -54,13 +57,13 @@ function listConfig() {
54
57
  out("");
55
58
  out("Settable keys:");
56
59
  for (const spec of CONFIG_KEYS)
57
- out(` ${spec.key.padEnd(22)} ${spec.describe}`);
60
+ out(` ${spec.key.padEnd(22)} ${spec.describe}${configKeyMark(spec.key, undefined, enabled)}`);
58
61
  return;
59
62
  }
60
63
  const width = Math.max(...rows.map(([k]) => k.length));
61
64
  out(`# ${LAUNCHER_CONFIG_PATH}`);
62
65
  for (const [k, v] of rows)
63
- out(`${k.padEnd(width)} ${v}`);
66
+ out(`${k.padEnd(width)} ${v}${configKeyMark(k, v, enabled)}`);
64
67
  }
65
68
  function getConfig(key) {
66
69
  if (!key)
@@ -14,7 +14,8 @@
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { existsSync } from "node:fs";
16
16
  import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
17
- import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost } from "../lib/agent.js";
17
+ import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
18
+ import { getApiUrl, getGatewayUrl } from "../lib/config.js";
18
19
  import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
19
20
  import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
20
21
  import { isLoggedIn } from "../lib/auth.js";
@@ -52,6 +53,14 @@ export async function agentDoctorCommand(options = {}) {
52
53
  add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
53
54
  else
54
55
  add("miss", "Anthropic key: none found", "export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-… (or ANTHROPIC_API_KEY), or run `mnemom agent` once and accept the save prompt");
56
+ // ── one cell: the launch door must be the CLI's own cell ──
57
+ const cellError = singleCellError({
58
+ door: resolveAnthropicDoor(env),
59
+ apiUrl: getApiUrl(),
60
+ cellGateway: getGatewayUrl(),
61
+ });
62
+ if (cellError)
63
+ add("miss", "cell: mixed — a launch would be refused", cellError);
55
64
  // ── gateway health ──
56
65
  const host = resolveGatewayHost(env);
57
66
  const cell = isProdGateway(env) ? "us-2/prod" : "custom";
@@ -8,12 +8,13 @@
8
8
  * 1. a SESSION NAME — labels the session in traces and the claude.ai session
9
9
  * list (defaults to the current folder's name).
10
10
  *
11
- * GOAL ALIGNMENT is not prompted for: it defaults to IMPLICIT. A bare launch
12
- * runs governed with the goal inferred from your own messages
13
- * (`x-mnemom-goal-mode: implicit`, no contract). `--goal` seals a one-line
14
- * contract instead (`x-mnemom-contract`); `--no-implicit` (or launcher.toml
15
- * `goal_mode = "off"`) turns alignment off — neither header is sent. The mode is
16
- * resolved upstream (`resolveImplicit`) and announced on the launch line.
11
+ * GOAL ALIGNMENT is not prompted for. `--goal` seals a one-line contract
12
+ * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
+ * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
+ * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
+ * (the default) a bare launch runs with alignment off. `--no-implicit` (or
16
+ * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
17
+ * (`resolveImplicit`) and announced on the launch line.
17
18
  *
18
19
  * Non-interactive runs (pipes, CI) never prompt — they get a clear error asking
19
20
  * for a name. Pure apart from the injected `ask` functions, so every arm is
@@ -8,12 +8,13 @@
8
8
  * 1. a SESSION NAME — labels the session in traces and the claude.ai session
9
9
  * list (defaults to the current folder's name).
10
10
  *
11
- * GOAL ALIGNMENT is not prompted for: it defaults to IMPLICIT. A bare launch
12
- * runs governed with the goal inferred from your own messages
13
- * (`x-mnemom-goal-mode: implicit`, no contract). `--goal` seals a one-line
14
- * contract instead (`x-mnemom-contract`); `--no-implicit` (or launcher.toml
15
- * `goal_mode = "off"`) turns alignment off — neither header is sent. The mode is
16
- * resolved upstream (`resolveImplicit`) and announced on the launch line.
11
+ * GOAL ALIGNMENT is not prompted for. `--goal` seals a one-line contract
12
+ * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
+ * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
+ * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
+ * (the default) a bare launch runs with alignment off. `--no-implicit` (or
16
+ * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
17
+ * (`resolveImplicit`) and announced on the launch line.
17
18
  *
18
19
  * Non-interactive runs (pipes, CI) never prompt — they get a clear error asking
19
20
  * for a name. Pure apart from the injected `ask` functions, so every arm is
@@ -65,11 +66,9 @@ export async function resolveSessionSetup(input, ask) {
65
66
  say(` (Next time you can pass it directly: mnemom agent <name>.)`);
66
67
  const typed = await ask.askInput(`Session name [${fallback}]:`);
67
68
  const scenario = sanitizeAgentName(typed.trim()) || fallback;
68
- // ── 2. goal alignment — resolved upstream, no prompt. Implicit is the DEFAULT
69
- // (a bare launch runs governed with the goal inferred from your messages), so
70
- // there is nothing to pick here: `--goal` seals a contract, the default (and
71
- // `--implicit` / `goal_mode = "implicit"`) is implicit, and `--no-implicit` /
72
- // `goal_mode = "off"` turns it off. The launch line announces the mode.
69
+ // ── 2. goal alignment — resolved upstream, no prompt: `--goal` seals a contract,
70
+ // the `implicit` experimental feature (when on) makes implicit the default, and
71
+ // `--no-implicit` / `goal_mode = "off"` turns it off. The launch line announces the mode.
73
72
  if (flagGoal) {
74
73
  return { scenario, goal: flagGoal, goalAlignment: "on", prompted: true };
75
74
  }
@@ -26,6 +26,11 @@ export interface SessionCommandDeps {
26
26
  * one `live goal` line an implicit session gets. Optional: absent ⇒ no line.
27
27
  */
28
28
  fetchGoal?: (record: SessionRecord) => Promise<GoalShowResponse>;
29
+ /**
30
+ * The gateway of the cell this CLI reads live state from. A session that ran on another
31
+ * cell is not looked up there (it would always read as "no sealed contract"). Absent ⇒ no check.
32
+ */
33
+ cellGateway?: () => string;
29
34
  out: (line: string) => void;
30
35
  err: (line: string) => void;
31
36
  }
@@ -14,7 +14,8 @@
14
14
  * only the server's hash is shown, labelled "sealed hash".
15
15
  */
16
16
  import { readSessionRecords } from "../lib/agent-ledger.js";
17
- import { MNEMOM_DIR } from "../lib/config.js";
17
+ import { getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
18
+ import { sameOrigin } from "../lib/agent.js";
18
19
  import { getAgentSession, MnemomApiError } from "../lib/api.js";
19
20
  import { fmt, formatRelativeAge } from "../lib/format.js";
20
21
  import { goalControlFetch } from "../lib/goal-api.js";
@@ -31,6 +32,7 @@ export const liveSessionDeps = {
31
32
  const creds = await resolveLaunchCreds();
32
33
  return goalControlFetch(record, "show", { thread: "main" }, { fetch, creds });
33
34
  },
35
+ cellGateway: () => getGatewayUrl(),
34
36
  out: (line) => console.log(line),
35
37
  err: (line) => console.error(line),
36
38
  };
@@ -180,6 +182,9 @@ export function buildShowView(local, remote, reason) {
180
182
  if (local.context_profile)
181
183
  launchBits.push(`context ${local.context_profile}`);
182
184
  L.push(fmt.label("launch: ", launchBits.join(" · ")));
185
+ if (local.experimental?.length) {
186
+ L.push(fmt.label("experimental: ", local.experimental.join(", ")));
187
+ }
183
188
  L.push(fmt.label("conversation: ", local.conversation_id));
184
189
  const goalMode = resolveGoalMode(local, remote);
185
190
  L.push(fmt.label("goal alignment:", goalBadge(goalMode)));
@@ -315,9 +320,13 @@ export async function agentShowCommand(ref, opts, deps) {
315
320
  const { record, others } = resolved;
316
321
  let remote = null;
317
322
  let reason;
323
+ const cell = deps.cellGateway?.();
318
324
  if (record.agent_id === null) {
319
325
  reason = "agent id was not resolved at launch";
320
326
  }
327
+ else if (cell !== undefined && !sameOrigin(record.gateway_host, cell)) {
328
+ reason = `this session ran on ${record.gateway_host}, not this CLI's cell (${cell}) — point the CLI at that cell to read it`;
329
+ }
321
330
  else {
322
331
  try {
323
332
  remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
@@ -26,10 +26,10 @@
26
26
  import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
27
27
  import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
28
28
  import { getActiveOrg } from "../lib/cli-config.js";
29
- import { getWebsiteUrl } from "../lib/config.js";
29
+ import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
30
30
  import { fmt } from "../lib/format.js";
31
31
  import { askYesNo, isInteractive } from "../lib/prompt.js";
32
- import { resolveGatewayHost, sanitizeAgentName } from "../lib/agent.js";
32
+ import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
33
33
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
34
34
  import { deriveHashProof } from "./agents.js";
35
35
  import { birthThroughGateway } from "./wrap.js";
@@ -273,6 +273,13 @@ async function publishDefaultCards(agentId) {
273
273
  /** `mnemom agent setup [--agent <slug>]` — the explicit provisioning entry point. */
274
274
  export async function agentSetupCommand(opts = {}) {
275
275
  const gatewayHost = resolveGatewayHost(process.env);
276
+ const cellError = singleCellError({
277
+ door: resolveAnthropicDoor(process.env),
278
+ apiUrl: getApiUrl(),
279
+ cellGateway: getGatewayUrl(),
280
+ });
281
+ if (cellError)
282
+ throw new Error(cellError.replace("mnemom agent:", "mnemom agent setup:"));
276
283
  // Sign in (offer sign-up) and enforce the MU balance before any account write.
277
284
  try {
278
285
  await ensureSessionAndBalance({});
@@ -25,6 +25,7 @@
25
25
  * line, or the proxy's stdin under RC). It is never logged, printed, or passed as
26
26
  * an argv value.
27
27
  */
28
+ import { type ExperimentalId } from "../lib/experimental.js";
28
29
  import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
29
30
  export interface AgentOptions {
30
31
  goal?: string;
@@ -87,6 +88,12 @@ export interface AgentOptions {
87
88
  * Undefined = the full default order.
88
89
  */
89
90
  keySource?: "env" | "store" | "prompt";
91
+ /**
92
+ * The experimental features on for this launch (`mnemom experimental`), already gated
93
+ * upstream in index.ts. Announced on stderr, recorded in the ledger, and sent in the
94
+ * session-config header's `experimental` array. Absent/empty ⇒ none of the three.
95
+ */
96
+ experimental?: readonly ExperimentalId[];
90
97
  passthrough?: string[];
91
98
  /** Print the resolved launch plan and exit — no key read, no spawn. */
92
99
  dryRun?: boolean;
@@ -245,4 +252,6 @@ export declare function printPlan(ctx: {
245
252
  contextProfile: ContextProfile;
246
253
  /** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
247
254
  sessionConfig: Record<string, string>;
255
+ /** The experimental features on for this launch (the header's `experimental` array). */
256
+ experimental?: readonly string[];
248
257
  }): void;
@@ -31,16 +31,17 @@ import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { MNEMOM_DIR } from "../lib/config.js";
34
+ import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
35
35
  import { getAgentByName } from "../lib/api.js";
36
36
  import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
37
37
  import { loadCliConfig } from "../lib/cli-config.js";
38
38
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
39
+ import { experimentalNotice } from "../lib/experimental.js";
39
40
  import { fmt } from "../lib/format.js";
40
41
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
41
42
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
42
43
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
43
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
44
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
44
45
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
45
46
  import { slackChannelArgs } from "../lib/slack-launch.js";
46
47
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -60,9 +61,9 @@ function sayContextProfile(profile) {
60
61
  }
61
62
  /** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
62
63
  export async function agentCommand(scenarioArg, options = {}) {
63
- // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for both,
64
- // goal alignment is implicit by default (inferred from your messages); --goal
65
- // seals a contract and --no-implicit turns it off — all resolved upstream.
64
+ // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
65
+ // goal alignment is resolved upstream: --goal seals a contract, implicit alignment
66
+ // is on by default only while the `implicit` experimental feature is on.
66
67
  const session = await resolveSessionSetup({
67
68
  scenario: scenarioArg,
68
69
  goal: options.goal,
@@ -108,6 +109,9 @@ export async function agentCommand(scenarioArg, options = {}) {
108
109
  // ── door ──
109
110
  const gatewayHost = resolveGatewayHost(env);
110
111
  const door = resolveAnthropicDoor(env);
112
+ const cellError = singleCellError({ door, apiUrl: getApiUrl(), cellGateway: getGatewayUrl() });
113
+ if (cellError)
114
+ throw new Error(cellError);
111
115
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
112
116
  const yolo = options.yolo !== false; // default ON
113
117
  const contextHint = options.contextHint !== false; // default ON
@@ -121,8 +125,14 @@ export async function agentCommand(scenarioArg, options = {}) {
121
125
  env,
122
126
  });
123
127
  const contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
128
+ const experimental = options.experimental ?? [];
124
129
  const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
125
- const sessionConfigB64 = buildSessionConfigB64({ profile: contextProfile, contextHandoff });
130
+ const sessionConfigB64 = buildSessionConfigB64({
131
+ profile: contextProfile,
132
+ contextHandoff,
133
+ experimental,
134
+ });
135
+ const expNotice = experimentalNotice(experimental);
126
136
  // ── which CLI executable (name or full path) ── resolve GRACEFULLY: if it's not
127
137
  // found we don't crash with a technical error — we ask for the path at launch.
128
138
  let cli;
@@ -163,7 +173,10 @@ export async function agentCommand(scenarioArg, options = {}) {
163
173
  cli: planCli,
164
174
  contextProfile,
165
175
  sessionConfig,
176
+ experimental,
166
177
  });
178
+ if (expNotice)
179
+ say(expNotice);
167
180
  if (previewShape === "remote-control") {
168
181
  say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
169
182
  }
@@ -290,6 +303,8 @@ export async function agentCommand(scenarioArg, options = {}) {
290
303
  contextProfile,
291
304
  });
292
305
  logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
306
+ if (expNotice)
307
+ say(expNotice);
293
308
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
294
309
  sayContextProfile(contextProfile);
295
310
  // ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
@@ -396,6 +411,7 @@ export async function recordSessionLaunch(ctx) {
396
411
  goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
397
412
  context_profile: ctx.contextProfile,
398
413
  cc_session_id: ctx.ccSessionId ?? null,
414
+ ...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
399
415
  }, {
400
416
  dir: MNEMOM_DIR,
401
417
  now: () => new Date(),
@@ -1110,6 +1126,8 @@ export function printPlan(ctx) {
1110
1126
  for (const [k, v] of overlay)
1111
1127
  L.push(` ${k}=${v}`);
1112
1128
  }
1129
+ if (ctx.experimental?.length)
1130
+ L.push(` experimental: ${ctx.experimental.join(", ")}`);
1113
1131
  if (ctx.envelope) {
1114
1132
  L.push(` contract: ${ctx.envelope.canonical}`);
1115
1133
  L.push(` contract sha: ${ctx.envelope.sha}`);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `mnemom experimental list [--json] | enable <id> | disable <id>` — turn experimental
3
+ * features on and off (registry: lib/experimental.ts; stored in ~/.mnemom/config.json).
4
+ */
5
+ import { type ExperimentalId, type ExperimentalPrefs } from "../lib/experimental.js";
6
+ export interface ExperimentalDeps {
7
+ stored(): ExperimentalPrefs | undefined;
8
+ persist(ids: ExperimentalId[]): void;
9
+ env: Record<string, string | undefined>;
10
+ log(line: string): void;
11
+ }
12
+ export declare function experimentalListCommand(opts?: {
13
+ json?: boolean;
14
+ }, deps?: ExperimentalDeps): void;
15
+ export declare function experimentalEnableCommand(id: string | undefined, deps?: ExperimentalDeps): void;
16
+ export declare function experimentalDisableCommand(id: string | undefined, deps?: ExperimentalDeps): void;
17
+ /** The one-time migration (lib/experimental.ts migrateExperimental) against the real files. */
18
+ export declare function runExperimentalMigration(): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `mnemom experimental list [--json] | enable <id> | disable <id>` — turn experimental
3
+ * features on and off (registry: lib/experimental.ts; stored in ~/.mnemom/config.json).
4
+ */
5
+ import { EXPERIMENTAL_CONTRACT, EXPERIMENTAL_ENV, EXPERIMENTAL_FEATURES, EXPERIMENTAL_IDS, isExperimentalId, migrateExperimental, resolveExperimental, } from "../lib/experimental.js";
6
+ import { loadCliConfig, setExperimentalEnabled } from "../lib/cli-config.js";
7
+ import { launcherEntries, loadLauncherConfig } from "../lib/launcher-config.js";
8
+ function defaultDeps() {
9
+ return {
10
+ stored: () => loadCliConfig().experimental,
11
+ persist: (ids) => setExperimentalEnabled(ids),
12
+ env: process.env,
13
+ log: (line) => console.log(line),
14
+ };
15
+ }
16
+ function requireId(id, verb) {
17
+ if (!id)
18
+ throw new Error(`mnemom experimental ${verb}: an <id> is required.`);
19
+ const v = id.trim().toLowerCase();
20
+ if (!isExperimentalId(v)) {
21
+ throw new Error(`Unknown experimental feature '${id}'. Valid ids: ${EXPERIMENTAL_IDS.join(", ")}.`);
22
+ }
23
+ return v;
24
+ }
25
+ export function experimentalListCommand(opts = {}, deps = defaultDeps()) {
26
+ const state = resolveExperimental({ stored: deps.stored(), env: deps.env });
27
+ const rows = EXPERIMENTAL_FEATURES.map((f) => ({
28
+ id: f.id,
29
+ enabled: state.enabled.includes(f.id),
30
+ summary: f.summary,
31
+ since: f.since,
32
+ gates: f.gates,
33
+ }));
34
+ if (opts.json) {
35
+ deps.log(JSON.stringify({ contract: EXPERIMENTAL_CONTRACT, source: state.source, features: rows }, null, 2));
36
+ return;
37
+ }
38
+ deps.log(EXPERIMENTAL_CONTRACT);
39
+ deps.log("");
40
+ const width = Math.max(...rows.map((r) => r.id.length));
41
+ for (const r of rows) {
42
+ deps.log(`${r.enabled ? "on " : "off"} ${r.id.padEnd(width)} ${r.summary}`);
43
+ }
44
+ deps.log("");
45
+ if (state.source === "env") {
46
+ deps.log(`(set by ${EXPERIMENTAL_ENV} in your environment — it overrides config.json)`);
47
+ }
48
+ deps.log("Turn one on with `mnemom experimental enable <id>`.");
49
+ }
50
+ function toggle(verb, rawId, deps) {
51
+ const id = requireId(rawId, verb);
52
+ const current = new Set(resolveExperimental({ stored: deps.stored(), env: {} }).enabled);
53
+ if (verb === "enable")
54
+ current.add(id);
55
+ else
56
+ current.delete(id);
57
+ const next = EXPERIMENTAL_IDS.filter((x) => current.has(x));
58
+ deps.persist(next);
59
+ deps.log(verb === "enable" ? `${id}: on. ${EXPERIMENTAL_CONTRACT}` : `${id}: off.`);
60
+ if ((deps.env[EXPERIMENTAL_ENV] ?? "").trim()) {
61
+ deps.log(`note: ${EXPERIMENTAL_ENV} is set in your environment and overrides this.`);
62
+ }
63
+ }
64
+ export function experimentalEnableCommand(id, deps = defaultDeps()) {
65
+ toggle("enable", id, deps);
66
+ }
67
+ export function experimentalDisableCommand(id, deps = defaultDeps()) {
68
+ toggle("disable", id, deps);
69
+ }
70
+ /** The one-time migration (lib/experimental.ts migrateExperimental) against the real files. */
71
+ export function runExperimentalMigration() {
72
+ migrateExperimental({
73
+ stored: () => loadCliConfig().experimental,
74
+ launcherEntries: () => launcherEntries(loadLauncherConfig()),
75
+ persist: (ids) => setExperimentalEnabled(ids),
76
+ writeErr: (line) => void process.stderr.write(line),
77
+ });
78
+ }
package/dist/index.d.ts CHANGED
@@ -7,4 +7,4 @@ export { program };
7
7
  * unknown — so it bypasses commander entirely and reaches the sub-verb parser
8
8
  * verbatim. Exported (not auto-run) so the bypass is unit-testable.
9
9
  */
10
- export declare function runEntrypoint(argv?: string[]): void;
10
+ export declare function runEntrypoint(argv?: string[], migrate?: () => void): void;