zmemory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/adapters/claude-code-adapter.sh +11 -0
  4. package/adapters/codex-adapter.sh +12 -0
  5. package/adapters/cursor-adapter.sh +14 -0
  6. package/adapters/opencode-adapter.sh +9 -0
  7. package/bin/zmemory.js +426 -0
  8. package/package.json +42 -0
  9. package/src/bus/server.js +51 -0
  10. package/src/commands/agent.js +42 -0
  11. package/src/commands/archive.js +32 -0
  12. package/src/commands/bootstrap.js +7 -0
  13. package/src/commands/bus.js +6 -0
  14. package/src/commands/claim.js +66 -0
  15. package/src/commands/config.js +29 -0
  16. package/src/commands/context.js +269 -0
  17. package/src/commands/daemon.js +6 -0
  18. package/src/commands/decision.js +12 -0
  19. package/src/commands/doctor.js +100 -0
  20. package/src/commands/event.js +149 -0
  21. package/src/commands/exec.js +25 -0
  22. package/src/commands/explain.js +68 -0
  23. package/src/commands/failure.js +12 -0
  24. package/src/commands/gitHook.js +22 -0
  25. package/src/commands/handoff.js +101 -0
  26. package/src/commands/health.js +18 -0
  27. package/src/commands/help.js +33 -0
  28. package/src/commands/index.js +21 -0
  29. package/src/commands/init.js +38 -0
  30. package/src/commands/install.js +28 -0
  31. package/src/commands/locks.js +9 -0
  32. package/src/commands/next.js +59 -0
  33. package/src/commands/orchestrator.js +6 -0
  34. package/src/commands/plan.js +30 -0
  35. package/src/commands/resume.js +79 -0
  36. package/src/commands/route.js +67 -0
  37. package/src/commands/runs.js +47 -0
  38. package/src/commands/search.js +159 -0
  39. package/src/commands/setup.js +148 -0
  40. package/src/commands/skill.js +103 -0
  41. package/src/commands/startRun.js +82 -0
  42. package/src/commands/state.js +68 -0
  43. package/src/commands/status.js +51 -0
  44. package/src/commands/stream.js +27 -0
  45. package/src/commands/summary.js +65 -0
  46. package/src/commands/sync.js +20 -0
  47. package/src/commands/tasks.js +89 -0
  48. package/src/commands/timeline.js +75 -0
  49. package/src/commands/upgrade.js +19 -0
  50. package/src/commands/version.js +12 -0
  51. package/src/commands/who.js +26 -0
  52. package/src/commands/worker.js +71 -0
  53. package/src/commands/workspaces.js +25 -0
  54. package/src/daemon/orchestrator.js +34 -0
  55. package/src/daemon/zmemoryd.js +17 -0
  56. package/src/dashboard/server.js +40 -0
  57. package/src/integrations/agent-hooks.md +39 -0
  58. package/src/integrations/auto-bootstrap.js +90 -0
  59. package/src/integrations/harness-adapter.md +39 -0
  60. package/src/lib/agents.js +36 -0
  61. package/src/lib/config.js +15 -0
  62. package/src/lib/coordination/agents.js +49 -0
  63. package/src/lib/coordination/claims.js +127 -0
  64. package/src/lib/coordination/who.js +49 -0
  65. package/src/lib/fileIndex.js +36 -0
  66. package/src/lib/fileLocks.js +44 -0
  67. package/src/lib/fs.js +140 -0
  68. package/src/lib/lock.js +103 -0
  69. package/src/lib/repoScan.js +19 -0
  70. package/src/lib/run.js +63 -0
  71. package/src/lib/sqlite.js +7 -0
  72. package/src/lib/tasks.js +77 -0
  73. package/src/lib/teamSync.js +36 -0
  74. package/src/lib/workspaces.js +29 -0
  75. package/src/mcp/autostart.js +12 -0
  76. package/src/mcp/server.js +420 -0
  77. package/src/mcp/tools.json +28 -0
