@memmo/cli 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/index.js +1957 -0
  4. package/package.json +59 -0
package/dist/index.js ADDED
@@ -0,0 +1,1957 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/version.ts
13
+ import { createRequire } from "node:module";
14
+ var require2, pkg, VERSION;
15
+ var init_version = __esm({
16
+ "src/version.ts"() {
17
+ "use strict";
18
+ require2 = createRequire(import.meta.url);
19
+ pkg = require2("../package.json");
20
+ VERSION = pkg.version;
21
+ }
22
+ });
23
+
24
+ // src/api.ts
25
+ var api_exports = {};
26
+ __export(api_exports, {
27
+ apiFetch: () => apiFetch
28
+ });
29
+ async function apiFetch(opts, path, init = {}) {
30
+ const headers = {
31
+ ...init.headers
32
+ };
33
+ if (init.body != null && headers["content-type"] === void 0) {
34
+ headers["content-type"] = "application/json";
35
+ }
36
+ if (opts.token) headers.authorization = `Bearer ${opts.token}`;
37
+ headers["user-agent"] = `memmo-cli/${VERSION} (${process.platform}; node ${process.versions.node})`;
38
+ const res = await fetch(`${opts.baseUrl}${path}`, { ...init, headers });
39
+ const text = await res.text();
40
+ const body = text ? JSON.parse(text) : null;
41
+ if (!res.ok) {
42
+ const message = body?.error ?? res.statusText;
43
+ throw new Error(`API ${res.status}: ${message}`);
44
+ }
45
+ return body;
46
+ }
47
+ var init_api = __esm({
48
+ "src/api.ts"() {
49
+ "use strict";
50
+ init_version();
51
+ }
52
+ });
53
+
54
+ // src/index.ts
55
+ import { Command } from "commander";
56
+
57
+ // src/commands/login.ts
58
+ import { createServer } from "node:http";
59
+ import { randomBytes } from "node:crypto";
60
+ import { hostname } from "node:os";
61
+ import { exec } from "node:child_process";
62
+ import { createInterface } from "node:readline/promises";
63
+
64
+ // src/config.ts
65
+ import { homedir } from "node:os";
66
+ import { join } from "node:path";
67
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
68
+ import { execSync } from "node:child_process";
69
+ var CRED_DIR = join(homedir(), ".memmo");
70
+ var CRED_FILE = join(CRED_DIR, "credentials.json");
71
+ var PROJECT_DIR = ".memmo";
72
+ var PROJECT_CONFIG = join(PROJECT_DIR, "config.json");
73
+ function saveCredentials(creds) {
74
+ if (!existsSync(CRED_DIR)) mkdirSync(CRED_DIR, { recursive: true });
75
+ writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 384 });
76
+ }
77
+ function loadCredentials() {
78
+ if (!existsSync(CRED_FILE)) return null;
79
+ return JSON.parse(readFileSync(CRED_FILE, "utf8"));
80
+ }
81
+ function loadProjectConfig(cwd = process.cwd()) {
82
+ const file = join(cwd, PROJECT_CONFIG);
83
+ if (!existsSync(file)) return null;
84
+ return JSON.parse(readFileSync(file, "utf8"));
85
+ }
86
+ var REPO_CACHE = join(CRED_DIR, "repos.json");
87
+ var REPO_CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
88
+ function readRepoCache() {
89
+ try {
90
+ return existsSync(REPO_CACHE) ? JSON.parse(readFileSync(REPO_CACHE, "utf8")) : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+ function cacheRepo(remote, entry) {
96
+ try {
97
+ if (!existsSync(CRED_DIR)) mkdirSync(CRED_DIR, { recursive: true });
98
+ const cache = readRepoCache();
99
+ cache[remote] = { ...entry, at: Date.now() };
100
+ writeFileSync(REPO_CACHE, JSON.stringify(cache, null, 2));
101
+ } catch {
102
+ }
103
+ }
104
+ function forgetRepo(remote) {
105
+ try {
106
+ const cache = readRepoCache();
107
+ delete cache[remote];
108
+ writeFileSync(REPO_CACHE, JSON.stringify(cache, null, 2));
109
+ } catch {
110
+ }
111
+ }
112
+ var DEFAULT_API_BASE_URL = process.env.MEMMO_API_URL ?? "https://api.memmo.dev";
113
+ function detectGitRemote(cwd = process.cwd()) {
114
+ try {
115
+ return execSync("git remote get-url origin", {
116
+ cwd,
117
+ stdio: ["ignore", "pipe", "ignore"]
118
+ }).toString().trim() || null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ async function resolveProjectConfig(cwd = process.cwd(), opts = {}) {
124
+ const creds = loadCredentials();
125
+ const apiBaseUrl = opts.apiBaseUrl ?? process.env.MEMMO_API_URL ?? creds?.apiBaseUrl ?? DEFAULT_API_BASE_URL;
126
+ const explicit = opts.repoId ?? process.env.MEMMO_REPO_ID;
127
+ if (explicit) return { apiBaseUrl, repoId: explicit };
128
+ const remote = detectGitRemote(cwd);
129
+ if (remote && creds) {
130
+ const hit = readRepoCache()[remote];
131
+ if (hit && hit.apiBaseUrl === apiBaseUrl && Date.now() - hit.at < REPO_CACHE_TTL_MS) {
132
+ return { apiBaseUrl, repoId: hit.repoId, repoFullName: hit.fullName };
133
+ }
134
+ try {
135
+ const { apiFetch: apiFetch2 } = await Promise.resolve().then(() => (init_api(), api_exports));
136
+ const r = await apiFetch2(
137
+ { baseUrl: apiBaseUrl, token: creds.token },
138
+ "/repos/resolve",
139
+ { method: "POST", body: JSON.stringify({ repoRemote: remote }) }
140
+ );
141
+ if (r?.repoId) {
142
+ cacheRepo(remote, { repoId: r.repoId, fullName: r.fullName, apiBaseUrl });
143
+ return { apiBaseUrl, repoId: r.repoId, repoFullName: r.fullName };
144
+ }
145
+ } catch {
146
+ }
147
+ }
148
+ const legacy = loadProjectConfig(cwd);
149
+ return legacy ? { ...legacy, apiBaseUrl: legacy.apiBaseUrl ?? apiBaseUrl } : null;
150
+ }
151
+ var NOT_CONNECTED = "This repo isn't connected to Memmo yet. Run `memmo init` to add it to a project.";
152
+ function repoFullNameFromRemote(remote) {
153
+ let s = remote.trim();
154
+ s = s.replace(/^git@[^:]+:/, "");
155
+ s = s.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\//i, "");
156
+ s = s.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
157
+ const parts = s.split("/").filter(Boolean);
158
+ return parts.length >= 2 ? parts.slice(-2).join("/") : null;
159
+ }
160
+
161
+ // src/commands/login.ts
162
+ async function whoami(baseUrl, token) {
163
+ const res = await fetch(`${baseUrl}/me`, { headers: { authorization: `Bearer ${token}` } });
164
+ if (!res.ok) throw new Error(`That token was rejected by ${baseUrl} (${res.status}).`);
165
+ return await res.json();
166
+ }
167
+ function openBrowser(url) {
168
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
169
+ exec(cmd, () => {
170
+ });
171
+ }
172
+ async function browserLogin(baseUrl, appUrl, noBrowser) {
173
+ const state = randomBytes(16).toString("hex");
174
+ return new Promise((resolve, reject) => {
175
+ const server = createServer((req, res) => {
176
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
177
+ if (url.pathname !== "/callback") {
178
+ res.writeHead(404).end();
179
+ return;
180
+ }
181
+ const token = url.searchParams.get("token");
182
+ const ok = token && url.searchParams.get("state") === state;
183
+ res.writeHead(ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
184
+ res.end(
185
+ ok ? "<!doctype html><body style='font-family:system-ui;background:#0a0711;color:#ece9f3;display:grid;place-items:center;height:100vh'><div><h2>Memmo CLI authorized \u2713</h2><p>You can close this tab and return to your terminal.</p></div></body>" : "<!doctype html><body style='font-family:system-ui'><h2>Invalid callback</h2><p>Run <code>memmo login</code> again.</p></body>"
186
+ );
187
+ if (ok) {
188
+ clearTimeout(timer);
189
+ server.close();
190
+ resolve(token);
191
+ }
192
+ });
193
+ const timer = setTimeout(() => {
194
+ server.close();
195
+ reject(new Error("Timed out waiting for the browser. Run `memmo login` again, or use `memmo login --token <token>`."));
196
+ }, 5 * 60 * 1e3);
197
+ server.listen(0, "127.0.0.1", () => {
198
+ const addr = server.address();
199
+ const port = typeof addr === "object" && addr ? addr.port : 0;
200
+ const url = new URL("/cli/auth", appUrl);
201
+ url.searchParams.set("port", String(port));
202
+ url.searchParams.set("state", state);
203
+ url.searchParams.set("host", hostname());
204
+ console.log(`Opening your browser to sign in to Memmo\u2026
205
+ ${url.toString()}
206
+ `);
207
+ if (!noBrowser) openBrowser(url.toString());
208
+ });
209
+ });
210
+ }
211
+ async function devLogin(baseUrl, email) {
212
+ if (!email) {
213
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
214
+ email = (await rl.question("Email: ")).trim();
215
+ rl.close();
216
+ }
217
+ const loginRes = await fetch(`${baseUrl}/auth/login`, {
218
+ method: "POST",
219
+ headers: { "content-type": "application/json" },
220
+ body: JSON.stringify({ email })
221
+ });
222
+ if (!loginRes.ok) throw new Error(`Login failed: ${loginRes.status} ${await loginRes.text()}`);
223
+ const cookie = loginRes.headers.getSetCookie?.().join("; ") ?? "";
224
+ const tokenRes = await fetch(`${baseUrl}/auth/tokens`, {
225
+ method: "POST",
226
+ headers: { "content-type": "application/json", cookie },
227
+ body: JSON.stringify({ name: `cli \xB7 ${hostname()}` })
228
+ });
229
+ if (!tokenRes.ok) throw new Error(`Token mint failed: ${tokenRes.status} ${await tokenRes.text()}`);
230
+ return (await tokenRes.json()).token;
231
+ }
232
+ async function loginCommand(opts) {
233
+ const baseUrl = (opts.api ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
234
+ let config = {};
235
+ try {
236
+ const r = await fetch(`${baseUrl}/auth/config`);
237
+ if (r.ok) config = await r.json();
238
+ } catch {
239
+ throw new Error(`Can't reach the Memmo API at ${baseUrl}. Is it the right URL? (--api <url> or MEMMO_API_URL)`);
240
+ }
241
+ let token;
242
+ if (opts.token) {
243
+ token = opts.token.trim();
244
+ } else if (opts.email || opts.browser !== false && !config.github && config.devLogin) {
245
+ token = await devLogin(baseUrl, opts.email);
246
+ } else {
247
+ token = await browserLogin(baseUrl, config.appUrl ?? "https://app.memmo.dev", opts.browser === false);
248
+ }
249
+ const me = await whoami(baseUrl, token);
250
+ saveCredentials({ apiBaseUrl: baseUrl, token });
251
+ console.log(`Logged in as ${me.email}. Credentials saved to ~/.memmo/credentials.json`);
252
+ console.log("Next: run `memmo init` inside a repo.");
253
+ }
254
+
255
+ // src/commands/init.ts
256
+ import { createInterface as createInterface2 } from "node:readline/promises";
257
+ import { existsSync as existsSync4 } from "node:fs";
258
+ import { join as join4 } from "node:path";
259
+ init_api();
260
+
261
+ // src/commands/claude-install.ts
262
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
263
+ import { join as join2 } from "node:path";
264
+ var MEMMO_CMD = "npx";
265
+ var MEMMO_ARGS = ["-y", "@memmo/cli"];
266
+ var hook = (event) => [MEMMO_CMD, ...MEMMO_ARGS, "hook", event].join(" ");
267
+ var MCP_SERVER = { type: "stdio", command: MEMMO_CMD, args: [...MEMMO_ARGS, "mcp"] };
268
+ var CLAUDE_MD_SECTION = `# Memmo Project Memory
269
+
270
+ Before making changes, use the Memmo MCP tools to retrieve relevant repo memory.
271
+
272
+ Recommended tools:
273
+ - get_repo_context \u2014 call before starting work in this repo
274
+ - search_memory \u2014 search approved memories relevant to your task
275
+ - get_relevant_warnings \u2014 call BEFORE editing files, passing the paths you'll touch, to surface known risks/outages
276
+ - propose_memories \u2014 record durable knowledge you discover (conventions, architecture, commands, risks); keep each memory ATOMIC (one fact) and CONCISE (1\u20133 sentences); NEVER include secrets (passwords, keys, tokens) and keep memories environment-agnostic (no ports, hostnames, or local paths); to bootstrap this repo, read its key files and propose memories
277
+ - record_session_summary \u2014 call at the end of a meaningful task so Memmo can propose new memories
278
+
279
+ Important:
280
+ - Respect approved project memories.
281
+ - Treat proposed memories as suggestions only.
282
+ - Always check get_relevant_warnings before modifying sensitive files, and heed the warnings.
283
+ - When asked to "scan" or "set up Memmo" for this repo, read the key files and call propose_memories.
284
+ `;
285
+ var HOOK_EVENTS = {
286
+ SessionStart: { hooks: [{ type: "command", command: hook("session-start") }] },
287
+ UserPromptSubmit: { hooks: [{ type: "command", command: hook("user-prompt") }] },
288
+ PreToolUse: {
289
+ matcher: "Edit|Write|MultiEdit|NotebookEdit",
290
+ hooks: [{ type: "command", command: hook("pre-edit") }]
291
+ },
292
+ SessionEnd: { hooks: [{ type: "command", command: hook("session-end") }] }
293
+ };
294
+ var isMemmoHook = (e) => e.hooks?.some((h) => typeof h.command === "string" && h.command.includes("memmo hook"));
295
+ function mergeMcpJson(path) {
296
+ if (!existsSync2(path)) {
297
+ writeFileSync2(path, JSON.stringify({ mcpServers: { memmo: MCP_SERVER } }, null, 2) + "\n");
298
+ return "created .mcp.json";
299
+ }
300
+ try {
301
+ const json = JSON.parse(readFileSync2(path, "utf8"));
302
+ json.mcpServers = json.mcpServers ?? {};
303
+ if (json.mcpServers.memmo) return ".mcp.json already has the memmo server \u2014 left as is";
304
+ json.mcpServers.memmo = MCP_SERVER;
305
+ writeFileSync2(path, JSON.stringify(json, null, 2) + "\n");
306
+ return "added memmo server to existing .mcp.json";
307
+ } catch {
308
+ return "could not parse existing .mcp.json \u2014 add the memmo server manually";
309
+ }
310
+ }
311
+ function mergeClaudeMd(path) {
312
+ if (!existsSync2(path)) {
313
+ writeFileSync2(path, CLAUDE_MD_SECTION);
314
+ return "created CLAUDE.md";
315
+ }
316
+ const existing = readFileSync2(path, "utf8");
317
+ if (existing.includes("Memmo Project Memory") || existing.includes("Memmo MCP tools")) {
318
+ return "CLAUDE.md already mentions Memmo \u2014 left as is";
319
+ }
320
+ writeFileSync2(path, existing.trimEnd() + "\n\n" + CLAUDE_MD_SECTION);
321
+ return "appended Memmo section to existing CLAUDE.md";
322
+ }
323
+ function mergeSettingsHooks(claudeDir) {
324
+ if (!existsSync2(claudeDir)) mkdirSync2(claudeDir, { recursive: true });
325
+ const path = join2(claudeDir, "settings.json");
326
+ let json = {};
327
+ if (existsSync2(path)) {
328
+ try {
329
+ json = JSON.parse(readFileSync2(path, "utf8"));
330
+ } catch {
331
+ return "could not parse .claude/settings.json \u2014 add the memmo hooks manually";
332
+ }
333
+ }
334
+ json.hooks = json.hooks ?? {};
335
+ let added = 0;
336
+ for (const [event, entry] of Object.entries(HOOK_EVENTS)) {
337
+ const list = json.hooks[event] = json.hooks[event] ?? [];
338
+ if (list.some(isMemmoHook)) continue;
339
+ list.push(entry);
340
+ added++;
341
+ }
342
+ if (added === 0) return ".claude/settings.json already has the memmo hooks \u2014 left as is";
343
+ writeFileSync2(path, JSON.stringify(json, null, 2) + "\n");
344
+ return existsSync2(path) ? `registered ${added} Memmo hook(s) in .claude/settings.json` : "wrote .claude/settings.json";
345
+ }
346
+ function writeClaudeAssets(cwd = process.cwd()) {
347
+ return [
348
+ mergeMcpJson(join2(cwd, ".mcp.json")),
349
+ mergeClaudeMd(join2(cwd, "CLAUDE.md")),
350
+ mergeSettingsHooks(join2(cwd, ".claude"))
351
+ ];
352
+ }
353
+ function removeMcpServer(cwd = process.cwd()) {
354
+ const path = join2(cwd, ".mcp.json");
355
+ if (!existsSync2(path)) return null;
356
+ try {
357
+ const json = JSON.parse(readFileSync2(path, "utf8"));
358
+ if (!json.mcpServers?.memmo) return null;
359
+ delete json.mcpServers.memmo;
360
+ if (Object.keys(json.mcpServers).length === 0 && Object.keys(json).length === 1) {
361
+ writeFileSync2(path, JSON.stringify({ mcpServers: {} }, null, 2) + "\n");
362
+ } else {
363
+ writeFileSync2(path, JSON.stringify(json, null, 2) + "\n");
364
+ }
365
+ return "removed memmo server from .mcp.json";
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function removeSettingsHooks(cwd = process.cwd()) {
371
+ const path = join2(cwd, ".claude", "settings.json");
372
+ if (!existsSync2(path)) return null;
373
+ try {
374
+ const json = JSON.parse(readFileSync2(path, "utf8"));
375
+ if (!json.hooks) return null;
376
+ let removed = 0;
377
+ for (const event of Object.keys(json.hooks)) {
378
+ const before = json.hooks[event].length;
379
+ json.hooks[event] = json.hooks[event].filter((e) => !isMemmoHook(e));
380
+ removed += before - json.hooks[event].length;
381
+ if (json.hooks[event].length === 0) delete json.hooks[event];
382
+ }
383
+ if (removed === 0) return null;
384
+ if (Object.keys(json.hooks).length === 0) delete json.hooks;
385
+ writeFileSync2(path, JSON.stringify(json, null, 2) + "\n");
386
+ return `removed ${removed} Memmo hook(s) from .claude/settings.json`;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+ function removeClaudeSection(cwd = process.cwd()) {
392
+ const path = join2(cwd, "CLAUDE.md");
393
+ if (!existsSync2(path)) return null;
394
+ const existing = readFileSync2(path, "utf8");
395
+ const idx = existing.indexOf("# Memmo Project Memory");
396
+ if (idx === -1) return null;
397
+ const trimmed = existing.slice(0, idx).trimEnd();
398
+ if (trimmed.length === 0) {
399
+ writeFileSync2(path, "");
400
+ return "cleared Memmo section from CLAUDE.md";
401
+ }
402
+ writeFileSync2(path, trimmed + "\n");
403
+ return "removed Memmo section from CLAUDE.md";
404
+ }
405
+ async function claudeInstallCommand() {
406
+ const actions = writeClaudeAssets();
407
+ console.log("Claude Code integration:");
408
+ for (const a of actions) console.log(" - " + a);
409
+ }
410
+
411
+ // src/commands/scan.ts
412
+ import { spawn } from "node:child_process";
413
+ import { existsSync as existsSync3 } from "node:fs";
414
+ import { join as join3, delimiter } from "node:path";
415
+ init_api();
416
+ function findOnPath(cmd) {
417
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
418
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
419
+ if (!dir) continue;
420
+ for (const ext of exts) {
421
+ const full = join3(dir, cmd + ext);
422
+ if (existsSync3(full)) return full;
423
+ }
424
+ }
425
+ return null;
426
+ }
427
+ var AGENT_PROMPT = `You are onboarding onto THIS repository to build durable operational memory for future AI coding agents \u2014 like writing the onboarding doc for a senior engineer.
428
+
429
+ FIRST call mcp__memmo__get_repo_context and run a few broad mcp__memmo__search_memory queries
430
+ (architecture, commands, testing, deploy, risks) to learn what this project ALREADY knows. Never
431
+ propose something already covered. If an existing memory is wrong or outdated, propose the corrected
432
+ memory with "supersedes": "<that memory's id>".
433
+
434
+ Then explore the codebase thoroughly. Read the README, package manifests, config files, the data model / schema, entry points, routes/services/handlers, CI workflows, and a representative sample of the most important source files. Use Read, Glob, and Grep freely.
435
+
436
+ Also review the COMMIT HISTORY and its descriptions: run \`git log --pretty=format:'%h %s%n%b' -n 500\` (and \`git log --oneline -n 500\` for a quick overview). Commit subjects and bodies are evidence of decisions, recurring failures, and conventions enforced over time.
437
+
438
+ Then call the propose_memories MCP tool (mcp__memmo__propose_memories) to record a THOROUGH set of memories a new engineer/agent must know:
439
+ - Overall architecture and how the major modules/areas fit together (one memory per significant area).
440
+ - The data model / key entities (from schema/models).
441
+ - The API/interface surface and important flows.
442
+ - Key commands: build, run, test, lint, migrate, deploy.
443
+ - Conventions and project rules (naming, structure, patterns, error handling, auth).
444
+ - Important dependencies and external services.
445
+ - Testing and deployment practices.
446
+ - Risks, gotchas, and "do not touch" areas.
447
+ - Recurring themes, decisions, and past failures evident from the commit history (type "decision"/"failure"/"project_rule").
448
+
449
+ Rules:
450
+ - Base every memory ONLY on what you actually read. Do not invent or assume.
451
+ - Be SPECIFIC and concrete \u2014 reference real modules, files, and names. No generic boilerplate.
452
+ - ATOMIC: each memory is ONE fact, rule, command, entity, or decision. Split multi-topic content
453
+ into separate memories \u2014 never pack a whole subsystem into one giant memory.
454
+ - CONCISE: 1\u20133 sentences, ideally under ~280 characters. State the fact, not an essay.
455
+ - For area- or risk-specific memories, include path globs in "paths" (e.g. "apps/api/src/routes/**").
456
+ - Set confidence 0..1 by certainty and reusability.
457
+ - Propose as many small, high-signal memories as the codebase warrants (up to 30). Favor many
458
+ atomic memories over a few long ones.
459
+
460
+ When you're done, briefly report how many memories you proposed.`;
461
+ async function countProposed(client, repoId) {
462
+ try {
463
+ const detail = await apiFetch(client, `/repos/${repoId}`);
464
+ const row = detail.memoryCounts?.find((c) => c.status === "proposed");
465
+ return row?._count ?? 0;
466
+ } catch {
467
+ return 0;
468
+ }
469
+ }
470
+ async function agentScan(claudeBin, client, config, cwd) {
471
+ const mcpConfig = join3(cwd, ".mcp.json");
472
+ if (!existsSync3(mcpConfig)) {
473
+ throw new Error("No .mcp.json found \u2014 run `memmo init` (or `memmo claude install`) first.");
474
+ }
475
+ const before = await countProposed(client, config.repoId);
476
+ const t0 = Date.now();
477
+ console.log("Deep scan with local Claude Code (no API key, runs on your Claude subscription)\u2026");
478
+ console.log("Claude is reading the codebase and proposing memories. This can take a few minutes.\n");
479
+ const args = [
480
+ "-p",
481
+ AGENT_PROMPT,
482
+ "--mcp-config",
483
+ mcpConfig,
484
+ "--permission-mode",
485
+ "default",
486
+ "--allowedTools",
487
+ "Read",
488
+ "Glob",
489
+ "Grep",
490
+ "LS",
491
+ "Bash(git log:*)",
492
+ "mcp__memmo__get_repo_context",
493
+ "mcp__memmo__search_memory",
494
+ "mcp__memmo__propose_memories"
495
+ ];
496
+ const childEnv = { ...process.env };
497
+ delete childEnv.ANTHROPIC_API_KEY;
498
+ const code = await new Promise((resolve, reject) => {
499
+ const child = spawn(claudeBin, args, { cwd, stdio: "inherit", env: childEnv });
500
+ child.on("error", reject);
501
+ child.on("close", (c) => resolve(c ?? 0));
502
+ });
503
+ if (code !== 0) {
504
+ throw new Error(
505
+ `Claude Code exited with code ${code}.
506
+ If you saw a 401/auth error, make sure Claude Code is logged in: run \`claude\` once and sign in (or \`claude login\`).
507
+ Alternatively, use \`memmo scan --server\` to scan with the project API key.`
508
+ );
509
+ }
510
+ const after = await countProposed(client, config.repoId);
511
+ const added = Math.max(0, after - before);
512
+ const secs = Math.round((Date.now() - t0) / 1e3);
513
+ console.log(`
514
+ Deep scan complete in ${secs}s \u2014 ${added} new memory proposal(s) waiting in your Inbox.`);
515
+ }
516
+ async function serverScan(client, config) {
517
+ console.log("Scanning the repo from GitHub\u2026");
518
+ const result = await apiFetch(client, `/repos/${config.repoId}/scan`, {
519
+ method: "POST"
520
+ });
521
+ const bits = [
522
+ result.filesRead != null ? `${result.filesRead} files` : null,
523
+ result.commitsRead != null ? `${result.commitsRead} commits` : null
524
+ ].filter(Boolean);
525
+ const from = bits.length ? ` (read ${bits.join(", ")})` : "";
526
+ console.log(
527
+ `Proposed ${result.proposedCount} memory(ies)${from}. Review them in the Memmo inbox.`
528
+ );
529
+ }
530
+ async function scanCommand(opts = {}) {
531
+ const creds = loadCredentials();
532
+ const config = await resolveProjectConfig();
533
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
534
+ if (!config) throw new Error(NOT_CONNECTED);
535
+ const client = {
536
+ baseUrl: config.apiBaseUrl ?? creds.apiBaseUrl,
537
+ token: creds.token
538
+ };
539
+ const cwd = process.cwd();
540
+ if (opts.server) {
541
+ return serverScan(client, config);
542
+ }
543
+ const claudeBin = findOnPath("claude");
544
+ if (opts.agent) {
545
+ if (!claudeBin) {
546
+ throw new Error(
547
+ "Claude Code (`claude`) was not found on your PATH. Install it from https://claude.com/claude-code, or run `memmo scan --server`."
548
+ );
549
+ }
550
+ return agentScan(claudeBin, client, config, cwd);
551
+ }
552
+ if (claudeBin) {
553
+ return agentScan(claudeBin, client, config, cwd);
554
+ }
555
+ console.log("Claude Code not found locally \u2014 falling back to the server scan.\n");
556
+ return serverScan(client, config);
557
+ }
558
+
559
+ // src/commands/init.ts
560
+ async function ask(q) {
561
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
562
+ const a = (await rl.question(q)).trim();
563
+ rl.close();
564
+ return a;
565
+ }
566
+ async function chooseProject(client, wanted, yes = false) {
567
+ const projects = await apiFetch(client, "/projects");
568
+ if (projects.length === 0) throw new Error("You have no project yet. Sign in to the web app once to create one.");
569
+ if (wanted) {
570
+ const hit = projects.find((p) => p.id === wanted || p.name.toLowerCase() === wanted.toLowerCase());
571
+ if (!hit) throw new Error(`No project matches "${wanted}". Yours: ${projects.map((p) => p.name).join(", ")}`);
572
+ return hit;
573
+ }
574
+ if (projects.length === 1 || yes) return projects[0];
575
+ console.log("\nWhich project should this repo belong to? (a project = the repos that share one memory)");
576
+ projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}${p.repoCount ? ` (${p.repoCount} repo${p.repoCount === 1 ? "" : "s"})` : ""}`));
577
+ const idx = Number(await ask("Number [1]: ")) || 1;
578
+ const pick = projects[idx - 1];
579
+ if (!pick) throw new Error("Invalid selection.");
580
+ return pick;
581
+ }
582
+ async function initCommand(opts) {
583
+ const creds = loadCredentials();
584
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
585
+ const client = { baseUrl: process.env.MEMMO_API_URL ?? creds.apiBaseUrl, token: creds.token };
586
+ const cwd = process.cwd();
587
+ if (!existsSync4(join4(cwd, ".git"))) {
588
+ console.log("note: no .git here \u2014 run `memmo init` at your repo root.");
589
+ }
590
+ const remote = detectGitRemote(cwd);
591
+ const fullName = remote ? repoFullNameFromRemote(remote) : null;
592
+ let repo = null;
593
+ if (opts.repo) {
594
+ const list = await apiFetch(client, "/repos");
595
+ repo = list.find((r) => r.id === opts.repo) ?? null;
596
+ if (!repo) throw new Error(`Repo ${opts.repo} not found in your account.`);
597
+ } else if (remote) {
598
+ try {
599
+ const r = await apiFetch(client, "/repos/resolve", {
600
+ method: "POST",
601
+ body: JSON.stringify({ repoRemote: remote })
602
+ });
603
+ repo = { id: r.repoId, fullName: r.fullName };
604
+ cacheRepo(remote, { repoId: repo.id, fullName: repo.fullName, apiBaseUrl: client.baseUrl });
605
+ console.log(`Already connected: ${repo.fullName}`);
606
+ } catch {
607
+ }
608
+ }
609
+ if (!repo) {
610
+ const name = fullName ?? await ask("Repo name (owner/repo): ");
611
+ if (!name.includes("/")) throw new Error("Expected owner/repo.");
612
+ const project = await chooseProject(client, opts.project, opts.yes);
613
+ if (!opts.yes) {
614
+ console.log(`
615
+ Add ${name} to project "${project.name}"?`);
616
+ console.log(` directory: ${cwd}`);
617
+ const ok = (await ask("Continue? (Y/n) ")).toLowerCase();
618
+ if (ok === "n" || ok === "no") {
619
+ console.log("Aborted \u2014 nothing written.");
620
+ return;
621
+ }
622
+ }
623
+ const created = await apiFetch(client, "/repos", {
624
+ method: "POST",
625
+ body: JSON.stringify({ projectId: project.id, provider: "github", name: name.split("/").pop(), fullName: name })
626
+ });
627
+ repo = { id: created.id, fullName: created.fullName };
628
+ if (remote) cacheRepo(remote, { repoId: repo.id, fullName: repo.fullName, apiBaseUrl: client.baseUrl });
629
+ console.log(`Connected ${repo.fullName} \u2192 project "${project.name}".`);
630
+ }
631
+ const actions = writeClaudeAssets(cwd);
632
+ for (const a of actions) console.log(" - " + a);
633
+ let doScan = opts.scan === true;
634
+ if (!doScan && !opts.yes) {
635
+ const ans = (await ask("\nScan this repo now to propose starter memories? (Y/n) ")).toLowerCase();
636
+ doScan = ans === "" || ans === "y" || ans === "yes";
637
+ }
638
+ if (doScan) {
639
+ try {
640
+ console.log("");
641
+ await scanCommand();
642
+ } catch (e) {
643
+ console.log(`Scan skipped: ${e instanceof Error ? e.message : String(e)}`);
644
+ console.log("Run it later with `memmo scan`.");
645
+ }
646
+ }
647
+ const appUrl = await appUrlFor(client);
648
+ console.log(`
649
+ Done. Open Claude Code in this repo \u2014 it gets a briefing at session start.`);
650
+ console.log(`Review proposed memories: ${appUrl}/inbox`);
651
+ console.log(`Teammates: commit the generated files; they only need \`npm i -g @memmo/cli && memmo login\`.`);
652
+ }
653
+ async function appUrlFor(client) {
654
+ try {
655
+ const c = await apiFetch(client, "/auth/config");
656
+ if (c.appUrl) return c.appUrl.replace(/\/$/, "");
657
+ } catch {
658
+ }
659
+ return "https://app.memmo.dev";
660
+ }
661
+
662
+ // src/commands/mcp.ts
663
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
664
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
665
+ import { z } from "zod";
666
+ init_api();
667
+ init_version();
668
+ async function mcpCommand(opts = {}) {
669
+ const creds = loadCredentials();
670
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
671
+ const resolved = await resolveProjectConfig(process.cwd(), { repoId: opts.repo, apiBaseUrl: opts.api });
672
+ if (!resolved) throw new Error(NOT_CONNECTED + " Or pass --repo <repoId>.");
673
+ const client = { baseUrl: resolved.apiBaseUrl, token: creds.token };
674
+ const config = { repoId: resolved.repoId };
675
+ const server = new McpServer({ name: "memmo", version: VERSION });
676
+ server.registerTool(
677
+ "search_memory",
678
+ {
679
+ title: "Search Memmo memory",
680
+ description: "Search approved operational memories for the connected repo (conventions, risks, commands, decisions).",
681
+ inputSchema: { query: z.string().describe("What to search for"), limit: z.number().optional() }
682
+ },
683
+ async ({ query, limit }) => {
684
+ const result = await apiFetch(client, "/mcp/search_memory", {
685
+ method: "POST",
686
+ body: JSON.stringify({ repoId: config.repoId, query: query ?? "", limit: limit ?? 10 })
687
+ });
688
+ const text = result.memories.length === 0 ? "No relevant approved memories found." : result.memories.map((m) => `- [${m.type}] ${m.title}${m.id ? ` (id: ${m.id})` : ""}
689
+ ${m.content}`).join("\n");
690
+ return { content: [{ type: "text", text }] };
691
+ }
692
+ );
693
+ server.registerTool(
694
+ "get_repo_context",
695
+ {
696
+ title: "Get Memmo repo context",
697
+ description: "Retrieve the stack, package manager, known risks/warnings, and recommended commands for the connected repo.",
698
+ inputSchema: {}
699
+ },
700
+ async () => {
701
+ const result = await apiFetch(client, "/mcp/get_repo_context", {
702
+ method: "POST",
703
+ body: JSON.stringify({ repoId: config.repoId })
704
+ });
705
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
706
+ }
707
+ );
708
+ server.registerTool(
709
+ "get_relevant_warnings",
710
+ {
711
+ title: "Get Memmo risk warnings for files",
712
+ description: "Before editing files, get risk/failure warnings that apply to them (past outages, gotchas). Call with the file paths you're about to modify.",
713
+ inputSchema: {
714
+ files: z.array(z.string()).describe("Repo-relative paths you're about to edit")
715
+ }
716
+ },
717
+ async ({ files }) => {
718
+ const result = await apiFetch(client, "/mcp/get_relevant_warnings", {
719
+ method: "POST",
720
+ body: JSON.stringify({ repoId: config.repoId, files })
721
+ });
722
+ const text = result.warnings.length === 0 ? "No known risks for these files." : result.warnings.map((w) => `\u26A0 [${w.type}] ${w.title}
723
+ ${w.content}`).join("\n");
724
+ return { content: [{ type: "text", text }] };
725
+ }
726
+ );
727
+ server.registerTool(
728
+ "record_session_summary",
729
+ {
730
+ title: "Record a Memmo session summary",
731
+ description: "Submit a summary of the work you just did so Memmo can propose new memories for review. Call this at the end of a meaningful task.",
732
+ inputSchema: {
733
+ task: z.string().optional().describe("What the task was"),
734
+ summary: z.string().optional().describe("What happened: decisions, findings, outcomes"),
735
+ filesChanged: z.array(z.string()).optional(),
736
+ commandsRun: z.array(z.string()).optional(),
737
+ errors: z.array(z.string()).optional()
738
+ }
739
+ },
740
+ async (args) => {
741
+ const result = await apiFetch(client, `/repos/${config.repoId}/sessions`, {
742
+ method: "POST",
743
+ body: JSON.stringify(args)
744
+ });
745
+ return {
746
+ content: [
747
+ {
748
+ type: "text",
749
+ text: `Session recorded. ${result.proposedCount} memory proposal(s) created for review in Memmo.`
750
+ }
751
+ ]
752
+ };
753
+ }
754
+ );
755
+ server.registerTool(
756
+ "propose_memories",
757
+ {
758
+ title: "Propose Memmo memories",
759
+ description: "Record durable, reusable knowledge you discovered (conventions, architecture, commands, risks, gotchas) as PROPOSED memories for human review. Use this to bootstrap a repo \u2014 read its key files (README, configs, manifests, schema, entry points) and propose memories \u2014 or any time you learn something a future agent should know. Keep each memory ATOMIC (one fact/rule/command/entity per memory \u2014 split multi-topic content) and CONCISE (1\u20133 sentences, ideally under ~280 characters); prefer many small memories over a few long ones. Add file globs in `paths` for risk/area-specific memories.",
760
+ inputSchema: {
761
+ memories: z.array(
762
+ z.object({
763
+ type: z.enum([
764
+ "project_rule",
765
+ "architecture",
766
+ "command",
767
+ "workflow",
768
+ "decision",
769
+ "failure",
770
+ "risk",
771
+ "dependency",
772
+ "testing",
773
+ "deployment",
774
+ "business_context"
775
+ ]),
776
+ title: z.string(),
777
+ content: z.string(),
778
+ confidence: z.number().min(0).max(1).optional(),
779
+ paths: z.array(z.string()).optional(),
780
+ evidence: z.string().optional(),
781
+ supersedes: z.string().optional().describe("Id of an existing memory this one corrects (from search_memory)")
782
+ })
783
+ )
784
+ }
785
+ },
786
+ async ({ memories }) => {
787
+ const result = await apiFetch(
788
+ client,
789
+ `/repos/${config.repoId}/proposals`,
790
+ { method: "POST", body: JSON.stringify({ memories }) }
791
+ );
792
+ return {
793
+ content: [
794
+ {
795
+ type: "text",
796
+ text: `Proposed ${result.proposedCount} memory(ies) for review in Memmo.`
797
+ }
798
+ ]
799
+ };
800
+ }
801
+ );
802
+ const transport = new StdioServerTransport();
803
+ await server.connect(transport);
804
+ }
805
+
806
+ // src/commands/hook.ts
807
+ import { readFileSync as readFileSync3, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "node:fs";
808
+ import { join as join5 } from "node:path";
809
+ import { tmpdir } from "node:os";
810
+ import { createHash } from "node:crypto";
811
+ init_api();
812
+ async function readStdin() {
813
+ if (process.stdin.isTTY) return "";
814
+ const chunks = [];
815
+ try {
816
+ for await (const c of process.stdin) chunks.push(c);
817
+ } catch {
818
+ return "";
819
+ }
820
+ return Buffer.concat(chunks).toString("utf8");
821
+ }
822
+ async function resolveHookEnv(cwd) {
823
+ const creds = loadCredentials();
824
+ if (!creds) return null;
825
+ const cfg = await resolveProjectConfig(cwd ?? process.cwd());
826
+ if (!cfg) return null;
827
+ return { client: { baseUrl: cfg.apiBaseUrl, token: creds.token }, repoId: cfg.repoId };
828
+ }
829
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
830
+ function uniq(arr) {
831
+ return [...new Set(arr.filter(Boolean))];
832
+ }
833
+ function capToBudget(lines, maxChars) {
834
+ const kept = [];
835
+ let used = 0;
836
+ for (const line2 of lines) {
837
+ if (used + line2.length > maxChars && kept.length > 0) break;
838
+ kept.push(line2);
839
+ used += line2.length;
840
+ }
841
+ return kept.join("\n\n");
842
+ }
843
+ async function sessionStart(input, env) {
844
+ const ctx = await apiFetch(env.client, "/mcp/get_repo_context", {
845
+ method: "POST",
846
+ body: JSON.stringify({ repoId: env.repoId, sessionId: input.session_id })
847
+ });
848
+ const lines = [];
849
+ const rc = ctx.repoContext;
850
+ if (rc?.stack?.length) lines.push(`Stack: ${rc.stack.join(", ")}${rc.packageManager ? ` (${rc.packageManager})` : ""}`);
851
+ if (rc?.notes) lines.push(`About: ${rc.notes}`);
852
+ const b = ctx.briefing;
853
+ const section = (title, items) => {
854
+ if (items?.length) lines.push(`${title}:
855
+ - ${items.join("\n- ")}`);
856
+ };
857
+ if (b) {
858
+ section("Project rules & decisions", b.rules);
859
+ section("Architecture", b.architecture);
860
+ section("Key commands", b.commands);
861
+ section("Known risks \u2014 check before touching related code", b.risks);
862
+ } else {
863
+ section("Key commands", ctx.recommendedCommands);
864
+ section("Known risks", ctx.warnings);
865
+ }
866
+ if (lines.length === 0) return;
867
+ const text = `Memmo \u2014 what this team already knows about this repo (approved memory; use search_memory for more):
868
+ ${lines.join("\n\n")}`;
869
+ process.stdout.write(
870
+ JSON.stringify({
871
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text }
872
+ })
873
+ );
874
+ }
875
+ async function userPrompt(input, env) {
876
+ const prompt = (input.prompt ?? "").trim();
877
+ if (prompt.length < 4) return;
878
+ const { memories } = await apiFetch(env.client, "/mcp/search_memory", {
879
+ method: "POST",
880
+ body: JSON.stringify({ repoId: env.repoId, query: prompt, limit: 4 })
881
+ });
882
+ if (!memories.length) return;
883
+ const body = memories.map((m) => `- [${m.type}] ${m.title}: ${m.content}`).join("\n");
884
+ const text = `Memmo memories relevant to this task:
885
+ ${body}`;
886
+ process.stdout.write(
887
+ JSON.stringify({
888
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: text }
889
+ })
890
+ );
891
+ }
892
+ async function preEdit(input, env) {
893
+ const ti = input.tool_input ?? {};
894
+ const file = ti.file_path ?? ti.notebook_path ?? ti.path;
895
+ if (!file) return;
896
+ const { warnings } = await apiFetch(env.client, "/mcp/get_relevant_warnings", {
897
+ method: "POST",
898
+ body: JSON.stringify({ repoId: env.repoId, files: [file], sessionId: input.session_id })
899
+ });
900
+ if (!warnings.length) return;
901
+ const key = createHash("sha1").update(`${input.session_id ?? "x"}:${file}`).digest("hex").slice(0, 16);
902
+ const marker = join5(tmpdir(), `memmo-warned-${key}`);
903
+ if (existsSync5(marker)) return;
904
+ try {
905
+ writeFileSync3(marker, "1");
906
+ } catch {
907
+ }
908
+ const body = warnings.map((w) => `\u26A0 [${w.type}] ${w.title}
909
+ ${w.content}`).join("\n");
910
+ process.stderr.write(`Memmo risk warnings for ${file}:
911
+ ${body}
912
+ `);
913
+ process.exit(2);
914
+ }
915
+ async function sessionEnd(input, env) {
916
+ if (!input.transcript_path || !existsSync5(input.transcript_path)) return;
917
+ const raw = readFileSync3(input.transcript_path, "utf8");
918
+ const filesChanged = [];
919
+ const commandsRun = [];
920
+ const errors = [];
921
+ const userTexts = [];
922
+ const agentTexts = [];
923
+ let task;
924
+ for (const line2 of raw.split("\n")) {
925
+ if (!line2.trim()) continue;
926
+ let entry;
927
+ try {
928
+ entry = JSON.parse(line2);
929
+ } catch {
930
+ continue;
931
+ }
932
+ const msg = entry.message;
933
+ const content = msg?.content;
934
+ if (!Array.isArray(content)) continue;
935
+ for (const item of content) {
936
+ if (item.type === "tool_use") {
937
+ const name = item.name;
938
+ const inp = item.input ?? {};
939
+ if (EDIT_TOOLS.has(name)) {
940
+ const f = inp.file_path ?? inp.notebook_path;
941
+ if (f) filesChanged.push(f);
942
+ } else if (name === "Bash" && typeof inp.command === "string") {
943
+ commandsRun.push(inp.command);
944
+ }
945
+ } else if (item.type === "tool_result" && item.is_error) {
946
+ const t = typeof item.content === "string" ? item.content : JSON.stringify(item.content);
947
+ if (t) errors.push(t.slice(0, 300));
948
+ } else if (item.type === "text" && typeof item.text === "string") {
949
+ const text = item.text.trim();
950
+ if (!text) continue;
951
+ if (msg?.role === "user") {
952
+ if (!task) task = text.slice(0, 280);
953
+ userTexts.push(text.slice(0, 400));
954
+ } else if (msg?.role === "assistant" && text.length > 40) {
955
+ agentTexts.push(text.slice(0, 600));
956
+ }
957
+ }
958
+ }
959
+ }
960
+ const files = uniq(filesChanged).slice(0, 50);
961
+ const cmds = uniq(commandsRun).slice(0, 50);
962
+ const errs = uniq(errors).slice(0, 20);
963
+ const userMessages = uniq(userTexts).slice(0, 15);
964
+ const reasoning = capToBudget(agentTexts.slice(-10), 3e3);
965
+ if (files.length === 0 && cmds.length === 0 && errs.length === 0) return;
966
+ const summary = `Claude Code session: ${files.length} file(s) edited, ${cmds.length} command(s) run${errs.length ? `, ${errs.length} error(s)` : ""}.`;
967
+ await apiFetch(env.client, `/repos/${env.repoId}/sessions`, {
968
+ method: "POST",
969
+ body: JSON.stringify({
970
+ agent: "claude-code",
971
+ sessionId: input.session_id,
972
+ task,
973
+ summary,
974
+ filesChanged: files,
975
+ commandsRun: cmds,
976
+ errors: errs,
977
+ userMessages,
978
+ reasoning
979
+ })
980
+ });
981
+ }
982
+ async function hookCommand(event) {
983
+ let input = {};
984
+ try {
985
+ const stdin = await readStdin();
986
+ if (stdin.trim()) input = JSON.parse(stdin);
987
+ } catch {
988
+ process.exit(0);
989
+ }
990
+ try {
991
+ const env = await resolveHookEnv(input.cwd);
992
+ if (!env) process.exit(0);
993
+ if (event === "session-start") await sessionStart(input, env);
994
+ else if (event === "user-prompt") await userPrompt(input, env);
995
+ else if (event === "pre-edit") await preEdit(input, env);
996
+ else if (event === "session-end") await sessionEnd(input, env);
997
+ } catch {
998
+ }
999
+ process.exit(0);
1000
+ }
1001
+
1002
+ // src/commands/status.ts
1003
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "node:fs";
1004
+ import { join as join6 } from "node:path";
1005
+ init_api();
1006
+ var OK = "\x1B[32m\u2713\x1B[0m";
1007
+ var NO = "\x1B[31m\u2717\x1B[0m";
1008
+ var WARN = "\x1B[33m!\x1B[0m";
1009
+ function line(mark, label, detail = "") {
1010
+ console.log(` ${mark} ${label}${detail ? ` \x1B[2m${detail}\x1B[0m` : ""}`);
1011
+ }
1012
+ function readJson(path) {
1013
+ try {
1014
+ return JSON.parse(readFileSync4(path, "utf8"));
1015
+ } catch {
1016
+ return null;
1017
+ }
1018
+ }
1019
+ function hasMemmoHooks(cwd) {
1020
+ const json = readJson(
1021
+ join6(cwd, ".claude", "settings.json")
1022
+ );
1023
+ if (!json?.hooks) return { present: false, events: [] };
1024
+ const events = Object.entries(json.hooks).filter(
1025
+ ([, list]) => list?.some((e) => e.hooks?.some((h) => h.command?.includes("memmo hook")))
1026
+ ).map(([event]) => event);
1027
+ return { present: events.length > 0, events };
1028
+ }
1029
+ async function statusCommand() {
1030
+ const cwd = process.cwd();
1031
+ console.log("Memmo setup status\n");
1032
+ const creds = loadCredentials();
1033
+ line(creds ? OK : NO, "Logged in", creds ? creds.apiBaseUrl : "run `memmo login`");
1034
+ const config = await resolveProjectConfig(cwd);
1035
+ line(
1036
+ config ? OK : NO,
1037
+ "Repo connected",
1038
+ config ? `${config.repoFullName ?? config.repoId}` : "run `memmo init`"
1039
+ );
1040
+ const mcp = readJson(join6(cwd, ".mcp.json"));
1041
+ line(mcp?.mcpServers?.memmo ? OK : NO, ".mcp.json (memmo MCP server)");
1042
+ const claudeMd = existsSync6(join6(cwd, "CLAUDE.md")) ? readFileSync4(join6(cwd, "CLAUDE.md"), "utf8") : "";
1043
+ line(claudeMd.includes("Memmo") ? OK : WARN, "CLAUDE.md guidance");
1044
+ const hooks = hasMemmoHooks(cwd);
1045
+ line(
1046
+ hooks.present ? OK : WARN,
1047
+ "Claude Code hooks",
1048
+ hooks.present ? hooks.events.join(", ") : "run `memmo claude install`"
1049
+ );
1050
+ if (creds && config) {
1051
+ const client = { baseUrl: config.apiBaseUrl ?? creds.apiBaseUrl, token: creds.token };
1052
+ try {
1053
+ const repo = await apiFetch(
1054
+ client,
1055
+ `/repos/${config.repoId}`
1056
+ );
1057
+ const approved = repo.memoryCounts?.find((c) => c.status === "approved")?._count ?? 0;
1058
+ const proposed = repo.memoryCounts?.find((c) => c.status === "proposed")?._count ?? 0;
1059
+ line(OK, "API reachable", `${approved} approved \xB7 ${proposed} proposed memories`);
1060
+ } catch (e) {
1061
+ line(NO, "API reachable", e instanceof Error ? e.message : "request failed");
1062
+ }
1063
+ }
1064
+ const ready = creds && config && mcp?.mcpServers?.memmo && hooks.present;
1065
+ console.log(
1066
+ ready ? "\n\x1B[32mMemmo is fully set up for this repo.\x1B[0m" : "\nSome steps are incomplete. Run `memmo init` to finish setup."
1067
+ );
1068
+ }
1069
+
1070
+ // src/commands/uninstall.ts
1071
+ import { existsSync as existsSync7, rmSync, readdirSync } from "node:fs";
1072
+ import { join as join7 } from "node:path";
1073
+ import { createInterface as createInterface3 } from "node:readline/promises";
1074
+ async function uninstallCommand(opts = {}) {
1075
+ const cwd = process.cwd();
1076
+ if (!opts.yes) {
1077
+ console.log("Remove Memmo from this repo?");
1078
+ console.log(` directory: ${cwd}`);
1079
+ console.log(" This removes the memmo MCP server, Claude Code hooks, the CLAUDE.md section,");
1080
+ console.log(" and any legacy .memmo/config.json. Your login (~/.memmo/credentials.json) is kept.");
1081
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
1082
+ const ok = (await rl.question("Continue? (y/N) ")).trim().toLowerCase();
1083
+ rl.close();
1084
+ if (ok !== "y" && ok !== "yes") {
1085
+ console.log("Aborted \u2014 nothing changed.");
1086
+ return;
1087
+ }
1088
+ }
1089
+ const actions = [];
1090
+ const push = (a) => {
1091
+ if (a) actions.push(a);
1092
+ };
1093
+ push(removeMcpServer(cwd));
1094
+ push(removeSettingsHooks(cwd));
1095
+ push(removeClaudeSection(cwd));
1096
+ const remote = detectGitRemote(cwd);
1097
+ if (remote) forgetRepo(remote);
1098
+ const configFile = join7(cwd, ".memmo", "config.json");
1099
+ if (existsSync7(configFile)) {
1100
+ rmSync(configFile, { force: true });
1101
+ actions.push("removed .memmo/config.json");
1102
+ const dir = join7(cwd, ".memmo");
1103
+ if (existsSync7(dir) && readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
1104
+ }
1105
+ for (const f of ["memmo-session-end.sh", "memmo-before-edit.sh"]) {
1106
+ const p = join7(cwd, ".claude", "hooks", f);
1107
+ if (existsSync7(p)) {
1108
+ rmSync(p, { force: true });
1109
+ actions.push(`removed legacy .claude/hooks/${f}`);
1110
+ }
1111
+ }
1112
+ if (actions.length === 0) {
1113
+ console.log("Nothing to remove \u2014 Memmo isn't set up in this repo.");
1114
+ return;
1115
+ }
1116
+ console.log("Removed Memmo from this repo:");
1117
+ for (const a of actions) console.log(" - " + a);
1118
+ console.log("\nRestart Claude Code so it stops loading the memmo MCP server and hooks.");
1119
+ }
1120
+
1121
+ // src/commands/chat.ts
1122
+ import { spawn as spawn2 } from "node:child_process";
1123
+ import { existsSync as existsSync8 } from "node:fs";
1124
+ import { join as join8, delimiter as delimiter2 } from "node:path";
1125
+ import { createInterface as createInterface4 } from "node:readline/promises";
1126
+ import Anthropic from "@anthropic-ai/sdk";
1127
+ init_api();
1128
+ var SYSTEM = `You are Memmo, answering questions about a software repository using ONLY the
1129
+ approved operational memories and repo context provided. Be concise and concrete, reference memory
1130
+ titles when relevant, and if the provided context doesn't answer the question, say so plainly
1131
+ instead of guessing.`;
1132
+ function findOnPath2(cmd) {
1133
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
1134
+ for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
1135
+ if (!dir) continue;
1136
+ for (const ext of exts) if (existsSync8(join8(dir, cmd + ext))) return join8(dir, cmd + ext);
1137
+ }
1138
+ return null;
1139
+ }
1140
+ async function buildContext(client, repoId, query) {
1141
+ const [search, ctx] = await Promise.all([
1142
+ apiFetch(client, "/mcp/search_memory", {
1143
+ method: "POST",
1144
+ body: JSON.stringify({ repoId, query, limit: 12 })
1145
+ }),
1146
+ apiFetch(client, "/mcp/get_repo_context", {
1147
+ method: "POST",
1148
+ body: JSON.stringify({ repoId })
1149
+ })
1150
+ ]);
1151
+ const parts = [];
1152
+ const rc = ctx.repoContext;
1153
+ if (rc.stack?.length) parts.push(`Stack: ${rc.stack.join(", ")}`);
1154
+ if (rc.packageManager) parts.push(`Package manager: ${rc.packageManager}`);
1155
+ if (rc.notes) parts.push(`Notes: ${rc.notes}`);
1156
+ if (ctx.recommendedCommands?.length) parts.push(`Key commands:
1157
+ - ${ctx.recommendedCommands.join("\n- ")}`);
1158
+ if (ctx.warnings?.length) parts.push(`Known risks:
1159
+ - ${ctx.warnings.join("\n- ")}`);
1160
+ const mems = search.memories.length === 0 ? "(no approved memories matched this question)" : search.memories.map((m) => `- [${m.type}] ${m.title}
1161
+ ${m.content}`).join("\n");
1162
+ return `Repo context:
1163
+ ${parts.join("\n") || "(none)"}
1164
+
1165
+ Relevant approved memories:
1166
+ ${mems}`;
1167
+ }
1168
+ async function answerWithKey(apiKey, context, question) {
1169
+ const client = new Anthropic({ apiKey });
1170
+ const model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6";
1171
+ const stream = client.messages.stream({
1172
+ model,
1173
+ max_tokens: 1024,
1174
+ system: SYSTEM,
1175
+ messages: [{ role: "user", content: `${context}
1176
+
1177
+ Question: ${question}` }]
1178
+ });
1179
+ stream.on("text", (t) => process.stdout.write(t));
1180
+ await stream.finalMessage();
1181
+ process.stdout.write("\n");
1182
+ }
1183
+ async function answerWithClaude(claudeBin, context, question) {
1184
+ const prompt = `${SYSTEM}
1185
+
1186
+ ${context}
1187
+
1188
+ Question: ${question}`;
1189
+ const env = { ...process.env };
1190
+ delete env.ANTHROPIC_API_KEY;
1191
+ const code = await new Promise((resolve, reject) => {
1192
+ const child = spawn2(claudeBin, ["-p", prompt], { stdio: ["ignore", "inherit", "inherit"], env });
1193
+ child.on("error", reject);
1194
+ child.on("close", (c) => resolve(c ?? 0));
1195
+ });
1196
+ if (code !== 0) {
1197
+ process.stderr.write(
1198
+ "\nClaude Code couldn't run headlessly here. Make sure it's logged in for terminal use:\n \u2022 run `claude`, then `/login` and finish the browser flow, and verify with `claude -p \"hi\"`\n \u2022 or set ANTHROPIC_API_KEY to use the API directly.\n"
1199
+ );
1200
+ }
1201
+ }
1202
+ async function chatCommand(questionArgs = []) {
1203
+ const creds = loadCredentials();
1204
+ const config = await resolveProjectConfig();
1205
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
1206
+ if (!config) throw new Error(NOT_CONNECTED);
1207
+ const client = { baseUrl: config.apiBaseUrl ?? creds.apiBaseUrl, token: creds.token };
1208
+ const envKey = process.env.ANTHROPIC_API_KEY;
1209
+ const claudeBin = envKey ? null : findOnPath2("claude");
1210
+ if (!envKey && !claudeBin) {
1211
+ throw new Error(
1212
+ "No Anthropic access found. Set ANTHROPIC_API_KEY (your own key), or install Claude Code (https://claude.com/claude-code) to use your subscription."
1213
+ );
1214
+ }
1215
+ const answer = async (q) => {
1216
+ const context = await buildContext(client, config.repoId, q);
1217
+ if (envKey) await answerWithKey(envKey, context, q);
1218
+ else await answerWithClaude(claudeBin, context, q);
1219
+ };
1220
+ const via = envKey ? "your ANTHROPIC_API_KEY" : "your Claude Code subscription";
1221
+ const oneShot = questionArgs.join(" ").trim();
1222
+ if (oneShot) {
1223
+ await answer(oneShot);
1224
+ return;
1225
+ }
1226
+ console.log(`Memmo chat \u2014 grounded in ${config.repoFullName ?? config.repoId} (via ${via}).`);
1227
+ console.log("Ask a question, or type 'exit' to quit.\n");
1228
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
1229
+ try {
1230
+ for (; ; ) {
1231
+ const q = (await rl.question("you \u203A ")).trim();
1232
+ if (!q) continue;
1233
+ if (q === "exit" || q === "quit") break;
1234
+ process.stdout.write("\nmemmo \u203A ");
1235
+ await answer(q);
1236
+ process.stdout.write("\n");
1237
+ }
1238
+ } finally {
1239
+ rl.close();
1240
+ }
1241
+ }
1242
+
1243
+ // src/commands/decision.ts
1244
+ init_api();
1245
+ async function decisionCommand(text, opts = {}) {
1246
+ const creds = loadCredentials();
1247
+ const config = await resolveProjectConfig();
1248
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
1249
+ if (!config) throw new Error(NOT_CONNECTED);
1250
+ const title = text.join(" ").trim();
1251
+ if (!title) {
1252
+ throw new Error('Usage: memmo decision "what changed" [--why "the reason"]');
1253
+ }
1254
+ const client = {
1255
+ baseUrl: config.apiBaseUrl ?? creds.apiBaseUrl,
1256
+ token: creds.token
1257
+ };
1258
+ const content = opts.why ? `${title}
1259
+
1260
+ Why: ${opts.why}` : title;
1261
+ await apiFetch(client, `/repos/${config.repoId}/memories`, {
1262
+ method: "POST",
1263
+ body: JSON.stringify({
1264
+ type: "decision",
1265
+ title: title.slice(0, 140),
1266
+ content,
1267
+ status: "approved",
1268
+ source: "cli_decision",
1269
+ confidence: 0.9
1270
+ })
1271
+ });
1272
+ console.log(`Recorded decision: ${title}`);
1273
+ }
1274
+
1275
+ // src/commands/sync.ts
1276
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync9 } from "node:fs";
1277
+ import { join as join9 } from "node:path";
1278
+ init_api();
1279
+ var START = "<!-- memmo:memory:start -->";
1280
+ var END = "<!-- memmo:memory:end -->";
1281
+ function renderBlock(memories) {
1282
+ const lines = [START, "", "## Memmo memory (auto-synced \u2014 do not edit by hand)", ""];
1283
+ if (memories.length === 0) {
1284
+ lines.push("_No approved memories yet. Approve some in the Memmo inbox._");
1285
+ } else {
1286
+ const byType = /* @__PURE__ */ new Map();
1287
+ for (const m of memories) {
1288
+ const arr = byType.get(m.type) ?? [];
1289
+ arr.push(m);
1290
+ byType.set(m.type, arr);
1291
+ }
1292
+ for (const [type, items] of byType) {
1293
+ lines.push(`### ${type}`, "");
1294
+ for (const m of items) {
1295
+ lines.push(`- **${m.title}** \u2014 ${m.content}`);
1296
+ if (m.paths?.length) lines.push(` - paths: ${m.paths.join(", ")}`);
1297
+ }
1298
+ lines.push("");
1299
+ }
1300
+ }
1301
+ lines.push(`_Last synced ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}._`, "", END);
1302
+ return lines.join("\n");
1303
+ }
1304
+ function upsertBlock(existing, block) {
1305
+ const s = existing.indexOf(START);
1306
+ const e = existing.indexOf(END);
1307
+ if (s !== -1 && e !== -1 && e > s) {
1308
+ return existing.slice(0, s) + block + existing.slice(e + END.length);
1309
+ }
1310
+ return existing.trimEnd() + "\n\n" + block + "\n";
1311
+ }
1312
+ async function syncCommand() {
1313
+ const creds = loadCredentials();
1314
+ const config = await resolveProjectConfig();
1315
+ if (!creds) throw new Error("Not logged in. Run `memmo login` first.");
1316
+ if (!config) throw new Error(NOT_CONNECTED);
1317
+ const client = {
1318
+ baseUrl: config.apiBaseUrl ?? creds.apiBaseUrl,
1319
+ token: creds.token
1320
+ };
1321
+ const memories = await apiFetch(
1322
+ client,
1323
+ `/repos/${config.repoId}/memories?status=approved`
1324
+ );
1325
+ const block = renderBlock(memories);
1326
+ const path = join9(process.cwd(), "CLAUDE.md");
1327
+ const existing = existsSync9(path) ? readFileSync5(path, "utf8") : "";
1328
+ writeFileSync4(path, upsertBlock(existing, block));
1329
+ console.log(`Synced ${memories.length} approved memory(ies) into CLAUDE.md.`);
1330
+ }
1331
+
1332
+ // src/commands/review.ts
1333
+ import { execFileSync } from "node:child_process";
1334
+ import { writeFileSync as writeFileSync5 } from "node:fs";
1335
+ init_api();
1336
+
1337
+ // ../../packages/shared/src/types.ts
1338
+ var MEMORY_TYPES = [
1339
+ "project_rule",
1340
+ "architecture",
1341
+ "command",
1342
+ "workflow",
1343
+ "decision",
1344
+ "failure",
1345
+ "risk",
1346
+ "dependency",
1347
+ "testing",
1348
+ "deployment",
1349
+ "business_context"
1350
+ ];
1351
+ var MEMORY_STATUSES = [
1352
+ "proposed",
1353
+ "approved",
1354
+ "rejected",
1355
+ "archived"
1356
+ ];
1357
+
1358
+ // ../../packages/shared/src/schemas.ts
1359
+ import { z as z2 } from "zod";
1360
+ var memoryTypeSchema = z2.enum(MEMORY_TYPES);
1361
+ var memoryStatusSchema = z2.enum(MEMORY_STATUSES);
1362
+ var loginSchema = z2.object({
1363
+ email: z2.string().email(),
1364
+ name: z2.string().min(1).optional()
1365
+ });
1366
+ var waitlistSchema = z2.object({
1367
+ email: z2.string().email(),
1368
+ source: z2.string().max(120).optional()
1369
+ });
1370
+ var TOKEN_SCOPES = ["cli", "mcp", "both"];
1371
+ var tokenScopeSchema = z2.enum(TOKEN_SCOPES);
1372
+ var moveProjectSchema = z2.object({ organizationId: z2.string().min(1) });
1373
+ var createTokenSchema = z2.object({
1374
+ name: z2.string().min(1).max(80).default("cli"),
1375
+ scope: tokenScopeSchema.default("both"),
1376
+ // When set, the token is bound to this project (project); the CLI/MCP can
1377
+ // only reach that project's repos. Omit for an account-wide token.
1378
+ projectId: z2.string().optional()
1379
+ });
1380
+ var ORG_ROLES = ["owner", "admin", "member"];
1381
+ var orgRoleSchema = z2.enum(ORG_ROLES);
1382
+ var createOrgSchema = z2.object({ name: z2.string().min(1).max(80) });
1383
+ var updateOrgSchema = z2.object({ name: z2.string().min(1).max(80) });
1384
+ var joinOrgSchema = z2.object({ joinCode: z2.string().min(1) });
1385
+ var inviteOrgMemberSchema = z2.object({
1386
+ email: z2.string().email(),
1387
+ role: orgRoleSchema.default("member")
1388
+ });
1389
+ var setOrgRoleSchema = z2.object({ role: orgRoleSchema });
1390
+ var createProjectSchema = z2.object({
1391
+ name: z2.string().min(1),
1392
+ slug: z2.string().min(1).regex(/^[a-z0-9-]+$/, "slug must be lowercase alphanumeric with dashes")
1393
+ });
1394
+ var joinProjectSchema = z2.object({
1395
+ joinCode: z2.string().min(1)
1396
+ });
1397
+ var inviteMemberSchema = z2.object({
1398
+ email: z2.string().email(),
1399
+ role: z2.enum(["admin", "member", "viewer"]).default("member")
1400
+ });
1401
+ var setMemberRoleSchema = z2.object({
1402
+ role: z2.enum(["owner", "admin", "member", "viewer"])
1403
+ });
1404
+ var assignMemberSchema = z2.object({
1405
+ userId: z2.string().min(1)
1406
+ });
1407
+ var updateProjectSchema = z2.object({
1408
+ name: z2.string().min(1).max(80).optional(),
1409
+ // null disables; a value in [0,1] auto-approves proposals at/above it.
1410
+ autoApproveThreshold: z2.number().min(0).max(1).nullable().optional(),
1411
+ // null disables; a value in [0,1] auto-rejects proposals below it.
1412
+ autoRejectThreshold: z2.number().min(0).max(1).nullable().optional()
1413
+ });
1414
+ var chatSchema = z2.object({
1415
+ message: z2.string().min(1).max(2e3)
1416
+ });
1417
+ var createRepoSchema = z2.object({
1418
+ projectId: z2.string().min(1),
1419
+ provider: z2.string().default("github"),
1420
+ name: z2.string().min(1),
1421
+ fullName: z2.string().min(1),
1422
+ defaultBranch: z2.string().optional()
1423
+ });
1424
+ var updateRepoSchema = z2.object({
1425
+ name: z2.string().min(1).optional(),
1426
+ stack: z2.array(z2.string().min(1)).max(40).optional(),
1427
+ packageManager: z2.string().max(40).optional(),
1428
+ notes: z2.string().max(5e3).optional(),
1429
+ defaultBranch: z2.string().max(120).optional(),
1430
+ reviewerEnabled: z2.boolean().optional(),
1431
+ reviewerInstructions: z2.string().max(4e3).optional()
1432
+ });
1433
+ var reviewPrSchema = z2.object({
1434
+ prNumber: z2.number().int().positive(),
1435
+ post: z2.boolean().optional()
1436
+ });
1437
+ var reviewDiffSchema = z2.object({
1438
+ prTitle: z2.string().min(1).max(500),
1439
+ prBody: z2.string().max(2e4).optional(),
1440
+ diff: z2.string().min(1).max(4e5)
1441
+ });
1442
+ var PLANS = ["free", "scale", "enterprise"];
1443
+ var planSchema = z2.enum(PLANS);
1444
+ var PLAN_SOURCES = ["none", "stripe", "comp", "manual"];
1445
+ var planSourceSchema = z2.enum(PLAN_SOURCES);
1446
+ var billingCheckoutSchema = z2.object({
1447
+ plan: planSchema
1448
+ });
1449
+ var requestUpgradeSchema = z2.object({
1450
+ plan: planSchema,
1451
+ note: z2.string().max(500).optional()
1452
+ });
1453
+ var setPlanSchema = z2.object({
1454
+ plan: planSchema,
1455
+ source: planSourceSchema.default("manual"),
1456
+ status: z2.enum(["active", "past_due", "canceled"]).default("active"),
1457
+ note: z2.string().max(500).optional()
1458
+ });
1459
+ var reviewFeedbackValueSchema = z2.enum(["pending", "accepted", "dismissed"]);
1460
+ var reviewFeedbackSchema = z2.object({
1461
+ feedback: reviewFeedbackValueSchema
1462
+ });
1463
+ var reviewFeedbackBulkSchema = z2.object({
1464
+ items: z2.array(z2.object({ key: z2.string().min(1), feedback: reviewFeedbackValueSchema })).min(1).max(200)
1465
+ });
1466
+ var reviewerSkillSchema = z2.object({
1467
+ name: z2.string().min(1).max(120),
1468
+ instructions: z2.string().min(1).max(8e3),
1469
+ paths: z2.array(z2.string().min(1)).max(40).optional()
1470
+ });
1471
+ var updateReviewerSkillSchema = reviewerSkillSchema.partial();
1472
+ var setRepoSkillsSchema = z2.object({
1473
+ skillIds: z2.array(z2.string().min(1)).max(50)
1474
+ });
1475
+ var memoryEvidenceSchema = z2.object({
1476
+ kind: z2.string().min(1),
1477
+ content: z2.string().min(1),
1478
+ url: z2.string().url().optional()
1479
+ });
1480
+ var createMemorySchema = z2.object({
1481
+ type: memoryTypeSchema,
1482
+ title: z2.string().min(1),
1483
+ content: z2.string().min(1),
1484
+ paths: z2.array(z2.string().min(1)).max(50).optional(),
1485
+ // Repos the memory applies to. Omitted on /repos/:id/memories = that repo;
1486
+ // omitted on /projects/:id/memories = every repo in the project.
1487
+ repoIds: z2.array(z2.string().min(1)).max(50).optional(),
1488
+ confidence: z2.number().min(0).max(1).default(0.7),
1489
+ status: memoryStatusSchema.default("proposed"),
1490
+ source: z2.string().optional(),
1491
+ evidence: z2.array(memoryEvidenceSchema).optional()
1492
+ });
1493
+ var updateMemorySchema = z2.object({
1494
+ type: memoryTypeSchema.optional(),
1495
+ title: z2.string().min(1).optional(),
1496
+ content: z2.string().min(1).optional(),
1497
+ paths: z2.array(z2.string().min(1)).max(50).optional(),
1498
+ repoIds: z2.array(z2.string().min(1)).max(50).optional(),
1499
+ confidence: z2.number().min(0).max(1).optional(),
1500
+ status: memoryStatusSchema.optional()
1501
+ });
1502
+ var memoryListQuerySchema = z2.object({
1503
+ status: memoryStatusSchema.optional(),
1504
+ type: memoryTypeSchema.optional(),
1505
+ search: z2.string().optional()
1506
+ });
1507
+ var sessionEventSchema = z2.object({
1508
+ type: z2.string().min(1),
1509
+ payload: z2.record(z2.unknown()).default({}),
1510
+ // Optional span fields — render a session's events as a timeline.
1511
+ name: z2.string().max(200).optional(),
1512
+ durationMs: z2.number().int().min(0).optional(),
1513
+ status: z2.enum(["ok", "error", "warning"]).optional()
1514
+ });
1515
+ var recordSessionSchema = z2.object({
1516
+ agent: z2.string().default("claude-code"),
1517
+ sessionId: z2.string().optional(),
1518
+ task: z2.string().optional(),
1519
+ summary: z2.string().optional(),
1520
+ filesChanged: z2.array(z2.string()).optional(),
1521
+ commandsRun: z2.array(z2.string()).optional(),
1522
+ errors: z2.array(z2.string()).optional(),
1523
+ // The reasoning arc — the human's steering (instructions + corrections) and a
1524
+ // digest of the agent's reasoning — so the extractor captures the *why*, not
1525
+ // just the mechanics.
1526
+ userMessages: z2.array(z2.string()).optional(),
1527
+ reasoning: z2.string().optional(),
1528
+ events: z2.array(sessionEventSchema).optional()
1529
+ });
1530
+ var extractedMemorySchema = z2.object({
1531
+ type: memoryTypeSchema,
1532
+ title: z2.string().min(1).max(140),
1533
+ content: z2.string().min(1),
1534
+ confidence: z2.number().min(0).max(1),
1535
+ paths: z2.array(z2.string().min(1)).max(50).optional(),
1536
+ evidence: z2.string().optional(),
1537
+ // Id of an existing memory this one corrects; approving it archives the old one.
1538
+ supersedes: z2.string().optional()
1539
+ });
1540
+ var extractedMemoriesSchema = z2.array(extractedMemorySchema).max(20);
1541
+ var proposedMemoryInputSchema = z2.object({
1542
+ type: memoryTypeSchema,
1543
+ title: z2.string().min(1).max(140),
1544
+ content: z2.string().min(1),
1545
+ confidence: z2.number().min(0).max(1).default(0.6),
1546
+ paths: z2.array(z2.string().min(1)).max(50).optional(),
1547
+ evidence: z2.string().optional(),
1548
+ supersedes: z2.string().optional()
1549
+ });
1550
+ var proposeMemoriesSchema = z2.object({
1551
+ memories: z2.array(proposedMemoryInputSchema).min(1).max(20)
1552
+ });
1553
+ var mcpSearchMemorySchema = z2.object({
1554
+ repoId: z2.string().min(1),
1555
+ query: z2.string().default(""),
1556
+ limit: z2.number().int().min(1).max(50).default(10)
1557
+ });
1558
+ var mcpRepoContextSchema = z2.object({
1559
+ repoId: z2.string().min(1),
1560
+ sessionId: z2.string().optional()
1561
+ });
1562
+ var mcpRelevantWarningsSchema = z2.object({
1563
+ repoId: z2.string().min(1),
1564
+ files: z2.array(z2.string().min(1)).min(1).max(100),
1565
+ sessionId: z2.string().optional()
1566
+ });
1567
+ var setLlmSchema = z2.object({
1568
+ provider: z2.enum(["anthropic", "openai", "google", "custom"]),
1569
+ key: z2.string().min(1, "Missing key"),
1570
+ model: z2.string().trim().min(1).optional(),
1571
+ baseUrl: z2.string().url("Base URL must be a valid URL").optional()
1572
+ }).refine((v) => v.provider !== "custom" || !!v.baseUrl && !!v.model, {
1573
+ message: "A custom provider needs both a base URL and a model.",
1574
+ path: ["baseUrl"]
1575
+ });
1576
+ var testLlmSchema = z2.object({
1577
+ provider: z2.enum(["anthropic", "openai", "google", "custom"]).optional(),
1578
+ key: z2.string().min(1).optional(),
1579
+ model: z2.string().trim().min(1).optional(),
1580
+ baseUrl: z2.string().url("Base URL must be a valid URL").optional()
1581
+ });
1582
+
1583
+ // ../../packages/shared/src/review.ts
1584
+ function slug(s) {
1585
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1586
+ }
1587
+ function findingKey(f) {
1588
+ return `${f.path ?? ""}:${f.line ?? ""}:${slug(f.title)}`;
1589
+ }
1590
+
1591
+ // src/github-review.ts
1592
+ var SEVERITY_LABEL = {
1593
+ blocker: "\u{1F534} Blocker",
1594
+ warning: "\u{1F7E1} Warning",
1595
+ nit: "\u{1F535} Nit",
1596
+ praise: "\u{1F7E2} Praise"
1597
+ };
1598
+ function parseDiffNewLines(diff) {
1599
+ const out = /* @__PURE__ */ new Map();
1600
+ let path = null;
1601
+ let newLine = 0;
1602
+ let inHunk = false;
1603
+ for (const raw of diff.split("\n")) {
1604
+ if (raw.startsWith("+++ ")) {
1605
+ const p = raw.slice(4).replace(/^b\//, "");
1606
+ path = p === "/dev/null" ? null : p;
1607
+ if (path && !out.has(path)) out.set(path, /* @__PURE__ */ new Set());
1608
+ inHunk = false;
1609
+ continue;
1610
+ }
1611
+ const hunk = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1612
+ if (hunk) {
1613
+ newLine = parseInt(hunk[1], 10);
1614
+ inHunk = true;
1615
+ continue;
1616
+ }
1617
+ if (!inHunk || !path) continue;
1618
+ if (raw.startsWith("-")) continue;
1619
+ if (raw.startsWith("+") || raw.startsWith(" ")) {
1620
+ out.get(path).add(newLine);
1621
+ newLine++;
1622
+ } else if (raw.startsWith("\\")) {
1623
+ } else {
1624
+ inHunk = false;
1625
+ }
1626
+ }
1627
+ return out;
1628
+ }
1629
+ var MARKER_RE = /<!-- memmo-review:(.+?) -->/g;
1630
+ function extractMarkers(bodies) {
1631
+ const keys = /* @__PURE__ */ new Set();
1632
+ for (const b of bodies) {
1633
+ for (const m of b.matchAll(MARKER_RE)) keys.add(m[1]);
1634
+ }
1635
+ return keys;
1636
+ }
1637
+ function findingLine(f) {
1638
+ const mem = f.memory ? ` _(memory: ${f.memory})_` : "";
1639
+ return `**${SEVERITY_LABEL[f.severity]}** ${f.title}${mem}
1640
+
1641
+ ${f.detail}`;
1642
+ }
1643
+ function buildReviewPayload(summary, findings, commentable, existingKeys = /* @__PURE__ */ new Set()) {
1644
+ const fresh = findings.filter((f) => !existingKeys.has(findingKey(f)));
1645
+ const comments = [];
1646
+ const leftover = [];
1647
+ for (const f of fresh) {
1648
+ if (f.path && f.line && commentable.get(f.path)?.has(f.line)) {
1649
+ comments.push({
1650
+ path: f.path,
1651
+ line: f.line,
1652
+ side: "RIGHT",
1653
+ body: `${findingLine(f)}
1654
+
1655
+ <!-- memmo-review:${findingKey(f)} -->`
1656
+ });
1657
+ } else {
1658
+ leftover.push(f);
1659
+ }
1660
+ }
1661
+ const bodyParts = ["### \u{1F9E0} Memmo Reviewer", "", summary || "_No summary._"];
1662
+ if (leftover.length > 0) {
1663
+ bodyParts.push("");
1664
+ for (const f of leftover) {
1665
+ const where = f.path ? ` \`${f.path}${f.line ? `:${f.line}` : ""}\`` : "";
1666
+ const mem = f.memory ? ` _(memory: ${f.memory})_` : "";
1667
+ bodyParts.push(`- **${SEVERITY_LABEL[f.severity]}**${where}: ${f.title}${mem}`);
1668
+ if (f.detail) bodyParts.push(` ${f.detail}`);
1669
+ bodyParts.push(` <!-- memmo-review:${findingKey(f)} -->`);
1670
+ }
1671
+ }
1672
+ bodyParts.push("", "_Grounded in this repo's approved Memmo memories._");
1673
+ return { body: bodyParts.join("\n"), event: "COMMENT", comments, newCount: fresh.length };
1674
+ }
1675
+
1676
+ // src/github.ts
1677
+ import { readFileSync as readFileSync6, existsSync as existsSync10 } from "node:fs";
1678
+ function readEvent() {
1679
+ const p = process.env.GITHUB_EVENT_PATH;
1680
+ if (!p || !existsSync10(p)) return {};
1681
+ try {
1682
+ const ev = JSON.parse(readFileSync6(p, "utf8"));
1683
+ return {
1684
+ title: ev.pull_request?.title,
1685
+ body: ev.pull_request?.body ?? void 0,
1686
+ prNumber: ev.pull_request?.number ?? ev.number
1687
+ };
1688
+ } catch {
1689
+ return {};
1690
+ }
1691
+ }
1692
+ function ghContext(prFromEvent) {
1693
+ const repo = process.env.GITHUB_REPOSITORY;
1694
+ const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
1695
+ let pr = prFromEvent;
1696
+ if (!pr) {
1697
+ const m = (process.env.GITHUB_REF ?? "").match(/refs\/pull\/(\d+)\//);
1698
+ if (m) pr = parseInt(m[1], 10);
1699
+ }
1700
+ if (!repo || !token || !pr) return null;
1701
+ return { repo, pr, token };
1702
+ }
1703
+ async function gh(ctx, path, init = {}) {
1704
+ const res = await fetch(`https://api.github.com${path}`, {
1705
+ ...init,
1706
+ headers: {
1707
+ authorization: `Bearer ${ctx.token}`,
1708
+ accept: "application/vnd.github+json",
1709
+ "user-agent": "memmo",
1710
+ ...init.body ? { "content-type": "application/json" } : {}
1711
+ }
1712
+ });
1713
+ const text = await res.text();
1714
+ if (!res.ok) throw new Error(`GitHub ${res.status}: ${text.slice(0, 300)}`);
1715
+ return text ? JSON.parse(text) : null;
1716
+ }
1717
+
1718
+ // src/commands/review.ts
1719
+ function git(args) {
1720
+ return execFileSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
1721
+ }
1722
+ function computeDiff(base) {
1723
+ for (const ref of [`origin/${base}`, base]) {
1724
+ try {
1725
+ git(["rev-parse", "--verify", "--quiet", ref]);
1726
+ return git(["diff", `${ref}...HEAD`]);
1727
+ } catch {
1728
+ }
1729
+ }
1730
+ try {
1731
+ return git(["diff", "HEAD~1...HEAD"]);
1732
+ } catch {
1733
+ return "";
1734
+ }
1735
+ }
1736
+ async function postInlineReview(ctx, diff, review) {
1737
+ const commentable = parseDiffNewLines(diff);
1738
+ const existing = await gh(
1739
+ ctx,
1740
+ `/repos/${ctx.repo}/pulls/${ctx.pr}/comments?per_page=100`
1741
+ ).catch(() => []);
1742
+ const existingKeys = extractMarkers(existing.map((c) => c.body ?? ""));
1743
+ const payload = buildReviewPayload(review.summary, review.findings, commentable, existingKeys);
1744
+ if (payload.newCount === 0 && existingKeys.size > 0) {
1745
+ console.error("Memmo: no new findings since the last review \u2014 nothing to post.");
1746
+ return;
1747
+ }
1748
+ await gh(ctx, `/repos/${ctx.repo}/pulls/${ctx.pr}/reviews`, {
1749
+ method: "POST",
1750
+ body: JSON.stringify({ body: payload.body, event: payload.event, comments: payload.comments })
1751
+ });
1752
+ console.error(
1753
+ `Memmo: posted review (${payload.comments.length} inline, ${payload.newCount} new findings).`
1754
+ );
1755
+ }
1756
+ async function reviewCommand(opts = {}) {
1757
+ const config = await resolveProjectConfig();
1758
+ if (!config) {
1759
+ throw new Error(NOT_CONNECTED);
1760
+ }
1761
+ const creds = loadCredentials();
1762
+ const token = process.env.MEMMO_TOKEN ?? creds?.token;
1763
+ if (!token) throw new Error("No API token. Set MEMMO_TOKEN (CI) or run `memmo login`.");
1764
+ const baseUrl = opts.api ?? process.env.MEMMO_API_URL ?? config.apiBaseUrl ?? creds?.apiBaseUrl;
1765
+ if (!baseUrl) throw new Error("No API base URL. Set MEMMO_API_URL or pass --api.");
1766
+ const client = { baseUrl, token };
1767
+ const base = opts.base ?? process.env.GITHUB_BASE_REF ?? "main";
1768
+ const diff = computeDiff(base).slice(0, 38e4);
1769
+ if (!diff.trim()) {
1770
+ console.error(`No diff detected against base "${base}" \u2014 nothing to review.`);
1771
+ return;
1772
+ }
1773
+ const ev = readEvent();
1774
+ const prTitle = (opts.title ?? ev.title ?? git(["log", "-1", "--pretty=%s"]).trim()).slice(0, 500);
1775
+ const prBody = opts.body ?? ev.body;
1776
+ const res = await apiFetch(client, `/repos/${config.repoId}/review-diff`, {
1777
+ method: "POST",
1778
+ body: JSON.stringify({ prTitle, prBody, diff })
1779
+ });
1780
+ if (res.skipped) {
1781
+ console.error("Memmo reviewer is disabled for this repo \u2014 skipping (enable it in the dashboard).");
1782
+ return;
1783
+ }
1784
+ const comment = res.comment ?? "";
1785
+ if (opts.post) {
1786
+ const ctx = ghContext(ev.prNumber);
1787
+ if (!ctx) {
1788
+ console.error(
1789
+ "--post needs GitHub Actions context (GITHUB_REPOSITORY, a PR event, and GH_TOKEN/GITHUB_TOKEN). Falling back to markdown output."
1790
+ );
1791
+ } else if (res.review) {
1792
+ try {
1793
+ await postInlineReview(ctx, diff, res.review);
1794
+ return;
1795
+ } catch (e) {
1796
+ console.error(
1797
+ `Memmo: inline review failed (${e.message}); posting a summary comment instead.`
1798
+ );
1799
+ await gh(ctx, `/repos/${ctx.repo}/issues/${ctx.pr}/comments`, {
1800
+ method: "POST",
1801
+ body: JSON.stringify({ body: comment })
1802
+ });
1803
+ return;
1804
+ }
1805
+ }
1806
+ }
1807
+ if (opts.out) {
1808
+ writeFileSync5(opts.out, comment);
1809
+ console.error(`Wrote review to ${opts.out} (${res.review?.findings.length ?? 0} findings).`);
1810
+ }
1811
+ process.stdout.write(comment + "\n");
1812
+ }
1813
+
1814
+ // src/commands/review-sync.ts
1815
+ init_api();
1816
+ function verdictFromReactions(reactions) {
1817
+ let up = false;
1818
+ let down = false;
1819
+ for (const r of reactions) {
1820
+ if (r.content === "+1") up = true;
1821
+ else if (r.content === "-1") down = true;
1822
+ }
1823
+ if (up) return "accepted";
1824
+ if (down) return "dismissed";
1825
+ return null;
1826
+ }
1827
+ async function reviewSyncCommand(opts = {}) {
1828
+ const config = await resolveProjectConfig();
1829
+ if (!config) {
1830
+ throw new Error(NOT_CONNECTED);
1831
+ }
1832
+ const creds = loadCredentials();
1833
+ const token = process.env.MEMMO_TOKEN ?? creds?.token;
1834
+ if (!token) throw new Error("No API token. Set MEMMO_TOKEN (CI) or run `memmo login`.");
1835
+ const baseUrl = opts.api ?? process.env.MEMMO_API_URL ?? config.apiBaseUrl ?? creds?.apiBaseUrl;
1836
+ if (!baseUrl) throw new Error("No API base URL. Set MEMMO_API_URL or pass --api.");
1837
+ const client = { baseUrl, token };
1838
+ const ev = readEvent();
1839
+ const ctx = ghContext(ev.prNumber);
1840
+ if (!ctx) {
1841
+ throw new Error(
1842
+ "review-sync needs GitHub Actions context (GITHUB_REPOSITORY, a PR event or refs/pull/<n>, and GH_TOKEN/GITHUB_TOKEN)."
1843
+ );
1844
+ }
1845
+ const comments = await listReviewComments(ctx);
1846
+ const items = [];
1847
+ for (const c of comments) {
1848
+ const keys = extractMarkers([c.body ?? ""]);
1849
+ if (keys.size === 0) continue;
1850
+ const reactions = await gh(
1851
+ ctx,
1852
+ `/repos/${ctx.repo}/pulls/comments/${c.id}/reactions?per_page=100`
1853
+ ).catch(() => []);
1854
+ const feedback = verdictFromReactions(reactions);
1855
+ if (!feedback) continue;
1856
+ for (const key of keys) items.push({ key, feedback });
1857
+ }
1858
+ if (items.length === 0) {
1859
+ console.error("Memmo: no \u{1F44D}/\u{1F44E} reactions on Memmo review comments \u2014 nothing to sync.");
1860
+ return;
1861
+ }
1862
+ const res = await apiFetch(
1863
+ client,
1864
+ `/repos/${config.repoId}/review-feedback`,
1865
+ { method: "POST", body: JSON.stringify({ items }) }
1866
+ );
1867
+ const accepted = items.filter((i) => i.feedback === "accepted").length;
1868
+ const dismissed = items.length - accepted;
1869
+ console.error(
1870
+ `Memmo: synced ${res.updated}/${items.length} finding(s) (${accepted} accepted, ${dismissed} dismissed).`
1871
+ );
1872
+ }
1873
+ async function listReviewComments(ctx) {
1874
+ return gh(ctx, `/repos/${ctx.repo}/pulls/${ctx.pr}/comments?per_page=100`).catch(
1875
+ () => []
1876
+ );
1877
+ }
1878
+
1879
+ // src/commands/ci.ts
1880
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync11 } from "node:fs";
1881
+ import { join as join10 } from "node:path";
1882
+ var WORKFLOW = `name: Memmo Review
1883
+ on:
1884
+ pull_request:
1885
+ types: [opened, synchronize, reopened, ready_for_review]
1886
+ permissions:
1887
+ contents: read
1888
+ pull-requests: write
1889
+ jobs:
1890
+ memmo-review:
1891
+ runs-on: ubuntu-latest
1892
+ steps:
1893
+ - uses: actions/checkout@v4
1894
+ with:
1895
+ fetch-depth: 0
1896
+ - uses: actions/setup-node@v4
1897
+ with:
1898
+ node-version: 20
1899
+ - run: npm install -g @memmo/cli
1900
+ - name: Memmo review
1901
+ run: memmo review --base "\${{ github.event.pull_request.base.ref }}" --post
1902
+ env:
1903
+ MEMMO_TOKEN: \${{ secrets.MEMMO_TOKEN }}
1904
+ MEMMO_API_URL: \${{ vars.MEMMO_API_URL }}
1905
+ GH_TOKEN: \${{ github.token }}
1906
+ `;
1907
+ async function ciCommand(opts = {}) {
1908
+ const dir = join10(process.cwd(), ".github", "workflows");
1909
+ const file = join10(dir, "memmo-review.yml");
1910
+ if (existsSync11(file) && !opts.force) {
1911
+ console.error(`${file} already exists. Re-run with --force to overwrite.`);
1912
+ return;
1913
+ }
1914
+ mkdirSync3(dir, { recursive: true });
1915
+ writeFileSync6(file, WORKFLOW);
1916
+ console.log("Wrote .github/workflows/memmo-review.yml\n");
1917
+ console.log("Next steps:");
1918
+ console.log(" 1. Commit .memmo/config.json and the workflow file.");
1919
+ console.log(" 2. Add your Memmo API token as a repo secret:");
1920
+ console.log(" gh secret set MEMMO_TOKEN --body <your-memmo-token>");
1921
+ console.log(" 3. If you self-host the API, set its URL as a repo variable:");
1922
+ console.log(" gh variable set MEMMO_API_URL --body https://your-memmo-api");
1923
+ console.log(" 4. Enable the reviewer for this repo in the Memmo dashboard (PR Reviewer card).");
1924
+ }
1925
+
1926
+ // src/index.ts
1927
+ init_version();
1928
+ var program = new Command();
1929
+ program.name("memmo").description("Long-term memory for AI coding agents").version(VERSION);
1930
+ program.command("login").description("Sign in (opens your browser) and store an API token in ~/.memmo").option("-t, --token <token>", "Use a token from Account \u2192 API tokens instead of the browser (CI/headless)").option("-e, --email <email>", "Dev-only email login (local API)").option("--no-browser", "Print the sign-in URL instead of opening a browser").option("--api <url>", "API base URL").action((opts) => run(() => loginCommand(opts)));
1931
+ program.command("init").description("Connect this repo to Memmo (registers it from the git remote) and wire up Claude Code").option("-r, --repo <repoId>", "Repo ID to connect (default: detected from the git remote)").option("-p, --project <idOrName>", "Project to add the repo to (default: your only project, or a prompt)").option("-y, --yes", "Skip prompts").option("--scan", "Run an initial codebase scan after connecting").action((opts) => run(() => initCommand(opts)));
1932
+ program.command("claude").description("Claude Code integration").command("install").description("Generate CLAUDE.md, .mcp.json, and hooks").action(() => run(() => claudeInstallCommand()));
1933
+ program.command("mcp").description("Run the Memmo MCP stdio server (used by Claude Code / Claude Desktop)").option("-r, --repo <repoId>", "Repo to serve (for Claude Desktop; defaults to .memmo/config.json)").option("--api <url>", "API base URL").action((opts) => run(() => mcpCommand(opts)));
1934
+ program.command("scan").description("Scan the connected repo and propose starter memories").option("--agent", "Deep scan with local Claude Code (no API key, uses your subscription)").option("--server", "Scan on the server from GitHub (uses the project Anthropic key)").action((opts) => run(() => scanCommand(opts)));
1935
+ program.command("chat [question...]").description("Chat with this repo's memory using your own Anthropic (key or Claude subscription)").action((question) => run(() => chatCommand(question)));
1936
+ program.command("decision <text...>").description("Record a project decision (what changed) as memory").option("--why <reason>", "Why the decision was made").action((text, opts) => run(() => decisionCommand(text, opts)));
1937
+ program.command("status").description("Check whether Memmo is set up in this repo").action(() => run(() => statusCommand()));
1938
+ program.command("uninstall").description("Remove Memmo wiring (MCP server, hooks, CLAUDE.md section) from this repo").option("-y, --yes", "Skip the confirmation prompt").action((opts) => run(() => uninstallCommand(opts)));
1939
+ program.command("hook <event>", { hidden: true }).description("Run a Memmo Claude Code hook (internal)").action((event) => hookCommand(event));
1940
+ program.command("sync").description("Write approved memories into a managed CLAUDE.md block").action(() => run(() => syncCommand()));
1941
+ program.command("review").description("Review the current PR's diff with Memmo (inline comments in CI, or Markdown output)").option("--base <ref>", "Base branch to diff against (default: $GITHUB_BASE_REF or main)").option("--title <title>", "PR title (default: from the GitHub event or last commit)").option("--body <body>", "PR description").option("--post", "Post a PR review with inline comments using GH_TOKEN (for CI)").option("--out <file>", "Write the review Markdown to a file instead of posting").option("--api <url>", "API base URL").action((opts) => run(() => reviewCommand(opts)));
1942
+ program.command("review-sync").description("Sync GitHub \u{1F44D}/\u{1F44E} reactions on Memmo review comments back as finding feedback").option("--api <url>", "API base URL").action((opts) => run(() => reviewSyncCommand(opts)));
1943
+ program.command("ci").description("Generate a GitHub Actions workflow that runs the Memmo PR reviewer").option("-f, --force", "Overwrite an existing workflow file").action((opts) => run(() => ciCommand(opts)));
1944
+ var memory = program.command("memory").description("(coming soon) manage memories from the CLI");
1945
+ for (const stub of ["list", "propose"]) {
1946
+ memory.command(stub).description(`(coming soon) memory ${stub}`).action(() => {
1947
+ console.error(`\`memmo memory ${stub}\` is not implemented in this MVP pass.`);
1948
+ process.exit(1);
1949
+ });
1950
+ }
1951
+ function run(fn) {
1952
+ fn().catch((err) => {
1953
+ console.error(err instanceof Error ? err.message : String(err));
1954
+ process.exit(1);
1955
+ });
1956
+ }
1957
+ program.parseAsync();