@mnemom/mnemom 0.7.2 → 0.8.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.
@@ -0,0 +1,308 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+ import yaml from "js-yaml";
6
+ import { getProtectionCard, putProtectionCard, resolveAgentId, } from "../lib/api.js";
7
+ import { requireAuth } from "../lib/auth.js";
8
+ import { fmt } from "../lib/format.js";
9
+ import { askYesNo, isInteractive } from "../lib/prompt.js";
10
+ const VALID_MODES = new Set(["observe", "warn", "block"]);
11
+ /**
12
+ * Validate a protection card object.
13
+ * Schema: mode, thresholds, screen_surfaces, trusted_sources
14
+ */
15
+ export function validateProtectionCard(card) {
16
+ const checks = [];
17
+ // mode: required, must be observe | warn | block
18
+ const mode = card.mode;
19
+ if (typeof mode === "string" && VALID_MODES.has(mode)) {
20
+ checks.push({ name: "mode", passed: true, message: mode });
21
+ }
22
+ else if (mode === undefined) {
23
+ checks.push({ name: "mode", passed: false, message: "Required (observe | warn | block)" });
24
+ }
25
+ else {
26
+ checks.push({ name: "mode", passed: false, message: `Invalid: "${mode}". Must be observe | warn | block` });
27
+ }
28
+ // thresholds: optional, if present must have warn/quarantine/block in ascending order (0-1)
29
+ const thresholds = card.thresholds;
30
+ if (thresholds !== undefined) {
31
+ if (typeof thresholds !== "object" || thresholds === null) {
32
+ checks.push({ name: "thresholds", passed: false, message: "Must be an object" });
33
+ }
34
+ else {
35
+ const w = thresholds.warn;
36
+ const q = thresholds.quarantine;
37
+ const b = thresholds.block;
38
+ if (w === undefined || q === undefined || b === undefined) {
39
+ checks.push({ name: "thresholds", passed: false, message: "Must have warn, quarantine, and block fields" });
40
+ }
41
+ else if (typeof w !== "number" || typeof q !== "number" || typeof b !== "number") {
42
+ checks.push({ name: "thresholds", passed: false, message: "Values must be numbers" });
43
+ }
44
+ else if (w < 0 || w > 1 || q < 0 || q > 1 || b < 0 || b > 1) {
45
+ checks.push({ name: "thresholds", passed: false, message: "Values must be between 0 and 1" });
46
+ }
47
+ else if (!(w <= q && q <= b)) {
48
+ checks.push({ name: "thresholds", passed: false, message: `Must be in ascending order (warn=${w} <= quarantine=${q} <= block=${b})` });
49
+ }
50
+ else {
51
+ checks.push({ name: "thresholds", passed: true, message: `warn=${w}, quarantine=${q}, block=${b}` });
52
+ }
53
+ }
54
+ }
55
+ // screen_surfaces: optional, array of strings
56
+ const surfaces = card.screen_surfaces;
57
+ if (surfaces !== undefined) {
58
+ if (!Array.isArray(surfaces)) {
59
+ checks.push({ name: "screen_surfaces", passed: false, message: "Must be an array" });
60
+ }
61
+ else if (surfaces.some((s) => typeof s !== "string")) {
62
+ checks.push({ name: "screen_surfaces", passed: false, message: "All entries must be strings" });
63
+ }
64
+ else {
65
+ checks.push({ name: "screen_surfaces", passed: true, message: `${surfaces.length} surface(s)` });
66
+ }
67
+ }
68
+ // trusted_sources: optional, array of objects
69
+ const trusted = card.trusted_sources;
70
+ if (trusted !== undefined) {
71
+ if (!Array.isArray(trusted)) {
72
+ checks.push({ name: "trusted_sources", passed: false, message: "Must be an array" });
73
+ }
74
+ else {
75
+ checks.push({ name: "trusted_sources", passed: true, message: `${trusted.length} source(s)` });
76
+ }
77
+ }
78
+ return checks;
79
+ }
80
+ // ============================================================================
81
+ // File parsing
82
+ // ============================================================================
83
+ function parseProtectionFile(filePath) {
84
+ const raw = fs.readFileSync(filePath, "utf-8");
85
+ const ext = path.extname(filePath).toLowerCase();
86
+ const format = (ext === ".yaml" || ext === ".yml") ? "yaml" : "json";
87
+ if (format === "yaml") {
88
+ const parsed = yaml.load(raw);
89
+ if (!parsed || typeof parsed !== "object") {
90
+ throw new Error("YAML did not produce a valid object");
91
+ }
92
+ return { parsed, raw, format };
93
+ }
94
+ return { parsed: JSON.parse(raw), raw, format };
95
+ }
96
+ // ============================================================================
97
+ // Subcommands
98
+ // ============================================================================
99
+ export async function protectionShowCommand(agentName) {
100
+ const agentId = await resolveAgentId(agentName);
101
+ console.log("\nFetching protection card...\n");
102
+ try {
103
+ const { body, contentType } = await getProtectionCard(agentId);
104
+ if (!body) {
105
+ console.log(fmt.warn("No protection card found"));
106
+ console.log("\nPublish one with:\n");
107
+ console.log(" mnemom protection publish <file.yaml>\n");
108
+ return;
109
+ }
110
+ if (contentType.includes("yaml") || contentType.includes("text/yaml")) {
111
+ console.log(fmt.header("Protection Card"));
112
+ console.log();
113
+ console.log(body);
114
+ }
115
+ else {
116
+ const parsed = JSON.parse(body);
117
+ console.log(fmt.header("Protection Card"));
118
+ console.log();
119
+ console.log(yaml.dump(parsed, { lineWidth: 120, noRefs: true }));
120
+ }
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ console.log("\n" + fmt.error(`Failed to fetch protection card: ${message}`) + "\n");
125
+ process.exit(1);
126
+ }
127
+ }
128
+ export async function protectionPublishCommand(file, agentName) {
129
+ const agentId = await resolveAgentId(agentName);
130
+ const filePath = path.resolve(file);
131
+ if (!fs.existsSync(filePath)) {
132
+ console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
133
+ process.exit(1);
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = parseProtectionFile(filePath);
138
+ }
139
+ catch (e) {
140
+ const msg = e instanceof Error ? e.message : String(e);
141
+ console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
142
+ process.exit(1);
143
+ }
144
+ // Validate locally
145
+ const checks = validateProtectionCard(parsed.parsed);
146
+ const allPassed = checks.every((c) => c.passed);
147
+ console.log(fmt.header("Protection Card Validation"));
148
+ console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
149
+ console.log();
150
+ for (const check of checks) {
151
+ if (check.passed) {
152
+ console.log(fmt.success(`${check.name}: ${check.message}`));
153
+ }
154
+ else {
155
+ console.log(fmt.error(`${check.name}: ${check.message}`));
156
+ }
157
+ }
158
+ console.log();
159
+ if (!allPassed) {
160
+ console.log(fmt.error("Validation failed. Fix the errors above before publishing.") + "\n");
161
+ process.exit(1);
162
+ }
163
+ await requireAuth();
164
+ if (isInteractive()) {
165
+ const confirm = await askYesNo(`Publish this protection card for agent ${agentId}?`, false);
166
+ if (!confirm) {
167
+ console.log("\nPublish cancelled.\n");
168
+ return;
169
+ }
170
+ }
171
+ try {
172
+ console.log("\nPublishing protection card...");
173
+ const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
174
+ const body = parsed.format === "yaml" ? parsed.raw : JSON.stringify(parsed.parsed);
175
+ const result = await putProtectionCard(agentId, body, contentType);
176
+ console.log(fmt.success("Protection card published!"));
177
+ console.log(fmt.label(" Card ID:", ` ${result.card_id}`));
178
+ if (result.composed) {
179
+ console.log(fmt.success("Canonical protection card recomposed"));
180
+ }
181
+ console.log();
182
+ }
183
+ catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ console.log("\n" + fmt.error(`Failed to publish protection card: ${message}`) + "\n");
186
+ process.exit(1);
187
+ }
188
+ }
189
+ export async function protectionValidateCommand(file) {
190
+ const filePath = path.resolve(file);
191
+ if (!fs.existsSync(filePath)) {
192
+ console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
193
+ process.exit(1);
194
+ }
195
+ let parsed;
196
+ try {
197
+ parsed = parseProtectionFile(filePath);
198
+ }
199
+ catch (e) {
200
+ const msg = e instanceof Error ? e.message : String(e);
201
+ console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
202
+ process.exit(1);
203
+ }
204
+ const checks = validateProtectionCard(parsed.parsed);
205
+ const allPassed = checks.every((c) => c.passed);
206
+ const passCount = checks.filter((c) => c.passed).length;
207
+ const failCount = checks.filter((c) => !c.passed).length;
208
+ console.log(fmt.header("Protection Card Validation Report"));
209
+ console.log();
210
+ console.log(fmt.label(" File:", ` ${filePath}`));
211
+ console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
212
+ console.log();
213
+ for (const check of checks) {
214
+ if (check.passed) {
215
+ console.log(fmt.success(`${check.name}: ${check.message}`));
216
+ }
217
+ else {
218
+ console.log(fmt.error(`${check.name}: ${check.message}`));
219
+ }
220
+ }
221
+ console.log();
222
+ if (allPassed) {
223
+ console.log(fmt.success(`All ${passCount} checks passed`) + "\n");
224
+ }
225
+ else {
226
+ console.log(fmt.error(`${failCount} check(s) failed, ${passCount} passed`) + "\n");
227
+ process.exit(1);
228
+ }
229
+ }
230
+ export async function protectionEditCommand(agentName) {
231
+ const agentId = await resolveAgentId(agentName);
232
+ await requireAuth();
233
+ console.log("\nFetching current protection card...\n");
234
+ const { body: original } = await getProtectionCard(agentId);
235
+ if (!original) {
236
+ console.log(fmt.warn("No protection card found. Creating a template..."));
237
+ }
238
+ const cardYaml = original || yaml.dump({
239
+ mode: "observe",
240
+ thresholds: { warn: 0.3, quarantine: 0.6, block: 0.9 },
241
+ screen_surfaces: ["system_prompt", "tool_input", "tool_output"],
242
+ trusted_sources: [],
243
+ }, { lineWidth: 120, noRefs: true });
244
+ const tmpDir = os.tmpdir();
245
+ const tmpFile = path.join(tmpDir, `mnemom-protection-${agentId}.yaml`);
246
+ fs.writeFileSync(tmpFile, cardYaml);
247
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
248
+ console.log(`Opening ${editor}...`);
249
+ const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
250
+ if (result.status !== 0) {
251
+ console.log("\n" + fmt.error("Editor exited with an error") + "\n");
252
+ try {
253
+ fs.unlinkSync(tmpFile);
254
+ }
255
+ catch { /* ignore */ }
256
+ process.exit(1);
257
+ }
258
+ const edited = fs.readFileSync(tmpFile, "utf-8");
259
+ try {
260
+ fs.unlinkSync(tmpFile);
261
+ }
262
+ catch { /* ignore */ }
263
+ if (edited === cardYaml) {
264
+ console.log("\nNo changes made.\n");
265
+ return;
266
+ }
267
+ let parsed;
268
+ try {
269
+ parsed = yaml.load(edited);
270
+ if (!parsed || typeof parsed !== "object")
271
+ throw new Error("Invalid YAML");
272
+ }
273
+ catch (e) {
274
+ const msg = e instanceof Error ? e.message : String(e);
275
+ console.log("\n" + fmt.error(`Invalid YAML: ${msg}`) + "\n");
276
+ process.exit(1);
277
+ }
278
+ const checks = validateProtectionCard(parsed);
279
+ const allPassed = checks.every((c) => c.passed);
280
+ if (!allPassed) {
281
+ console.log(fmt.header("Validation Errors"));
282
+ console.log();
283
+ for (const check of checks.filter((c) => !c.passed)) {
284
+ console.log(fmt.error(`${check.name}: ${check.message}`));
285
+ }
286
+ console.log();
287
+ console.log(fmt.error("Validation failed. Card not published.") + "\n");
288
+ process.exit(1);
289
+ }
290
+ if (isInteractive()) {
291
+ const confirm = await askYesNo("Publish updated protection card?", true);
292
+ if (!confirm) {
293
+ console.log("\nPublish cancelled.\n");
294
+ return;
295
+ }
296
+ }
297
+ try {
298
+ console.log("\nPublishing protection card...");
299
+ const putResult = await putProtectionCard(agentId, edited, "text/yaml");
300
+ console.log(fmt.success("Protection card published!"));
301
+ console.log(fmt.label(" Card ID:", ` ${putResult.card_id}`) + "\n");
302
+ }
303
+ catch (error) {
304
+ const message = error instanceof Error ? error.message : String(error);
305
+ console.log("\n" + fmt.error(`Failed to publish protection card: ${message}`) + "\n");
306
+ process.exit(1);
307
+ }
308
+ }
@@ -1,10 +1,10 @@
1
- import { configExists, loadConfig, requireAgent } from "../lib/config.js";
2
- import { getAgent, getIntegrity, getTraces } from "../lib/api.js";
1
+ import { getGatewayUrl } from "../lib/config.js";
2
+ import { resolveAgentId, getAgent, getIntegrity, getTraces } from "../lib/api.js";
3
+ import { isLoggedIn, getAuthInfo } from "../lib/auth.js";
3
4
  import { detectOpenClaw, detectProviders, getCurrentModel, getSmoltbotConfiguredProviders, PROVIDER_CONFIG_KEYS, } from "../lib/openclaw.js";
