@mnemom/mnemom 0.16.2 → 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 +9 -0
- 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 +10 -0
- package/dist/lib/config.js +39 -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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent CLI preference store — ~/.mnemom/config.json.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately SEPARATE from auth.ts → auth.json: auth.json holds bearer
|
|
5
|
+
* credentials and is wiped on logout; config.json holds preferences that
|
|
6
|
+
* survive re-login. Today it holds one preference: the ACTIVE ORG.
|
|
7
|
+
*
|
|
8
|
+
* Why an active org exists at all: `mnemom login` binds no org — the OAuth
|
|
9
|
+
* token carries only scope + expiry (see lib/auth.ts), so every org-scoped
|
|
10
|
+
* command historically needed an explicit `--org` or silently defaulted to
|
|
11
|
+
* the caller's personal org (the "why did my agent land in Personal?"
|
|
12
|
+
* footgun). `mnemom org use <slug>` records a durable default; commands
|
|
13
|
+
* resolve org as: explicit flag > active org > loud personal-org fallback.
|
|
14
|
+
*
|
|
15
|
+
* The stored value is the VALIDATED membership snapshot ({org_id, slug,
|
|
16
|
+
* name}) so consumers can send org_id without a per-command round-trip.
|
|
17
|
+
* Membership can change after it's stored — consumers must treat a server
|
|
18
|
+
* 403 as the truth (the claim path already renders the teaching list).
|
|
19
|
+
*/
|
|
20
|
+
export interface ActiveOrg {
|
|
21
|
+
org_id: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CliConfig {
|
|
26
|
+
activeOrg?: ActiveOrg;
|
|
27
|
+
}
|
|
28
|
+
/** Load the config; a missing or corrupt file degrades to `{}`. */
|
|
29
|
+
export declare function loadCliConfig(): CliConfig;
|
|
30
|
+
/** Persist (or with `null`, clear) the active org. */
|
|
31
|
+
export declare function setActiveOrg(org: ActiveOrg | null): void;
|
|
32
|
+
/** The active org, or undefined when none is set. */
|
|
33
|
+
export declare function getActiveOrg(): ActiveOrg | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent CLI preference store — ~/.mnemom/config.json.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately SEPARATE from auth.ts → auth.json: auth.json holds bearer
|
|
5
|
+
* credentials and is wiped on logout; config.json holds preferences that
|
|
6
|
+
* survive re-login. Today it holds one preference: the ACTIVE ORG.
|
|
7
|
+
*
|
|
8
|
+
* Why an active org exists at all: `mnemom login` binds no org — the OAuth
|
|
9
|
+
* token carries only scope + expiry (see lib/auth.ts), so every org-scoped
|
|
10
|
+
* command historically needed an explicit `--org` or silently defaulted to
|
|
11
|
+
* the caller's personal org (the "why did my agent land in Personal?"
|
|
12
|
+
* footgun). `mnemom org use <slug>` records a durable default; commands
|
|
13
|
+
* resolve org as: explicit flag > active org > loud personal-org fallback.
|
|
14
|
+
*
|
|
15
|
+
* The stored value is the VALIDATED membership snapshot ({org_id, slug,
|
|
16
|
+
* name}) so consumers can send org_id without a per-command round-trip.
|
|
17
|
+
* Membership can change after it's stored — consumers must treat a server
|
|
18
|
+
* 403 as the truth (the claim path already renders the teaching list).
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { MNEMOM_DIR } from "./config.js";
|
|
23
|
+
function configFile() {
|
|
24
|
+
return path.join(MNEMOM_DIR, "config.json");
|
|
25
|
+
}
|
|
26
|
+
/** Load the config; a missing or corrupt file degrades to `{}`. */
|
|
27
|
+
export function loadCliConfig() {
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(configFile()))
|
|
30
|
+
return {};
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(configFile(), "utf-8"));
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function saveCliConfig(config) {
|
|
39
|
+
if (!fs.existsSync(MNEMOM_DIR)) {
|
|
40
|
+
// 0700 to match the auth store — the directory also holds credentials.
|
|
41
|
+
fs.mkdirSync(MNEMOM_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
}
|
|
43
|
+
const resolvedPath = path.resolve(configFile());
|
|
44
|
+
const tmpFile = `${resolvedPath}.${process.pid}.tmp`;
|
|
45
|
+
// No secrets in here, but keep the same owner-only posture as auth.json —
|
|
46
|
+
// the active org is still account metadata. Write-then-rename = atomic.
|
|
47
|
+
fs.writeFileSync(tmpFile, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
48
|
+
try {
|
|
49
|
+
fs.chmodSync(tmpFile, 0o600);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* best effort on platforms without POSIX perms */
|
|
53
|
+
}
|
|
54
|
+
fs.renameSync(tmpFile, resolvedPath);
|
|
55
|
+
}
|
|
56
|
+
/** Persist (or with `null`, clear) the active org. */
|
|
57
|
+
export function setActiveOrg(org) {
|
|
58
|
+
const config = loadCliConfig();
|
|
59
|
+
if (org === null) {
|
|
60
|
+
delete config.activeOrg;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
config.activeOrg = org;
|
|
64
|
+
}
|
|
65
|
+
saveCliConfig(config);
|
|
66
|
+
}
|
|
67
|
+
/** The active org, or undefined when none is set. */
|
|
68
|
+
export function getActiveOrg() {
|
|
69
|
+
return loadCliConfig().activeOrg;
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/code-config.ts — persistent per-customer settings for `mnemom code`.
|
|
3
|
+
*
|
|
4
|
+
* Settings live in a human-editable TOML file at ~/.mnemom/code.toml (the
|
|
5
|
+
* Cargo/Codex `config.toml` convention — comments allowed, easy to hand-edit) so
|
|
6
|
+
* a customer never has to retype their flags. The SECRET Anthropic key is
|
|
7
|
+
* deliberately NOT stored here: it stays in ~/.mnemom/code.json at 0600 (see
|
|
8
|
+
* commands/code.ts), so the file customers open, edit, and share never carries a
|
|
9
|
+
* credential — the same split gh (config.yml/hosts.yml), aws (config/credentials)
|
|
10
|
+
* and Codex (config.toml/auth.json) use.
|
|
11
|
+
*
|
|
12
|
+
* Precedence, highest wins: command-line FLAG > ENV > code.toml > built-in.
|
|
13
|
+
* `loadCodeConfig` is pure parse+validate; `applyConfigToEnv` folds the
|
|
14
|
+
* env-backed settings UNDER any real env var (so env still wins and the
|
|
15
|
+
* resolvers in lib/code.ts keep the built-in default as the final fallback);
|
|
16
|
+
* the caller folds the plain-value settings under the CLI flags.
|
|
17
|
+
*/
|
|
18
|
+
import { type LaunchShape } from "./code.js";
|
|
19
|
+
/** The human-editable settings file. Secret key lives elsewhere (code.json). */
|
|
20
|
+
export declare const CODE_CONFIG_PATH: string;
|
|
21
|
+
/** Where the CLI resolves the Anthropic key from; advisory ordering/restriction. */
|
|
22
|
+
export type KeySource = "env" | "store" | "prompt";
|
|
23
|
+
/** The validated settings, camelCased for TS (TOML keys are snake_case). */
|
|
24
|
+
export interface CodeConfig {
|
|
25
|
+
gateway?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
agent?: string;
|
|
28
|
+
effort?: string;
|
|
29
|
+
cli?: string;
|
|
30
|
+
launch?: LaunchShape;
|
|
31
|
+
keySource?: KeySource;
|
|
32
|
+
guardrails?: {
|
|
33
|
+
maxTurns?: number;
|
|
34
|
+
budgetUsd?: number;
|
|
35
|
+
stallTurns?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A settable key: its dotted config name, the getter/setter into a CodeConfig,
|
|
40
|
+
* and a one-line description for `config list`/help. Kept as data so get/set/list
|
|
41
|
+
* stay a single source of truth.
|
|
42
|
+
*/
|
|
43
|
+
export interface ConfigKeySpec {
|
|
44
|
+
key: string;
|
|
45
|
+
describe: string;
|
|
46
|
+
get(cfg: CodeConfig): string | number | undefined;
|
|
47
|
+
set(cfg: CodeConfig, raw: string): void;
|
|
48
|
+
}
|
|
49
|
+
/** The full set of settable keys — drives get/set/unset/list and validation. */
|
|
50
|
+
export declare const CONFIG_KEYS: ConfigKeySpec[];
|
|
51
|
+
export declare function configKeySpec(key: string): ConfigKeySpec | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Parse + validate a TOML settings document into a CodeConfig. Pure (no fs), so
|
|
54
|
+
* it is unit-testable. Unknown top-level keys are ignored (forward-compatible);
|
|
55
|
+
* known keys with the wrong shape throw a human-readable error.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseCodeConfig(text: string): CodeConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Load the settings from ~/.mnemom/code.toml (or `path`). A missing file is an
|
|
60
|
+
* empty config (never an error — first run has none). A malformed or invalid
|
|
61
|
+
* file throws with the path so the customer can fix it, rather than silently
|
|
62
|
+
* launching with the wrong settings.
|
|
63
|
+
*/
|
|
64
|
+
export declare function loadCodeConfig(path?: string): CodeConfig;
|
|
65
|
+
/** Serialize a CodeConfig back to TOML (snake_case keys), with a header comment. */
|
|
66
|
+
export declare function serializeCodeConfig(cfg: CodeConfig): string;
|
|
67
|
+
/** Persist a CodeConfig to `path` (default code.toml), atomically (temp+rename), 0600. */
|
|
68
|
+
export declare function writeCodeConfig(cfg: CodeConfig, path?: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Fold the ENV-backed settings from config UNDER the environment: set the
|
|
71
|
+
* MNEMOM_CODE_* var ONLY when it is not already present, so a real env var (and
|
|
72
|
+
* therefore a flag that set one) still wins. The resolvers in lib/code.ts then
|
|
73
|
+
* apply the built-in default when neither env nor config supplied a value.
|
|
74
|
+
*
|
|
75
|
+
* launch maps to MNEMOM_CODE_REMOTE_CONTROL using that var's existing contract
|
|
76
|
+
* (see resolveLaunchShape): terminal→"0", remote-control→"1", server→"server".
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyConfigToEnv(cfg: CodeConfig, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/code-config.ts — persistent per-customer settings for `mnemom code`.
|
|
3
|
+
*
|
|
4
|
+
* Settings live in a human-editable TOML file at ~/.mnemom/code.toml (the
|
|
5
|
+
* Cargo/Codex `config.toml` convention — comments allowed, easy to hand-edit) so
|
|
6
|
+
* a customer never has to retype their flags. The SECRET Anthropic key is
|
|
7
|
+
* deliberately NOT stored here: it stays in ~/.mnemom/code.json at 0600 (see
|
|
8
|
+
* commands/code.ts), so the file customers open, edit, and share never carries a
|
|
9
|
+
* credential — the same split gh (config.yml/hosts.yml), aws (config/credentials)
|
|
10
|
+
* and Codex (config.toml/auth.json) use.
|
|
11
|
+
*
|
|
12
|
+
* Precedence, highest wins: command-line FLAG > ENV > code.toml > built-in.
|
|
13
|
+
* `loadCodeConfig` is pure parse+validate; `applyConfigToEnv` folds the
|
|
14
|
+
* env-backed settings UNDER any real env var (so env still wins and the
|
|
15
|
+
* resolvers in lib/code.ts keep the built-in default as the final fallback);
|
|
16
|
+
* the caller folds the plain-value settings under the CLI flags.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { parse as parseToml, stringify as stringifyToml, TomlError } from "smol-toml";
|
|
21
|
+
import { MNEMOM_DIR } from "./config.js";
|
|
22
|
+
import { isPositiveInt, isPositiveNumber } from "./code.js";
|
|
23
|
+
/** The human-editable settings file. Secret key lives elsewhere (code.json). */
|
|
24
|
+
export const CODE_CONFIG_PATH = join(MNEMOM_DIR, "code.toml");
|
|
25
|
+
const LAUNCH_SHAPES = ["terminal", "remote-control", "server"];
|
|
26
|
+
const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
27
|
+
const KEY_SOURCES = ["env", "store", "prompt"];
|
|
28
|
+
function coerceLaunch(raw) {
|
|
29
|
+
const v = raw.trim();
|
|
30
|
+
if (LAUNCH_SHAPES.includes(v))
|
|
31
|
+
return v;
|
|
32
|
+
throw new Error(`launch must be one of ${LAUNCH_SHAPES.join(", ")} (got '${raw}').`);
|
|
33
|
+
}
|
|
34
|
+
function coerceEffort(raw) {
|
|
35
|
+
const v = raw.trim();
|
|
36
|
+
if (EFFORTS.includes(v))
|
|
37
|
+
return v;
|
|
38
|
+
throw new Error(`effort must be one of ${EFFORTS.join(", ")} (got '${raw}').`);
|
|
39
|
+
}
|
|
40
|
+
function coerceKeySource(raw) {
|
|
41
|
+
const v = raw.trim();
|
|
42
|
+
if (KEY_SOURCES.includes(v))
|
|
43
|
+
return v;
|
|
44
|
+
throw new Error(`key_source must be one of ${KEY_SOURCES.join(", ")} (got '${raw}').`);
|
|
45
|
+
}
|
|
46
|
+
function coerceNonEmpty(name, raw) {
|
|
47
|
+
const v = raw.trim();
|
|
48
|
+
if (!v)
|
|
49
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
50
|
+
return v;
|
|
51
|
+
}
|
|
52
|
+
function ensureGuardrails(cfg) {
|
|
53
|
+
if (!cfg.guardrails)
|
|
54
|
+
cfg.guardrails = {};
|
|
55
|
+
return cfg.guardrails;
|
|
56
|
+
}
|
|
57
|
+
/** The full set of settable keys — drives get/set/unset/list and validation. */
|
|
58
|
+
export const CONFIG_KEYS = [
|
|
59
|
+
{
|
|
60
|
+
key: "gateway",
|
|
61
|
+
describe: "Gateway host (default https://gateway.mnemom.ai — the us-2/prod cell)",
|
|
62
|
+
get: (c) => c.gateway,
|
|
63
|
+
set: (c, v) => {
|
|
64
|
+
c.gateway = coerceNonEmpty("gateway", v);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "model",
|
|
69
|
+
describe: "Model to launch (CLI alias or concrete id, e.g. claude-sonnet-5)",
|
|
70
|
+
get: (c) => c.model,
|
|
71
|
+
set: (c, v) => {
|
|
72
|
+
c.model = coerceNonEmpty("model", v);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "agent",
|
|
77
|
+
describe: "Governed agent identity slug (x-mnemom-agent)",
|
|
78
|
+
get: (c) => c.agent,
|
|
79
|
+
set: (c, v) => {
|
|
80
|
+
c.agent = coerceNonEmpty("agent", v);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "effort",
|
|
85
|
+
describe: `Reasoning effort (${EFFORTS.join("|")})`,
|
|
86
|
+
get: (c) => c.effort,
|
|
87
|
+
set: (c, v) => {
|
|
88
|
+
c.effort = coerceEffort(v);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "cli",
|
|
93
|
+
describe: "Coding-agent CLI to launch (name or full path; default claude)",
|
|
94
|
+
get: (c) => c.cli,
|
|
95
|
+
set: (c, v) => {
|
|
96
|
+
c.cli = coerceNonEmpty("cli", v);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "launch",
|
|
101
|
+
describe: `Launch shape (${LAUNCH_SHAPES.join("|")})`,
|
|
102
|
+
get: (c) => c.launch,
|
|
103
|
+
set: (c, v) => {
|
|
104
|
+
c.launch = coerceLaunch(v);
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "key_source",
|
|
109
|
+
describe: `Where to source the Anthropic key (${KEY_SOURCES.join("|")})`,
|
|
110
|
+
get: (c) => c.keySource,
|
|
111
|
+
set: (c, v) => {
|
|
112
|
+
c.keySource = coerceKeySource(v);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "guardrails.max_turns",
|
|
117
|
+
describe: "Contract guardrail: turn ceiling (positive integer)",
|
|
118
|
+
get: (c) => c.guardrails?.maxTurns,
|
|
119
|
+
set: (c, v) => {
|
|
120
|
+
if (!isPositiveInt(v))
|
|
121
|
+
throw new Error(`guardrails.max_turns must be a positive integer (got '${v}').`);
|
|
122
|
+
ensureGuardrails(c).maxTurns = Number(v);
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "guardrails.budget_usd",
|
|
127
|
+
describe: "Contract guardrail: spend ceiling in USD (positive number)",
|
|
128
|
+
get: (c) => c.guardrails?.budgetUsd,
|
|
129
|
+
set: (c, v) => {
|
|
130
|
+
if (!isPositiveNumber(v))
|
|
131
|
+
throw new Error(`guardrails.budget_usd must be a positive number (got '${v}').`);
|
|
132
|
+
ensureGuardrails(c).budgetUsd = Number(v);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "guardrails.stall_turns",
|
|
137
|
+
describe: "Contract guardrail: consecutive no-write turns (positive integer)",
|
|
138
|
+
get: (c) => c.guardrails?.stallTurns,
|
|
139
|
+
set: (c, v) => {
|
|
140
|
+
if (!isPositiveInt(v))
|
|
141
|
+
throw new Error(`guardrails.stall_turns must be a positive integer (got '${v}').`);
|
|
142
|
+
ensureGuardrails(c).stallTurns = Number(v);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
export function configKeySpec(key) {
|
|
147
|
+
return CONFIG_KEYS.find((k) => k.key === key);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Parse + validate a TOML settings document into a CodeConfig. Pure (no fs), so
|
|
151
|
+
* it is unit-testable. Unknown top-level keys are ignored (forward-compatible);
|
|
152
|
+
* known keys with the wrong shape throw a human-readable error.
|
|
153
|
+
*/
|
|
154
|
+
export function parseCodeConfig(text) {
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = parseToml(text);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const detail = err instanceof TomlError ? err.message : String(err);
|
|
161
|
+
throw new Error(`could not parse code.toml: ${detail}`);
|
|
162
|
+
}
|
|
163
|
+
const cfg = {};
|
|
164
|
+
const str = (v) => typeof v === "string" && v.trim() ? v.trim() : v === undefined ? undefined : String(v);
|
|
165
|
+
if (raw.gateway !== undefined)
|
|
166
|
+
cfg.gateway = coerceNonEmpty("gateway", str(raw.gateway) ?? "");
|
|
167
|
+
if (raw.model !== undefined)
|
|
168
|
+
cfg.model = coerceNonEmpty("model", str(raw.model) ?? "");
|
|
169
|
+
if (raw.agent !== undefined)
|
|
170
|
+
cfg.agent = coerceNonEmpty("agent", str(raw.agent) ?? "");
|
|
171
|
+
if (raw.effort !== undefined)
|
|
172
|
+
cfg.effort = coerceEffort(str(raw.effort) ?? "");
|
|
173
|
+
if (raw.cli !== undefined)
|
|
174
|
+
cfg.cli = coerceNonEmpty("cli", str(raw.cli) ?? "");
|
|
175
|
+
if (raw.launch !== undefined)
|
|
176
|
+
cfg.launch = coerceLaunch(str(raw.launch) ?? "");
|
|
177
|
+
if (raw.key_source !== undefined)
|
|
178
|
+
cfg.keySource = coerceKeySource(str(raw.key_source) ?? "");
|
|
179
|
+
const g = raw.guardrails;
|
|
180
|
+
if (g !== undefined) {
|
|
181
|
+
if (typeof g !== "object" || g === null || Array.isArray(g)) {
|
|
182
|
+
throw new Error("[guardrails] must be a table.");
|
|
183
|
+
}
|
|
184
|
+
const gr = g;
|
|
185
|
+
if (gr.max_turns !== undefined)
|
|
186
|
+
configKeySpec("guardrails.max_turns").set(cfg, String(gr.max_turns));
|
|
187
|
+
if (gr.budget_usd !== undefined)
|
|
188
|
+
configKeySpec("guardrails.budget_usd").set(cfg, String(gr.budget_usd));
|
|
189
|
+
if (gr.stall_turns !== undefined)
|
|
190
|
+
configKeySpec("guardrails.stall_turns").set(cfg, String(gr.stall_turns));
|
|
191
|
+
}
|
|
192
|
+
return cfg;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load the settings from ~/.mnemom/code.toml (or `path`). A missing file is an
|
|
196
|
+
* empty config (never an error — first run has none). A malformed or invalid
|
|
197
|
+
* file throws with the path so the customer can fix it, rather than silently
|
|
198
|
+
* launching with the wrong settings.
|
|
199
|
+
*/
|
|
200
|
+
export function loadCodeConfig(path = CODE_CONFIG_PATH) {
|
|
201
|
+
if (!existsSync(path))
|
|
202
|
+
return {};
|
|
203
|
+
let text;
|
|
204
|
+
try {
|
|
205
|
+
text = readFileSync(path, "utf8");
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return parseCodeConfig(text);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
215
|
+
throw new Error(`mnemom code: ${msg}\n Fix or remove ${path}.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Serialize a CodeConfig back to TOML (snake_case keys), with a header comment. */
|
|
219
|
+
export function serializeCodeConfig(cfg) {
|
|
220
|
+
const doc = {};
|
|
221
|
+
if (cfg.gateway !== undefined)
|
|
222
|
+
doc.gateway = cfg.gateway;
|
|
223
|
+
if (cfg.model !== undefined)
|
|
224
|
+
doc.model = cfg.model;
|
|
225
|
+
if (cfg.agent !== undefined)
|
|
226
|
+
doc.agent = cfg.agent;
|
|
227
|
+
if (cfg.effort !== undefined)
|
|
228
|
+
doc.effort = cfg.effort;
|
|
229
|
+
if (cfg.cli !== undefined)
|
|
230
|
+
doc.cli = cfg.cli;
|
|
231
|
+
if (cfg.launch !== undefined)
|
|
232
|
+
doc.launch = cfg.launch;
|
|
233
|
+
if (cfg.keySource !== undefined)
|
|
234
|
+
doc.key_source = cfg.keySource;
|
|
235
|
+
const g = cfg.guardrails;
|
|
236
|
+
if (g && (g.maxTurns !== undefined || g.budgetUsd !== undefined || g.stallTurns !== undefined)) {
|
|
237
|
+
const gr = {};
|
|
238
|
+
if (g.maxTurns !== undefined)
|
|
239
|
+
gr.max_turns = g.maxTurns;
|
|
240
|
+
if (g.budgetUsd !== undefined)
|
|
241
|
+
gr.budget_usd = g.budgetUsd;
|
|
242
|
+
if (g.stallTurns !== undefined)
|
|
243
|
+
gr.stall_turns = g.stallTurns;
|
|
244
|
+
doc.guardrails = gr;
|
|
245
|
+
}
|
|
246
|
+
const header = "# ~/.mnemom/code.toml — settings for `mnemom code` (the governed coding-agent launcher).\n" +
|
|
247
|
+
"# Command-line flags override these; these override the built-in defaults.\n" +
|
|
248
|
+
"# The secret Anthropic key is NOT here — it lives in ~/.mnemom/code.json (0600).\n" +
|
|
249
|
+
"# Edit with `mnemom code config edit`, or set one key with `mnemom code config set <key> <value>`.\n\n";
|
|
250
|
+
const body = Object.keys(doc).length ? stringifyToml(doc) : "";
|
|
251
|
+
return `${header}${body}${body.endsWith("\n") || !body ? "" : "\n"}`;
|
|
252
|
+
}
|
|
253
|
+
/** Persist a CodeConfig to `path` (default code.toml), atomically (temp+rename), 0600. */
|
|
254
|
+
export function writeCodeConfig(cfg, path = CODE_CONFIG_PATH) {
|
|
255
|
+
const dir = dirname(path);
|
|
256
|
+
mkdirSync(dir, { recursive: true });
|
|
257
|
+
const tmp = join(dir, `.code-toml-tmp-${process.pid}-${Date.now()}`);
|
|
258
|
+
writeFileSync(tmp, serializeCodeConfig(cfg), { mode: 0o600 });
|
|
259
|
+
renameSync(tmp, path);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Fold the ENV-backed settings from config UNDER the environment: set the
|
|
263
|
+
* MNEMOM_CODE_* var ONLY when it is not already present, so a real env var (and
|
|
264
|
+
* therefore a flag that set one) still wins. The resolvers in lib/code.ts then
|
|
265
|
+
* apply the built-in default when neither env nor config supplied a value.
|
|
266
|
+
*
|
|
267
|
+
* launch maps to MNEMOM_CODE_REMOTE_CONTROL using that var's existing contract
|
|
268
|
+
* (see resolveLaunchShape): terminal→"0", remote-control→"1", server→"server".
|
|
269
|
+
*/
|
|
270
|
+
export function applyConfigToEnv(cfg, env = process.env) {
|
|
271
|
+
if (cfg.gateway && !(env.MNEMOM_CODE_GATEWAY ?? "").trim()) {
|
|
272
|
+
env.MNEMOM_CODE_GATEWAY = cfg.gateway;
|
|
273
|
+
}
|
|
274
|
+
if (cfg.cli && !(env.MNEMOM_CODE_CLI ?? "").trim()) {
|
|
275
|
+
env.MNEMOM_CODE_CLI = cfg.cli;
|
|
276
|
+
}
|
|
277
|
+
if (cfg.launch && !(env.MNEMOM_CODE_REMOTE_CONTROL ?? "").trim()) {
|
|
278
|
+
env.MNEMOM_CODE_REMOTE_CONTROL =
|
|
279
|
+
cfg.launch === "server" ? "server" : cfg.launch === "remote-control" ? "1" : "0";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/code.ts — the pure, IO-free core of `mnemom code`, the CLI's first-class
|
|
3
|
+
* governed-coding-agent launcher.
|
|
4
|
+
*
|
|
5
|
+
* `mnemom code` launches your coding-agent CLI (Claude Code in v1) with its
|
|
6
|
+
* dev-time model traffic routed through the Mnemom gateway for observability and
|
|
7
|
+
* governance, under a governed agent identity and (optionally) a governed
|
|
8
|
+
* per-conversation CONTRACT (goal / requirements / path allow-forbid lists +
|
|
9
|
+
* guardrail ceilings) that the gateway SEALS on the first turn of a conversation.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is deterministic and side-effect-free so it can be unit-tested
|
|
12
|
+
* without spawning a process or touching the network: the contract builder + its
|
|
13
|
+
* canonical seal (byte-identical to the gateway's `parseContractHeader` +
|
|
14
|
+
* `canonicalHash`), the guardrail validators, agent-name sanitisation, and the
|
|
15
|
+
* gateway-door resolution. The command shell (commands/code.ts) does the IO —
|
|
16
|
+
* key resolution, env assembly, and spawning the coding-agent CLI.
|
|
17
|
+
*
|
|
18
|
+
* The us-2/PROD gateway is the DEFAULT door (https://gateway.mnemom.ai). An
|
|
19
|
+
* explicit MNEMOM_CODE_GATEWAY still wins (so any other cell is reachable when
|
|
20
|
+
* NAMED), but prod is never selected silently — see `resolveGatewayHost`.
|
|
21
|
+
*/
|
|
22
|
+
/** The us-2/prod gateway host. The default door — never any other cell silently. */
|
|
23
|
+
export declare const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
|
|
24
|
+
/**
|
|
25
|
+
* The sealed-contract shape sent as `x-mnemom-contract` and re-parsed by the
|
|
26
|
+
* gateway's goal-contract hook (gateway/src/goal-contract.ts `SealedContract`).
|
|
27
|
+
* Empty arrays / empty `goal_id` are omitted, and the guardrail ceilings are
|
|
28
|
+
* plain finite numbers.
|
|
29
|
+
*/
|
|
30
|
+
export interface SealedContract {
|
|
31
|
+
goal_id?: string;
|
|
32
|
+
statement: string;
|
|
33
|
+
requirements?: string[];
|
|
34
|
+
allowed_paths?: string[];
|
|
35
|
+
forbidden_paths?: string[];
|
|
36
|
+
/** Guardrail ceilings — positive finite numbers, sealed into the contract hash. */
|
|
37
|
+
max_turns?: number;
|
|
38
|
+
budget_usd?: number;
|
|
39
|
+
stall_turns?: number;
|
|
40
|
+
}
|
|
41
|
+
/** The raw contract/guardrail flags as commander hands them to the command. */
|
|
42
|
+
export interface ContractFlags {
|
|
43
|
+
goal?: string;
|
|
44
|
+
requirement?: string[];
|
|
45
|
+
allow?: string[];
|
|
46
|
+
forbid?: string[];
|
|
47
|
+
goalId?: string;
|
|
48
|
+
maxTurns?: string | number;
|
|
49
|
+
budget?: string | number;
|
|
50
|
+
stall?: string | number;
|
|
51
|
+
}
|
|
52
|
+
/** The three launch shapes the command supports (see commands/code.ts). */
|
|
53
|
+
export type LaunchShape = "terminal" | "remote-control" | "server";
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
|
|
56
|
+
* `MNEMOM_CODE_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
|
|
57
|
+
* is PROD — an unset var resolves to prod, and any other cell is reached only by
|
|
58
|
+
* NAMING it. Trailing slash trimmed so door concatenation is clean.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
|
|
61
|
+
/**
|
|
62
|
+
* The Anthropic door the CLI points Claude Code at:
|
|
63
|
+
* `${MNEMOM_CODE_GATEWAY}/anthropic`, or the full-door override
|
|
64
|
+
* `MNEMOM_CODE_ANTHROPIC_GATEWAY` when set. v1 is Anthropic-only (no router door
|
|
65
|
+
* / Mnemom key), so the /anthropic door carries the whole contract: x-api-key +
|
|
66
|
+
* x-mnemom-{agent,conversation-id,contract}.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveAnthropicDoor(env?: NodeJS.ProcessEnv): string;
|
|
69
|
+
/** True when the resolved gateway host is the us-2/prod cell (used for the launch banner). */
|
|
70
|
+
export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Sanitise a header value into a safe wire token: lowercase, alphanumerics and
|
|
73
|
+
* single hyphens only, no leading/trailing hyphen. Returns "" when nothing
|
|
74
|
+
* survives (the caller rejects that).
|
|
75
|
+
*/
|
|
76
|
+
export declare function sanitizeAgentName(raw: string): string;
|
|
77
|
+
/** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
|
|
78
|
+
export declare function resolveConversationId(raw: string | undefined): string;
|
|
79
|
+
/** A strictly-positive integer (rejects 0, negatives, decimals, NaN, non-numeric strings). */
|
|
80
|
+
export declare function isPositiveInt(value: unknown): boolean;
|
|
81
|
+
/** A strictly-positive finite number (rejects 0, negatives, NaN, Infinity, non-numeric). */
|
|
82
|
+
export declare function isPositiveNumber(value: unknown): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Canonical JSON — object keys sorted recursively, arrays in order, no
|
|
85
|
+
* whitespace, `undefined` keys dropped. Byte-identical to the gateway's
|
|
86
|
+
* `canonicalJson` (gateway/src/goal-contract.ts), so the base64 we send and the
|
|
87
|
+
* sha we print match what the gateway seals.
|
|
88
|
+
*/
|
|
89
|
+
export declare function canonicalJson(value: unknown): string;
|
|
90
|
+
/** sha256 hex (first 16 chars) of a string — the operator-eyeball contract hash. */
|
|
91
|
+
export declare function sha256Hex16(text: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Was ANY contract/guardrail flag supplied? A contract is built only then; if so
|
|
94
|
+
* `--goal` becomes required (see `assembleContract`). Purely presence-based.
|
|
95
|
+
*/
|
|
96
|
+
export declare function contractFlagsPresent(flags: ContractFlags): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The output of sealing a contract: the canonical JSON, its base64 (the
|
|
99
|
+
* `x-mnemom-contract` header value), and the display hash.
|
|
100
|
+
*/
|
|
101
|
+
export interface SealedContractEnvelope {
|
|
102
|
+
contract: SealedContract;
|
|
103
|
+
canonical: string;
|
|
104
|
+
base64: string;
|
|
105
|
+
sha: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build + seal the contract from the raw flags, or return null when no contract
|
|
109
|
+
* flag was given. Throws (with a human message) on the two invalid states the
|
|
110
|
+
* gateway would otherwise silently swallow:
|
|
111
|
+
* - a contract flag present without `--goal` (the statement is required);
|
|
112
|
+
* - a guardrail ceiling that is not strictly positive (`parseContractHeader`
|
|
113
|
+
* drops those, so a "guarded" launch would actually be unguarded).
|
|
114
|
+
*
|
|
115
|
+
* The canonical form OMITS empty arrays and an empty goal_id, so the sha printed
|
|
116
|
+
* here matches whatever the gateway seals for the same flags.
|
|
117
|
+
*/
|
|
118
|
+
export declare function assembleContract(flags: ContractFlags): SealedContractEnvelope | null;
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the launch shape from the flags and the `MNEMOM_CODE_REMOTE_CONTROL`
|
|
121
|
+
* env var (the flags win). Default is `terminal` — a plain governed launch in
|
|
122
|
+
* THIS terminal. `--remote-control` → `remote-control` (interactive RC in this
|
|
123
|
+
* terminal); `--server` (or `MNEMOM_CODE_REMOTE_CONTROL=server`) → `server` (the
|
|
124
|
+
* headless `claude remote-control` dispatcher). Throws on an invalid env value.
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolveLaunchShape(opts: {
|
|
127
|
+
remoteControl?: boolean;
|
|
128
|
+
server?: boolean;
|
|
129
|
+
}, env?: NodeJS.ProcessEnv): LaunchShape;
|
|
130
|
+
/**
|
|
131
|
+
* Did anyone EXPLICITLY choose a launch shape — a `--remote-control`/`--server`
|
|
132
|
+
* flag, or a non-empty `MNEMOM_CODE_REMOTE_CONTROL` (which config.launch also
|
|
133
|
+
* sets)? When false, the command is free to AUTO-select the best shape: prefer
|
|
134
|
+
* Remote-Control-in-terminal when the machine can do it, else terminal-only. An
|
|
135
|
+
* explicit `terminal` choice (config `launch = "terminal"` → env "0") is
|
|
136
|
+
* explicit and must be respected — never auto-upgraded.
|
|
137
|
+
*/
|
|
138
|
+
export declare function launchPreferenceExplicit(opts: {
|
|
139
|
+
remoteControl?: boolean;
|
|
140
|
+
server?: boolean;
|
|
141
|
+
}, env?: NodeJS.ProcessEnv): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
|
|
144
|
+
* real Anthropic key as `x-api-key`, plus the governed-identity headers and the
|
|
145
|
+
* sealed contract when present. Newline-separated, exactly the wire shape Claude
|
|
146
|
+
* Code sends to the /anthropic door. NB: the returned string contains the key —
|
|
147
|
+
* callers must place it only in the child's env, never in a log line.
|
|
148
|
+
*/
|
|
149
|
+
export declare function buildCustomHeaders(opts: {
|
|
150
|
+
anthropicKey: string;
|
|
151
|
+
agent: string;
|
|
152
|
+
conversationId: string;
|
|
153
|
+
contractB64?: string;
|
|
154
|
+
}): string;
|