claude-code-swarm 0.3.17 → 0.3.19
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/map-sidecar.mjs +1 -2
- package/src/__tests__/memory-watcher.test.mjs +138 -13
- package/src/memory-watcher.mjs +31 -23
- package/src/sessionlog.mjs +26 -1
- package/src/sidecar-server.mjs +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
3
|
"description": "Spin up Claude Code agent teams from openteams YAML topologies with optional MAP (Multi-Agent Protocol) observability and coordination. Provides hooks for session lifecycle, agent spawn/complete tracking, and a /swarm skill to launch team configurations.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.19",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "alexngai"
|
|
7
7
|
},
|
package/package.json
CHANGED
package/scripts/map-sidecar.mjs
CHANGED
|
@@ -523,8 +523,6 @@ async function main() {
|
|
|
523
523
|
if (sidecarConfig.minimem?.enabled) {
|
|
524
524
|
const minimemDir = sidecarConfig.minimem?.dir || ".swarm/minimem";
|
|
525
525
|
const memWatcher = startMemoryWatcher(minimemDir, (_event) => {
|
|
526
|
-
// Send bridge-memory-sync through the command handler
|
|
527
|
-
// This reuses the same callExtension path as the PostToolUse hook
|
|
528
526
|
const fakeClient = {
|
|
529
527
|
write: () => {},
|
|
530
528
|
writable: true,
|
|
@@ -533,6 +531,7 @@ async function main() {
|
|
|
533
531
|
action: "bridge-memory-sync",
|
|
534
532
|
agentId: SESSION_ID || "minimem",
|
|
535
533
|
timestamp: new Date().toISOString(),
|
|
534
|
+
memoryDir: minimemDir,
|
|
536
535
|
}, fakeClient);
|
|
537
536
|
});
|
|
538
537
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for memory file watcher
|
|
2
|
+
* Unit tests for memory file watcher (fs.watch implementation)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
6
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs";
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, unlinkSync, rmSync, existsSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
9
|
import { startMemoryWatcher } from "../memory-watcher.mjs";
|
|
@@ -23,6 +23,8 @@ describe("startMemoryWatcher", () => {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
// ── Null/skip cases ─────────────────────────────────────────────
|
|
27
|
+
|
|
26
28
|
it("returns null for non-existent directory", () => {
|
|
27
29
|
const result = startMemoryWatcher("/nonexistent/path", () => {});
|
|
28
30
|
expect(result).toBeNull();
|
|
@@ -33,6 +35,8 @@ describe("startMemoryWatcher", () => {
|
|
|
33
35
|
expect(startMemoryWatcher(undefined, () => {})).toBeNull();
|
|
34
36
|
});
|
|
35
37
|
|
|
38
|
+
// ── Watcher lifecycle ───────────────────────────────────────────
|
|
39
|
+
|
|
36
40
|
it("returns a watcher handle with close method", () => {
|
|
37
41
|
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
38
42
|
mkdirSync(join(tmpDir, "memory"), { recursive: true });
|
|
@@ -42,6 +46,18 @@ describe("startMemoryWatcher", () => {
|
|
|
42
46
|
expect(typeof watcher.close).toBe("function");
|
|
43
47
|
});
|
|
44
48
|
|
|
49
|
+
it("close() can be called multiple times without error", () => {
|
|
50
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
51
|
+
|
|
52
|
+
watcher = startMemoryWatcher(tmpDir, () => {});
|
|
53
|
+
expect(watcher).not.toBeNull();
|
|
54
|
+
watcher.close();
|
|
55
|
+
watcher.close(); // should not throw
|
|
56
|
+
watcher = null;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── File detection ──────────────────────────────────────────────
|
|
60
|
+
|
|
45
61
|
it("detects new .md file and calls onSync", async () => {
|
|
46
62
|
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
47
63
|
mkdirSync(join(tmpDir, "memory"), { recursive: true });
|
|
@@ -49,46 +65,108 @@ describe("startMemoryWatcher", () => {
|
|
|
49
65
|
const onSync = vi.fn();
|
|
50
66
|
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
51
67
|
|
|
52
|
-
// Wait for watcher to
|
|
53
|
-
await new Promise((r) => setTimeout(r,
|
|
68
|
+
// Wait for watcher to initialize
|
|
69
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
54
70
|
|
|
55
71
|
// Write a new .md file
|
|
56
72
|
writeFileSync(join(tmpDir, "memory", "test-note.md"), "# Test Note\nContent here.");
|
|
57
73
|
|
|
58
74
|
// Wait for debounce (2s) + buffer
|
|
59
|
-
await new Promise((r) => setTimeout(r,
|
|
75
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
60
76
|
|
|
61
77
|
expect(onSync).toHaveBeenCalled();
|
|
62
78
|
const call = onSync.mock.calls[0][0];
|
|
63
|
-
expect(call.type).toBe("add");
|
|
64
79
|
expect(call.path).toContain("test-note.md");
|
|
80
|
+
// fs.watch reports 'rename' for new files, 'change' for modifications
|
|
81
|
+
expect(["rename", "change"]).toContain(call.type);
|
|
65
82
|
}, 10_000);
|
|
66
83
|
|
|
84
|
+
it("detects changes to existing .md file", async () => {
|
|
85
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
86
|
+
mkdirSync(join(tmpDir, "memory"), { recursive: true });
|
|
87
|
+
|
|
88
|
+
// Create file before starting watcher
|
|
89
|
+
const filePath = join(tmpDir, "memory", "existing.md");
|
|
90
|
+
writeFileSync(filePath, "# Original");
|
|
91
|
+
|
|
92
|
+
const onSync = vi.fn();
|
|
93
|
+
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
94
|
+
|
|
95
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
96
|
+
|
|
97
|
+
// Modify the file
|
|
98
|
+
writeFileSync(filePath, "# Updated content");
|
|
99
|
+
|
|
100
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
101
|
+
|
|
102
|
+
expect(onSync).toHaveBeenCalled();
|
|
103
|
+
const call = onSync.mock.calls[0][0];
|
|
104
|
+
expect(call.path).toContain("existing.md");
|
|
105
|
+
}, 10_000);
|
|
106
|
+
|
|
107
|
+
// ── Ignore patterns ─────────────────────────────────────────────
|
|
108
|
+
|
|
67
109
|
it("ignores non-.md files", async () => {
|
|
68
110
|
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
69
111
|
|
|
70
112
|
const onSync = vi.fn();
|
|
71
113
|
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
72
114
|
|
|
73
|
-
await new Promise((r) => setTimeout(r,
|
|
115
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
74
116
|
|
|
75
117
|
// Write non-.md files
|
|
76
|
-
writeFileSync(join(tmpDir, "
|
|
77
|
-
writeFileSync(join(tmpDir, "config.
|
|
118
|
+
writeFileSync(join(tmpDir, "data.json"), "{}");
|
|
119
|
+
writeFileSync(join(tmpDir, "config.yaml"), "key: value");
|
|
120
|
+
writeFileSync(join(tmpDir, "README.txt"), "readme");
|
|
121
|
+
|
|
122
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
123
|
+
|
|
124
|
+
expect(onSync).not.toHaveBeenCalled();
|
|
125
|
+
}, 10_000);
|
|
78
126
|
|
|
79
|
-
|
|
127
|
+
it("ignores files in .minimem directory", async () => {
|
|
128
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
129
|
+
mkdirSync(join(tmpDir, ".minimem"), { recursive: true });
|
|
130
|
+
|
|
131
|
+
const onSync = vi.fn();
|
|
132
|
+
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
133
|
+
|
|
134
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
135
|
+
|
|
136
|
+
// Write to .minimem (should be ignored even if .md)
|
|
137
|
+
writeFileSync(join(tmpDir, ".minimem", "index.md"), "ignored");
|
|
138
|
+
|
|
139
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
80
140
|
|
|
81
141
|
expect(onSync).not.toHaveBeenCalled();
|
|
82
142
|
}, 10_000);
|
|
83
143
|
|
|
84
|
-
it("
|
|
144
|
+
it("ignores files in .cache directory", async () => {
|
|
145
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
146
|
+
mkdirSync(join(tmpDir, ".cache"), { recursive: true });
|
|
147
|
+
|
|
148
|
+
const onSync = vi.fn();
|
|
149
|
+
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
150
|
+
|
|
151
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
152
|
+
|
|
153
|
+
writeFileSync(join(tmpDir, ".cache", "embeddings.md"), "cached");
|
|
154
|
+
|
|
155
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
156
|
+
|
|
157
|
+
expect(onSync).not.toHaveBeenCalled();
|
|
158
|
+
}, 10_000);
|
|
159
|
+
|
|
160
|
+
// ── Debounce ────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
it("debounces rapid changes into a single callback", async () => {
|
|
85
163
|
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
86
164
|
mkdirSync(join(tmpDir, "memory"), { recursive: true });
|
|
87
165
|
|
|
88
166
|
const onSync = vi.fn();
|
|
89
167
|
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
90
168
|
|
|
91
|
-
await new Promise((r) => setTimeout(r,
|
|
169
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
92
170
|
|
|
93
171
|
// Write multiple files rapidly
|
|
94
172
|
writeFileSync(join(tmpDir, "memory", "note1.md"), "# Note 1");
|
|
@@ -96,9 +174,56 @@ describe("startMemoryWatcher", () => {
|
|
|
96
174
|
writeFileSync(join(tmpDir, "memory", "note3.md"), "# Note 3");
|
|
97
175
|
|
|
98
176
|
// Wait for debounce
|
|
99
|
-
await new Promise((r) => setTimeout(r,
|
|
177
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
100
178
|
|
|
101
179
|
// Should only fire once (debounced)
|
|
102
180
|
expect(onSync).toHaveBeenCalledTimes(1);
|
|
103
181
|
}, 10_000);
|
|
182
|
+
|
|
183
|
+
// ── Minimem directory structure ─────────────────────────────────
|
|
184
|
+
|
|
185
|
+
it("watches the real minimem layout: memory/*.md + MEMORY.md", async () => {
|
|
186
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
187
|
+
|
|
188
|
+
// Recreate minimem structure
|
|
189
|
+
mkdirSync(join(tmpDir, "memory"), { recursive: true });
|
|
190
|
+
mkdirSync(join(tmpDir, ".minimem"), { recursive: true });
|
|
191
|
+
|
|
192
|
+
// Pre-existing files (like a real minimem store)
|
|
193
|
+
writeFileSync(join(tmpDir, "MEMORY.md"), "# Memory Index");
|
|
194
|
+
writeFileSync(join(tmpDir, "memory", "existing-note.md"), "# Existing");
|
|
195
|
+
writeFileSync(join(tmpDir, ".minimem", "config.json"), "{}");
|
|
196
|
+
writeFileSync(join(tmpDir, ".minimem", "index.db"), "binary");
|
|
197
|
+
|
|
198
|
+
const onSync = vi.fn();
|
|
199
|
+
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
200
|
+
|
|
201
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
202
|
+
|
|
203
|
+
// Agent writes a new memory file (should trigger)
|
|
204
|
+
writeFileSync(join(tmpDir, "memory", "knowledge-dns.md"), "# DNS Knowledge\nContent");
|
|
205
|
+
|
|
206
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
207
|
+
|
|
208
|
+
expect(onSync).toHaveBeenCalled();
|
|
209
|
+
expect(onSync.mock.calls[0][0].path).toContain("knowledge-dns.md");
|
|
210
|
+
}, 10_000);
|
|
211
|
+
|
|
212
|
+
it("does not fire for .minimem/index.db updates in real layout", async () => {
|
|
213
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mem-watch-"));
|
|
214
|
+
mkdirSync(join(tmpDir, ".minimem"), { recursive: true });
|
|
215
|
+
writeFileSync(join(tmpDir, ".minimem", "index.db"), "v1");
|
|
216
|
+
|
|
217
|
+
const onSync = vi.fn();
|
|
218
|
+
watcher = startMemoryWatcher(tmpDir, onSync);
|
|
219
|
+
|
|
220
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
221
|
+
|
|
222
|
+
// Simulate index rebuild
|
|
223
|
+
writeFileSync(join(tmpDir, ".minimem", "index.db"), "v2");
|
|
224
|
+
|
|
225
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
226
|
+
|
|
227
|
+
expect(onSync).not.toHaveBeenCalled();
|
|
228
|
+
}, 10_000);
|
|
104
229
|
});
|
package/src/memory-watcher.mjs
CHANGED
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
* watcher is the only way to detect when an agent writes to memory.
|
|
8
8
|
*
|
|
9
9
|
* Runs inside the MAP sidecar process (persistent for the session).
|
|
10
|
+
*
|
|
11
|
+
* Uses Node's built-in fs.watch (recursive) instead of chokidar to avoid
|
|
12
|
+
* an external dependency. Requires Node 20+ for recursive on Linux.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
15
|
+
import { watch, existsSync } from "fs";
|
|
16
|
+
import { resolve } from "path";
|
|
14
17
|
import { createLogger } from "./log.mjs";
|
|
15
18
|
|
|
16
19
|
const log = createLogger("memory-watcher");
|
|
17
20
|
|
|
18
21
|
const DEBOUNCE_MS = 2000;
|
|
19
22
|
|
|
23
|
+
/** Patterns to ignore (matched against the relative filename). */
|
|
24
|
+
const IGNORED = [/node_modules/, /\.git/, /index\.db/, /\.cache/, /\.minimem/];
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* Start watching a minimem directory for file changes.
|
|
22
28
|
* When changes are detected (debounced), calls the provided callback.
|
|
@@ -32,37 +38,39 @@ export function startMemoryWatcher(memoryDir, onSync) {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
let debounceTimer = null;
|
|
41
|
+
const absDir = resolve(memoryDir);
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
43
|
+
let watcher;
|
|
44
|
+
try {
|
|
45
|
+
watcher = watch(absDir, { recursive: true }, (eventType, filename) => {
|
|
46
|
+
if (!filename) return;
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!filePath.endsWith(".md")) return;
|
|
48
|
+
// Only react to .md file changes
|
|
49
|
+
if (!filename.endsWith(".md")) return;
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
debounceTimer = null;
|
|
49
|
-
log.debug("memory change detected", { event: eventType, path: filePath });
|
|
50
|
-
onSync({ type: eventType, path: filePath });
|
|
51
|
-
}, DEBOUNCE_MS);
|
|
52
|
-
}
|
|
51
|
+
// Skip ignored patterns
|
|
52
|
+
if (IGNORED.some((re) => re.test(filename))) return;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
watcher.on("change", (p) => debouncedSync("change", p));
|
|
56
|
-
watcher.on("unlink", (p) => debouncedSync("unlink", p));
|
|
54
|
+
const fullPath = resolve(absDir, filename);
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
57
|
+
debounceTimer = setTimeout(() => {
|
|
58
|
+
debounceTimer = null;
|
|
59
|
+
log.debug("memory change detected", { event: eventType, path: fullPath });
|
|
60
|
+
onSync({ type: eventType, path: fullPath });
|
|
61
|
+
}, DEBOUNCE_MS);
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
log.warn("memory watcher failed to start", { error: err.message });
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
watcher.on("error", (err) => {
|
|
63
69
|
log.warn("memory watcher error", { error: err.message });
|
|
64
70
|
});
|
|
65
71
|
|
|
72
|
+
log.info("memory watcher started", { dir: memoryDir });
|
|
73
|
+
|
|
66
74
|
return {
|
|
67
75
|
close() {
|
|
68
76
|
if (debounceTimer) clearTimeout(debounceTimer);
|
package/src/sessionlog.mjs
CHANGED
|
@@ -30,6 +30,28 @@ function getGitBranch() {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function getGitCommitHash() {
|
|
34
|
+
try {
|
|
35
|
+
return execSync("git rev-parse HEAD", {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
38
|
+
}).trim() || null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getGitRemoteUrl() {
|
|
45
|
+
try {
|
|
46
|
+
return execSync("git remote get-url origin", {
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
}).trim() || null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* Check if sessionlog is installed and active.
|
|
35
57
|
* Returns 'active', 'installed but not enabled', or 'not installed'.
|
|
@@ -176,8 +198,11 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
|
|
|
176
198
|
turnId: state.turnID,
|
|
177
199
|
startedAt: state.startedAt,
|
|
178
200
|
label: `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`,
|
|
179
|
-
// Project context for display
|
|
201
|
+
// Project context for display + auto-import
|
|
180
202
|
project: path.basename(process.cwd()),
|
|
203
|
+
projectPath: process.cwd(),
|
|
204
|
+
gitRemoteUrl: getGitRemoteUrl(),
|
|
205
|
+
gitCommitHash: getGitCommitHash(),
|
|
181
206
|
firstPrompt: state.firstPrompt ? state.firstPrompt.slice(0, 200) : undefined,
|
|
182
207
|
template: config.template || undefined,
|
|
183
208
|
};
|
package/src/sidecar-server.mjs
CHANGED
|
@@ -396,13 +396,13 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
396
396
|
const c = conn || await waitForConn();
|
|
397
397
|
if (c) {
|
|
398
398
|
try {
|
|
399
|
-
// Use callExtension for JSON-RPC vendor-prefixed method
|
|
400
|
-
// Same pattern as trajectory/checkpoint
|
|
401
399
|
const params = {
|
|
402
400
|
resource_id: _memoryResourceId || "",
|
|
403
401
|
agent_id: command.agentId || "minimem",
|
|
404
402
|
commit_hash: `memory-${Date.now()}`,
|
|
405
403
|
timestamp: command.timestamp || new Date().toISOString(),
|
|
404
|
+
// Include the memory directory path for resource resolution on first call
|
|
405
|
+
path: command.memoryDir || "",
|
|
406
406
|
};
|
|
407
407
|
const result = await c.callExtension("x-openhive/memory.sync", params);
|
|
408
408
|
// Cache resource_id from server response
|