4
5
  import { formatModelName, detectProvider } from "../lib/models.js";
5
6
  import { refreshModelCache } from "../lib/model-cache.js";
6
7
  import { fmt } from "../lib/format.js";
7
- const GATEWAY_URL = "https://gateway.mnemom.ai";
8
8
  const DASHBOARD_URL = "https://mnemom.ai";
9
9
  const PROVIDER_LABELS = {
10
10
  anthropic: "Anthropic",
@@ -17,19 +17,20 @@ const AIP_SUPPORT = {
17
17
  gemini: "Full (thought parts)",
18
18
  };
19
19
  export async function statusCommand(agentName) {
20
- console.log(fmt.header("smoltbot status"));
20
+ console.log(fmt.header("mnemom status"));
21
21
  console.log();
22
22
  const checks = [];
23
- // 1. Check smoltbot config
24
- const configCheck = checkSmoltbotConfig();
25
- checks.push(configCheck);
26
- if (configCheck.status === "error") {
23
+ // 1. Check auth status
24
+ const authCheck = await checkAuthStatus();
25
+ checks.push(authCheck);
26
+ if (authCheck.status === "error") {
27
27
  printChecks(checks);
28
- console.log("\nRun `smoltbot init` to get started.\n");
28
+ console.log("\nRun `mnemom login` to authenticate.\n");
29
29
  process.exit(1);
30
30
  }
31
- const config = loadConfig();
32
- const agent = await requireAgent(agentName);
31
+ // Resolve agent ID from server
32
+ const agentId = await resolveAgentId(agentName);
33
+ const gatewayUrl = getGatewayUrl();
33
34
  // 2. Check OpenClaw configuration
34
35
  const openclawCheck = checkOpenClawConfig();
35
36
  checks.push(openclawCheck);
@@ -40,28 +41,19 @@ export async function statusCommand(agentName) {
40
41
  const modelCheck = checkCurrentModel();
41
42
  checks.push(modelCheck);
42
43
  // 5. Test gateway connectivity
43
- const gatewayCheck = await checkGatewayConnectivity();
44
+ const gatewayCheck = await checkGatewayConnectivity(gatewayUrl);
44
45
  checks.push(gatewayCheck);
45
46
  // 6. Test API connectivity
46
- const apiCheck = await checkApiConnectivity(agent.agentId);
47
+ const apiCheck = await checkApiConnectivity(agentId);
47
48
  checks.push(apiCheck);
48
49
  // Print all checks
49
50
  printChecks(checks);
50
51
  // Show configuration details
51
52
  console.log(fmt.section("Configuration"));
52
53
  console.log();
53
- console.log(fmt.label("Agent ID: ", agent.agentId));
54
- console.log(fmt.label("Gateway: ", config.gateway || GATEWAY_URL));
55
- console.log(fmt.label("Dashboard:", ` ${DASHBOARD_URL}/agents/${agent.agentId}`));
56
- if (config.mnemomApiKey) {
57
- console.log(`Mnemom Key: [CONFIGURED] (billing enabled)`);
58
- }
59
- else {
60
- console.log(`Mnemom Key: Not configured (free tier)`);
61
- }
62
- if (agent.openclawConfigured) {
63
- console.log(`Configured: ${agent.configuredAt || "yes"}`);
64
- }
54
+ console.log(fmt.label("Agent ID: ", agentId));
55
+ console.log(fmt.label("Gateway: ", gatewayUrl));
56
+ console.log(fmt.label("Dashboard:", ` ${DASHBOARD_URL}/agents/${agentId}`));
65
57
  // Show current model info
66
58
  const { fullPath, provider, modelId } = getCurrentModel();
67
59
  if (fullPath) {
@@ -76,7 +68,7 @@ export async function statusCommand(agentName) {
76
68
  console.log(" Status: Traced mode NOT ACTIVE");
77
69
  if (modelId) {
78
70
  const detectedProvider = detectProvider(modelId);
79
- const configKey = detectedProvider ? PROVIDER_CONFIG_KEYS[detectedProvider] : "smoltbot";
71
+ const configKey = detectedProvider ? PROVIDER_CONFIG_KEYS[detectedProvider] : "mnemom";
80
72
  console.log(`\n To enable: openclaw models set ${configKey}/${modelId}`);
81
73
  }
82
74
  }
@@ -85,7 +77,7 @@ export async function statusCommand(agentName) {
85
77
  showProviderSummary();
86
78
  // Show trace summary if available
87
79
  if (apiCheck.status === "ok") {
88
- await showTraceSummary(agent.agentId);
80
+ await showTraceSummary(agentId);
89
81
  }
90
82
  // Show overall status
91
83
  const hasErrors = checks.some((c) => c.status === "error");
@@ -105,29 +97,29 @@ export async function statusCommand(agentName) {
105
97
  // Refresh model cache in background (non-blocking)
106
98
  refreshModelCache().catch(() => { });
107
99
  }
108
- function checkSmoltbotConfig() {
109
- if (!configExists()) {
100
+ async function checkAuthStatus() {
101
+ const loggedIn = await isLoggedIn();
102
+ if (!loggedIn) {
110
103
  return {
111
- name: "Smoltbot Config",
104
+ name: "Authentication",
112
105
  status: "error",
113
- message: "Not initialized",
114
- details: "Run `smoltbot init` to configure",
106
+ message: "Not authenticated",
107
+ details: "Run `mnemom login` or set MNEMOM_API_KEY",
115
108
  };
116
109
  }
117
- const config = loadConfig();
118
- if (!config) {
110
+ const auth = getAuthInfo();
111
+ if (auth) {
119
112
  return {
120
- name: "Smoltbot Config",
121
- status: "error",
122
- message: "Config file corrupted",
123
- details: "Delete ~/.smoltbot/config.json and run `smoltbot init`",
113
+ name: "Authentication",
114
+ status: "ok",
115
+ message: `Logged in as ${auth.email}`,
124
116
  };
125
117
  }
126
- const defaultAgent = config.agents[config.defaultAgent];
118
+ // API key auth (no email available)
127
119
  return {
128
- name: "Smoltbot Config",
120
+ name: "Authentication",
129
121
  status: "ok",
130
- message: `Agent ID: ${defaultAgent?.agentId ?? "unknown"}`,
122
+ message: "Authenticated via API key",
131
123
  };
132
124
  }
133
125
  function checkOpenClawConfig() {
@@ -146,7 +138,7 @@ function checkOpenClawConfig() {
146
138
  name: "OpenClaw",
147
139
  status: "error",
148
140
  message: "OAuth auth (not supported)",
149
- details: "smoltbot requires API key authentication",
141
+ details: "mnemom requires API key authentication",
150
142
  };
151
143
  }
152
144
  // Check if any provider has a key (not just Anthropic)
@@ -170,14 +162,14 @@ function checkOpenClawConfig() {
170
162
  return {
171
163
  name: "OpenClaw",
172
164
  status: "warning",
173
- message: "smoltbot provider not configured",
174
- details: "Run `smoltbot init` to configure",
165
+ message: "mnemom provider not configured",
166
+ details: "Run `mnemom register <name>` to configure",
175
167
  };
176
168
  }
177
169
  return {
178
170
  name: "OpenClaw",
179
171
  status: "ok",
180
- message: "smoltbot provider configured",
172
+ message: "mnemom provider configured",
181
173
  };
182
174
  }
183
175
  function checkConfiguredProviders() {
@@ -201,7 +193,7 @@ function checkConfiguredProviders() {
201
193
  name: `${PROVIDER_LABELS[provider]}`,
202
194
  status: "warning",
203
195
  message: "API key found but not configured",
204
- details: "Run `smoltbot init` to configure",
196
+ details: "Run `mnemom register <name>` to configure",
205
197
  });
206
198
  }
207
199
  // Don't show providers without keys (too noisy)
@@ -215,7 +207,7 @@ function checkCurrentModel() {
215
207
  name: "Current Model",
216
208
  status: "warning",
217
209
  message: "No default model set",
218
- details: "Run `openclaw models set smoltbot/<model>`",
210
+ details: "Run `openclaw models set mnemom/<model>`",
219
211
  };
220
212
  }
221
213
  if (provider && (provider === "smoltbot" || provider.startsWith("smoltbot"))) {
@@ -229,7 +221,7 @@ function checkCurrentModel() {
229
221
  name: "Current Model",
230
222
  status: "warning",
231
223
  message: `${fullPath} (not traced)`,
232
- details: `Switch with: openclaw models set smoltbot/${modelId}`,
224
+ details: `Switch with: openclaw models set mnemom/${modelId}`,
233
225
  };
234
226
  }
235
227
  function showProviderSummary() {
@@ -248,9 +240,9 @@ function showProviderSummary() {
248
240
  console.log(` ${label}: ${configKey}/* (AIP: ${aip})`);
249
241
  }
250
242
  }
251
- async function checkGatewayConnectivity() {
243
+ async function checkGatewayConnectivity(gatewayUrl) {
252
244
  try {
253
- const response = await fetch(`${GATEWAY_URL}/health`, {
245
+ const response = await fetch(`${gatewayUrl}/health`, {
254
246
  signal: AbortSignal.timeout(5000),
255
247
  });
256
248
  if (response.ok) {