@memstack/cli 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,58 +1,247 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'util';
3
3
  import { readFile, writeFile } from 'fs/promises';
4
- import { MemStack, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter, InMemoryStorageAdapter } from '@memstack/core';
4
+ import { createRequire } from 'module';
5
+ import { projectNamespace, MemStack, HarnessMemory, defaultRecallNamespaces, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter, InMemoryStorageAdapter } from '@memstack/core';
6
+ import { realpathSync, existsSync, mkdirSync, readFileSync, writeFileSync, statSync, chmodSync, renameSync, rmSync, copyFileSync, constants } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { resolve, join, dirname, delimiter, basename } from 'path';
9
+ import { execFile, execFileSync, spawn } from 'child_process';
10
+ import { createHash } from 'crypto';
11
+ import { createInterface } from 'readline/promises';
5
12
 
6
- function buildLLMAdapter() {
7
- if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
13
+ var STORAGE_TYPES = ["memory", "markdown", "disk", "postgres", "sqlite", "redis"];
14
+ function memstackHome() {
15
+ return process.env.MEMSTACK_HOME ?? join(homedir(), ".memstack");
16
+ }
17
+ function configFilePath(home = memstackHome()) {
18
+ return join(home, "config.json");
19
+ }
20
+ function readConfigFile(path = configFilePath()) {
21
+ if (!existsSync(path)) return void 0;
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(readFileSync(path, "utf8"));
25
+ } catch (error) {
26
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
27
+ }
28
+ return validateConfigFile(parsed, path);
29
+ }
30
+ function writeConfigFile(config, path = configFilePath()) {
31
+ validateConfigFile(config, path);
32
+ const dir = join(path, "..");
33
+ mkdirSync(dir, { recursive: true, mode: 448 });
34
+ chmodSync(dir, 448);
35
+ const tmp = `${path}.${process.pid}.tmp`;
36
+ writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
37
+ `, { mode: 384 });
38
+ chmodSync(tmp, 384);
39
+ renameSync(tmp, path);
40
+ }
41
+ function checkConfigPermissions(path = configFilePath()) {
42
+ if (process.platform === "win32" || !existsSync(path)) return [];
43
+ const warnings = [];
44
+ const fileMode = statSync(path).mode & 511;
45
+ if (fileMode & 63) {
46
+ warnings.push(`${path} is readable by other users (mode ${fileMode.toString(8)}). Run: chmod 600 ${path}`);
47
+ }
48
+ const dir = join(path, "..");
49
+ const dirMode = statSync(dir).mode & 511;
50
+ if (dirMode & 63) {
51
+ warnings.push(`${dir} is accessible by other users (mode ${dirMode.toString(8)}). Run: chmod 700 ${dir}`);
52
+ }
53
+ return warnings;
54
+ }
55
+ function validateConfigFile(value, path) {
56
+ const fail2 = (message) => {
57
+ throw new Error(`Invalid ${path}: ${message}`);
58
+ };
59
+ if (!isObject(value)) fail2("expected a JSON object");
60
+ const config = value;
61
+ if (config.version !== 1) fail2(`unsupported version ${JSON.stringify(config.version)}, expected 1`);
62
+ if (config.llm !== void 0) {
63
+ if (!isObject(config.llm)) fail2("llm must be an object");
64
+ const llm = config.llm;
65
+ if (llm.provider !== "openai-compatible" && llm.provider !== "anthropic") {
66
+ fail2('llm.provider must be "openai-compatible" or "anthropic"');
67
+ }
68
+ if (typeof llm.apiKey !== "string" || !llm.apiKey) fail2("llm.apiKey must be a non-empty string");
69
+ for (const key of ["baseURL", "model"]) {
70
+ if (llm[key] !== void 0 && typeof llm[key] !== "string") fail2(`llm.${key} must be a string`);
71
+ }
72
+ }
73
+ if (config.storage !== void 0) {
74
+ if (!isObject(config.storage)) fail2("storage must be an object");
75
+ const storage = config.storage;
76
+ if (!STORAGE_TYPES.includes(storage.type)) {
77
+ fail2(`storage.type must be one of ${STORAGE_TYPES.join(", ")}`);
78
+ }
79
+ for (const key of ["path", "url"]) {
80
+ if (storage[key] !== void 0 && typeof storage[key] !== "string") fail2(`storage.${key} must be a string`);
81
+ }
82
+ }
83
+ return config;
84
+ }
85
+ function isObject(value) {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+ var PIN_FILE = ".memstack.json";
89
+ var PIN_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
90
+ function resolveProject(cwd = process.cwd()) {
91
+ const dir = realpathSync(resolve(cwd));
92
+ const root = git(dir, ["rev-parse", "--show-toplevel"]);
93
+ const base = root ?? dir;
94
+ const pinned = readPin(base);
95
+ const fallbacks = transitionalIds(dir, root);
96
+ if (pinned) return { id: pinned, source: "pin", key: pinned, root: base, previousIds: [] };
97
+ if (!root) return { ...identity("directory", dir), root: dir, previousIds: [] };
98
+ const shallow = git(root, ["rev-parse", "--is-shallow-repository"]) === "true";
99
+ const rootCommit = shallow ? null : firstCommit(root);
100
+ if (rootCommit) return { ...identity("root-commit", rootCommit), root, previousIds: fallbacks };
101
+ const remote = git(root, ["config", "--get", "remote.origin.url"]);
102
+ const normalized = shallow && remote ? normalizeRemote(remote) : null;
103
+ if (normalized) return { ...identity("remote", normalized), root, previousIds: fallbacks };
104
+ const gitId = identity("git", gitRepoKey(root));
105
+ return { ...gitId, root, previousIds: fallbacks.filter((id) => id !== gitId.id) };
106
+ }
107
+ function pinProject(name, cwd = process.cwd()) {
108
+ if (!PIN_PATTERN.test(name)) {
109
+ throw new Error("A pinned project ID uses letters, digits, '.', '_', or '-', starts with a letter or digit, and is at most 64 characters.");
110
+ }
111
+ const dir = realpathSync(resolve(cwd));
112
+ const base = git(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
113
+ const path = join(base, PIN_FILE);
114
+ const existing = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
115
+ writeFileSync(path, `${JSON.stringify({ ...existing, project: name }, null, 2)}
116
+ `);
117
+ return path;
118
+ }
119
+ function normalizeRemote(url) {
120
+ const value = url.trim();
121
+ if (!value) return null;
122
+ let host;
123
+ let path;
124
+ const scp = value.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/);
125
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
126
+ let parsed;
127
+ try {
128
+ parsed = new URL(value);
129
+ } catch {
130
+ return null;
131
+ }
132
+ if (parsed.protocol === "file:" || !parsed.hostname) return null;
133
+ host = parsed.hostname;
134
+ path = decodeURIComponent(parsed.pathname);
135
+ } else if (scp) {
136
+ host = scp[1];
137
+ path = scp[2];
138
+ } else {
139
+ return null;
140
+ }
141
+ path = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
142
+ if (!path) return null;
143
+ return `${host.toLowerCase()}/${path}`;
144
+ }
145
+ function readPin(base) {
146
+ const path = join(base, PIN_FILE);
147
+ if (!existsSync(path)) return null;
148
+ let project;
149
+ try {
150
+ project = JSON.parse(readFileSync(path, "utf8")).project;
151
+ } catch (error) {
152
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
153
+ }
154
+ if (project === void 0) return null;
155
+ if (typeof project !== "string" || !PIN_PATTERN.test(project)) {
156
+ throw new Error(`${path}: "project" must use letters, digits, '.', '_', or '-' and be at most 64 characters.`);
157
+ }
158
+ return project;
159
+ }
160
+ function firstCommit(root) {
161
+ const out2 = git(root, ["rev-list", "--max-parents=0", "--first-parent", "HEAD"]);
162
+ return out2 ? out2.split("\n").pop().trim() : null;
163
+ }
164
+ function gitRepoKey(root) {
165
+ const commonDir = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
166
+ return commonDir ? dirname(realpathSync(commonDir)) : root;
167
+ }
168
+ function transitionalIds(dir, root) {
169
+ const ids = /* @__PURE__ */ new Set();
170
+ if (root) ids.add(identity("git", gitRepoKey(root)).id);
171
+ ids.add(identity("directory", dir).id);
172
+ return [...ids];
173
+ }
174
+ function identity(source, key) {
175
+ const id = createHash("sha256").update(`${source}:${key}`).digest("hex").slice(0, 16);
176
+ return { id, source, key };
177
+ }
178
+ function git(cwd, args) {
179
+ try {
180
+ const out2 = execFileSync("git", ["-C", cwd, ...args], {
181
+ encoding: "utf8",
182
+ stdio: ["ignore", "pipe", "ignore"],
183
+ timeout: 5e3
184
+ }).trim();
185
+ return out2 || null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function isMissingModule(error) {
191
+ const code = error?.code;
192
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
193
+ }
194
+ function buildLLMAdapter(env) {
195
+ if (!env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY) {
8
196
  throw new Error("At least one of OPENAI_API_KEY or ANTHROPIC_API_KEY must be set");
9
197
  }
10
- if (process.env.ANTHROPIC_API_KEY) {
11
- return new AnthropicLLMAdapter({ apiKey: process.env.ANTHROPIC_API_KEY });
198
+ if (env.ANTHROPIC_API_KEY) {
199
+ return new AnthropicLLMAdapter({ apiKey: env.ANTHROPIC_API_KEY, defaultModel: env.MEMSTACK_LLM_MODEL });
12
200
  }
13
201
  return new OpenAILLMAdapter({
14
- apiKey: process.env.OPENAI_API_KEY ?? "",
15
- baseURL: process.env.MEMSTACK_OPENAI_BASE_URL,
16
- defaultModel: process.env.MEMSTACK_LLM_MODEL
202
+ apiKey: env.OPENAI_API_KEY ?? "",
203
+ baseURL: env.MEMSTACK_OPENAI_BASE_URL,
204
+ defaultModel: env.MEMSTACK_LLM_MODEL
17
205
  });
18
206
  }
19
- function buildEmbeddingAdapter() {
20
- if (process.env.MEMSTACK_OPENAI_BASE_URL) return void 0;
21
- if (process.env.OPENAI_API_KEY) {
22
- return new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY });
207
+ function buildEmbeddingAdapter(env) {
208
+ if (env.MEMSTACK_OPENAI_BASE_URL) return void 0;
209
+ if (env.OPENAI_API_KEY) {
210
+ return new OpenAIEmbeddingAdapter({ apiKey: env.OPENAI_API_KEY });
23
211
  }
24
212
  return void 0;
25
213
  }
26
- async function buildStorageAdapter() {
27
- const storage = process.env.MEMSTACK_STORAGE ?? "memory";
214
+ async function buildStorageAdapter(env) {
215
+ const storage = env.MEMSTACK_STORAGE ?? "memory";
28
216
  switch (storage) {
29
217
  case "memory":
30
218
  return new InMemoryStorageAdapter();
31
219
  case "markdown":
32
220
  return new MarkdownStorageAdapter({
33
- dir: process.env.MEMSTACK_DIR ?? "./memories"
221
+ dir: env.MEMSTACK_DIR ?? "./memories"
34
222
  });
35
223
  case "disk":
36
224
  return new DiskStorageAdapter({
37
- storageDir: process.env.MEMSTACK_DIR ?? "./memstack-data"
225
+ storageDir: env.MEMSTACK_DIR ?? "./memstack-data"
38
226
  });
39
227
  case "postgres":
40
- if (!process.env.DATABASE_URL) {
228
+ if (!env.DATABASE_URL) {
41
229
  throw new Error("postgres storage requires DATABASE_URL");
42
230
  }
43
231
  return new PostgresStorageAdapter({
44
- connectionString: process.env.DATABASE_URL
232
+ connectionString: env.DATABASE_URL
45
233
  });
46
234
  case "sqlite": {
47
235
  try {
48
236
  const BetterSqlite3 = await import('better-sqlite3');
49
237
  const db = new BetterSqlite3.default(
50
- process.env.SQLITE_PATH ?? "./memstack.db"
238
+ env.SQLITE_PATH ?? "./memstack.db"
51
239
  );
52
240
  return new SQLiteStorageAdapter({ db });
53
- } catch {
241
+ } catch (error) {
242
+ if (!isMissingModule(error)) throw error;
54
243
  throw new Error(
55
- "SQLite requires better-sqlite3. Install: npm install better-sqlite3"
244
+ "SQLite requires better-sqlite3. Install: npm install better-sqlite3@^11.10.0"
56
245
  );
57
246
  }
58
247
  }
@@ -60,12 +249,13 @@ async function buildStorageAdapter() {
60
249
  try {
61
250
  const Redis = await import('ioredis');
62
251
  const client = new Redis.default(
63
- process.env.REDIS_URL ?? "redis://localhost:6379"
252
+ env.REDIS_URL ?? "redis://localhost:6379"
64
253
  );
65
254
  return new RedisStorageAdapter({ redis: client });
66
- } catch {
255
+ } catch (error) {
256
+ if (!isMissingModule(error)) throw error;
67
257
  throw new Error(
68
- "Redis requires ioredis. Install: npm install ioredis"
258
+ "Redis requires ioredis. Install: npm install ioredis@^5.11.1"
69
259
  );
70
260
  }
71
261
  }
@@ -73,12 +263,37 @@ async function buildStorageAdapter() {
73
263
  throw new Error(`Unknown MEMSTACK_STORAGE: "${storage}". Supported: memory, markdown, disk, postgres, sqlite, redis`);
74
264
  }
75
265
  }
76
- async function loadConfigFromEnv() {
77
- const llm = buildLLMAdapter();
78
- const embedding = buildEmbeddingAdapter();
79
- const storage = await buildStorageAdapter();
80
- const embedOnStore = process.env.MEMSTACK_EMBED_ON_STORE !== "false";
81
- const defaultActorId = process.env.MEMSTACK_ACTOR ?? "default";
266
+ async function loadConfig(options = {}) {
267
+ return buildConfig(mergeConfigFile(readConfigFile(options.configPath), process.env));
268
+ }
269
+ function mergeConfigFile(file, env) {
270
+ const merged = { ...env };
271
+ const envHasLLM = LLM_ENV.some((key) => env[key] !== void 0);
272
+ if (file?.llm && !envHasLLM) {
273
+ if (file.llm.provider === "anthropic") merged.ANTHROPIC_API_KEY = file.llm.apiKey;
274
+ else {
275
+ merged.OPENAI_API_KEY = file.llm.apiKey;
276
+ merged.MEMSTACK_OPENAI_BASE_URL = file.llm.baseURL;
277
+ }
278
+ merged.MEMSTACK_LLM_MODEL = file.llm.model;
279
+ }
280
+ if (file?.storage && env.MEMSTACK_STORAGE === void 0) {
281
+ const { type, path, url } = file.storage;
282
+ merged.MEMSTACK_STORAGE = type;
283
+ if (type === "sqlite") merged.SQLITE_PATH = path;
284
+ if (type === "disk" || type === "markdown") merged.MEMSTACK_DIR = path;
285
+ if (type === "postgres") merged.DATABASE_URL = url;
286
+ if (type === "redis") merged.REDIS_URL = url;
287
+ }
288
+ return merged;
289
+ }
290
+ var LLM_ENV = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MEMSTACK_OPENAI_BASE_URL", "MEMSTACK_LLM_MODEL"];
291
+ async function buildConfig(env) {
292
+ const llm = buildLLMAdapter(env);
293
+ const embedding = buildEmbeddingAdapter(env);
294
+ const storage = await buildStorageAdapter(env);
295
+ const embedOnStore = env.MEMSTACK_EMBED_ON_STORE !== "false";
296
+ const defaultActorId = env.MEMSTACK_ACTOR ?? "default";
82
297
  return {
83
298
  config: { llm, embedding, storage, defaults: { embedOnStore } },
84
299
  defaultActorId
@@ -86,12 +301,1103 @@ async function loadConfigFromEnv() {
86
301
  }
87
302
 
88
303
  // src/config.ts
89
- async function loadConfig() {
90
- const { config } = await loadConfigFromEnv();
304
+ async function loadConfig2() {
305
+ const { config } = await loadConfig();
91
306
  return config;
92
307
  }
308
+ var createRunner = (env = process.env) => (command, args) => new Promise((resolve2) => {
309
+ execFile(command, args, { env, timeout: 6e4, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
310
+ if (!error) return resolve2({ code: 0, stdout, stderr });
311
+ const code = error.code === "ENOENT" ? 127 : typeof error.code === "number" ? error.code : 1;
312
+ resolve2({ code, stdout, stderr: stderr || error.message });
313
+ });
314
+ });
315
+ var run = createRunner();
316
+
317
+ // src/harness/types.ts
318
+ var SERVER_NAME = "memstack";
319
+ function sameLaunch(a, b) {
320
+ if (!a || !b) return a === b;
321
+ return a.command === b.command && JSON.stringify(a.args) === JSON.stringify(b.args) && JSON.stringify(a.env ?? {}) === JSON.stringify(b.env ?? {});
322
+ }
323
+
324
+ // src/harness/claude-code.ts
325
+ function claudeCodeAdapter(options = {}) {
326
+ const env = options.env ?? process.env;
327
+ const runner = options.runner ?? createRunner(env);
328
+ const binary = "claude";
329
+ const configPath = join(env.CLAUDE_CONFIG_DIR ?? homedir(), ".claude.json");
330
+ const settingsPath = env.CLAUDE_CONFIG_DIR ? join(env.CLAUDE_CONFIG_DIR, "settings.json") : join(homedir(), ".claude", "settings.json");
331
+ return {
332
+ id: "claude-code",
333
+ displayName: "Claude Code",
334
+ binary,
335
+ // Also on clear and compact, so the memories survive /clear and context compaction.
336
+ sessionHook: { path: settingsPath, matcher: "startup|resume|clear|compact" },
337
+ async inspect() {
338
+ const version2 = await runner(binary, ["--version"]);
339
+ const state = { installed: version2.code === 0, configPath };
340
+ if (state.installed) state.version = version2.stdout.trim().split(/\s+/)[0];
341
+ state.entry = readEntry(configPath);
342
+ return state;
343
+ },
344
+ addSteps(launch) {
345
+ const json = JSON.stringify({ type: "stdio", command: launch.command, args: launch.args, ...launch.env ? { env: launch.env } : {} });
346
+ return [{ command: binary, args: ["mcp", "add-json", "-s", "user", SERVER_NAME, json] }];
347
+ },
348
+ removeSteps() {
349
+ return [{ command: binary, args: ["mcp", "remove", "-s", "user", SERVER_NAME] }];
350
+ }
351
+ };
352
+ }
353
+ function readEntry(configPath) {
354
+ if (!existsSync(configPath)) return void 0;
355
+ let config;
356
+ try {
357
+ config = JSON.parse(readFileSync(configPath, "utf8"));
358
+ } catch (error) {
359
+ throw new Error(`Could not read ${configPath}: ${error.message}`);
360
+ }
361
+ const entry = config.mcpServers?.[SERVER_NAME];
362
+ if (!entry || typeof entry.command !== "string") return void 0;
363
+ return {
364
+ command: entry.command,
365
+ args: Array.isArray(entry.args) ? entry.args.map(String) : [],
366
+ ...entry.env && typeof entry.env === "object" ? { env: entry.env } : {}
367
+ };
368
+ }
369
+ function writeFileAtomic(path, content) {
370
+ if (content === null) {
371
+ rmSync(path, { force: true });
372
+ return;
373
+ }
374
+ mkdirSync(dirname(path), { recursive: true });
375
+ const mode = existsSync(path) ? statSync(path).mode & 511 : 420;
376
+ const tmp = `${path}.memstack-${process.pid}.tmp`;
377
+ writeFileSync(tmp, content, { mode });
378
+ renameSync(tmp, path);
379
+ }
380
+ function backupFile(path, backupDir) {
381
+ mkdirSync(backupDir, { recursive: true, mode: 448 });
382
+ const base = join(backupDir, `${basename(path)}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
383
+ for (let n = 0; ; n++) {
384
+ try {
385
+ copyFileSync(path, n === 0 ? base : `${base}-${n}`, constants.COPYFILE_EXCL);
386
+ return;
387
+ } catch (error) {
388
+ if (error.code !== "EEXIST") throw error;
389
+ }
390
+ }
391
+ }
392
+ function snapshot(path) {
393
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
394
+ }
395
+
396
+ // src/harness/instructions.ts
397
+ var BEGIN = "<!-- memstack:begin";
398
+ var END = "<!-- memstack:end -->";
399
+ var BLOCK_PATTERN = /\n?<!-- memstack:begin[^\n]*-->\n[\s\S]*?<!-- memstack:end -->\n?/;
400
+ var CODEX_INSTRUCTIONS = [
401
+ "## Project memory (MemStack)",
402
+ "",
403
+ "When the user asks you to remember something, save it with the memstack `memory_store` tool. Do not only acknowledge it, and do not write it into project files unless the user asks for a file change.",
404
+ "At the start of a task, and before answering questions about this project's conventions, decisions, or the user's preferences, check `memory_retrieve`: it holds what earlier sessions, in Codex or other agents, were told to remember."
405
+ ].join("\n");
406
+ function renderBlock(body, harness) {
407
+ return `${BEGIN} (managed by \`memstack connect ${harness}\`; removed by \`memstack disconnect ${harness}\`) -->
408
+ ${body}
409
+ ${END}
410
+ `;
411
+ }
412
+ function readBlock(path) {
413
+ if (!existsSync(path)) return null;
414
+ const match = readFileSync(path, "utf8").match(BLOCK_PATTERN);
415
+ return match ? match[0].replace(/^\n/, "") : null;
416
+ }
417
+ function upsertBlock(path, block, backupDir) {
418
+ const current = existsSync(path) ? readFileSync(path, "utf8") : null;
419
+ if (current !== null && readBlock(path) === block) return false;
420
+ let next;
421
+ if (current === null || current.trim() === "") next = block;
422
+ else if (BLOCK_PATTERN.test(current)) next = current.replace(BLOCK_PATTERN, (m) => `${m.startsWith("\n") ? "\n" : ""}${block}`);
423
+ else next = `${current}${current.endsWith("\n") ? "" : "\n"}
424
+ ${block}`;
425
+ write(path, next, current, backupDir);
426
+ return true;
427
+ }
428
+ function removeBlock(path, backupDir) {
429
+ if (!existsSync(path)) return false;
430
+ const current = readFileSync(path, "utf8");
431
+ if (!BLOCK_PATTERN.test(current)) return false;
432
+ const next = current.replace(BLOCK_PATTERN, "");
433
+ if (next.trim() === "") {
434
+ backupFile(path, backupDir);
435
+ rmSync(path);
436
+ } else {
437
+ write(path, next, current, backupDir);
438
+ }
439
+ return true;
440
+ }
441
+ function write(path, content, previous, backupDir) {
442
+ if (previous !== null) backupFile(path, backupDir);
443
+ writeFileAtomic(path, content);
444
+ }
445
+
446
+ // src/harness/codex.ts
447
+ function codexAdapter(options = {}) {
448
+ const env = options.env ?? process.env;
449
+ const runner = options.runner ?? createRunner(env);
450
+ const binary = "codex";
451
+ const home = env.CODEX_HOME ?? join(homedir(), ".codex");
452
+ const configPath = join(home, "config.toml");
453
+ return {
454
+ id: "codex",
455
+ displayName: "Codex",
456
+ binary,
457
+ // Codex gives MCP server instructions little weight, so without this it
458
+ // acknowledges "remember that..." instead of calling memory_store.
459
+ instructions: { path: join(home, "AGENTS.md"), body: CODEX_INSTRUCTIONS },
460
+ sessionHook: { path: join(home, "hooks.json"), matcher: "startup|resume" },
461
+ async inspect() {
462
+ const version2 = await runner(binary, ["--version"]);
463
+ const state = { installed: version2.code === 0, configPath };
464
+ if (!state.installed) return state;
465
+ state.version = version2.stdout.trim().split(/\s+/).pop();
466
+ const got = await runner(binary, ["mcp", "get", SERVER_NAME, "--json"]);
467
+ if (got.code !== 0) {
468
+ if (/No MCP server named/i.test(got.stderr + got.stdout)) return state;
469
+ throw new Error(`codex mcp get failed: ${(got.stderr || got.stdout).trim()}`);
470
+ }
471
+ const transport = JSON.parse(got.stdout).transport;
472
+ if (transport && typeof transport.command === "string") {
473
+ state.entry = {
474
+ command: transport.command,
475
+ args: Array.isArray(transport.args) ? transport.args.map(String) : [],
476
+ ...transport.env && typeof transport.env === "object" && Object.keys(transport.env).length > 0 ? { env: transport.env } : {}
477
+ };
478
+ }
479
+ return state;
480
+ },
481
+ addSteps(launch) {
482
+ const envFlags = Object.entries(launch.env ?? {}).flatMap(([key, value]) => ["--env", `${key}=${value}`]);
483
+ return [{ command: binary, args: ["mcp", "add", SERVER_NAME, ...envFlags, "--", launch.command, ...launch.args] }];
484
+ },
485
+ removeSteps() {
486
+ return [{ command: binary, args: ["mcp", "remove", SERVER_NAME] }];
487
+ }
488
+ };
489
+ }
490
+ var OURS = /\bhook session-start --harness (claude-code|codex)\b/;
491
+ function sessionStartHook(launch, harness) {
492
+ const script = launch.args[0]?.endsWith(".js") ? [launch.args[0]] : [];
493
+ const parts = [launch.command, ...script].map(shellQuote);
494
+ return {
495
+ type: "command",
496
+ command: `${parts.join(" ")} hook session-start --harness ${harness}`,
497
+ timeout: 10,
498
+ statusMessage: "Loading MemStack project memory"
499
+ };
500
+ }
501
+ function readHook(path) {
502
+ const file = read(path);
503
+ for (const group of file?.hooks?.SessionStart ?? []) {
504
+ for (const hook of group.hooks ?? []) {
505
+ if (typeof hook.command === "string" && OURS.test(hook.command)) return hook;
506
+ }
507
+ }
508
+ return null;
509
+ }
510
+ function upsertHook(path, hook, matcher, backupDir) {
511
+ const file = read(path) ?? {};
512
+ const current = readHook(path);
513
+ if (current && JSON.stringify(current) === JSON.stringify(hook) && ourGroups(file).every((g) => g.matcher === matcher)) return false;
514
+ const hooks = file.hooks ??= {};
515
+ hooks.SessionStart = [...withoutOurs(hooks.SessionStart ?? []), { matcher, hooks: [hook] }];
516
+ save(path, file, backupDir);
517
+ return true;
518
+ }
519
+ function removeHook(path, backupDir) {
520
+ const file = read(path);
521
+ if (!file || !readHook(path)) return false;
522
+ const remaining = withoutOurs(file.hooks.SessionStart ?? []);
523
+ if (remaining.length > 0) file.hooks.SessionStart = remaining;
524
+ else delete file.hooks.SessionStart;
525
+ if (Object.keys(file.hooks).length === 0) delete file.hooks;
526
+ save(path, file, backupDir);
527
+ return true;
528
+ }
529
+ function ourGroups(file) {
530
+ return (file.hooks?.SessionStart ?? []).filter((g) => (g.hooks ?? []).some((h) => typeof h.command === "string" && OURS.test(h.command)));
531
+ }
532
+ function withoutOurs(groups) {
533
+ return groups.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !(typeof h.command === "string" && OURS.test(h.command))) })).filter((g, i) => g.hooks.length > 0 || (groups[i].hooks ?? []).length === 0);
534
+ }
535
+ function read(path) {
536
+ if (!existsSync(path)) return null;
537
+ const text = readFileSync(path, "utf8");
538
+ if (!text.trim()) return {};
539
+ try {
540
+ const parsed = JSON.parse(text);
541
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("expected a JSON object");
542
+ return parsed;
543
+ } catch (error) {
544
+ throw new Error(`${path} is not valid JSON, so MemStack left it unchanged: ${error.message}`);
545
+ }
546
+ }
547
+ function save(path, file, backupDir) {
548
+ const previous = existsSync(path) ? readFileSync(path, "utf8") : null;
549
+ if (previous !== null) backupFile(path, backupDir);
550
+ const indent = previous?.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
551
+ const newline = previous === null || previous.endsWith("\n") ? "\n" : "";
552
+ writeFileAtomic(path, Object.keys(file).length === 0 ? null : `${JSON.stringify(file, null, indent)}${newline}`);
553
+ }
554
+ function shellQuote(value) {
555
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
556
+ }
557
+ var HARNESS_TOOLS = ["memory_store", "memory_retrieve", "memory_get", "memory_delete", "memory_stats"];
558
+ var verifyServer = (launch, options = {}) => new Promise((resolve2) => {
559
+ const child = spawn(launch.command, launch.args, {
560
+ cwd: options.cwd,
561
+ env: { ...options.env ?? process.env, ...launch.env },
562
+ stdio: ["pipe", "pipe", "pipe"]
563
+ });
564
+ let stdout = "";
565
+ let stderr = "";
566
+ let settled = false;
567
+ const pending = /* @__PURE__ */ new Map();
568
+ const finish = (result) => {
569
+ if (settled) return;
570
+ settled = true;
571
+ clearTimeout(timer);
572
+ child.kill();
573
+ resolve2(result);
574
+ };
575
+ const timer = setTimeout(() => finish({ ok: false, error: `No response within 20 s. ${lastLine(stderr)}`.trim() }), 2e4);
576
+ child.on("error", (error) => finish({ ok: false, error: `Could not start ${launch.command}: ${error.message}` }));
577
+ child.on("exit", (code) => finish({ ok: false, error: `Server exited with code ${code}. ${lastLine(stderr)}`.trim() }));
578
+ child.stderr.on("data", (chunk) => stderr += chunk);
579
+ child.stdout.on("data", (chunk) => {
580
+ stdout += chunk;
581
+ let newline;
582
+ while ((newline = stdout.indexOf("\n")) >= 0) {
583
+ const line = stdout.slice(0, newline).trim();
584
+ stdout = stdout.slice(newline + 1);
585
+ if (!line) continue;
586
+ try {
587
+ const message = JSON.parse(line);
588
+ pending.get(message.id)?.(message);
589
+ } catch {
590
+ }
591
+ }
592
+ });
593
+ let nextId = 1;
594
+ const request = (method, params) => new Promise((reply) => {
595
+ const id = nextId++;
596
+ pending.set(id, reply);
597
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
598
+ `);
599
+ });
600
+ void (async () => {
601
+ const init2 = await request("initialize", {
602
+ protocolVersion: "2025-06-18",
603
+ capabilities: {},
604
+ clientInfo: { name: "memstack-connect", version: "1" }
605
+ });
606
+ if (init2.error) return finish({ ok: false, error: init2.error.message });
607
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
608
+ `);
609
+ const list = await request("tools/list", {});
610
+ const names = (list.result?.tools ?? []).map((t) => t.name);
611
+ const missing = HARNESS_TOOLS.filter((t) => !names.includes(t));
612
+ if (missing.length > 0) return finish({ ok: false, error: `Server is missing harness tools: ${missing.join(", ")}` });
613
+ const stats = await request("tools/call", { name: "memory_stats", arguments: {} });
614
+ const result = stats.result;
615
+ const text = result?.content?.map((c) => c.text).join("\n") ?? stats.error?.message ?? "";
616
+ if (!result || result.isError) return finish({ ok: false, error: text || "memory_stats failed" });
617
+ finish({ ok: true, stats: text });
618
+ })();
619
+ });
620
+ function lastLine(text) {
621
+ return text.trim().split("\n").filter(Boolean).pop() ?? "";
622
+ }
623
+
624
+ // src/harness/connect.ts
625
+ async function connectHarness({
626
+ adapter,
627
+ launch,
628
+ runner = run,
629
+ verify = verifyServer,
630
+ dryRun,
631
+ instructions = true,
632
+ hooks = true,
633
+ backupDir = join(memstackHome(), "backups")
634
+ }) {
635
+ const state = await adapter.inspect();
636
+ if (!state.installed) throw new Error(`${adapter.displayName} is not installed: \`${adapter.binary}\` is not on PATH.`);
637
+ const alreadyConnected = sameLaunch(state.entry, launch);
638
+ const plan = {
639
+ harness: adapter.id,
640
+ previous: state.entry,
641
+ target: launch,
642
+ steps: alreadyConnected ? [] : [...state.entry ? adapter.removeSteps() : [], ...adapter.addSteps(launch)]
643
+ };
644
+ const block = adapter.instructions && instructions ? renderBlock(adapter.instructions.body, adapter.id) : void 0;
645
+ if (adapter.instructions && block) {
646
+ const current = readBlock(adapter.instructions.path);
647
+ plan.instructions = { path: adapter.instructions.path, change: current === null ? "add" : current === block ? "none" : "update" };
648
+ }
649
+ const hook = adapter.sessionHook && hooks ? sessionStartHook(launch, adapter.id) : void 0;
650
+ if (adapter.sessionHook && hook) {
651
+ const current = readHook(adapter.sessionHook.path);
652
+ plan.hook = { path: adapter.sessionHook.path, change: current === null ? "add" : JSON.stringify(current) === JSON.stringify(hook) ? "none" : "update" };
653
+ }
654
+ const verification = await verify(launch);
655
+ if (!verification.ok) {
656
+ throw new Error(`MemStack's MCP server did not start correctly, so ${adapter.displayName} was not changed: ${verification.error}`);
657
+ }
658
+ if (dryRun) return { plan, changed: false, verification };
659
+ const undo = [];
660
+ let changed = false;
661
+ try {
662
+ if (!alreadyConnected) {
663
+ await applyWithRollback(adapter, plan, runner);
664
+ undo.push(() => restore(adapter, plan.previous, runner));
665
+ changed = true;
666
+ }
667
+ if (block && plan.instructions && plan.instructions.change !== "none") {
668
+ const path = plan.instructions.path;
669
+ const before = snapshot(path);
670
+ fileStep(path, () => upsertBlock(path, block, backupDir));
671
+ undo.push(async () => (writeFileAtomic(path, before), true));
672
+ changed = true;
673
+ }
674
+ if (hook && adapter.sessionHook && plan.hook && plan.hook.change !== "none") {
675
+ const path = plan.hook.path;
676
+ const before = snapshot(path);
677
+ const matcher = adapter.sessionHook.matcher;
678
+ fileStep(path, () => upsertHook(path, hook, matcher, backupDir));
679
+ undo.push(async () => (writeFileAtomic(path, before), true));
680
+ changed = true;
681
+ }
682
+ } catch (error) {
683
+ let restored = true;
684
+ for (const step of undo.reverse()) restored = await step().catch(() => false) && restored;
685
+ const message = error instanceof Error ? error.message : String(error);
686
+ if (/previous configuration/.test(message)) throw error;
687
+ throw new Error(`${message} ${restored ? "The previous configuration was restored." : "Restoring the previous configuration failed; run `memstack doctor`."}`);
688
+ }
689
+ return { plan, changed, verification };
690
+ }
691
+ async function disconnectHarness({
692
+ adapter,
693
+ runner = run,
694
+ dryRun,
695
+ backupDir = join(memstackHome(), "backups")
696
+ }) {
697
+ const state = await adapter.inspect();
698
+ if (!state.installed) throw new Error(`${adapter.displayName} is not installed: \`${adapter.binary}\` is not on PATH.`);
699
+ const plan = { harness: adapter.id, previous: state.entry, steps: state.entry ? adapter.removeSteps() : [] };
700
+ if (adapter.instructions) {
701
+ plan.instructions = { path: adapter.instructions.path, change: readBlock(adapter.instructions.path) === null ? "none" : "remove" };
702
+ }
703
+ if (adapter.sessionHook) {
704
+ plan.hook = { path: adapter.sessionHook.path, change: readHook(adapter.sessionHook.path) === null ? "none" : "remove" };
705
+ }
706
+ if (dryRun) return { plan, changed: false };
707
+ let changed = false;
708
+ if (plan.steps.length > 0) {
709
+ await applyWithRollback(adapter, plan, runner);
710
+ changed = true;
711
+ }
712
+ if (plan.instructions?.change === "remove") changed = removeBlock(plan.instructions.path, backupDir) || changed;
713
+ if (plan.hook?.change === "remove") changed = removeHook(plan.hook.path, backupDir) || changed;
714
+ return { plan, changed };
715
+ }
716
+ async function applyWithRollback(adapter, plan, runner) {
717
+ try {
718
+ for (const step of plan.steps) await runStep(step, runner);
719
+ const after = await adapter.inspect();
720
+ if (!sameLaunch(after.entry, plan.target)) {
721
+ throw new Error(`${adapter.displayName} does not show the expected MemStack entry after the change.`);
722
+ }
723
+ } catch (error) {
724
+ const restored = await restore(adapter, plan.previous, runner);
725
+ const message = error instanceof Error ? error.message : String(error);
726
+ throw new Error(`${message} ${restored ? "The previous configuration was restored." : "Restoring the previous configuration failed; run `memstack doctor`."}`);
727
+ }
728
+ }
729
+ async function restore(adapter, previous, runner) {
730
+ try {
731
+ const current = await adapter.inspect();
732
+ if (sameLaunch(current.entry, previous)) return true;
733
+ if (current.entry) for (const step of adapter.removeSteps()) await runStep(step, runner);
734
+ if (previous) for (const step of adapter.addSteps(previous)) await runStep(step, runner);
735
+ return sameLaunch((await adapter.inspect()).entry, previous);
736
+ } catch {
737
+ return false;
738
+ }
739
+ }
740
+ function fileStep(path, apply) {
741
+ try {
742
+ apply();
743
+ } catch (error) {
744
+ throw new Error(`Could not update ${path}: ${error.message}.`);
745
+ }
746
+ }
747
+ async function runStep(step, runner) {
748
+ const result = await runner(step.command, step.args);
749
+ if (result.code !== 0) {
750
+ throw new Error(`\`${step.command} ${step.args.slice(0, 3).join(" ")}\` failed: ${(result.stderr || result.stdout).trim()}`);
751
+ }
752
+ }
753
+ function describeStep(step) {
754
+ return [step.command, ...step.args.map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, "'\\''")}'`)].join(" ");
755
+ }
756
+ var STORAGE_DRIVERS = {
757
+ sqlite: { module: "better-sqlite3", spec: "better-sqlite3@^11.10.0" },
758
+ postgres: { module: "postgres", spec: "postgres@^3.4.9" },
759
+ redis: { module: "ioredis", spec: "ioredis@^5.11.1" }
760
+ };
761
+ function installCommand(storage) {
762
+ const driver = storage ? STORAGE_DRIVERS[storage] : void 0;
763
+ return `npm install -g @memstack/mcp${driver ? ` ${driver.spec}` : ""}`;
764
+ }
765
+ function findMemstackMcp(env = process.env) {
766
+ const names = process.platform === "win32" ? ["memstack-mcp.cmd", "memstack-mcp"] : ["memstack-mcp"];
767
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
768
+ if (!dir) continue;
769
+ for (const name of names) {
770
+ const candidate = join(dir, name);
771
+ if (existsSync(candidate)) return realpathSync(candidate);
772
+ }
773
+ }
774
+ return null;
775
+ }
776
+ function canResolveFrom(scriptPath, module) {
777
+ let dir = dirname(scriptPath);
778
+ for (; ; ) {
779
+ if (existsSync(join(dir, "node_modules", module, "package.json"))) return true;
780
+ const parent = dirname(dir);
781
+ if (parent === dir) return false;
782
+ dir = parent;
783
+ }
784
+ }
785
+ function harnessLaunch(mcpScript, harness) {
786
+ const isScript = /\.(c|m)?js$/.test(mcpScript);
787
+ const args = ["--profile", "harness", "--harness", harness];
788
+ return isScript ? { command: process.execPath, args: [mcpScript, ...args] } : { command: mcpScript, args };
789
+ }
790
+
791
+ // src/harness/index.ts
792
+ var HARNESS_IDS = ["claude-code", "codex"];
793
+ function harnessAdapter(id, options = {}) {
794
+ if (id === "claude-code") return claudeCodeAdapter(options);
795
+ if (id === "codex") return codexAdapter(options);
796
+ throw new Error(`Unknown harness "${id}". Supported: ${HARNESS_IDS.join(", ")}.`);
797
+ }
798
+
799
+ // src/harness-commands.ts
800
+ var HARNESS_COMMANDS = ["init", "connect", "disconnect", "status", "doctor", "memories", "project"];
801
+ var out = (line = "") => process.stdout.write(`${line}
802
+ `);
803
+ async function runHarnessCommand(command, args, flags) {
804
+ switch (command) {
805
+ case "init":
806
+ return init(flags);
807
+ case "connect":
808
+ return connect(args, flags);
809
+ case "disconnect":
810
+ return disconnect(args, flags);
811
+ case "status":
812
+ return status();
813
+ case "doctor":
814
+ return doctor(flags);
815
+ case "memories":
816
+ return memories(args, flags);
817
+ case "project":
818
+ return projectCommand(args);
819
+ default:
820
+ throw new Error(`Unknown command: ${command}`);
821
+ }
822
+ }
823
+ async function init(flags) {
824
+ const existing = readConfigFile();
825
+ const interactive = process.stdin.isTTY && !flags.yes;
826
+ const prompt = interactive ? createPrompter() : void 0;
827
+ try {
828
+ const provider = flags.provider ?? await prompt?.choose("LLM provider", ["openai-compatible", "anthropic"], existing?.llm?.provider ?? "openai-compatible") ?? existing?.llm?.provider ?? "openai-compatible";
829
+ if (provider !== "openai-compatible" && provider !== "anthropic") throw new Error('--provider must be "openai-compatible" or "anthropic"');
830
+ const sameProvider = existing?.llm?.provider === provider;
831
+ let baseURL;
832
+ if (provider === "openai-compatible") {
833
+ baseURL = flags["base-url"] ?? await prompt?.ask("Base URL (empty for OpenAI)", sameProvider ? existing?.llm?.baseURL ?? "" : "") ?? (sameProvider ? existing?.llm?.baseURL : void 0);
834
+ baseURL = baseURL || void 0;
835
+ }
836
+ const model = flags.model ?? await prompt?.ask("Model (empty for the provider default)", sameProvider ? existing?.llm?.model ?? "" : "") ?? (sameProvider ? existing?.llm?.model : void 0);
837
+ let apiKey;
838
+ if (flags["api-key-env"]) {
839
+ apiKey = process.env[flags["api-key-env"]];
840
+ if (!apiKey) throw new Error(`Environment variable ${flags["api-key-env"]} is empty.`);
841
+ } else if (prompt) {
842
+ apiKey = await prompt.secret(sameProvider && existing?.llm?.apiKey ? "API key (empty to keep the current one)" : "API key") || (sameProvider ? existing?.llm?.apiKey : void 0);
843
+ } else if (sameProvider) {
844
+ apiKey = existing?.llm?.apiKey;
845
+ }
846
+ if (!apiKey) throw new Error("An API key is required. Pass --api-key-env <VAR> to read it from an environment variable.");
847
+ const store = flags.store ?? await prompt?.choose("Storage", [...STORAGE_TYPES], existing?.storage?.type ?? "sqlite") ?? existing?.storage?.type ?? "sqlite";
848
+ if (!STORAGE_TYPES.includes(store)) throw new Error(`--store must be one of ${STORAGE_TYPES.join(", ")}`);
849
+ const storage = await storageSettings(store, flags, existing, prompt);
850
+ const llm = { provider, apiKey, ...baseURL ? { baseURL } : {}, ...model ? { model } : {} };
851
+ out("Checking the LLM provider with a test request...");
852
+ await checkLLM(buildLLM(llm));
853
+ out(" \u2713 The provider answered.");
854
+ const config = { version: 1, llm, storage };
855
+ if (storage.type === "sqlite" && storage.path) mkdirSync(join(storage.path, ".."), { recursive: true, mode: 448 });
856
+ writeConfigFile(config);
857
+ out(`Saved ${configFilePath()} (readable only by you).`);
858
+ const mcp = findMemstackMcp();
859
+ const driver = STORAGE_DRIVERS[store];
860
+ out();
861
+ if (!mcp || driver && !canResolveFrom(mcp, driver.module)) {
862
+ out("Next, install the MCP server and your storage driver (MemStack does not install them for you):");
863
+ out(` ${installCommand(store)}`);
864
+ }
865
+ out("Then connect your agents:");
866
+ out(" memstack connect claude-code");
867
+ out(" memstack connect codex");
868
+ return 0;
869
+ } finally {
870
+ prompt?.close();
871
+ }
872
+ }
873
+ async function storageSettings(type, flags, existing, prompt) {
874
+ const previous = existing?.storage?.type === type ? existing.storage : void 0;
875
+ switch (type) {
876
+ case "sqlite":
877
+ case "disk":
878
+ case "markdown": {
879
+ const fallback = previous?.path ?? join(memstackHome(), type === "sqlite" ? "memstack.db" : type === "disk" ? "data" : "memories");
880
+ const path = flags.path ?? await prompt?.ask(type === "sqlite" ? "Database file" : "Directory", fallback) ?? fallback;
881
+ return { type, path };
882
+ }
883
+ case "postgres":
884
+ case "redis": {
885
+ const url = flags.url ?? await prompt?.ask(`${type === "postgres" ? "Postgres" : "Redis"} connection URL`, previous?.url ?? "") ?? previous?.url;
886
+ if (!url) throw new Error(`--url is required for ${type}.`);
887
+ return { type, url };
888
+ }
889
+ case "memory":
890
+ return { type };
891
+ }
892
+ }
893
+ function buildLLM(llm) {
894
+ return llm.provider === "anthropic" ? new AnthropicLLMAdapter({ apiKey: llm.apiKey, defaultModel: llm.model }) : new OpenAILLMAdapter({ apiKey: llm.apiKey, baseURL: llm.baseURL, defaultModel: llm.model });
895
+ }
896
+ async function checkLLM(llm) {
897
+ let text;
898
+ try {
899
+ ({ text } = await llm.complete({ system: "Reply with the single word OK.", user: "ping", maxTokens: 1024, temperature: 0 }));
900
+ } catch (error) {
901
+ throw new Error(`The LLM provider rejected the test request: ${error.message}`);
902
+ }
903
+ if (!text.trim()) throw new Error("The LLM provider returned an empty reply to the test request.");
904
+ }
905
+ async function connect(ids, flags) {
906
+ if (ids.length === 0) throw new Error(`Name a harness: memstack connect <${HARNESS_IDS.join("|")}>`);
907
+ const file = readConfigFile();
908
+ if (!file && !process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
909
+ throw new Error("No MemStack configuration found. Run `memstack init` first.");
910
+ }
911
+ const mcp = findMemstackMcp();
912
+ if (!mcp) {
913
+ throw new Error(`memstack-mcp is not installed. MemStack does not install it for you; run:
914
+ ${installCommand(file?.storage?.type)}`);
915
+ }
916
+ for (const id of ids) {
917
+ const adapter = harnessAdapter(id);
918
+ const launch = harnessLaunch(mcp, adapter.id);
919
+ const result = await connectHarness({
920
+ adapter,
921
+ launch,
922
+ dryRun: flags["dry-run"],
923
+ instructions: !flags["no-agents-md"],
924
+ hooks: !flags["no-hooks"]
925
+ });
926
+ const instructions = result.plan.instructions;
927
+ const hook = result.plan.hook;
928
+ if (flags["dry-run"]) {
929
+ out(`${adapter.displayName}: the server works (${result.verification.stats}).`);
930
+ const pending = result.plan.steps.length > 0 || instructions && instructions.change !== "none" || hook && hook.change !== "none";
931
+ out(pending ? "Would run:" : "Already connected; nothing to change.");
932
+ for (const step of result.plan.steps) out(` ${describeStep(step)}`);
933
+ if (instructions && instructions.change !== "none") {
934
+ out(` ${instructions.change === "add" ? "add" : "update"} the marked MemStack block in ${instructions.path}:`);
935
+ for (const line of renderBlock(adapter.instructions.body, adapter.id).trimEnd().split("\n")) out(` ${line}`);
936
+ }
937
+ if (hook && hook.change !== "none") {
938
+ out(` ${hook.change} the MemStack session-start hook in ${hook.path}:`);
939
+ out(` ${sessionStartHook(launch, adapter.id).command}`);
940
+ }
941
+ } else if (result.changed) {
942
+ out(`\u2713 Connected ${adapter.displayName}. ${result.verification.stats}`);
943
+ if (instructions && instructions.change !== "none") {
944
+ out(` ${instructions.change === "add" ? "Added" : "Updated"} MemStack guidance in ${instructions.path} (between memstack:begin/end markers).`);
945
+ }
946
+ if (hook && hook.change !== "none") {
947
+ out(` ${hook.change === "add" ? "Added" : "Updated"} a session-start hook in ${hook.path} that loads project memories.`);
948
+ if (adapter.id === "codex") out(" Codex runs new hooks only after you approve them: open Codex and run /hooks once.");
949
+ }
950
+ out(` ${adapter.id === "claude-code" ? "Restart Claude Code" : "Start a new Codex session"} to load MemStack.`);
951
+ } else {
952
+ out(`\u2713 ${adapter.displayName} is already connected. ${result.verification.stats}`);
953
+ }
954
+ }
955
+ return 0;
956
+ }
957
+ async function disconnect(ids, flags) {
958
+ if (ids.length === 0) throw new Error(`Name a harness: memstack disconnect <${HARNESS_IDS.join("|")}>`);
959
+ for (const id of ids) {
960
+ const adapter = harnessAdapter(id);
961
+ const result = await disconnectHarness({ adapter, dryRun: flags["dry-run"] });
962
+ const removeBlock2 = result.plan.instructions?.change === "remove";
963
+ const removeHook2 = result.plan.hook?.change === "remove";
964
+ if (flags["dry-run"]) {
965
+ const pending = result.plan.steps.length > 0 || removeBlock2 || removeHook2;
966
+ out(pending ? `${adapter.displayName}: would run:` : `${adapter.displayName}: not connected; nothing to change.`);
967
+ for (const step of result.plan.steps) out(` ${describeStep(step)}`);
968
+ if (removeBlock2) out(` remove the marked MemStack block from ${result.plan.instructions.path}`);
969
+ if (removeHook2) out(` remove the MemStack session-start hook from ${result.plan.hook.path}`);
970
+ } else {
971
+ out(result.changed ? `\u2713 Disconnected ${adapter.displayName}. Your memories are kept.` : `${adapter.displayName} was not connected.`);
972
+ }
973
+ }
974
+ return 0;
975
+ }
976
+ var label = (name) => (name ? `${name}:` : "").padEnd(14);
977
+ async function status() {
978
+ const file = readConfigFile();
979
+ const fromEnv = envOverrides();
980
+ const config = file ? configFilePath() : fromEnv.llm || fromEnv.storage ? "none, using environment variables" : "none (run `memstack init`)";
981
+ out(`${label("Config")}${config}`);
982
+ const env = mergeConfigFile(file, process.env);
983
+ const envNote = (overridden) => overridden ? " (from environment variables)" : "";
984
+ if (env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY) {
985
+ const provider = env.ANTHROPIC_API_KEY ? "anthropic" : "openai-compatible";
986
+ const baseURL = provider === "openai-compatible" ? env.MEMSTACK_OPENAI_BASE_URL : void 0;
987
+ out(`${label("LLM")}${provider}${env.MEMSTACK_LLM_MODEL ? ` (${env.MEMSTACK_LLM_MODEL})` : ""}${baseURL ? ` at ${baseURL}` : ""}${envNote(fromEnv.llm)}`);
988
+ } else if (file || fromEnv.llm) {
989
+ out(`${label("LLM")}no API key (run \`memstack init\`)`);
990
+ }
991
+ if (env.MEMSTACK_STORAGE) out(`${label("Storage")}${describeStorage(env)}${envNote(fromEnv.storage)}`);
992
+ const project = resolveProject();
993
+ out(`${label("Project")}${project.id} (${describeSource(project.source)}: ${project.key})`);
994
+ const mcp = findMemstackMcp();
995
+ out(`${label("Server")}${mcp ?? "memstack-mcp not installed"}`);
996
+ for (const id of HARNESS_IDS) {
997
+ const adapter = harnessAdapter(id);
998
+ const state = await adapter.inspect();
999
+ let line;
1000
+ if (!state.installed) line = "not installed";
1001
+ else if (!state.entry) line = "not connected";
1002
+ else if (!mcp) {
1003
+ const present = [state.entry.command, ...state.entry.args.filter((a) => a.startsWith("/"))].every((p) => existsSync(p));
1004
+ line = present ? "connected (memstack-mcp is not on PATH, so updates can't be checked)" : "connected to a command that no longer exists (run `memstack doctor`)";
1005
+ } else if (sameLaunch(state.entry, harnessLaunch(mcp, adapter.id))) line = "connected";
1006
+ else line = "connected with a different command (run `memstack connect` to update)";
1007
+ out(`${label(adapter.displayName)}${line}`);
1008
+ if (adapter.instructions && state.installed) {
1009
+ const block = readBlock(adapter.instructions.path);
1010
+ const current = block === renderBlock(adapter.instructions.body, adapter.id);
1011
+ out(`${label("")}guidance in ${adapter.instructions.path}: ${block === null ? "none" : current ? "present" : "outdated (run `memstack connect`)"}`);
1012
+ }
1013
+ if (adapter.sessionHook && state.installed) {
1014
+ out(`${label("")}session-start hook in ${adapter.sessionHook.path}: ${hookState(adapter, mcp)}`);
1015
+ }
1016
+ }
1017
+ return 0;
1018
+ }
1019
+ function envOverrides() {
1020
+ return {
1021
+ llm: ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MEMSTACK_OPENAI_BASE_URL", "MEMSTACK_LLM_MODEL"].some((key) => process.env[key] !== void 0),
1022
+ storage: process.env.MEMSTACK_STORAGE !== void 0
1023
+ };
1024
+ }
1025
+ function describeStorage(env) {
1026
+ const type = env.MEMSTACK_STORAGE;
1027
+ if (type === "memory") return "memory (lost when the process exits)";
1028
+ const path = type === "sqlite" ? env.SQLITE_PATH : type === "disk" || type === "markdown" ? env.MEMSTACK_DIR : void 0;
1029
+ if (path) return `${type} at ${path}`;
1030
+ const url = type === "postgres" ? env.DATABASE_URL : type === "redis" ? env.REDIS_URL : void 0;
1031
+ return url ? `${type} (URL configured)` : type;
1032
+ }
1033
+ function hookState(adapter, mcp) {
1034
+ const current = readHook(adapter.sessionHook.path);
1035
+ if (!current) return "missing";
1036
+ if (mcp && JSON.stringify(current) !== JSON.stringify(sessionStartHook(harnessLaunch(mcp, adapter.id), adapter.id))) return "outdated";
1037
+ return "present";
1038
+ }
1039
+ async function doctor(flags) {
1040
+ let failures = 0;
1041
+ const pass = (message) => out(`\u2713 ${message}`);
1042
+ const warn = (message) => out(`! ${message}`);
1043
+ const fail2 = (message) => {
1044
+ failures++;
1045
+ out(`\u2717 ${message}`);
1046
+ };
1047
+ let file;
1048
+ try {
1049
+ file = readConfigFile();
1050
+ if (file) pass(`Config file ${configFilePath()}`);
1051
+ else if (envOverrides().llm || envOverrides().storage) pass(`Using environment variables (no config file at ${configFilePath()})`);
1052
+ else warn(`No config file at ${configFilePath()}; run \`memstack init\`.`);
1053
+ } catch (error) {
1054
+ fail2(error.message);
1055
+ }
1056
+ for (const warning of checkConfigPermissions()) warn(warning);
1057
+ const env = mergeConfigFile(file, process.env);
1058
+ if (env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY) {
1059
+ pass(`LLM key configured (${env.ANTHROPIC_API_KEY ? "anthropic" : "openai-compatible"}${env.MEMSTACK_LLM_MODEL ? `, ${env.MEMSTACK_LLM_MODEL}` : ""})`);
1060
+ if (flags.live && file?.llm && !process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
1061
+ try {
1062
+ await checkLLM(buildLLM(file.llm));
1063
+ pass("LLM provider answered a test request");
1064
+ } catch (error) {
1065
+ fail2(error.message);
1066
+ }
1067
+ }
1068
+ } else {
1069
+ fail2("No LLM key. Run `memstack init`.");
1070
+ }
1071
+ const storageType = env.MEMSTACK_STORAGE ?? "memory";
1072
+ if (storageType === "memory") warn("Storage is in-memory: memories are lost when a session ends. Run `memstack init` to choose a store.");
1073
+ else pass(`Storage: ${storageType}`);
1074
+ const mcp = findMemstackMcp();
1075
+ if (!mcp) {
1076
+ fail2(`memstack-mcp is not installed. Run: ${installCommand(storageType)}`);
1077
+ } else {
1078
+ pass(`memstack-mcp at ${mcp}`);
1079
+ const driver = STORAGE_DRIVERS[storageType];
1080
+ if (driver) {
1081
+ if (canResolveFrom(mcp, driver.module)) pass(`Storage driver ${driver.module} is installed`);
1082
+ else fail2(`Storage driver ${driver.module} is not installed next to memstack-mcp. Run: npm install -g ${driver.spec}`);
1083
+ }
1084
+ }
1085
+ const project = resolveProject();
1086
+ pass(`Project ${project.id} (${describeSource(project.source)}: ${project.key})`);
1087
+ for (const id of HARNESS_IDS) {
1088
+ const adapter = harnessAdapter(id);
1089
+ let state;
1090
+ try {
1091
+ state = await adapter.inspect();
1092
+ } catch (error) {
1093
+ fail2(`${adapter.displayName}: ${error.message}`);
1094
+ continue;
1095
+ }
1096
+ if (!state.installed) {
1097
+ out(`- ${adapter.displayName} is not installed`);
1098
+ continue;
1099
+ }
1100
+ if (!state.entry) {
1101
+ warn(`${adapter.displayName} is not connected. Run: memstack connect ${id}`);
1102
+ continue;
1103
+ }
1104
+ const missing = [state.entry.command, ...state.entry.args.filter((a) => a.startsWith("/"))].filter((p) => !existsSync(p));
1105
+ if (missing.length > 0) fail2(`${adapter.displayName} points at ${missing.join(", ")}, which no longer exists. Run: memstack connect ${id}`);
1106
+ else if (mcp && !sameLaunch(state.entry, harnessLaunch(mcp, adapter.id))) warn(`${adapter.displayName} uses a different MemStack command. Run: memstack connect ${id}`);
1107
+ else pass(`${adapter.displayName} is connected`);
1108
+ if (adapter.instructions) {
1109
+ const block = readBlock(adapter.instructions.path);
1110
+ if (block === null) warn(`${adapter.displayName} has no MemStack guidance in ${adapter.instructions.path}, so it may not save memories when asked. Run: memstack connect ${id}`);
1111
+ else if (block !== renderBlock(adapter.instructions.body, adapter.id)) warn(`MemStack guidance in ${adapter.instructions.path} is outdated. Run: memstack connect ${id}`);
1112
+ else pass(`${adapter.displayName} guidance in ${adapter.instructions.path}`);
1113
+ }
1114
+ if (adapter.sessionHook) {
1115
+ const hookStatus = hookState(adapter, mcp);
1116
+ if (hookStatus === "present") {
1117
+ pass(`${adapter.displayName} session-start hook in ${adapter.sessionHook.path}${id === "codex" ? " (approve it once in Codex with /hooks)" : ""}`);
1118
+ } else {
1119
+ warn(`${adapter.displayName} session-start hook is ${hookStatus}. Run: memstack connect ${id}`);
1120
+ }
1121
+ }
1122
+ }
1123
+ if (mcp) {
1124
+ const verification = await verifyServer(harnessLaunch(mcp, "claude-code"));
1125
+ if (verification.ok) pass(`MCP server starts and answers: ${verification.stats}`);
1126
+ else fail2(`MCP server check failed: ${verification.error}`);
1127
+ }
1128
+ out();
1129
+ out(failures === 0 ? "No problems found." : `${failures} problem${failures === 1 ? "" : "s"} found.`);
1130
+ return failures === 0 ? 0 : 1;
1131
+ }
1132
+ async function memories(args, flags) {
1133
+ const { config } = await loadConfig();
1134
+ if (!config.storage) throw new Error("No storage configured. Run `memstack init`.");
1135
+ const project = resolveProject();
1136
+ const namespaces = defaultRecallNamespaces(project.id);
1137
+ const memory = new HarnessMemory({ storage: config.storage, llm: config.llm, autoTags: false });
1138
+ try {
1139
+ await memory.adoptProjects(project.previousIds, project.id);
1140
+ if (flags.delete) {
1141
+ await memory.forget(flags.delete, namespaces);
1142
+ out(`Deleted ${flags.delete}.`);
1143
+ return 0;
1144
+ }
1145
+ const query = args.join(" ").trim() || void 0;
1146
+ const { hits, fallback } = await memory.recall({
1147
+ namespaces: flags.global ? ["global"] : namespaces,
1148
+ query,
1149
+ limit: flags.limit ? Number(flags.limit) : 20,
1150
+ maxChars: 5e4
1151
+ });
1152
+ out(`Project ${project.id} (${project.key})`);
1153
+ if (hits.length === 0) {
1154
+ out("No memories.");
1155
+ return 0;
1156
+ }
1157
+ if (query && fallback) out(`Nothing matched "${query}". The most important memories instead:`);
1158
+ for (const { memory: m } of hits) {
1159
+ const source = m.metadata?.source?.harness;
1160
+ const scope = m.actorId === "global" ? "global" : "project";
1161
+ out(`${m.id} ${m.memoryType}, ${scope}${source ? `, from ${source}` : ""}, ${m.createdAt.toISOString().slice(0, 10)}`);
1162
+ out(` ${m.content}`);
1163
+ }
1164
+ return 0;
1165
+ } finally {
1166
+ await config.storage.close();
1167
+ }
1168
+ }
1169
+ async function projectCommand(args) {
1170
+ const [sub, value] = args;
1171
+ if (sub === "pin") {
1172
+ if (!value) throw new Error("Name the project: memstack project pin <id>");
1173
+ const before = resolveProject();
1174
+ const path = pinProject(value);
1175
+ out(`Pinned this repository to project "${value}" in ${path}. Commit it so every clone uses the same project.`);
1176
+ if (before.id !== value) {
1177
+ out(`Memories stored under the previous ID stay there. To move them: memstack project merge ${before.id}`);
1178
+ }
1179
+ return 0;
1180
+ }
1181
+ if (sub === "merge") {
1182
+ if (!value) throw new Error("Name the old project ID: memstack project merge <old-id>");
1183
+ const oldId = value.replace(/^project:/, "");
1184
+ const project2 = resolveProject();
1185
+ const { config } = await loadConfig();
1186
+ if (!config.storage) throw new Error("No storage configured. Run `memstack init`.");
1187
+ const memory = new HarnessMemory({ storage: config.storage, llm: config.llm, autoTags: false });
1188
+ try {
1189
+ const moved = await memory.moveNamespace(projectNamespace(oldId), projectNamespace(project2.id));
1190
+ out(`Moved ${moved} ${moved === 1 ? "memory" : "memories"} from ${oldId} to ${project2.id}.`);
1191
+ } finally {
1192
+ await config.storage.close();
1193
+ }
1194
+ return 0;
1195
+ }
1196
+ if (sub) throw new Error(`Unknown subcommand "${sub}". Use: memstack project [pin <id> | merge <old-id>]`);
1197
+ const project = resolveProject();
1198
+ out(`${label("Project")}${project.id}`);
1199
+ out(`${label("From")}${describeSource(project.source)}: ${project.key}`);
1200
+ out(`${label("Root")}${project.root}`);
1201
+ return 0;
1202
+ }
1203
+ function describeSource(source) {
1204
+ switch (source) {
1205
+ case "pin":
1206
+ return `pinned in ${PIN_FILE}`;
1207
+ case "root-commit":
1208
+ return "first commit";
1209
+ case "remote":
1210
+ return "origin remote (shallow clone)";
1211
+ case "git":
1212
+ return "git directory (no commits yet)";
1213
+ default:
1214
+ return "directory";
1215
+ }
1216
+ }
1217
+ function createPrompter() {
1218
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1219
+ let muted = false;
1220
+ const output = rl;
1221
+ const write2 = output._writeToOutput.bind(rl);
1222
+ output._writeToOutput = (text) => {
1223
+ if (!muted || text.includes("\n") || text.includes("\r")) write2(text);
1224
+ };
1225
+ return {
1226
+ async ask(question, fallback) {
1227
+ const answer = (await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)).trim();
1228
+ return answer || fallback;
1229
+ },
1230
+ async choose(question, options, fallback) {
1231
+ for (; ; ) {
1232
+ const answer = (await rl.question(`${question} (${options.join(", ")}) [${fallback}]: `)).trim() || fallback;
1233
+ if (options.includes(answer)) return answer;
1234
+ out(`Choose one of: ${options.join(", ")}`);
1235
+ }
1236
+ },
1237
+ async secret(question) {
1238
+ const pending = rl.question(`${question}: `);
1239
+ muted = true;
1240
+ try {
1241
+ return (await pending).trim();
1242
+ } finally {
1243
+ muted = false;
1244
+ out();
1245
+ }
1246
+ },
1247
+ close() {
1248
+ rl.close();
1249
+ }
1250
+ };
1251
+ }
1252
+
1253
+ // src/help.ts
1254
+ var ACTOR = ` --actor <id> Actor ID that groups related memories
1255
+ --project Use this repository's project instead of --actor, the same
1256
+ memories Claude Code and Codex see through \`memstack connect\``;
1257
+ var MEMORY_ENV = `
1258
+ Reads ~/.memstack/config.json (written by \`memstack init\`); environment variables
1259
+ such as OPENAI_API_KEY and MEMSTACK_STORAGE override it. Prints JSON.
1260
+ `;
1261
+ var COMMAND_HELP = {
1262
+ init: `memstack init [flags]
1263
+
1264
+ Choose an LLM provider and a store, check the key with a real request, and write
1265
+ ~/.memstack/config.json (readable only by you). Prompts for anything not given
1266
+ as a flag; with --yes or without a terminal it uses flags and existing values.
1267
+
1268
+ --provider <name> openai-compatible (default) or anthropic
1269
+ --base-url <url> OpenAI-compatible endpoint, e.g. https://api.deepseek.com
1270
+ --model <name> Model override
1271
+ --api-key-env <VAR> Read the API key from this environment variable
1272
+ --store <type> Storage type (default: sqlite)
1273
+ --path <path> File or directory for sqlite, disk, or markdown
1274
+ --url <url> Connection URL for postgres, redis, or mongodb
1275
+ --yes Don't prompt
1276
+ `,
1277
+ connect: `memstack connect <claude-code|codex> [flags]
1278
+
1279
+ Register MemStack's MCP server with a harness, add a session-start hook that
1280
+ loads the project's memories, and for Codex add a marked block to
1281
+ ~/.codex/AGENTS.md. Checks the server first and undoes every change if a step
1282
+ fails. Safe to run again.
1283
+
1284
+ --dry-run Show what would change without changing it
1285
+ --no-hooks Skip the session-start hook
1286
+ --no-agents-md Skip the Codex AGENTS.md block
1287
+ `,
1288
+ disconnect: `memstack disconnect <claude-code|codex> [flags]
1289
+
1290
+ Remove everything \`memstack connect\` added. Memories are kept.
1291
+
1292
+ --dry-run Show what would change without changing it
1293
+ `,
1294
+ status: `memstack status
1295
+
1296
+ Show where configuration comes from (config file or environment variables), the
1297
+ LLM and storage in use, this repository's project ID, and each harness's
1298
+ connection, guidance, and session-start hook.
1299
+ `,
1300
+ doctor: `memstack doctor [flags]
1301
+
1302
+ Check the configuration, storage driver, MCP server, and harness connections,
1303
+ and print a fix for each problem. Exits non-zero when it finds any.
1304
+
1305
+ --live Also send a test request to the LLM provider
1306
+ `,
1307
+ memories: `memstack memories [query] [flags]
1308
+
1309
+ List this repository's memories (project and global), or search them.
1310
+
1311
+ --global Only global memories
1312
+ --limit <n> Maximum to show (default: 20)
1313
+ --delete <id> Delete one memory
1314
+ `,
1315
+ project: `memstack project [pin <id> | merge <old-id>]
1316
+
1317
+ memstack project Show this repository's project ID and where it comes from
1318
+ memstack project pin <id> Write .memstack.json so every clone uses <id>
1319
+ memstack project merge <old> Move memories stored under an old project ID to this one
1320
+ `,
1321
+ store: `memstack store --content <text> (--actor <id> | --project) [flags]
1322
+
1323
+ ${ACTOR}
1324
+ --content <text> Memory text (required)
1325
+ --type <type> interaction (default), summary, observation, fact, reflection,
1326
+ preference, decision, or instruction
1327
+ --importance <n> 0.0 to 1.0
1328
+ --tags <a,b> Comma-separated tags
1329
+ ${MEMORY_ENV}`,
1330
+ retrieve: `memstack retrieve (--actor <id> | --project) [flags]
1331
+
1332
+ ${ACTOR}
1333
+ --query <text> Search text
1334
+ --strategy <name> recent (default), important, semantic, or hybrid
1335
+ --limit <n> Default: 10
1336
+ --tags <a,b> Only memories with these tags
1337
+ --created-after <iso> e.g. 2026-01-01T00:00:00Z
1338
+ --created-before <iso>
1339
+ ${MEMORY_ENV}`,
1340
+ context: `memstack context (--actor <id> | --project) [flags]
1341
+
1342
+ Compile memories into a token-budgeted system prompt.
1343
+
1344
+ ${ACTOR}
1345
+ --max-tokens <n> Token budget (default: 2000)
1346
+ ${MEMORY_ENV}`,
1347
+ summarize: `memstack summarize (--actor <id> | --project) [flags]
1348
+
1349
+ Compress old interactions into summaries.
1350
+
1351
+ ${ACTOR}
1352
+ --older-than <d> Only memories older than this, e.g. 7d, 24h, 30m
1353
+ ${MEMORY_ENV}`,
1354
+ prune: `memstack prune (--actor <id> | --project) [flags]
1355
+
1356
+ ${ACTOR}
1357
+ --type <name> byAge (default), byImportance, or byCount
1358
+ --max-age <d> For byAge, e.g. 30d
1359
+ --min-importance <n> For byImportance
1360
+ --max-count <n> For byCount: keep the top n
1361
+ --dry-run Show what would be removed
1362
+ ${MEMORY_ENV}`,
1363
+ purge: `memstack purge (--actor <id> | --project)
1364
+
1365
+ Delete every memory for the actor.
1366
+
1367
+ ${ACTOR}
1368
+ ${MEMORY_ENV}`,
1369
+ merge: `memstack merge --ids <id,id,...>
1370
+
1371
+ Merge several memories into one.
1372
+ ${MEMORY_ENV}`,
1373
+ stats: `memstack stats [--actor <id> | --project]
1374
+
1375
+ Memory counts and diagnostics; all actors when neither flag is given.
1376
+ ${MEMORY_ENV}`,
1377
+ delete: `memstack delete --id <id>
1378
+ ${MEMORY_ENV}`,
1379
+ health: `memstack health
1380
+
1381
+ Check storage, LLM, and embedding connectivity.
1382
+ ${MEMORY_ENV}`,
1383
+ export: `memstack export [--actor <id> | --project] [--out <file>]
1384
+
1385
+ Export memories as a JSON snapshot; all actors when neither flag is given.
1386
+ ${MEMORY_ENV}`,
1387
+ import: `memstack import (--actor <id> | --project) --file <file>
1388
+
1389
+ Import memories from a JSON snapshot.
1390
+
1391
+ ${ACTOR}
1392
+ --file <file> Snapshot path (required)
1393
+ ${MEMORY_ENV}`
1394
+ };
93
1395
 
