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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/adapters/claude-code-adapter.sh +11 -0
- package/adapters/codex-adapter.sh +12 -0
- package/adapters/cursor-adapter.sh +14 -0
- package/adapters/opencode-adapter.sh +9 -0
- package/bin/zmemory.js +426 -0
- package/package.json +42 -0
- package/src/bus/server.js +51 -0
- package/src/commands/agent.js +42 -0
- package/src/commands/archive.js +32 -0
- package/src/commands/bootstrap.js +7 -0
- package/src/commands/bus.js +6 -0
- package/src/commands/claim.js +66 -0
- package/src/commands/config.js +29 -0
- package/src/commands/context.js +269 -0
- package/src/commands/daemon.js +6 -0
- package/src/commands/decision.js +12 -0
- package/src/commands/doctor.js +100 -0
- package/src/commands/event.js +149 -0
- package/src/commands/exec.js +25 -0
- package/src/commands/explain.js +68 -0
- package/src/commands/failure.js +12 -0
- package/src/commands/gitHook.js +22 -0
- package/src/commands/handoff.js +101 -0
- package/src/commands/health.js +18 -0
- package/src/commands/help.js +33 -0
- package/src/commands/index.js +21 -0
- package/src/commands/init.js +38 -0
- package/src/commands/install.js +28 -0
- package/src/commands/locks.js +9 -0
- package/src/commands/next.js +59 -0
- package/src/commands/orchestrator.js +6 -0
- package/src/commands/plan.js +30 -0
- package/src/commands/resume.js +79 -0
- package/src/commands/route.js +67 -0
- package/src/commands/runs.js +47 -0
- package/src/commands/search.js +159 -0
- package/src/commands/setup.js +148 -0
- package/src/commands/skill.js +103 -0
- package/src/commands/startRun.js +82 -0
- package/src/commands/state.js +68 -0
- package/src/commands/status.js +51 -0
- package/src/commands/stream.js +27 -0
- package/src/commands/summary.js +65 -0
- package/src/commands/sync.js +20 -0
- package/src/commands/tasks.js +89 -0
- package/src/commands/timeline.js +75 -0
- package/src/commands/upgrade.js +19 -0
- package/src/commands/version.js +12 -0
- package/src/commands/who.js +26 -0
- package/src/commands/worker.js +71 -0
- package/src/commands/workspaces.js +25 -0
- package/src/daemon/orchestrator.js +34 -0
- package/src/daemon/zmemoryd.js +17 -0
- package/src/dashboard/server.js +40 -0
- package/src/integrations/agent-hooks.md +39 -0
- package/src/integrations/auto-bootstrap.js +90 -0
- package/src/integrations/harness-adapter.md +39 -0
- package/src/lib/agents.js +36 -0
- package/src/lib/config.js +15 -0
- package/src/lib/coordination/agents.js +49 -0
- package/src/lib/coordination/claims.js +127 -0
- package/src/lib/coordination/who.js +49 -0
- package/src/lib/fileIndex.js +36 -0
- package/src/lib/fileLocks.js +44 -0
- package/src/lib/fs.js +140 -0
- package/src/lib/lock.js +103 -0
- package/src/lib/repoScan.js +19 -0
- package/src/lib/run.js +63 -0
- package/src/lib/sqlite.js +7 -0
- package/src/lib/tasks.js +77 -0
- package/src/lib/teamSync.js +36 -0
- package/src/lib/workspaces.js +29 -0
- package/src/mcp/autostart.js +12 -0
- package/src/mcp/server.js +420 -0
- package/src/mcp/tools.json +28 -0
package/src/lib/lock.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { getRunFile } from "./run.js";
|
|
3
|
+
|
|
4
|
+
function processAlive(pid) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
} catch (err) {
|
|
9
|
+
if (err && err.code === "EPERM") return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function acquireLock(run) {
|
|
15
|
+
const p = getRunFile(run, "lock");
|
|
16
|
+
const ttl = 30 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
function tryAcquire() {
|
|
19
|
+
try {
|
|
20
|
+
const fd = fs.openSync(p, "wx");
|
|
21
|
+
try {
|
|
22
|
+
const meta = { pid: process.pid, created_at: Date.now() };
|
|
23
|
+
fs.writeFileSync(fd, JSON.stringify(meta));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
try { fs.closeSync(fd); } catch {}
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
28
|
+
} catch {}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
fs.closeSync(fd);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err && err.code === "EEXIST") return false;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (tryAcquire()) return;
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(p)) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = fs.statSync(p);
|
|
44
|
+
const raw = fs.readFileSync(p, "utf8").trim();
|
|
45
|
+
let stale = false;
|
|
46
|
+
|
|
47
|
+
if (/^\d+$/.test(raw)) {
|
|
48
|
+
const pid = Number(raw);
|
|
49
|
+
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
50
|
+
if (!processAlive(pid)) stale = true;
|
|
51
|
+
} else {
|
|
52
|
+
const age = Date.now() - stat.mtimeMs;
|
|
53
|
+
if (age > ttl) stale = true;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
let parsed = false;
|
|
57
|
+
try {
|
|
58
|
+
const meta = JSON.parse(raw);
|
|
59
|
+
if (
|
|
60
|
+
meta &&
|
|
61
|
+
typeof meta === "object" &&
|
|
62
|
+
!Array.isArray(meta) &&
|
|
63
|
+
typeof meta.pid === "number" &&
|
|
64
|
+
meta.pid > 0 &&
|
|
65
|
+
typeof meta.created_at === "number"
|
|
66
|
+
) {
|
|
67
|
+
parsed = true;
|
|
68
|
+
const age = Date.now() - meta.created_at;
|
|
69
|
+
if (!processAlive(meta.pid) || age > ttl) stale = true;
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
const age = Date.now() - stat.mtimeMs;
|
|
75
|
+
if (age > ttl) stale = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (stale) {
|
|
80
|
+
fs.unlinkSync(p);
|
|
81
|
+
if (tryAcquire()) return;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error("run locked by another agent");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function releaseLock(run) {
|
|
90
|
+
const p = getRunFile(run, "lock");
|
|
91
|
+
if (!fs.existsSync(p)) return;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
95
|
+
try {
|
|
96
|
+
const meta = JSON.parse(raw);
|
|
97
|
+
if (meta.pid === process.pid) fs.unlinkSync(p);
|
|
98
|
+
} catch {
|
|
99
|
+
const pid = Number(raw);
|
|
100
|
+
if (pid === process.pid || !processAlive(pid)) fs.unlinkSync(p);
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
// Simple repo scanner to detect top-level modules for planning
|
|
5
|
+
export function scanModules(root = "src") {
|
|
6
|
+
if (!fs.existsSync(root)) return [];
|
|
7
|
+
|
|
8
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
9
|
+
const modules = [];
|
|
10
|
+
|
|
11
|
+
for (const e of entries) {
|
|
12
|
+
if (e.isDirectory()) {
|
|
13
|
+
modules.push(path.join(root, e.name));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return modules;
|
|
18
|
+
}
|
|
19
|
+
|
package/src/lib/run.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const ACTIVE_ROOT = path.resolve(".zmemory/runs/active");
|
|
5
|
+
const ARCHIVE_ROOT = path.resolve(".zmemory/runs/archived");
|
|
6
|
+
|
|
7
|
+
const RUN_ID_REGEX = /^run_\d{4}_\d{1,2}_\d{1,2}_\d+$/;
|
|
8
|
+
|
|
9
|
+
export function isValidRunId(run) {
|
|
10
|
+
if (!run || typeof run !== "string") return false;
|
|
11
|
+
if (run.includes("..") || run.includes("/") || run.includes("\\")) return false;
|
|
12
|
+
if (path.isAbsolute(run)) return false;
|
|
13
|
+
return RUN_ID_REGEX.test(run);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function assertValidRunId(run) {
|
|
17
|
+
if (!isValidRunId(run)) {
|
|
18
|
+
throw new Error("invalid run id");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ensureInside(base, target) {
|
|
23
|
+
const resolved = path.resolve(target);
|
|
24
|
+
if (!(resolved === base || resolved.startsWith(base + path.sep))) {
|
|
25
|
+
throw new Error("invalid run path");
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getRunBase(run) {
|
|
31
|
+
assertValidRunId(run);
|
|
32
|
+
const p = path.join(ACTIVE_ROOT, run);
|
|
33
|
+
return ensureInside(ACTIVE_ROOT, p);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getArchivedRunBase(run) {
|
|
37
|
+
assertValidRunId(run);
|
|
38
|
+
const p = path.join(ARCHIVE_ROOT, run);
|
|
39
|
+
return ensureInside(ARCHIVE_ROOT, p);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getRunFile(run, file) {
|
|
43
|
+
const base = getRunBase(run);
|
|
44
|
+
const p = path.join(base, file);
|
|
45
|
+
return ensureInside(base, p);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ensureRunExists(run) {
|
|
49
|
+
const base = getRunBase(run);
|
|
50
|
+
if (!fs.existsSync(base)) {
|
|
51
|
+
throw new Error("run not found");
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readRunState(run) {
|
|
57
|
+
ensureRunExists(run);
|
|
58
|
+
const statePath = getRunFile(run, "state.json");
|
|
59
|
+
if (!fs.existsSync(statePath)) {
|
|
60
|
+
throw new Error("state.json missing for run");
|
|
61
|
+
}
|
|
62
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
63
|
+
}
|
package/src/lib/tasks.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readJSON, writeJSON, withFileLock } from "./fs.js";
|
|
2
|
+
|
|
3
|
+
const TASK_FILE = ".zmemory/tasks.json";
|
|
4
|
+
const LOCK = ".zmemory/tasks.json.lock";
|
|
5
|
+
|
|
6
|
+
function readTasks() {
|
|
7
|
+
return readJSON(TASK_FILE) || [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function writeTasks(tasks) {
|
|
11
|
+
writeJSON(TASK_FILE, tasks);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function addTask(title, files = []) {
|
|
15
|
+
return withFileLock(LOCK, () => {
|
|
16
|
+
const tasks = readTasks();
|
|
17
|
+
const task = {
|
|
18
|
+
id: `task_${Date.now()}`,
|
|
19
|
+
title,
|
|
20
|
+
files,
|
|
21
|
+
status: "pending",
|
|
22
|
+
worker: null,
|
|
23
|
+
file_locks: files,
|
|
24
|
+
depends_on: [],
|
|
25
|
+
file_scope: files
|
|
26
|
+
};
|
|
27
|
+
tasks.push(task);
|
|
28
|
+
writeTasks(tasks);
|
|
29
|
+
return task;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addTaskWithDeps(title, deps = []) {
|
|
34
|
+
return withFileLock(LOCK, () => {
|
|
35
|
+
const tasks = readTasks();
|
|
36
|
+
const task = {
|
|
37
|
+
id: `task_${Date.now()}`,
|
|
38
|
+
title,
|
|
39
|
+
files: [],
|
|
40
|
+
status: "pending",
|
|
41
|
+
worker: null,
|
|
42
|
+
file_locks: [],
|
|
43
|
+
depends_on: deps
|
|
44
|
+
};
|
|
45
|
+
tasks.push(task);
|
|
46
|
+
writeTasks(tasks);
|
|
47
|
+
return task;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listTasks() {
|
|
52
|
+
return readTasks();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function claimTask(id, worker) {
|
|
56
|
+
return withFileLock(LOCK, () => {
|
|
57
|
+
const tasks = readTasks();
|
|
58
|
+
const t = tasks.find((x) => x.id === id);
|
|
59
|
+
if (!t) return null;
|
|
60
|
+
if (t.worker) return null;
|
|
61
|
+
t.worker = worker;
|
|
62
|
+
t.status = "in_progress";
|
|
63
|
+
writeTasks(tasks);
|
|
64
|
+
return t;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function completeTask(id) {
|
|
69
|
+
return withFileLock(LOCK, () => {
|
|
70
|
+
const tasks = readTasks();
|
|
71
|
+
const t = tasks.find((x) => x.id === id);
|
|
72
|
+
if (!t) return null;
|
|
73
|
+
t.status = "done";
|
|
74
|
+
writeTasks(tasks);
|
|
75
|
+
return t;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
// Very simple optional team sync mechanism using a shared directory.
|
|
4
|
+
// This is intentionally lightweight and local-first.
|
|
5
|
+
|
|
6
|
+
export function syncPush(remoteDir = ".zmemory-remote") {
|
|
7
|
+
if (!fs.existsSync(".zmemory")) return;
|
|
8
|
+
if (!fs.existsSync(remoteDir)) fs.mkdirSync(remoteDir, { recursive: true });
|
|
9
|
+
|
|
10
|
+
const files = fs.readdirSync(".zmemory");
|
|
11
|
+
|
|
12
|
+
files.forEach((f) => {
|
|
13
|
+
const src = `.zmemory/${f}`;
|
|
14
|
+
const dst = `${remoteDir}/${f}`;
|
|
15
|
+
|
|
16
|
+
if (fs.lstatSync(src).isDirectory()) return;
|
|
17
|
+
|
|
18
|
+
fs.copyFileSync(src, dst);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function syncPull(remoteDir = ".zmemory-remote") {
|
|
23
|
+
if (!fs.existsSync(remoteDir)) return;
|
|
24
|
+
|
|
25
|
+
const files = fs.readdirSync(remoteDir);
|
|
26
|
+
|
|
27
|
+
files.forEach((f) => {
|
|
28
|
+
const src = `${remoteDir}/${f}`;
|
|
29
|
+
const dst = `.zmemory/${f}`;
|
|
30
|
+
|
|
31
|
+
if (fs.lstatSync(src).isDirectory()) return;
|
|
32
|
+
|
|
33
|
+
fs.copyFileSync(src, dst);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readJSON, writeJSON, withFileLock } from "./fs.js";
|
|
2
|
+
|
|
3
|
+
const FILE = ".zmemory/workspaces.json";
|
|
4
|
+
const LOCK = ".zmemory/workspaces.json.lock";
|
|
5
|
+
|
|
6
|
+
function read() {
|
|
7
|
+
try {
|
|
8
|
+
return readJSON(FILE, []) || [];
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function write(data) {
|
|
15
|
+
writeJSON(FILE, data);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function addWorkspace(path) {
|
|
19
|
+
return withFileLock(LOCK, () => {
|
|
20
|
+
const list = read();
|
|
21
|
+
if (!list.includes(path)) list.push(path);
|
|
22
|
+
write(list);
|
|
23
|
+
return list;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function listWorkspaces() {
|
|
28
|
+
return read();
|
|
29
|
+
}
|