@@ -0,0 +1,90 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import { registerAgent } from "../lib/coordination/agents.js";
5
+
6
+ // Automatically ensure ZMemory is initialized when a repo is opened
7
+ export function ensureZMemory() {
8
+ const dir = ".zmemory";
9
+ if (!fs.existsSync(dir)) {
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ fs.writeFileSync(path.join(dir, "project.md"), "# Project Memory\n");
12
+ fs.writeFileSync(path.join(dir, "decisions.md"), "# Decisions\n");
13
+ fs.writeFileSync(path.join(dir, "failures.md"), "# Failures\n");
14
+ fs.mkdirSync(path.join(dir, "runs/active"), { recursive: true });
15
+ fs.mkdirSync(path.join(dir, "runs/archived"), { recursive: true });
16
+ fs.mkdirSync(path.join(dir, "logs"), { recursive: true });
17
+ fs.writeFileSync(path.join(dir, "logs/events.jsonl"), "");
18
+ }
19
+
20
+ const skillsDir = path.join(dir, "skills");
21
+ fs.mkdirSync(skillsDir, { recursive: true });
22
+ const skillFile = path.join(skillsDir, "coordination.md");
23
+ if (!fs.existsSync(skillFile)) {
24
+ const skill = `# ZMemory Coordination
25
+
26
+ This repository uses **ZMemory** for multi‑agent coordination.
27
+
28
+ Workflow:
29
+
30
+ 1. Inspect project state
31
+ zmemory resume
32
+
33
+ 2. Before editing a file
34
+ zmemory who <file>
35
+
36
+ 3. When starting work on a module
37
+ zmemory claim <pattern> --session <id>
38
+
39
+ 4. Log meaningful work
40
+ zmemory event --summary "what you did" --files <file>
41
+
42
+ 5. When work is complete
43
+ zmemory release --session <id>
44
+
45
+ ZMemory provides shared awareness across agents so avoid editing files
46
+ that are actively claimed unless coordination is intentional.
47
+ `;
48
+ fs.writeFileSync(skillFile, skill);
49
+ }
50
+
51
+ // Ensure MCP manifest exists so harnesses can discover tools automatically
52
+ const mcpDir = path.join(dir, "mcp");
53
+ fs.mkdirSync(mcpDir, { recursive: true });
54
+ const manifest = path.join(mcpDir, "manifest.json");
55
+ if (!fs.existsSync(manifest)) {
56
+ const m = {
57
+ name: "zmemory",
58
+ protocol: "mcp",
59
+ transport: "stdio",
60
+ command: "zmemory mcp",
61
+ description: "Shared memory + coordination layer for AI coding agents"
62
+ };
63
+ fs.writeFileSync(manifest, JSON.stringify(m, null, 2));
64
+ }
65
+
66
+ // --- Optional auto agent bootstrap ---
67
+ // Enabled when environment variable is provided by the agent runtime
68
+ const agentName = process.env.ZMEMORY_AGENT;
69
+ if (agentName) {
70
+ try {
71
+ let session = process.env.ZMEMORY_SESSION;
72
+
73
+ if (!session) {
74
+ const id = crypto.randomBytes(3).toString("hex");
75
+ session = `${agentName}-${id}`;
76
+ }
77
+
78
+ registerAgent({
79
+ agent: agentName,
80
+ session,
81
+ task: process.env.ZMEMORY_TASK || ""
82
+ });
83
+
84
+ // expose session id for child tools if needed
85
+ if (!process.env.ZMEMORY_SESSION) {
86
+ process.env.ZMEMORY_SESSION = session;
87
+ }
88
+ } catch {}
89
+ }
90
+ }
@@ -0,0 +1,39 @@
1
+ # ZMemory Harness Adapter
2
+
3
+ This file explains how coding tools can automatically integrate with ZMemory.
4
+
5
+ Suggested adapter loop for harnesses:
6
+
7
+ 1. On start
8
+
9
+ ```
10
+ zmemory bootstrap
11
+ zmemory resume
12
+ ```
13
+
14
+ 2. Before executing a task
15
+
16
+ ```
17
+ zmemory context --role executor
18
+ ```
19
+
20
+ 3. After writing files
21
+
22
+ ```
23
+ zmemory event --role executor --type file_changed
24
+ ```
25
+
26
+ 4. After running commands
27
+
28
+ ```
29
+ zmemory exec <command>
30
+ ```
31
+
32
+ 5. On completion
33
+
34
+ ```
35
+ zmemory handoff
36
+ ```
37
+
38
+ Adapters can wrap these calls automatically so users do not need to run them manually.
39
+
@@ -0,0 +1,36 @@
1
+ import { readJSON, writeJSON, withFileLock } from "./fs.js";
2
+
3
+ const AGENTS_FILE = ".zmemory/agents.json";
4
+ const LOCK = ".zmemory/agents.json.lock";
5
+
6
+ function readAgents() {
7
+ return readJSON(AGENTS_FILE) || {};
8
+ }
9
+
10
+ function writeAgents(data) {
11
+ writeJSON(AGENTS_FILE, data);
12
+ }
13
+
14
+ export function registerAgent(name, role) {
15
+ return withFileLock(LOCK, () => {
16
+ const agents = readAgents();
17
+ agents[name] = {
18
+ role,
19
+ last_heartbeat: new Date().toISOString()
20
+ };
21
+ writeAgents(agents);
22
+ });
23
+ }
24
+
25
+ export function heartbeat(name) {
26
+ return withFileLock(LOCK, () => {
27
+ const agents = readAgents();
28
+ if (!agents[name]) return;
29
+ agents[name].last_heartbeat = new Date().toISOString();
30
+ writeAgents(agents);
31
+ });
32
+ }
33
+
34
+ export function listAgents() {
35
+ return readAgents();
36
+ }
@@ -0,0 +1,15 @@
1
+ import { readJSON, writeJSON } from "./fs.js";
2
+
3
+ const CONFIG = ".zmemory/config.json";
4
+
5
+ export function readConfig() {
6
+ try {
7
+ return readJSON(CONFIG, {}) || {};
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+
13
+ export function writeConfig(cfg) {
14
+ writeJSON(CONFIG, cfg);
15
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const BASE = ".zmemory/agents";
5
+
6
+ function ensureDir() {
7
+ fs.mkdirSync(BASE, { recursive: true });
8
+ }
9
+
10
+ function agentPath(session) {
11
+ return path.join(BASE, `${session}.json`);
12
+ }
13
+
14
+ export function registerAgent({ agent, session, task = "", files = [], cwd = process.cwd() }) {
15
+ ensureDir();
16
+
17
+ const data = {
18
+ agent,
19
+ session,
20
+ task,
21
+ files,
22
+ cwd,
23
+ started_at: new Date().toISOString()
24
+ };
25
+
26
+ fs.writeFileSync(agentPath(session), JSON.stringify(data, null, 2));
27
+ return data;
28
+ }
29
+
30
+ export function listAgents() {
31
+ if (!fs.existsSync(BASE)) return [];
32
+
33
+ const files = fs.readdirSync(BASE);
34
+ const agents = [];
35
+
36
+ for (const f of files) {
37
+ try {
38
+ const raw = fs.readFileSync(path.join(BASE, f), "utf8");
39
+ agents.push(JSON.parse(raw));
40
+ } catch {}
41
+ }
42
+
43
+ return agents;
44
+ }
45
+
46
+ export function removeAgent(session) {
47
+ const p = agentPath(session);
48
+ if (fs.existsSync(p)) fs.unlinkSync(p);
49
+ }
@@ -0,0 +1,127 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ const BASE = ".zmemory/claims";
6
+ const DEFAULT_TTL = 600; // seconds
7
+
8
+ function ensureDir() {
9
+ fs.mkdirSync(BASE, { recursive: true });
10
+ }
11
+
12
+ function hash(pattern) {
13
+ return crypto.createHash("sha1").update(pattern).digest("hex").slice(0, 8);
14
+ }
15
+
16
+ function claimPath(pattern) {
17
+ return path.join(BASE, `${hash(pattern)}.json`);
18
+ }
19
+
20
+ function isExpired(agent) {
21
+ if (!agent?.ts) return false;
22
+ const ttl = agent.ttl || DEFAULT_TTL;
23
+ const t = new Date(agent.ts).getTime() + ttl * 1000;
24
+ return Date.now() > t;
25
+ }
26
+
27
+ function pruneExpired(data) {
28
+ data.agents = (data.agents || []).filter(a => !isExpired(a));
29
+ return data;
30
+ }
31
+
32
+ export function claimPattern(pattern, { session, task = "" }) {
33
+ ensureDir();
34
+
35
+ const p = claimPath(pattern);
36
+ let data = { pattern, agents: [] };
37
+
38
+ if (fs.existsSync(p)) {
39
+ try {
40
+ data = JSON.parse(fs.readFileSync(p, "utf8"));
41
+ } catch {}
42
+ }
43
+
44
+ data = pruneExpired(data);
45
+
46
+ const existing = data.agents.find((a) => a.session === session);
47
+ if (!existing) {
48
+ data.agents.push({
49
+ session,
50
+ task,
51
+ ts: new Date().toISOString(),
52
+ ttl: DEFAULT_TTL
53
+ });
54
+ } else {
55
+ // refresh lease
56
+ existing.ts = new Date().toISOString();
57
+ }
58
+
59
+ fs.writeFileSync(p, JSON.stringify(data, null, 2));
60
+ return data;
61
+ }
62
+
63
+ export function listClaims() {
64
+ if (!fs.existsSync(BASE)) return [];
65
+
66
+ const files = fs.readdirSync(BASE);
67
+ const claims = [];
68
+
69
+ for (const f of files) {
70
+ try {
71
+ const raw = fs.readFileSync(path.join(BASE, f), "utf8");
72
+ const data = pruneExpired(JSON.parse(raw));
73
+ if ((data.agents || []).length) claims.push(data);
74
+ } catch {}
75
+ }
76
+
77
+ return claims;
78
+ }
79
+
80
+ export function releaseClaims(session) {
81
+ if (!fs.existsSync(BASE)) return;
82
+
83
+ const files = fs.readdirSync(BASE);
84
+
85
+ for (const f of files) {
86
+ const p = path.join(BASE, f);
87
+ try {
88
+ const data = JSON.parse(fs.readFileSync(p, "utf8"));
89
+ data.agents = (data.agents || []).filter((a) => a.session !== session && !isExpired(a));
90
+
91
+ if (!data.agents.length) {
92
+ fs.unlinkSync(p);
93
+ } else {
94
+ fs.writeFileSync(p, JSON.stringify(data, null, 2));
95
+ }
96
+ } catch {}
97
+ }
98
+ }
99
+
100
+ export function refreshClaims(session) {
101
+ if (!fs.existsSync(BASE)) return;
102
+ const files = fs.readdirSync(BASE);
103
+
104
+ for (const f of files) {
105
+ const p = path.join(BASE, f);
106
+ try {
107
+ const data = JSON.parse(fs.readFileSync(p, "utf8"));
108
+ let changed = false;
109
+
110
+ for (const a of data.agents || []) {
111
+ if (a.session === session) {
112
+ a.ts = new Date().toISOString();
113
+ if (!a.ttl) a.ttl = DEFAULT_TTL;
114
+ changed = true;
115
+ }
116
+ }
117
+
118
+ const pruned = pruneExpired(data);
119
+
120
+ if (!pruned.agents.length) {
121
+ fs.unlinkSync(p);
122
+ } else if (changed) {
123
+ fs.writeFileSync(p, JSON.stringify(pruned, null, 2));
124
+ }
125
+ } catch {}
126
+ }
127
+ }
@@ -0,0 +1,49 @@
1
+ import { listClaims } from "./claims.js";
2
+ // Minimal glob matcher to avoid external deps. Supports "*" wildcard only.
3
+ function globMatch(pattern, file) {
4
+ if (!pattern.includes("*")) return pattern === file;
5
+ const parts = pattern.split("*");
6
+ if (parts.length === 2) {
7
+ const [start, end] = parts;
8
+ return file.startsWith(start) && file.endsWith(end || "");
9
+ }
10
+ // fallback: simple contains sequence check
11
+ let idx = 0;
12
+ for (const p of parts) {
13
+ if (!p) continue;
14
+ const i = file.indexOf(p, idx);
15
+ if (i === -1) return false;
16
+ idx = i + p.length;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ function withExpiry(agent) {
22
+ if (!agent?.ts) return agent;
23
+ const ttl = agent.ttl || 600;
24
+ const expireAt = new Date(agent.ts).getTime() + ttl * 1000;
25
+ const remaining = Math.max(0, Math.floor((expireAt - Date.now()) / 1000));
26
+ return {
27
+ ...agent,
28
+ expires_in_seconds: remaining,
29
+ expires_in_minutes: Math.floor(remaining / 60)
30
+ };
31
+ }
32
+
33
+ export function who(filePath) {
34
+ const claims = listClaims();
35
+ const matches = [];
36
+
37
+ for (const c of claims) {
38
+ try {
39
+ if (globMatch(c.pattern, filePath)) {
40
+ matches.push({
41
+ pattern: c.pattern,
42
+ agents: (c.agents || []).map(withExpiry)
43
+ });
44
+ }
45
+ } catch {}
46
+ }
47
+
48
+ return matches;
49
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { writeJSON, readJSON } from "./fs.js";
4
+
5
+ const INDEX_FILE = ".zmemory/fileindex.json";
6
+
7
+ function walk(dir, ignore = ["node_modules", ".git", ".zmemory"]) {
8
+ const results = [];
9
+ if (!fs.existsSync(dir)) return results;
10
+
11
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
12
+
13
+ for (const e of entries) {
14
+ if (ignore.includes(e.name)) continue;
15
+
16
+ const full = path.join(dir, e.name);
17
+
18
+ if (e.isDirectory()) {
19
+ results.push(...walk(full, ignore));
20
+ } else {
21
+ results.push(full);
22
+ }
23
+ }
24
+
25
+ return results;
26
+ }
27
+
28
+ export function buildFileIndex(root = ".") {
29
+ const files = walk(root);
30
+ writeJSON(INDEX_FILE, files);
31
+ return files;
32
+ }
33
+
34
+ export function readFileIndex() {
35
+ return readJSON(INDEX_FILE) || [];
36
+ }
@@ -0,0 +1,44 @@
1
+ import { readJSON, writeJSON, withFileLock } from "./fs.js";
2
+
3
+ const LOCK_FILE = ".zmemory/filelocks.json";
4
+ const MUTEX = ".zmemory/filelocks.json.lock";
5
+
6
+ function readLocks() {
7
+ return readJSON(LOCK_FILE) || {};
8
+ }
9
+
10
+ function writeLocks(data) {
11
+ writeJSON(LOCK_FILE, data);
12
+ }
13
+
14
+ export function lockFiles(worker, files) {
15
+ return withFileLock(MUTEX, () => {
16
+ const locks = readLocks();
17
+
18
+ for (const f of files) {
19
+ if (locks[f] && locks[f] !== worker) {
20
+ throw new Error(`file locked: ${f}`);
21
+ }
22
+ }
23
+
24
+ files.forEach((f) => {
25
+ locks[f] = worker;
26
+ });
27
+
28
+ writeLocks(locks);
29
+ });
30
+ }
31
+
32
+ export function releaseFiles(worker) {
33
+ return withFileLock(MUTEX, () => {
34
+ const locks = readLocks();
35
+ Object.keys(locks).forEach((f) => {
36
+ if (locks[f] === worker) delete locks[f];
37
+ });
38
+ writeLocks(locks);
39
+ });
40
+ }
41
+
42
+ export function listLocks() {
43
+ return readLocks();
44
+ }
package/src/lib/fs.js ADDED
@@ -0,0 +1,140 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export const ZMEMORY_DIR = ".zmemory";
5
+
6
+ export function ensureDir(p) {
7
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
8
+ }
9
+
10
+ export function writeFile(p, data) {
11
+ atomicWriteFile(p, data);
12
+ }
13
+
14
+ export function appendFile(p, data) {
15
+ ensureDir(path.dirname(p));
16
+ fs.appendFileSync(p, data);
17
+ }
18
+
19
+ export function readJSON(p, fallback = null) {
20
+ if (!fs.existsSync(p)) return fallback;
21
+ const raw = fs.readFileSync(p, "utf8");
22
+ return JSON.parse(raw);
23
+ }
24
+
25
+ export function writeJSON(p, obj) {
26
+ writeFile(p, JSON.stringify(obj, null, 2));
27
+ }
28
+
29
+ export function now() {
30
+ return new Date().toISOString();
31
+ }
32
+
33
+ export function atomicWriteFile(filePath, data) {
34
+ ensureDir(path.dirname(filePath));
35
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
36
+ try {
37
+ fs.writeFileSync(tmp, data);
38
+ fs.renameSync(tmp, filePath);
39
+ } catch (e) {
40
+ try { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); } catch {}
41
+ throw e;
42
+ }
43
+ }
44
+
45
+ function processAlive(pid) {
46
+ try {
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch (err) {
50
+ if (err && err.code === "EPERM") return true;
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export function withFileLock(lockPath, fn, options = {}) {
56
+ const ttl = options.ttl || 30 * 60 * 1000;
57
+ ensureDir(path.dirname(lockPath));
58
+ let owned = false;
59
+
60
+ function acquire() {
61
+ try {
62
+ const fd = fs.openSync(lockPath, "wx");
63
+ try {
64
+ const meta = { pid: process.pid, created_at: Date.now() };
65
+ fs.writeFileSync(fd, JSON.stringify(meta));
66
+ } catch (e) {
67
+ try { fs.closeSync(fd); } catch {}
68
+ try { if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath); } catch {}
69
+ throw e;
70
+ }
71
+ fs.closeSync(fd);
72
+ owned = true;
73
+ return true;
74
+ } catch (err) {
75
+ if (err && err.code !== "EEXIST") throw err;
76
+
77
+ if (!fs.existsSync(lockPath)) return false;
78
+ try {
79
+ const stat = fs.statSync(lockPath);
80
+ const raw = fs.readFileSync(lockPath, "utf8").trim();
81
+
82
+ let stale = false;
83
+
84
+ if (/^\d+$/.test(raw)) {
85
+ const pid = Number(raw);
86
+ if (Number.isSafeInteger(pid) && pid > 0) {
87
+ if (!processAlive(pid)) stale = true;
88
+ } else {
89
+ const age = Date.now() - stat.mtimeMs;
90
+ if (age > ttl) stale = true;
91
+ }
92
+ } else {
93
+ let parsed = false;
94
+ try {
95
+ const meta = JSON.parse(raw);
96
+ if (
97
+ meta &&
98
+ typeof meta === "object" &&
99
+ !Array.isArray(meta) &&
100
+ typeof meta.pid === "number" &&
101
+ meta.pid > 0 &&
102
+ typeof meta.created_at === "number"
103
+ ) {
104
+ parsed = true;
105
+ const age = Date.now() - meta.created_at;
106
+ if (!processAlive(meta.pid) || age > ttl) stale = true;
107
+ }
108
+ } catch {}
109
+
110
+ if (!parsed) {
111
+ const age = Date.now() - stat.mtimeMs;
112
+ if (age > ttl) stale = true;
113
+ }
114
+ }
115
+
116
+ if (stale) {
117
+ fs.unlinkSync(lockPath);
118
+ return acquire();
119
+ }
120
+ } catch {}
121
+ return false;
122
+ }
123
+ }
124
+
125
+ if (!acquire()) throw new Error("file lock busy");
126
+
127
+ try {
128
+ return fn();
129
+ } finally {
130
+ if (owned) {
131
+ try {
132
+ if (fs.existsSync(lockPath)) {
133
+ const raw = fs.readFileSync(lockPath, "utf8");
134
+ const meta = JSON.parse(raw);
135
+ if (meta.pid === process.pid) fs.unlinkSync(lockPath);
136
+ }
137
+ } catch {}
138
+ }
139
+ }
140
+ }