zmemory 0.1.0 → 0.1.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/README.md CHANGED
@@ -19,6 +19,10 @@ ZMemory stores all coordination state inside the project in a `.zmemory/` folder
19
19
 
20
20
  # Install
21
21
 
22
+ Requirements:
23
+
24
+ - Node.js >=20
25
+
22
26
  ```
23
27
  npm install -g zmemory
24
28
  ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ await import("../src/mcp/server.js");
package/bin/zmemory.js CHANGED
@@ -49,12 +49,12 @@ const args = process.argv.slice(2);
49
49
  const cmd = args[0];
50
50
 
51
51
  // Global flags
52
- if (args.includes("-v") || args.includes("--version")) {
52
+ if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
53
53
  await versionCommand();
54
54
  process.exit(0);
55
55
  }
56
56
 
57
- if (args.includes("-h") || args.includes("--help")) {
57
+ if (args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
58
58
  await helpCommand();
59
59
  process.exit(0);
60
60
  }
@@ -414,12 +414,10 @@ async function main() {
414
414
  await doctor();
415
415
  break;
416
416
  case "mcp":
417
- console.error("ZMemory MCP server started (stdio mode)");
418
417
  await import("../src/mcp/server.js");
419
418
  break;
420
419
  default:
421
- console.log("ZMemory CLI");
422
- console.log("Commands: init status start-run context event handoff decision failure next summary");
420
+ await helpCommand();
423
421
  }
424
422
  }
425
423
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "zmemory",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local-first shared memory layer for AI coding agents",
5
5
  "bin": {
6
- "zmemory": "bin/zmemory.js"
6
+ "zmemory": "bin/zmemory.js",
7
+ "zmemory-mcp": "bin/zmemory-mcp.js"
7
8
  },
8
9
  "keywords": [
9
10
  "ai",
@@ -36,7 +37,6 @@
36
37
  "prepare": "node scripts/prepare.js"
37
38
  },
38
39
  "dependencies": {
39
- "picomatch": "^4.0.0",
40
40
  "@clack/prompts": "^0.7.0"
41
41
  }
42
42
  }
@@ -1,7 +1,12 @@
1
1
  export async function helpCommand() {
2
- console.log("ZMemory");
2
+ console.log("ZMemory CLI");
3
3
  console.log("Shared project memory for AI agents\n");
4
4
 
5
+ console.log("Common commands:");
6
+ console.log(" zmemory setup Interactive platform setup");
7
+ console.log(" zmemory init Initialize .zmemory in this repo");
8
+ console.log(" zmemory bootstrap Ensure .zmemory structure exists\n");
9
+
5
10
  console.log("Core:");
6
11
  console.log(" zmemory setup Interactive platform setup");
7
12
  console.log(" zmemory init Initialize .zmemory in this repo");
@@ -10,6 +10,8 @@ export async function initProject() {
10
10
  ensureDir(path.join(base, "logs"));
11
11
  ensureDir(path.join(base, "packets"));
12
12
  ensureDir(path.join(base, "skills"));
13
+ ensureDir(path.join(base, "agents"));
14
+ ensureDir(path.join(base, "claims"));
13
15
 
14
16
  writeJSON(path.join(base, "config.json"), {
15
17
  project_name: "project",
@@ -1,7 +1,8 @@
1
1
  import fs from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
- import { multiselect, confirm, intro, outro } from "@clack/prompts";
4
+
5
+ const MCP_COMMAND = "zmemory-mcp";
5
6
 
6
7
  function ensureDir(p) {
7
8
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -12,11 +13,31 @@ function writeJSONMerge(file, patch) {
12
13
  try {
13
14
  if (fs.existsSync(file)) data = JSON.parse(fs.readFileSync(file, "utf8"));
14
15
  } catch {}
15
- const merged = { ...data, ...patch };
16
+
17
+ const merged = deepMerge(data, patch);
16
18
  ensureDir(file);
17
19
  fs.writeFileSync(file, JSON.stringify(merged, null, 2));
18
20
  }
19
21
 
22
+ function deepMerge(base, patch) {
23
+ const out = { ...base };
24
+ for (const [key, value] of Object.entries(patch)) {
25
+ if (value && typeof value === "object" && !Array.isArray(value)) {
26
+ out[key] = deepMerge(out[key] && typeof out[key] === "object" && !Array.isArray(out[key]) ? out[key] : {}, value);
27
+ } else {
28
+ out[key] = value;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function removePluginValue(data, value) {
35
+ if (Array.isArray(data.plugin)) {
36
+ data.plugin = data.plugin.filter((p) => p !== value);
37
+ if (!data.plugin.length) delete data.plugin;
38
+ }
39
+ }
40
+
20
41
  function installClaude() {
21
42
  const file = path.join(os.homedir(), ".claude", "settings.json");
22
43
  let data = {};
@@ -25,7 +46,7 @@ function installClaude() {
25
46
  } catch {}
26
47
 
27
48
  data.mcpServers = data.mcpServers || {};
28
- data.mcpServers.zmemory = { command: "zmemory mcp" };
49
+ data.mcpServers.zmemory = { command: MCP_COMMAND };
29
50
 
30
51
  ensureDir(file);
31
52
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
@@ -38,8 +59,11 @@ function installCodex() {
38
59
  if (fs.existsSync(file)) content = fs.readFileSync(file, "utf8");
39
60
  } catch {}
40
61
 
41
- if (!content.includes("[mcp_servers.zmemory]")) {
42
- content += `\n[mcp_servers.zmemory]\ncommand = "zmemory mcp"\n`;
62
+ const block = `[mcp_servers.zmemory]\ncommand = "${MCP_COMMAND}"\n`;
63
+ if (content.includes("[mcp_servers.zmemory]")) {
64
+ content = content.replace(/\n?\[mcp_servers\.zmemory\][\s\S]*?(?=\n\[|$)/, `\n${block}`);
65
+ } else {
66
+ content += `\n${block}`;
43
67
  }
44
68
 
45
69
  ensureDir(file);
@@ -54,7 +78,7 @@ function installCursor() {
54
78
  } catch {}
55
79
 
56
80
  data.mcpServers = data.mcpServers || {};
57
- data.mcpServers.zmemory = { command: "zmemory mcp" };
81
+ data.mcpServers.zmemory = { command: MCP_COMMAND };
58
82
 
59
83
  ensureDir(file);
60
84
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
@@ -67,8 +91,12 @@ function installOpenCode() {
67
91
  if (fs.existsSync(file)) data = JSON.parse(fs.readFileSync(file, "utf8"));
68
92
  } catch {}
69
93
 
70
- data.plugin = data.plugin || [];
71
- if (!data.plugin.includes("zmemory")) data.plugin.push("zmemory");
94
+ removePluginValue(data, "zmemory");
95
+ data.mcp = data.mcp || {};
96
+ data.mcp.zmemory = {
97
+ type: "local",
98
+ command: [MCP_COMMAND]
99
+ };
72
100
 
73
101
  ensureDir(file);
74
102
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
@@ -81,28 +109,36 @@ function installKilo() {
81
109
  if (fs.existsSync(file)) data = JSON.parse(fs.readFileSync(file, "utf8"));
82
110
  } catch {}
83
111
 
84
- data.plugin = data.plugin || [];
85
- if (!data.plugin.includes("zmemory")) data.plugin.push("zmemory");
112
+ data.$schema = "https://app.kilo.ai/config.json";
113
+ removePluginValue(data, "zmemory");
114
+ if (data.mcpServers?.zmemory) delete data.mcpServers.zmemory;
115
+ if (data.mcpServers && !Object.keys(data.mcpServers).length) delete data.mcpServers;
116
+ data.mcp = data.mcp || {};
117
+ data.mcp.zmemory = {
118
+ type: "local",
119
+ command: [MCP_COMMAND]
120
+ };
86
121
 
87
122
  ensureDir(file);
88
123
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
89
124
  }
90
125
 
91
126
  function installAGY() {
92
- const file = path.join(os.homedir(), ".copilot", "mcp-config.json");
127
+ const file = path.join(os.homedir(), ".gemini", "config", "mcp_config.json");
93
128
  writeJSONMerge(file, {
94
- mcpServers: { zmemory: { command: "zmemory mcp" } }
129
+ mcpServers: { zmemory: { command: MCP_COMMAND } }
95
130
  });
96
131
  }
97
132
 
98
133
  function installKimi() {
99
134
  const file = path.join(os.homedir(), ".kimi-code", "mcp.json");
100
135
  writeJSONMerge(file, {
101
- mcpServers: { zmemory: { command: "zmemory mcp" } }
136
+ mcpServers: { zmemory: { command: MCP_COMMAND, args: [] } }
102
137
  });
103
138
  }
104
139
 
105
140
  export async function setupCommand() {
141
+ const { multiselect, confirm, intro, outro, isCancel } = await import("@clack/prompts");
106
142
  intro("ZMemory Setup");
107
143
 
108
144
  const selected = await multiselect({
@@ -118,7 +154,7 @@ export async function setupCommand() {
118
154
  ]
119
155
  });
120
156
 
121
- if (!selected || !selected.length) {
157
+ if (isCancel(selected) || !selected || !selected.length) {
122
158
  outro("No platforms selected.");
123
159
  return;
124
160
  }
@@ -132,7 +168,7 @@ export async function setupCommand() {
132
168
  if (selected.includes("kimi")) installKimi();
133
169
 
134
170
  const initRepo = await confirm({ message: "Initialize .zmemory in this repo?" });
135
- if (initRepo) {
171
+ if (!isCancel(initRepo) && initRepo) {
136
172
  const { initProject } = await import("./init.js");
137
173
  await initProject();
138
174
  }
@@ -14,9 +14,14 @@ export function ensureZMemory() {
14
14
  fs.mkdirSync(path.join(dir, "runs/active"), { recursive: true });
15
15
  fs.mkdirSync(path.join(dir, "runs/archived"), { recursive: true });
16
16
  fs.mkdirSync(path.join(dir, "logs"), { recursive: true });
17
+ fs.mkdirSync(path.join(dir, "agents"), { recursive: true });
18
+ fs.mkdirSync(path.join(dir, "claims"), { recursive: true });
17
19
  fs.writeFileSync(path.join(dir, "logs/events.jsonl"), "");
18
20
  }
19
21
 
22
+ fs.mkdirSync(path.join(dir, "agents"), { recursive: true });
23
+ fs.mkdirSync(path.join(dir, "claims"), { recursive: true });
24
+
20
25
  const skillsDir = path.join(dir, "skills");
21
26
  fs.mkdirSync(skillsDir, { recursive: true });
22
27
  const skillFile = path.join(skillsDir, "coordination.md");
package/src/mcp/server.js CHANGED
@@ -9,12 +9,14 @@ import { planCommand } from "../commands/plan.js";
9
9
  import { routeRun } from "../commands/route.js";
10
10
  import { readJSON, writeJSON } from "../lib/fs.js";
11
11
  import { acquireLock, releaseLock } from "../lib/lock.js";
12
+ import { claimPattern, listClaims, releaseClaims } from "../lib/coordination/claims.js";
13
+ import { who as whoLookup } from "../lib/coordination/who.js";
14
+
12
15
  const EVENTS_FILE = `.zmemory/logs/events.jsonl`;
13
16
  const ARCHIVE_DIR = `.zmemory/logs/archive`;
14
17
  const MAX_ACTIVE_EVENTS = 5000;
15
18
  const ARCHIVE_BATCH = 2000;
16
- import { claimPattern, listClaims, releaseClaims } from "../lib/coordination/claims.js";
17
- import { who as whoLookup } from "../lib/coordination/who.js";
19
+ const SERVER_VERSION = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
18
20
 
19
21
  // ---- Tool Contracts (source of truth) ----
20
22
  const toolContracts = {
@@ -324,6 +326,139 @@ try {
324
326
  let buffer = "";
325
327
  let lineQueue = Promise.resolve();
326
328
 
329
+ function writeProtocolJSON(value) {
330
+ process.stdout.write(JSON.stringify(value) + "\n");
331
+ }
332
+
333
+ function typeToJSONSchema(type) {
334
+ if (type === "number") return { type: "number" };
335
+ if (type === "boolean") return { type: "boolean" };
336
+ if (type === "array:string") return { type: "array", items: { type: "string", minLength: 1 } };
337
+ if (type === "run_id" || type === "nonempty:string") return { type: "string", minLength: 1 };
338
+ return { type: "string" };
339
+ }
340
+
341
+ function contractToMCPTool(name, contract) {
342
+ const required = Object.keys(contract.input.required || {});
343
+ const properties = {};
344
+ for (const [key, type] of Object.entries(contract.input.required || {})) {
345
+ properties[key] = typeToJSONSchema(type);
346
+ }
347
+ for (const [key, type] of Object.entries(contract.input.optional || {})) {
348
+ properties[key] = typeToJSONSchema(type);
349
+ }
350
+
351
+ return {
352
+ name,
353
+ description: contract.description,
354
+ inputSchema: {
355
+ type: "object",
356
+ properties,
357
+ required,
358
+ additionalProperties: false
359
+ }
360
+ };
361
+ }
362
+
363
+ async function invokeTool(toolName, input) {
364
+ const fn = tools[toolName];
365
+ if (!fn) throw new Error("unknown tool");
366
+
367
+ let capturedOut = "";
368
+ let capturedErr = "";
369
+ const origLog = console.log;
370
+ const origErr = console.error;
371
+
372
+ console.log = (...a) => {
373
+ capturedOut += a.map(v => String(v)).join(" ") + "\n";
374
+ };
375
+ console.error = (...a) => {
376
+ capturedErr += a.map(v => String(v)).join(" ") + "\n";
377
+ };
378
+
379
+ try {
380
+ validateInput(toolName, input);
381
+ const raw = await fn(input);
382
+ return {
383
+ result: raw === undefined ? null : raw,
384
+ stdout: capturedOut,
385
+ stderr: capturedErr
386
+ };
387
+ } finally {
388
+ console.log = origLog;
389
+ console.error = origErr;
390
+ }
391
+ }
392
+
393
+ function jsonRpcResult(id, result) {
394
+ writeProtocolJSON({ jsonrpc: "2.0", id, result });
395
+ }
396
+
397
+ function jsonRpcError(id, code, message) {
398
+ writeProtocolJSON({ jsonrpc: "2.0", id, error: { code, message } });
399
+ }
400
+
401
+ function mcpToolContent(payload) {
402
+ const hasOnlyResult = !payload.stdout && !payload.stderr;
403
+ const value = hasOnlyResult ? payload.result : payload;
404
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
405
+ return { content: [{ type: "text", text }] };
406
+ }
407
+
408
+ async function handleJsonRpcMessage(msg) {
409
+ const id = msg.id ?? null;
410
+ const method = msg.method;
411
+
412
+ if (method === "notifications/initialized" || method === "initialized") return;
413
+
414
+ if (method === "initialize") {
415
+ jsonRpcResult(id, {
416
+ protocolVersion: msg.params?.protocolVersion || "2024-11-05",
417
+ capabilities: { tools: {} },
418
+ serverInfo: { name: "zmemory", version: SERVER_VERSION }
419
+ });
420
+ return;
421
+ }
422
+
423
+ if (method === "ping") {
424
+ jsonRpcResult(id, {});
425
+ return;
426
+ }
427
+
428
+ if (method === "tools/list") {
429
+ jsonRpcResult(id, {
430
+ tools: Object.entries(toolContracts).map(([name, contract]) => contractToMCPTool(name, contract))
431
+ });
432
+ return;
433
+ }
434
+
435
+ if (method === "tools/call") {
436
+ const name = msg.params?.name;
437
+ const input = msg.params?.arguments || {};
438
+ if (typeof name !== "string" || !name.trim()) {
439
+ jsonRpcError(id, -32602, "invalid tool name");
440
+ return;
441
+ }
442
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
443
+ jsonRpcError(id, -32602, "invalid tool arguments");
444
+ return;
445
+ }
446
+
447
+ try {
448
+ const payload = await invokeTool(name.trim(), input);
449
+ jsonRpcResult(id, mcpToolContent(payload));
450
+ } catch (e) {
451
+ jsonRpcResult(id, {
452
+ isError: true,
453
+ content: [{ type: "text", text: e?.message || String(e) }]
454
+ });
455
+ }
456
+ return;
457
+ }
458
+
459
+ jsonRpcError(id, -32601, "method not found");
460
+ }
461
+
327
462
 
328
463
 
329
464
  async function handleLine(line) {
@@ -333,7 +468,7 @@ async function handleLine(line) {
333
468
  try {
334
469
  msg = JSON.parse(line);
335
470
  } catch {
336
- process.stdout.write(JSON.stringify({ error: "malformed json" }) + "\n");
471
+ writeProtocolJSON({ error: "malformed json" });
337
472
  return;
338
473
  }
339
474
 
@@ -341,64 +476,42 @@ async function handleLine(line) {
341
476
  if (!msg || typeof msg !== "object" || Array.isArray(msg)) {
342
477
  const out = { error: "invalid message" };
343
478
  if (id !== undefined) out.id = id;
344
- process.stdout.write(JSON.stringify(out) + "\n");
479
+ writeProtocolJSON(out);
480
+ return;
481
+ }
482
+
483
+ if (typeof msg.method === "string") {
484
+ await handleJsonRpcMessage(msg);
345
485
  return;
346
486
  }
347
487
 
348
488
  if (typeof msg.tool !== "string" || !msg.tool.trim()) {
349
489
  const out = { error: "invalid message: tool" };
350
490
  if (id !== undefined) out.id = id;
351
- process.stdout.write(JSON.stringify(out) + "\n");
491
+ writeProtocolJSON(out);
352
492
  return;
353
493
  }
354
494
 
355
495
  if (msg.input !== undefined && (msg.input === null || typeof msg.input !== "object" || Array.isArray(msg.input))) {
356
496
  const out = { error: "invalid message: input" };
357
497
  if (id !== undefined) out.id = id;
358
- process.stdout.write(JSON.stringify(out) + "\n");
498
+ writeProtocolJSON(out);
359
499
  return;
360
500
  }
361
501
  const toolName = msg.tool.trim();
362
- const fn = tools[toolName];
363
- if (!fn) {
364
- const out = { error: "unknown tool" };
365
- if (id !== undefined) out.id = id;
366
- process.stdout.write(JSON.stringify(out) + "\n");
367
- return;
368
- }
369
-
370
- let capturedOut = "";
371
- let capturedErr = "";
372
-
373
- const origLog = console.log;
374
- const origErr = console.error;
375
-
376
- console.log = (...a) => {
377
- capturedOut += a.map(v => String(v)).join(" ") + "\n";
378
- };
379
- console.error = (...a) => {
380
- capturedErr += a.map(v => String(v)).join(" ") + "\n";
381
- };
382
502
 
383
503
  try {
384
504
  const input = msg.input || {};
385
- validateInput(toolName, input);
386
- const raw = await fn(input);
387
- const result = raw === undefined ? null : raw;
388
- const out = { result };
505
+ const payload = await invokeTool(toolName, input);
506
+ const out = { result: payload.result };
389
507
  if (id !== undefined) out.id = id;
390
- if (capturedOut) out.stdout = capturedOut;
391
- if (capturedErr) out.stderr = capturedErr;
392
- process.stdout.write(JSON.stringify(out) + "\n");
508
+ if (payload.stdout) out.stdout = payload.stdout;
509
+ if (payload.stderr) out.stderr = payload.stderr;
510
+ writeProtocolJSON(out);
393
511
  } catch (e) {
394
512
  const out = { error: e?.message || String(e) };
395
513
  if (id !== undefined) out.id = id;
396
- if (capturedOut) out.stdout = capturedOut;
397
- if (capturedErr) out.stderr = capturedErr;
398
- process.stdout.write(JSON.stringify(out) + "\n");
399
- } finally {
400
- console.log = origLog;
401
- console.error = origErr;
514
+ writeProtocolJSON(out);
402
515
  }
403
516
  }
404
517