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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Launch Claude Code with swarmkit capabilities, including team orchestration, MAP observability, and session tracking.",
5
5
  "owner": {
6
6
  "name": "alexngai"
@@ -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.17",
4
+ "version": "0.3.19",
5
5
  "author": {
6
6
  "name": "alexngai"
7
7
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 be ready
53
- await new Promise((r) => setTimeout(r, 500));
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, 3500));
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, 500));
115
+ await new Promise((r) => setTimeout(r, 300));
74
116
 
75
117
  // Write non-.md files
76
- writeFileSync(join(tmpDir, "index.db"), "binary data");
77
- writeFileSync(join(tmpDir, "config.json"), "{}");
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
- await new Promise((r) => setTimeout(r, 3500));
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("debounces rapid changes", async () => {
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, 500));
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, 3500));
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
  });
@@ -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 chokidar from "chokidar";
13
- import { existsSync } from "fs";
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
- const watcher = chokidar.watch(memoryDir, {
37
- ignoreInitial: true,
38
- ignored: [/node_modules/, /\.git/, /index\.db/, /\.cache/, /\.minimem/],
39
- depth: 3,
40
- });
43
+ let watcher;
44
+ try {
45
+ watcher = watch(absDir, { recursive: true }, (eventType, filename) => {
46
+ if (!filename) return;
41
47
 
42
- function debouncedSync(eventType, filePath) {
43
- // Only react to .md file changes
44
- if (!filePath.endsWith(".md")) return;
48
+ // Only react to .md file changes
49
+ if (!filename.endsWith(".md")) return;
45
50
 
46
- if (debounceTimer) clearTimeout(debounceTimer);
47
- debounceTimer = setTimeout(() => {
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
- watcher.on("add", (p) => debouncedSync("add", p));
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
- watcher.on("ready", () => {
59
- log.info("memory watcher started", { dir: memoryDir });
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);
@@ -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
  };
@@ -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