94
1396
  // src/cli.ts
1397
+ process.stdout.on("error", (error) => {
1398
+ if (error.code === "EPIPE") process.exit(0);
1399
+ throw error;
1400
+ });
95
1401
  async function main() {
96
1402
  const { positionals, values } = parseArgs({
97
1403
  allowPositionals: true,
@@ -115,20 +1421,56 @@ async function main() {
115
1421
  "created-before": { type: "string" },
116
1422
  "dry-run": { type: "boolean" },
117
1423
  out: { type: "string" },
118
- file: { type: "string" }
1424
+ file: { type: "string" },
1425
+ provider: { type: "string" },
1426
+ "base-url": { type: "string" },
1427
+ model: { type: "string" },
1428
+ "api-key-env": { type: "string" },
1429
+ store: { type: "string" },
1430
+ path: { type: "string" },
1431
+ url: { type: "string" },
1432
+ yes: { type: "boolean" },
1433
+ live: { type: "boolean" },
1434
+ global: { type: "boolean" },
1435
+ delete: { type: "string" },
1436
+ "no-agents-md": { type: "boolean" },
1437
+ "no-hooks": { type: "boolean" },
1438
+ project: { type: "boolean" },
1439
+ help: { type: "boolean", short: "h" },
1440
+ version: { type: "boolean", short: "v" }
119
1441
  }
120
1442
  });
1443
+ if (values.version) {
1444
+ process.stdout.write(`${version()}
1445
+ `);
1446
+ return;
1447
+ }
121
1448
  const command = positionals[0];
1449
+ if (command === "help" || values.help) {
1450
+ const topic = command === "help" ? positionals[1] : command;
1451
+ if (topic && !COMMAND_HELP[topic]) fail(`Unknown command: ${topic}. Run memstack --help for the list.`);
1452
+ process.stdout.write(topic ? COMMAND_HELP[topic] : USAGE);
1453
+ return;
1454
+ }
122
1455
  if (!command) {
123
- printUsage();
1456
+ process.stderr.write(USAGE);
124
1457
  process.exit(1);
125
1458
  }
126
- const config = await loadConfig();
1459
+ if (HARNESS_COMMANDS.includes(command)) {
1460
+ process.exitCode = await runHarnessCommand(command, positionals.slice(1), values);
1461
+ return;
1462
+ }
1463
+ if (!COMMAND_HELP[command]) fail(`Unknown command: ${command}. Run memstack --help for the list.`);
1464
+ if (values.project) {
1465
+ if (values.actor) fail("Use either --actor or --project, not both");
1466
+ values.actor = projectNamespace(resolveProject().id);
1467
+ }
1468
+ const config = await loadConfig2();
127
1469
  const ms = new MemStack(config);
128
1470
  let result;
129
1471
  switch (command) {
130
1472
  case "store": {
131
- if (!values.actor) fail("--actor is required");
1473
+ if (!values.actor) fail("--actor (or --project) is required");
132
1474
  if (!values.content) fail("--content is required");
133
1475
  const importance = values.importance ? Math.max(0, Math.min(1, Number(values.importance))) : void 0;
134
1476
  result = await ms.memory.store({
@@ -141,7 +1483,7 @@ async function main() {
141
1483
  break;
142
1484
  }
143
1485
  case "retrieve": {
144
- if (!values.actor) fail("--actor is required");
1486
+ if (!values.actor) fail("--actor (or --project) is required");
145
1487
  result = await ms.memory.retrieve({
146
1488
  actorId: values.actor,
147
1489
  query: values.query,
@@ -154,7 +1496,7 @@ async function main() {
154
1496
  break;
155
1497
  }
156
1498
  case "context": {
157
- if (!values.actor) fail("--actor is required");
1499
+ if (!values.actor) fail("--actor (or --project) is required");
158
1500
  result = await ms.memory.compileContext({
159
1501
  actorId: String(values.actor),
160
1502
  maxTokens: values["max-tokens"] ? Number(values["max-tokens"]) : void 0
@@ -162,7 +1504,7 @@ async function main() {
162
1504
  break;
163
1505
  }
164
1506
  case "summarize": {
165
- if (!values.actor) fail("--actor is required");
1507
+ if (!values.actor) fail("--actor (or --project) is required");
166
1508
  const durationMs = values["older-than"] ? parseDuration(String(values["older-than"])) : void 0;
167
1509
  const olderThan = durationMs ? new Date(Date.now() - durationMs) : void 0;
168
1510
  result = await ms.memory.summarize({
@@ -172,7 +1514,7 @@ async function main() {
172
1514
  break;
173
1515
  }
174
1516
  case "prune": {
175
- if (!values.actor) fail("--actor is required");
1517
+ if (!values.actor) fail("--actor (or --project) is required");
176
1518
  const VALID_TYPES = ["byAge", "byImportance", "byCount", "byType", "custom", "compose"];
177
1519
  const pruneType = values.type ?? "byAge";
178
1520
  if (!VALID_TYPES.includes(String(pruneType))) {
@@ -193,7 +1535,7 @@ async function main() {
193
1535
  break;
194
1536
  }
195
1537
  case "purge": {
196
- if (!values.actor) fail("--actor is required");
1538
+ if (!values.actor) fail("--actor (or --project) is required");
197
1539
  result = await ms.memory.purgeActor(String(values.actor));
198
1540
  break;
199
1541
  }
@@ -219,32 +1561,31 @@ async function main() {
219
1561
  break;
220
1562
  }
221
1563
  case "export": {
222
- const snapshot = await ms.export(values.actor);
1564
+ const snapshot2 = await ms.export(values.actor);
223
1565
  const outPath = values.out;
224
1566
  if (outPath) {
225
- await writeFile(outPath, JSON.stringify(snapshot, null, 2), "utf-8");
1567
+ await writeFile(outPath, JSON.stringify(snapshot2, null, 2), "utf-8");
226
1568
  }
227
- result = outPath ? { saved: outPath, count: snapshot.memories.length } : snapshot;
1569
+ result = outPath ? { saved: outPath, count: snapshot2.memories.length } : snapshot2;
228
1570
  break;
229
1571
  }
230
1572
  case "import": {
231
- if (!values.actor) fail("--actor is required");
1573
+ if (!values.actor) fail("--actor (or --project) is required");
232
1574
  if (!values.file) fail("--file is required");
233
1575
  const raw = await readFile(String(values.file), "utf-8");
234
- let snapshot = JSON.parse(raw);
235
- if (Array.isArray(snapshot)) {
236
- snapshot = { version: 1, memories: snapshot, exportedAt: (/* @__PURE__ */ new Date()).toISOString() };
1576
+ let snapshot2 = JSON.parse(raw);
1577
+ if (Array.isArray(snapshot2)) {
1578
+ snapshot2 = { version: 1, memories: snapshot2, exportedAt: (/* @__PURE__ */ new Date()).toISOString() };
237
1579
  }
238
- if (!snapshot.memories || !Array.isArray(snapshot.memories) || snapshot.memories.length === 0) {
1580
+ if (!snapshot2.memories || !Array.isArray(snapshot2.memories) || snapshot2.memories.length === 0) {
239
1581
  fail("Snapshot contains no memories to import");
240
1582
  }
241
- await ms.import(snapshot);
242
- result = { imported: snapshot.memories.length };
1583
+ await ms.import(snapshot2);
1584
+ result = { imported: snapshot2.memories.length };
243
1585
  break;
244
1586
  }
245
1587
  default:
246
- printUsage();
247
- process.exit(1);
1588
+ fail(`Unknown command: ${command}. Run memstack --help for the list.`);
248
1589
  }
249
1590
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
250
1591
  await ms.close();
@@ -270,8 +1611,21 @@ function parseDuration(input) {
270
1611
  return value;
271
1612
  }
272
1613
  }
273
- function printUsage() {
274
- process.stderr.write(`memstack <command> [flags]
1614
+ function version() {
1615
+ return createRequire(import.meta.url)("../package.json").version;
1616
+ }
1617
+ var USAGE = `memstack <command> [flags]
1618
+
1619
+ Agent harnesses (Claude Code, Codex):
1620
+ init Choose an LLM provider and store; writes ~/.memstack/config.json
1621
+ connect Register MemStack with a harness: connect claude-code|codex [--dry-run]
1622
+ Adds a session-start hook that loads project memories (skip with --no-hooks)
1623
+ For Codex, also adds a marked block to ~/.codex/AGENTS.md (skip with --no-agents-md)
1624
+ disconnect Remove MemStack from a harness; memories are kept
1625
+ status Show configuration, project, and harness connections
1626
+ doctor Diagnose setup problems [--live to test the LLM key]
1627
+ memories List or search this project's memories [query] [--delete <id>]
1628
+ project Show this repository's project ID; project pin <id>; project merge <old-id>
275
1629
 
276
1630
  Commands:
277
1631
  store Store a memory
@@ -287,9 +1641,8 @@ Commands:
287
1641
  export Export all memories
288
1642
  import Import memories from a JSON snapshot
289
1643
 
290
- Run memstack <command> --help for flags.
291
- `);
292
- }
1644
+ Run memstack <command> --help for flags, or memstack --version.
1645
+ `;
293
1646
  function fail(message) {
294
1647
  process.stderr.write(`Error: ${message}
295
1648
  `);