swarmkit 0.0.4 → 0.0.6

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,5 +1,24 @@
1
+ import { readdirSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
  import { PACKAGES, getActiveIntegrations } from "../packages/registry.js";
4
+ /** Check if a directory exists (via ctx.exists) and contains at least one file */
5
+ function dirHasContent(dir, exists) {
6
+ if (!exists(dir))
7
+ return false;
8
+ try {
9
+ const entries = readdirSync(dir, { withFileTypes: true });
10
+ for (const entry of entries) {
11
+ if (entry.isFile() || entry.isSymbolicLink())
12
+ return true;
13
+ if (entry.isDirectory() && dirHasContent(join(dir, entry.name), exists))
14
+ return true;
15
+ }
16
+ return false;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
3
22
  /** Config directories — prefixed layout (.swarm/) */
4
23
  const PREFIXED_CONFIG_DIRS = {
5
24
  opentasks: ".swarm/opentasks",
@@ -139,8 +158,11 @@ export function checkProjectConfigs(ctx) {
139
158
  continue; // Package has no project-level config
140
159
  if (PACKAGES[pkg]?.globalOnly)
141
160
  continue;
142
- const hasPrefixed = ctx.exists(join(ctx.cwd, prefixedDir));
143
- const hasFlat = flatDir ? ctx.exists(join(ctx.cwd, flatDir)) : false;
161
+ const prefixedPath = join(ctx.cwd, prefixedDir);
162
+ const flatPath = flatDir ? join(ctx.cwd, flatDir) : null;
163
+ const checkContent = ctx.hasContent ?? ((p) => dirHasContent(p, ctx.exists));
164
+ const hasPrefixed = checkContent(prefixedPath);
165
+ const hasFlat = flatPath ? checkContent(flatPath) : false;
144
166
  if (hasPrefixed || hasFlat) {
145
167
  const found = hasPrefixed ? prefixedDir : flatDir;
146
168
  results.push({
@@ -150,10 +172,13 @@ export function checkProjectConfigs(ctx) {
150
172
  });
151
173
  }
152
174
  else {
175
+ const dirExists = ctx.exists(prefixedPath) || (flatPath ? ctx.exists(flatPath) : false);
153
176
  results.push({
154
177
  name: `${pkg}-config`,
155
178
  status: "warn",
156
- message: `${prefixedDir}/ not found`,
179
+ message: dirExists
180
+ ? `${prefixedDir}/ exists but is empty`
181
+ : `${prefixedDir}/ not found`,
157
182
  fix: `swarmkit init (or ${pkg} init)`,
158
183
  });
159
184
  }
@@ -151,22 +151,26 @@ describe("checkProjectConfigs", () => {
151
151
  const results = checkProjectConfigs(ctx);
152
152
  expect(results).toEqual([]);
153
153
  });
154
- it("passes when prefixed config directory exists", () => {
154
+ it("passes when prefixed config directory exists with content", () => {
155
+ const hasDir = (path) => path.endsWith(".swarm/opentasks");
155
156
  const ctx = createContext({
156
157
  isProject: true,
157
158
  installedPackages: ["opentasks"],
158
- exists: (path) => path.endsWith(".swarm/opentasks"),
159
+ exists: hasDir,
160
+ hasContent: hasDir,
159
161
  });
160
162
  const results = checkProjectConfigs(ctx);
161
163
  expect(results).toHaveLength(1);
162
164
  expect(results[0].status).toBe("pass");
163
165
  expect(results[0].message).toContain(".swarm/opentasks");
164
166
  });
165
- it("passes when flat config directory exists", () => {
167
+ it("passes when flat config directory exists with content", () => {
168
+ const hasDir = (path) => path.endsWith(".opentasks");
166
169
  const ctx = createContext({
167
170
  isProject: true,
168
171
  installedPackages: ["opentasks"],
169
- exists: (path) => path.endsWith(".opentasks"),
172
+ exists: hasDir,
173
+ hasContent: hasDir,
170
174
  });
171
175
  const results = checkProjectConfigs(ctx);
172
176
  expect(results).toHaveLength(1);
@@ -186,10 +190,12 @@ describe("checkProjectConfigs", () => {
186
190
  expect(results[0].fix).toContain("opentasks init");
187
191
  });
188
192
  it("passes for claude-code-swarm with prefixed config", () => {
193
+ const hasDir = (path) => path.endsWith(".swarm/claude-swarm");
189
194
  const ctx = createContext({
190
195
  isProject: true,
191
196
  installedPackages: ["claude-code-swarm"],
192
- exists: (path) => path.endsWith(".swarm/claude-swarm"),
197
+ exists: hasDir,
198
+ hasContent: hasDir,
193
199
  });
194
200
  const results = checkProjectConfigs(ctx);
195
201
  expect(results).toHaveLength(1);
@@ -210,11 +216,13 @@ describe("checkProjectConfigs", () => {
210
216
  "/tmp/test-project/.swarm/opentasks",
211
217
  "/tmp/test-project/.minimem",
212
218
  ]);
219
+ const checkDir = (path) => existingDirs.has(path);
213
220
  const ctx = createContext({
214
221
  isProject: true,
215
222
  cwd: "/tmp/test-project",
216
223
  installedPackages: ["opentasks", "minimem", "cognitive-core"],
217
- exists: (path) => existingDirs.has(path),
224
+ exists: checkDir,
225
+ hasContent: checkDir,
218
226
  });
219
227
  const results = checkProjectConfigs(ctx);
220
228
  expect(results).toHaveLength(3);
@@ -222,6 +230,18 @@ describe("checkProjectConfigs", () => {
222
230
  expect(results[1].status).toBe("pass"); // minimem (flat)
223
231
  expect(results[2].status).toBe("warn"); // cognitive-core (missing)
224
232
  });
233
+ it("warns with 'empty' message when directory exists but has no content", () => {
234
+ const ctx = createContext({
235
+ isProject: true,
236
+ installedPackages: ["opentasks"],
237
+ exists: (path) => path.endsWith(".swarm/opentasks"),
238
+ hasContent: () => false,
239
+ });
240
+ const results = checkProjectConfigs(ctx);
241
+ expect(results).toHaveLength(1);
242
+ expect(results[0].status).toBe("warn");
243
+ expect(results[0].message).toContain("empty");
244
+ });
225
245
  });
226
246
  describe("checkIntegrations", () => {
227
247
  it("passes when both packages in an integration are installed", async () => {
@@ -283,6 +303,7 @@ describe("runAllChecks", () => {
283
303
  env: { HOME: "/home/testuser" },
284
304
  getInstalledVersion: async () => "0.1.0",
285
305
  exists: () => true,
306
+ hasContent: () => true,
286
307
  });
287
308
  const report = await runAllChecks(ctx);
288
309
  expect(report.packages).toHaveLength(2);
@@ -24,6 +24,8 @@ export interface CheckContext {
24
24
  getInstalledVersion: (pkg: string) => Promise<string | null>;
25
25
  /** Check if a file/directory exists — injectable for testing */
26
26
  exists: (path: string) => boolean;
27
+ /** Check if a directory has files inside — injectable for testing */
28
+ hasContent?: (path: string) => boolean;
27
29
  /** Environment variables */
28
30
  env: Record<string, string | undefined>;
29
31
  }
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, symlinkSync, writeFileSync, } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  const execFileAsync = promisify(execFile);
@@ -51,8 +51,8 @@ export const PROJECT_INIT_ORDER = [
51
51
  export function isProjectInit(cwd, pkg) {
52
52
  const prefixed = PROJECT_CONFIG_DIRS[pkg];
53
53
  const flat = FLAT_PROJECT_CONFIG_DIRS[pkg];
54
- return (prefixed ? existsSync(join(cwd, prefixed)) : false) ||
55
- (flat ? existsSync(join(cwd, flat)) : false);
54
+ return (prefixed ? hasContent(join(cwd, prefixed)) : false) ||
55
+ (flat ? hasContent(join(cwd, flat)) : false);
56
56
  }
57
57
  /** Ensure the .swarm/ project root directory exists */
58
58
  function ensureProjectRoot(cwd) {
@@ -63,9 +63,9 @@ function ensureProjectRoot(cwd) {
63
63
  }
64
64
  /**
65
65
  * After init, relocate a package's config dir into .swarm/ if the CLI
66
- * created it at the legacy top-level location, then leave a symlink at
67
- * the old path so the package can still find its data at runtime.
68
- * This is a no-op when the package already respects its env-var override.
66
+ * created it at the legacy top-level location.
67
+ * All supported packages natively check .swarm/<pkg> before .<pkg>,
68
+ * so no symlink is needed.
69
69
  */
70
70
  function relocate(cwd, legacyName, targetName) {
71
71
  const src = join(cwd, legacyName);
@@ -76,26 +76,9 @@ function relocate(cwd, legacyName, targetName) {
76
76
  renameSync(src, dest);
77
77
  }
78
78
  catch {
79
- return; // Non-fatal — leave in legacy location
79
+ // Non-fatal — leave in legacy location
80
80
  }
81
81
  }
82
- // Create a symlink at the legacy location so packages find their data
83
- ensureSymlink(cwd, legacyName, targetName);
84
- }
85
- /** Create a relative symlink: cwd/<legacyName> → .swarm/<targetName> */
86
- function ensureSymlink(cwd, legacyName, targetName) {
87
- const link = join(cwd, legacyName);
88
- const target = join(PROJECT_ROOT, targetName); // relative path
89
- if (isSymlink(link))
90
- return; // Already symlinked
91
- if (existsSync(link))
92
- return; // Real directory exists — don't overwrite
93
- try {
94
- symlinkSync(target, link);
95
- }
96
- catch {
97
- // Non-fatal — package can still be configured via env var
98
- }
99
82
  }
100
83
  function isSymlink(path) {
101
84
  try {
@@ -119,6 +102,10 @@ export async function initProjectPackage(pkg, ctx) {
119
102
  // for packages that create directly in .swarm/, this is a no-op.
120
103
  if (ctx.usePrefix)
121
104
  relocate(ctx.cwd, ".opentasks", "opentasks");
105
+ // Older opentasks versions write .gitattributes at the project root.
106
+ // Newer versions write it inside the opentasks dir. Clean up the
107
+ // root-level file as a backwards-compat fallback.
108
+ cleanupGitattributes(ctx.cwd);
122
109
  return result;
123
110
  }
124
111
  case "minimem":
@@ -156,7 +143,7 @@ export function isGlobalInit(pkg) {
156
143
  const dir = GLOBAL_CONFIG_DIRS[pkg];
157
144
  if (!dir)
158
145
  return false;
159
- return existsSync(join(homedir(), dir));
146
+ return hasContent(join(homedir(), dir));
160
147
  }
161
148
  /** Initialize a single global package */
162
149
  export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
@@ -206,40 +193,52 @@ async function shellInit(command, args, cwd, envOverrides) {
206
193
  }
207
194
  async function initMinimem(ctx) {
208
195
  const targetDir = ctx.usePrefix
209
- ? join(ctx.cwd, PROJECT_ROOT, "minimem")
210
- : join(ctx.cwd, ".minimem");
211
- try {
212
- await execFileAsync("minimem", ["init"], {
213
- cwd: ctx.cwd,
214
- timeout: 30_000,
215
- env: ctx.usePrefix
216
- ? { ...cleanEnv(), MINIMEM_CONFIG_DIR: join(PROJECT_ROOT, "minimem") }
217
- : cleanEnv(),
218
- });
219
- // relocate handles packages that don't yet respect the env var;
220
- // for packages that create directly in .swarm/, this is a no-op.
221
- if (ctx.usePrefix) {
222
- relocate(ctx.cwd, ".minimem", "minimem");
196
+ ? join(PROJECT_ROOT, "minimem")
197
+ : ".minimem";
198
+ const absTarget = join(ctx.cwd, targetDir);
199
+ // Try CLI init first (creates contained layout in newer versions)
200
+ const result = await shellInit("minimem", ["init", targetDir], ctx.cwd);
201
+ // Verify the CLI created the contained layout (config.json at root).
202
+ // Older minimem versions create a nested .minimem/ subdir instead.
203
+ if (!existsSync(join(absTarget, "config.json"))) {
204
+ initMinimemInline(absTarget);
205
+ }
206
+ // Patch embedding provider if the wizard chose one
207
+ const provider = ctx.embeddingProvider && ctx.embeddingProvider !== "local"
208
+ ? ctx.embeddingProvider
209
+ : null;
210
+ if (provider) {
211
+ const configPath = join(absTarget, "config.json");
212
+ try {
213
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
214
+ config.embedding = { ...config.embedding, provider };
215
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
223
216
  }
224
- // Patch embedding provider if configured
225
- if (ctx.embeddingProvider && ctx.embeddingProvider !== "local") {
226
- const configPath = join(targetDir, "config.json");
227
- if (existsSync(configPath)) {
228
- try {
229
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
230
- config.embedding = config.embedding || {};
231
- config.embedding.provider = ctx.embeddingProvider;
232
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
233
- }
234
- catch {
235
- // Non-fatal — user can configure manually
236
- }
237
- }
217
+ catch {
218
+ // Non-fatal
238
219
  }
239
- return { package: "minimem", success: true };
240
220
  }
241
- catch (err) {
242
- return { package: "minimem", success: false, message: formatError(err) };
221
+ return { package: "minimem", success: true };
222
+ }
223
+ /** Fallback: create contained layout inline when CLI is unavailable or outdated */
224
+ function initMinimemInline(targetDir) {
225
+ mkdirSync(targetDir, { recursive: true });
226
+ mkdirSync(join(targetDir, "memory"), { recursive: true });
227
+ const configPath = join(targetDir, "config.json");
228
+ if (!existsSync(configPath)) {
229
+ writeFileSync(configPath, JSON.stringify({
230
+ embedding: { provider: "auto" },
231
+ hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
232
+ query: { maxResults: 10, minScore: 0.3 },
233
+ }, null, 2) + "\n");
234
+ }
235
+ const memoryPath = join(targetDir, "MEMORY.md");
236
+ if (!existsSync(memoryPath)) {
237
+ writeFileSync(memoryPath, "# Memory\n\nAdd notes, decisions, and context here.\n");
238
+ }
239
+ const gitignorePath = join(targetDir, ".gitignore");
240
+ if (!existsSync(gitignorePath)) {
241
+ writeFileSync(gitignorePath, "index.db\nindex.db-*\n");
243
242
  }
244
243
  }
245
244
  async function initSdr(ctx) {
@@ -260,6 +259,12 @@ async function initSkillTreeProject(ctx) {
260
259
  : join(ctx.cwd, ".skilltree");
261
260
  mkdirSync(targetDir, { recursive: true });
262
261
  mkdirSync(join(targetDir, "skills"), { recursive: true });
262
+ mkdirSync(join(targetDir, ".cache"), { recursive: true });
263
+ // Gitignore the cache directory
264
+ const gitignorePath = join(targetDir, ".gitignore");
265
+ if (!existsSync(gitignorePath)) {
266
+ writeFileSync(gitignorePath, ".cache/\n");
267
+ }
263
268
  return { package: "skill-tree", success: true };
264
269
  }
265
270
  catch (err) {
@@ -271,15 +276,24 @@ async function initSkillTreeProject(ctx) {
271
276
  }
272
277
  }
273
278
  async function initOpenteamsProject(ctx) {
279
+ const targetDir = ctx.usePrefix
280
+ ? join(ctx.cwd, PROJECT_ROOT, "openteams")
281
+ : join(ctx.cwd, ".openteams");
282
+ // Try CLI first — `openteams template init` creates config.json with proper defaults
283
+ const result = await shellInit("openteams", ["template", "init", "-d", ctx.cwd], ctx.cwd, ctx.usePrefix
284
+ ? { OPENTEAMS_PROJECT_DIR: join(PROJECT_ROOT, "openteams") }
285
+ : undefined);
286
+ if (result.success) {
287
+ if (ctx.usePrefix)
288
+ relocate(ctx.cwd, ".openteams", "openteams");
289
+ return result;
290
+ }
291
+ // Fallback: create config inline if CLI is not available
274
292
  try {
275
- const targetDir = ctx.usePrefix
276
- ? join(ctx.cwd, PROJECT_ROOT, "openteams")
277
- : join(ctx.cwd, ".openteams");
278
293
  mkdirSync(targetDir, { recursive: true });
279
- // Write default config.json (matches `openteams template init` with no options)
280
294
  const configPath = join(targetDir, "config.json");
281
295
  if (!existsSync(configPath)) {
282
- writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
296
+ writeFileSync(configPath, JSON.stringify({ defaults: {} }, null, 2) + "\n");
283
297
  }
284
298
  return { package: "openteams", success: true };
285
299
  }
@@ -300,7 +314,14 @@ async function initSessionlogProject(ctx) {
300
314
  // Write default settings.json
301
315
  const settingsPath = join(targetDir, "settings.json");
302
316
  if (!existsSync(settingsPath)) {
303
- writeFileSync(settingsPath, JSON.stringify({ enabled: false, strategy: "manual-commit" }, null, 2) + "\n");
317
+ const defaultSettings = {
318
+ enabled: false,
319
+ strategy: "manual-commit",
320
+ logLevel: "warn",
321
+ telemetryEnabled: false,
322
+ summarizationEnabled: false,
323
+ };
324
+ writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
304
325
  }
305
326
  return { package: "sessionlog", success: true };
306
327
  }
@@ -371,10 +392,24 @@ async function initClaudeSwarmProject(ctx) {
371
392
  ? join(ctx.cwd, PROJECT_ROOT, "claude-swarm")
372
393
  : join(ctx.cwd, ".claude-swarm");
373
394
  mkdirSync(targetDir, { recursive: true });
374
- // Write default config.json
395
+ // Write default config.json with meaningful defaults
375
396
  const configPath = join(targetDir, "config.json");
376
397
  if (!existsSync(configPath)) {
377
- writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
398
+ const defaultConfig = {
399
+ template: "",
400
+ map: {
401
+ enabled: false,
402
+ server: "ws://localhost:8080",
403
+ scope: "",
404
+ systemId: "system-claude-swarm",
405
+ sidecar: "session",
406
+ },
407
+ sessionlog: {
408
+ enabled: false,
409
+ sync: "off",
410
+ },
411
+ };
412
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
378
413
  }
379
414
  // Write .gitignore for tmp/
380
415
  const gitignorePath = join(targetDir, ".gitignore");
@@ -392,6 +427,51 @@ async function initClaudeSwarmProject(ctx) {
392
427
  }
393
428
  }
394
429
  // ─── Helpers ─────────────────────────────────────────────────────────────────
430
+ /** Check if a directory exists and contains at least one file (recursively) */
431
+ function hasContent(dir) {
432
+ if (!existsSync(dir))
433
+ return false;
434
+ try {
435
+ const entries = readdirSync(dir, { withFileTypes: true });
436
+ for (const entry of entries) {
437
+ if (entry.isFile() || entry.isSymbolicLink())
438
+ return true;
439
+ if (entry.isDirectory() && hasContent(join(dir, entry.name)))
440
+ return true;
441
+ }
442
+ return false;
443
+ }
444
+ catch {
445
+ return false;
446
+ }
447
+ }
448
+ /**
449
+ * Remove root-level .gitattributes created by older opentasks versions.
450
+ * Newer versions write .gitattributes inside the opentasks directory.
451
+ */
452
+ function cleanupGitattributes(cwd) {
453
+ const attrPath = join(cwd, ".gitattributes");
454
+ if (!existsSync(attrPath))
455
+ return;
456
+ try {
457
+ const content = readFileSync(attrPath, "utf-8");
458
+ const cleaned = content
459
+ .split("\n")
460
+ .filter((line) => !line.includes("merge=opentasks") &&
461
+ !line.includes("OpenTasks merge driver"))
462
+ .join("\n")
463
+ .trim();
464
+ if (!cleaned) {
465
+ unlinkSync(attrPath);
466
+ }
467
+ else {
468
+ writeFileSync(attrPath, cleaned + "\n");
469
+ }
470
+ }
471
+ catch {
472
+ // Non-fatal
473
+ }
474
+ }
395
475
  function getProjectName(cwd) {
396
476
  const pkgPath = join(cwd, "package.json");
397
477
  if (existsSync(pkgPath)) {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync, existsSync, lstatSync, readFileSync, readlinkSync, writeFileSync, realpathSync, } from "node:fs";
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
3
3
  import { join, basename } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { randomUUID } from "node:crypto";
@@ -105,12 +105,20 @@ describe("isProjectInit", () => {
105
105
  expect(isProjectInit(testDir, pkg)).toBe(false);
106
106
  }
107
107
  });
108
- it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
109
- mkdirSync(join(testDir, configDir), { recursive: true });
108
+ it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (prefixed)", (pkg, configDir) => {
109
+ const dir = join(testDir, configDir);
110
+ mkdirSync(dir, { recursive: true });
111
+ writeFileSync(join(dir, "config.json"), "{}");
110
112
  expect(isProjectInit(testDir, pkg)).toBe(true);
111
113
  });
112
- it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (flat)", (pkg, configDir) => {
114
+ it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns false for %s when %s/ exists but is empty (prefixed)", (pkg, configDir) => {
113
115
  mkdirSync(join(testDir, configDir), { recursive: true });
116
+ expect(isProjectInit(testDir, pkg)).toBe(false);
117
+ });
118
+ it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (flat)", (pkg, configDir) => {
119
+ const dir = join(testDir, configDir);
120
+ mkdirSync(dir, { recursive: true });
121
+ writeFileSync(join(dir, "config.json"), "{}");
114
122
  expect(isProjectInit(testDir, pkg)).toBe(true);
115
123
  });
116
124
  it("returns false for unknown package", () => {
@@ -127,10 +135,16 @@ describe("isGlobalInit", () => {
127
135
  expect(isGlobalInit(pkg)).toBe(false);
128
136
  }
129
137
  });
130
- it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists", (pkg, configDir) => {
131
- mkdirSync(join(testHome, configDir), { recursive: true });
138
+ it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists with content", (pkg, configDir) => {
139
+ const dir = join(testHome, configDir);
140
+ mkdirSync(dir, { recursive: true });
141
+ writeFileSync(join(dir, "config.json"), "{}");
132
142
  expect(isGlobalInit(pkg)).toBe(true);
133
143
  });
144
+ it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns false for %s when ~/%s/ exists but is empty", (pkg, configDir) => {
145
+ mkdirSync(join(testHome, configDir), { recursive: true });
146
+ expect(isGlobalInit(pkg)).toBe(false);
147
+ });
134
148
  it("returns false for unknown package", () => {
135
149
  expect(isGlobalInit("nonexistent")).toBe(false);
136
150
  });
@@ -138,7 +152,7 @@ describe("isGlobalInit", () => {
138
152
  // ─── Project: opentasks (real CLI) ───────────────────────────────────────────
139
153
  describe("initProjectPackage — opentasks (real CLI)", async () => {
140
154
  const installed = await hasCliInstalled("opentasks");
141
- it.skipIf(!installed)("runs opentasks init, relocates to .swarm/, and creates symlink", async () => {
155
+ it.skipIf(!installed)("runs opentasks init and relocates to .swarm/", async () => {
142
156
  createProject("test-opentasks");
143
157
  const result = await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
144
158
  expect(result.success).toBe(true);
@@ -148,12 +162,10 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
148
162
  expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
149
163
  expect(existsSync(join(testDir, ".swarm", "opentasks", "graph.jsonl"))).toBe(true);
150
164
  expect(existsSync(join(testDir, ".swarm", "opentasks", ".gitignore"))).toBe(true);
151
- // Verify symlink at legacy location
152
- const link = join(testDir, ".opentasks");
153
- expect(lstatSync(link).isSymbolicLink()).toBe(true);
154
- expect(readlinkSync(link)).toBe(".swarm/opentasks");
155
- // Accessible via symlink
156
- expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
165
+ // No symlink at legacy location (packages natively check .swarm/)
166
+ expect(existsSync(join(testDir, ".opentasks"))).toBe(false);
167
+ // .gitattributes should be cleaned up (merge driver creates it at root)
168
+ expect(existsSync(join(testDir, ".gitattributes"))).toBe(false);
157
169
  // Verify config content
158
170
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
159
171
  expect(config.version).toBe("1.0");
@@ -175,24 +187,30 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
175
187
  expect(config.location.name).toBe(basename(testDir));
176
188
  });
177
189
  });
178
- // ─── Project: minimem (real CLI) + embedding patching ────────────────────────
179
- describe("initProjectPackage — minimem (real CLI)", async () => {
180
- const installed = await hasCliInstalled("minimem");
181
- it.skipIf(!installed)("runs minimem init and creates .minimem/", async () => {
190
+ // ─── Project: minimem (CLI with inline fallback) ─────────────────────────────
191
+ describe("initProjectPackage — minimem", () => {
192
+ it("creates all artifacts inside .swarm/minimem/", async () => {
182
193
  createProject("test-minimem");
183
194
  const result = await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
184
195
  expect(result.success).toBe(true);
185
- // Verify real filesystem artifacts
196
+ // Core artifacts inside .swarm/minimem/
186
197
  expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
187
198
  expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
188
199
  expect(existsSync(join(testDir, ".swarm", "minimem", ".gitignore"))).toBe(true);
189
- expect(existsSync(join(testDir, "MEMORY.md"))).toBe(true);
200
+ expect(existsSync(join(testDir, ".swarm", "minimem", "MEMORY.md"))).toBe(true);
201
+ expect(existsSync(join(testDir, ".swarm", "minimem", "memory"))).toBe(true);
202
+ // Nothing at project root
203
+ expect(existsSync(join(testDir, "MEMORY.md"))).toBe(false);
204
+ expect(existsSync(join(testDir, "memory"))).toBe(false);
190
205
  // Verify default config
191
206
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
192
207
  expect(config.embedding.provider).toBe("auto");
193
208
  expect(config.hybrid.enabled).toBe(true);
209
+ // Verify .gitignore content
210
+ const gitignore = readFileSync(join(testDir, ".swarm", "minimem", ".gitignore"), "utf-8");
211
+ expect(gitignore).toContain("index.db");
194
212
  });
195
- it.skipIf(!installed)("patches embedding.provider to openai", async () => {
213
+ it("sets embedding.provider to openai", async () => {
196
214
  createProject("test-embed");
197
215
  await initProjectPackage("minimem", projectCtx({
198
216
  cwd: testDir,
@@ -201,11 +219,8 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
201
219
  }));
202
220
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
203
221
  expect(config.embedding.provider).toBe("openai");
204
- // Other fields preserved
205
- expect(config.hybrid.enabled).toBe(true);
206
- expect(config.query.maxResults).toBe(10);
207
222
  });
208
- it.skipIf(!installed)("patches embedding.provider to gemini", async () => {
223
+ it("sets embedding.provider to gemini", async () => {
209
224
  createProject("test-embed-gemini");
210
225
  await initProjectPackage("minimem", projectCtx({
211
226
  cwd: testDir,
@@ -215,7 +230,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
215
230
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
216
231
  expect(config.embedding.provider).toBe("gemini");
217
232
  });
218
- it.skipIf(!installed)("does NOT patch when provider is local", async () => {
233
+ it("uses 'auto' when provider is local", async () => {
219
234
  createProject("test-local");
220
235
  await initProjectPackage("minimem", projectCtx({
221
236
  cwd: testDir,
@@ -225,7 +240,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
225
240
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
226
241
  expect(config.embedding.provider).toBe("auto");
227
242
  });
228
- it.skipIf(!installed)("does NOT patch when provider is null", async () => {
243
+ it("uses 'auto' when provider is null", async () => {
229
244
  createProject("test-null");
230
245
  await initProjectPackage("minimem", projectCtx({
231
246
  cwd: testDir,
@@ -276,12 +291,17 @@ describe("initProjectPackage — skill-tree", () => {
276
291
  expect(result.package).toBe("skill-tree");
277
292
  expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
278
293
  expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
294
+ expect(existsSync(join(testDir, ".swarm", "skilltree", ".cache"))).toBe(true);
295
+ expect(existsSync(join(testDir, ".swarm", "skilltree", ".gitignore"))).toBe(true);
296
+ expect(readFileSync(join(testDir, ".swarm", "skilltree", ".gitignore"), "utf-8")).toBe(".cache/\n");
279
297
  });
280
298
  it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
281
299
  const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
282
300
  expect(result.success).toBe(true);
283
301
  expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
284
302
  expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
303
+ expect(existsSync(join(testDir, ".skilltree", ".cache"))).toBe(true);
304
+ expect(existsSync(join(testDir, ".skilltree", ".gitignore"))).toBe(true);
285
305
  });
286
306
  it("is idempotent", async () => {
287
307
  await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
@@ -298,6 +318,7 @@ describe("initProjectPackage — openteams", () => {
298
318
  expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
299
319
  expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
300
320
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
321
+ // openteams CLI writes {} by default (all built-in templates active)
301
322
  expect(config).toEqual({});
302
323
  });
303
324
  it("creates .openteams/ with config.json (flat)", async () => {
@@ -329,8 +350,13 @@ describe("initProjectPackage — sessionlog", () => {
329
350
  expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
330
351
  expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
331
352
  const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
332
- expect(settings.enabled).toBe(false);
333
- expect(settings.strategy).toBe("manual-commit");
353
+ expect(settings).toEqual({
354
+ enabled: false,
355
+ strategy: "manual-commit",
356
+ logLevel: "warn",
357
+ telemetryEnabled: false,
358
+ summarizationEnabled: false,
359
+ });
334
360
  });
335
361
  it("creates .sessionlog/ with settings.json (flat)", async () => {
336
362
  const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
@@ -363,7 +389,20 @@ describe("initProjectPackage — claude-code-swarm", () => {
363
389
  expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
364
390
  expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
365
391
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
366
- expect(config).toEqual({});
392
+ expect(config).toEqual({
393
+ template: "",
394
+ map: {
395
+ enabled: false,
396
+ server: "ws://localhost:8080",
397
+ scope: "",
398
+ systemId: "system-claude-swarm",
399
+ sidecar: "session",
400
+ },
401
+ sessionlog: {
402
+ enabled: false,
403
+ sync: "off",
404
+ },
405
+ });
367
406
  const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
368
407
  expect(gitignore).toBe("tmp/\n");
369
408
  });
@@ -417,7 +456,7 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
417
456
  expect(existsSync(configPath)).toBe(true);
418
457
  const yaml = readFileSync(configPath, "utf-8");
419
458
  expect(yaml).toContain("storage:");
420
- expect(yaml).toContain("sqlite");
459
+ expect(yaml.toLowerCase()).toContain("sqlite");
421
460
  expect(yaml).toContain("indexer:");
422
461
  });
423
462
  it.skipIf(!installed)("skips when config.yaml already exists", async () => {
@@ -514,8 +553,7 @@ describe("initGlobalPackage — unknown", () => {
514
553
  // ─── E2E: full project init flow (solo bundle) ──────────────────────────────
515
554
  describe("e2e: project init — solo bundle", async () => {
516
555
  const opentasksOk = await hasCliInstalled("opentasks");
517
- const minimemOk = await hasCliInstalled("minimem");
518
- const allOk = opentasksOk && minimemOk;
556
+ const allOk = opentasksOk; // minimem init is inline (no CLI needed)
519
557
  it.skipIf(!allOk)("initializes opentasks → minimem with cross-wiring", async () => {
520
558
  createProject("solo-e2e");
521
559
  const ctx = projectCtx({
@@ -576,9 +614,8 @@ describe("e2e: project init — solo bundle", async () => {
576
614
  // ─── E2E: full project init flow (team bundle — init order) ─────────────────
577
615
  describe("e2e: project init — team bundle (init order)", async () => {
578
616
  const opentasksOk = await hasCliInstalled("opentasks");
579
- const minimemOk = await hasCliInstalled("minimem");
580
617
  const ccOk = await hasCliInstalled("cognitive-core");
581
- const allOk = opentasksOk && minimemOk && ccOk;
618
+ const allOk = opentasksOk && ccOk; // minimem init is inline (no CLI needed)
582
619
  it.skipIf(!allOk)("cognitive-core runs after minimem (can detect .minimem/)", async () => {
583
620
  createProject("team-e2e");
584
621
  const ctx = projectCtx({
@@ -699,7 +736,20 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
699
736
  expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
700
737
  expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
701
738
  const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
702
- expect(config).toEqual({});
739
+ expect(config).toEqual({
740
+ template: "",
741
+ map: {
742
+ enabled: false,
743
+ server: "ws://localhost:8080",
744
+ scope: "",
745
+ systemId: "system-claude-swarm",
746
+ sidecar: "session",
747
+ },
748
+ sessionlog: {
749
+ enabled: false,
750
+ sync: "off",
751
+ },
752
+ });
703
753
  const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
704
754
  expect(gitignore).toBe("tmp/\n");
705
755
  // isProjectInit now returns true for both
@@ -734,8 +784,13 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
734
784
  expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
735
785
  // sessionlog has default settings
736
786
  const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
737
- expect(settings.enabled).toBe(false);
738
- expect(settings.strategy).toBe("manual-commit");
787
+ expect(settings).toEqual({
788
+ enabled: false,
789
+ strategy: "manual-commit",
790
+ logLevel: "warn",
791
+ telemetryEnabled: false,
792
+ summarizationEnabled: false,
793
+ });
739
794
  });
740
795
  it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
741
796
  const cwd = testDir;
@@ -789,9 +844,8 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
789
844
  });
790
845
  });
791
846
  // ─── E2E: embedding provider propagation ─────────────────────────────────────
792
- describe("e2e: embedding provider propagation", async () => {
793
- const minimemOk = await hasCliInstalled("minimem");
794
- it.skipIf(!minimemOk)("openai embedding flows through to real minimem config", async () => {
847
+ describe("e2e: embedding provider propagation", () => {
848
+ it("openai embedding flows through to minimem config", async () => {
795
849
  createProject("embed-e2e");
796
850
  await initProjectPackage("minimem", projectCtx({
797
851
  cwd: testDir,
@@ -801,7 +855,6 @@ describe("e2e: embedding provider propagation", async () => {
801
855
  }));
802
856
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
803
857
  expect(config.embedding.provider).toBe("openai");
804
- // Real minimem config has these fields too
805
858
  expect(config.hybrid).toBeDefined();
806
859
  expect(config.query).toBeDefined();
807
860
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmkit",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Multi-agent infa toolkit",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",