swarmkit 0.0.5 → 0.0.7

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 (38) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/configure/configure.d.ts +5 -0
  3. package/dist/commands/configure/configure.js +354 -0
  4. package/dist/commands/configure/configure.test.d.ts +1 -0
  5. package/dist/commands/configure/configure.test.js +539 -0
  6. package/dist/commands/configure/read-config.d.ts +12 -0
  7. package/dist/commands/configure/read-config.js +81 -0
  8. package/dist/commands/configure.d.ts +2 -0
  9. package/dist/commands/configure.js +14 -0
  10. package/dist/commands/init/phases/configure.js +0 -21
  11. package/dist/commands/init/phases/global-setup.d.ts +1 -1
  12. package/dist/commands/init/phases/global-setup.js +22 -44
  13. package/dist/commands/init/phases/integrations.d.ts +16 -0
  14. package/dist/commands/init/phases/integrations.js +172 -0
  15. package/dist/commands/init/phases/package-config.d.ts +10 -0
  16. package/dist/commands/init/phases/package-config.js +117 -0
  17. package/dist/commands/init/phases/phases.test.d.ts +1 -0
  18. package/dist/commands/init/phases/phases.test.js +711 -0
  19. package/dist/commands/init/phases/project.js +17 -0
  20. package/dist/commands/init/phases/review.d.ts +8 -0
  21. package/dist/commands/init/phases/review.js +79 -0
  22. package/dist/commands/init/phases/use-case.js +41 -27
  23. package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
  24. package/dist/commands/init/phases/wizard-flow.test.js +657 -0
  25. package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
  26. package/dist/commands/init/phases/wizard-modes.test.js +270 -0
  27. package/dist/commands/init/state.d.ts +31 -1
  28. package/dist/commands/init/state.js +4 -0
  29. package/dist/commands/init/state.test.js +7 -0
  30. package/dist/commands/init/wizard.d.ts +1 -0
  31. package/dist/commands/init/wizard.js +31 -23
  32. package/dist/commands/init.js +2 -0
  33. package/dist/packages/registry.d.ts +66 -0
  34. package/dist/packages/registry.js +258 -0
  35. package/dist/packages/setup.d.ts +42 -0
  36. package/dist/packages/setup.js +311 -56
  37. package/dist/packages/setup.test.js +546 -42
  38. package/package.json +1 -1
@@ -1,9 +1,99 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { existsSync, lstatSync, mkdirSync, readdirSync, 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);
7
+ /**
8
+ * Run a package's own setup wizard.
9
+ *
10
+ * In interactive mode (default): uses `stdio: 'inherit'` so the package's
11
+ * prompts take over the terminal.
12
+ *
13
+ * In non-interactive mode: uses `stdio: 'pipe'` with `nonInteractiveArgs`
14
+ * (falling back to `args`), suitable for --quick mode, CI, and testing.
15
+ */
16
+ export async function runPackageWizard(pkg, wizardConfig, state, opts) {
17
+ // Backwards compat: opts used to be just cwd string
18
+ const options = typeof opts === "string"
19
+ ? { cwd: opts }
20
+ : opts ?? {};
21
+ const interactive = options.interactive ?? true;
22
+ const env = {
23
+ ...cleanEnv(),
24
+ HOME: homedir(),
25
+ };
26
+ // Pass swarmkit context via env vars
27
+ if (state.embeddingProvider) {
28
+ env.SWARMKIT_EMBEDDING_PROVIDER = state.embeddingProvider;
29
+ }
30
+ if (state.usePrefix !== undefined) {
31
+ env.SWARMKIT_USE_PREFIX = String(state.usePrefix);
32
+ }
33
+ if (state.apiKeys.openai) {
34
+ env.OPENAI_API_KEY = state.apiKeys.openai;
35
+ }
36
+ if (state.apiKeys.gemini) {
37
+ env.GEMINI_API_KEY = state.apiKeys.gemini;
38
+ }
39
+ // Merge package-specific env overrides
40
+ if (wizardConfig.env) {
41
+ Object.assign(env, wizardConfig.env);
42
+ }
43
+ const timeout = wizardConfig.timeout ?? 120_000;
44
+ const args = (!interactive && wizardConfig.nonInteractiveArgs)
45
+ ? wizardConfig.nonInteractiveArgs
46
+ : wizardConfig.args;
47
+ const stdio = interactive ? "inherit" : "pipe";
48
+ return new Promise((resolve) => {
49
+ const child = spawn(wizardConfig.command, args, {
50
+ stdio,
51
+ env,
52
+ cwd: options.cwd,
53
+ timeout,
54
+ });
55
+ child.on("close", (code) => {
56
+ if (code === 0) {
57
+ resolve({ package: pkg, success: true });
58
+ }
59
+ else {
60
+ resolve({
61
+ package: pkg,
62
+ success: false,
63
+ message: `exited with code ${code}`,
64
+ });
65
+ }
66
+ });
67
+ child.on("error", (err) => {
68
+ resolve({
69
+ package: pkg,
70
+ success: false,
71
+ message: formatError(err),
72
+ });
73
+ });
74
+ });
75
+ }
76
+ /**
77
+ * Env var overrides that tell each package's CLI to create config in the
78
+ * .swarm/<pkg>/ prefix layout instead of the default flat location.
79
+ * Used by both initProjectPackage (via shellInit) and runPackageWizard.
80
+ */
81
+ export const PREFIX_ENV_VARS = {
82
+ opentasks: "OPENTASKS_PROJECT_DIR",
83
+ "cognitive-core": "COGNITIVE_CORE_HOME",
84
+ "self-driving-repo": "SDR_CONFIG_DIR",
85
+ openteams: "OPENTEAMS_PROJECT_DIR",
86
+ };
87
+ /**
88
+ * Flat dir name → prefixed dir name for relocation after CLI wizard runs.
89
+ * Packages that create at .<name>/ need relocation to .swarm/<name>/.
90
+ */
91
+ export const RELOCATE_MAP = {
92
+ opentasks: [".opentasks", "opentasks"],
93
+ "cognitive-core": [".cognitive-core", "cognitive-core"],
94
+ "self-driving-repo": [".self-driving", "self-driving"],
95
+ openteams: [".openteams", "openteams"],
96
+ };
7
97
  // ─── Project-level setup ─────────────────────────────────────────────────────
8
98
  /** Root directory for all swarmkit project-level config */
9
99
  export const PROJECT_ROOT = ".swarm";
@@ -33,6 +123,23 @@ export const FLAT_PROJECT_CONFIG_DIRS = {
33
123
  export function projectConfigDirs(usePrefix) {
34
124
  return usePrefix ? PROJECT_CONFIG_DIRS : FLAT_PROJECT_CONFIG_DIRS;
35
125
  }
126
+ /**
127
+ * After a CLI wizard runs for a project-level package, relocate its output
128
+ * from the flat location to the .swarm/ prefix location if needed.
129
+ */
130
+ export function relocateAfterWizard(cwd, pkg, usePrefix) {
131
+ if (!usePrefix)
132
+ return;
133
+ const entry = RELOCATE_MAP[pkg];
134
+ if (!entry)
135
+ return;
136
+ const root = join(cwd, PROJECT_ROOT);
137
+ if (!existsSync(root)) {
138
+ mkdirSync(root, { recursive: true });
139
+ }
140
+ const [legacyName, targetName] = entry;
141
+ relocate(cwd, legacyName, targetName);
142
+ }
36
143
  /**
37
144
  * Project-level packages in correct init order.
38
145
  * Order matters: minimem before cognitive-core (runtime detection).
@@ -63,9 +170,9 @@ function ensureProjectRoot(cwd) {
63
170
  }
64
171
  /**
65
172
  * 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.
173
+ * created it at the legacy top-level location.
174
+ * All supported packages natively check .swarm/<pkg> before .<pkg>,
175
+ * so no symlink is needed.
69
176
  */
70
177
  function relocate(cwd, legacyName, targetName) {
71
178
  const src = join(cwd, legacyName);
@@ -76,26 +183,9 @@ function relocate(cwd, legacyName, targetName) {
76
183
  renameSync(src, dest);
77
184
  }
78
185
  catch {
79
- return; // Non-fatal — leave in legacy location
186
+ // Non-fatal — leave in legacy location
80
187
  }
81
188
  }
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
189
  }
100
190
  function isSymlink(path) {
101
191
  try {
@@ -119,6 +209,10 @@ export async function initProjectPackage(pkg, ctx) {
119
209
  // for packages that create directly in .swarm/, this is a no-op.
120
210
  if (ctx.usePrefix)
121
211
  relocate(ctx.cwd, ".opentasks", "opentasks");
212
+ // Older opentasks versions write .gitattributes at the project root.
213
+ // Newer versions write it inside the opentasks dir. Clean up the
214
+ // root-level file as a backwards-compat fallback.
215
+ cleanupGitattributes(ctx.cwd);
122
216
  return result;
123
217
  }
124
218
  case "minimem":
@@ -150,6 +244,7 @@ export async function initProjectPackage(pkg, ctx) {
150
244
  export const GLOBAL_CONFIG_DIRS = {
151
245
  "skill-tree": ".skill-tree",
152
246
  openhive: ".openhive",
247
+ "claude-code-swarm": ".claude-swarm",
153
248
  };
154
249
  /** Check whether a global package is already configured */
155
250
  export function isGlobalInit(pkg) {
@@ -176,7 +271,7 @@ export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
176
271
  case "sessionlog":
177
272
  return { package: "sessionlog", success: true, message: "no setup required" };
178
273
  case "claude-code-swarm":
179
- return { package: "claude-code-swarm", success: true, message: "no setup required" };
274
+ return initClaudeSwarmGlobal(ctx);
180
275
  default:
181
276
  return { package: pkg, success: false, message: "Unknown package" };
182
277
  }
@@ -206,44 +301,65 @@ async function shellInit(command, args, cwd, envOverrides) {
206
301
  }
207
302
  async function initMinimem(ctx) {
208
303
  const targetDir = ctx.usePrefix
209
- ? join(ctx.cwd, PROJECT_ROOT, "minimem")
210
- : join(ctx.cwd, ".minimem");
304
+ ? join(PROJECT_ROOT, "minimem")
305
+ : ".minimem";
306
+ const absTarget = join(ctx.cwd, targetDir);
307
+ // Try CLI init first (creates contained layout in newer versions)
308
+ const result = await shellInit("minimem", ["init", targetDir], ctx.cwd);
309
+ // Verify the CLI created the contained layout (config.json at root).
310
+ // Older minimem versions create a nested .minimem/ subdir instead.
311
+ if (!existsSync(join(absTarget, "config.json"))) {
312
+ initMinimemInline(absTarget);
313
+ }
314
+ // Patch config with wizard choices
315
+ const configPath = join(absTarget, "config.json");
211
316
  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");
317
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
318
+ // Patch embedding provider if the wizard chose one
319
+ const provider = ctx.embeddingProvider && ctx.embeddingProvider !== "local"
320
+ ? ctx.embeddingProvider
321
+ : null;
322
+ if (provider) {
323
+ config.embedding = { ...config.embedding, provider };
223
324
  }
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
- }
325
+ // Apply per-package config overrides from the interactive config phase
326
+ const overrides = ctx.packageConfigs?.minimem?.values;
327
+ if (overrides) {
328
+ applyNestedOverrides(config, overrides);
238
329
  }
239
- return { package: "minimem", success: true };
330
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
240
331
  }
241
- catch (err) {
242
- return { package: "minimem", success: false, message: formatError(err) };
332
+ catch {
333
+ // Non-fatal
334
+ }
335
+ return { package: "minimem", success: true };
336
+ }
337
+ /** Fallback: create contained layout inline when CLI is unavailable or outdated */
338
+ function initMinimemInline(targetDir) {
339
+ mkdirSync(targetDir, { recursive: true });
340
+ mkdirSync(join(targetDir, "memory"), { recursive: true });
341
+ const configPath = join(targetDir, "config.json");
342
+ if (!existsSync(configPath)) {
343
+ writeFileSync(configPath, JSON.stringify({
344
+ embedding: { provider: "auto" },
345
+ hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
346
+ query: { maxResults: 10, minScore: 0.3 },
347
+ }, null, 2) + "\n");
348
+ }
349
+ const memoryPath = join(targetDir, "MEMORY.md");
350
+ if (!existsSync(memoryPath)) {
351
+ writeFileSync(memoryPath, "# Memory\n\nAdd notes, decisions, and context here.\n");
352
+ }
353
+ const gitignorePath = join(targetDir, ".gitignore");
354
+ if (!existsSync(gitignorePath)) {
355
+ writeFileSync(gitignorePath, "index.db\nindex.db-*\n");
243
356
  }
244
357
  }
245
358
  async function initSdr(ctx) {
246
- const result = await shellInit("sdr", ["init", "-t", "triage-only"], ctx.cwd, ctx.usePrefix
359
+ // Use template from interactive config if available, otherwise default
360
+ const template = ctx.packageConfigs?.["self-driving-repo"]?.values?.template ??
361
+ "triage-only";
362
+ const result = await shellInit("sdr", ["init", "-t", template], ctx.cwd, ctx.usePrefix
247
363
  ? { SDR_CONFIG_DIR: join(PROJECT_ROOT, "self-driving") }
248
364
  : undefined);
249
365
  // relocate handles packages that don't yet respect the env var;
@@ -312,7 +428,15 @@ async function initSessionlogProject(ctx) {
312
428
  ? join(ctx.cwd, PROJECT_ROOT, "sessionlog")
313
429
  : join(ctx.cwd, ".sessionlog");
314
430
  mkdirSync(targetDir, { recursive: true });
315
- // Write default settings.json
431
+ // Apply per-package config overrides
432
+ const overrides = ctx.packageConfigs?.sessionlog?.values;
433
+ // Extract session repo config from dot-notation overrides.
434
+ // sessionRepo.remote and sessionRepo.directory are committable (go to settings.json).
435
+ // sessionRepo.localPath is machine-specific (goes to settings.local.json).
436
+ const repoRemote = overrides?.["sessionRepo.remote"];
437
+ const repoDirectory = overrides?.["sessionRepo.directory"];
438
+ const repoLocalPath = overrides?.["sessionRepo.localPath"];
439
+ // Write settings.json with defaults (committed, shared across clones)
316
440
  const settingsPath = join(targetDir, "settings.json");
317
441
  if (!existsSync(settingsPath)) {
318
442
  const defaultSettings = {
@@ -322,8 +446,48 @@ async function initSessionlogProject(ctx) {
322
446
  telemetryEnabled: false,
323
447
  summarizationEnabled: false,
324
448
  };
449
+ // Apply overrides, stripping session repo dot-notation keys
450
+ if (overrides) {
451
+ const sessionRepoKeys = ["sessionRepo.remote", "sessionRepo.directory", "sessionRepo.localPath", "sessionRepo.autoPush"];
452
+ for (const [key, value] of Object.entries(overrides)) {
453
+ if (!sessionRepoKeys.includes(key)) {
454
+ defaultSettings[key] = value;
455
+ }
456
+ }
457
+ }
458
+ // Build sessionRepo object for committable fields (remote + directory)
459
+ if (repoRemote) {
460
+ const sessionRepo = { remote: repoRemote };
461
+ if (repoDirectory) {
462
+ sessionRepo.directory = repoDirectory;
463
+ }
464
+ defaultSettings.sessionRepo = sessionRepo;
465
+ }
325
466
  writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
326
467
  }
468
+ // Write machine-specific session repo fields to settings.local.json
469
+ if (repoLocalPath) {
470
+ const localSettingsPath = join(targetDir, "settings.local.json");
471
+ const localSettings = {};
472
+ if (existsSync(localSettingsPath)) {
473
+ try {
474
+ Object.assign(localSettings, JSON.parse(readFileSync(localSettingsPath, "utf-8")));
475
+ }
476
+ catch {
477
+ // Start fresh
478
+ }
479
+ }
480
+ localSettings.sessionRepo = {
481
+ ...(localSettings.sessionRepo ?? {}),
482
+ localPath: repoLocalPath,
483
+ };
484
+ writeFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2) + "\n");
485
+ }
486
+ // Ensure settings.local.json is gitignored
487
+ const gitignorePath = join(targetDir, ".gitignore");
488
+ if (!existsSync(gitignorePath)) {
489
+ writeFileSync(gitignorePath, "# Sessionlog local files (not committed)\nsettings.local.json\ntmp/\nlogs/\n");
490
+ }
327
491
  return { package: "sessionlog", success: true };
328
492
  }
329
493
  catch (err) {
@@ -387,6 +551,40 @@ async function initOpenhive(opts) {
387
551
  };
388
552
  }
389
553
  }
554
+ async function initClaudeSwarmGlobal(ctx) {
555
+ try {
556
+ const targetDir = join(homedir(), ".claude-swarm");
557
+ mkdirSync(targetDir, { recursive: true });
558
+ // Write default global config.json (user preferences that apply across projects)
559
+ const configPath = join(targetDir, "config.json");
560
+ if (!existsSync(configPath)) {
561
+ const defaultConfig = {
562
+ map: {
563
+ server: "",
564
+ sidecar: "session",
565
+ },
566
+ sessionlog: {
567
+ enabled: false,
568
+ sync: "off",
569
+ },
570
+ };
571
+ // Apply per-package config overrides
572
+ const overrides = ctx.packageConfigs?.["claude-code-swarm"]?.values;
573
+ if (overrides) {
574
+ applyNestedOverrides(defaultConfig, overrides);
575
+ }
576
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { mode: 0o600 });
577
+ }
578
+ return { package: "claude-code-swarm", success: true };
579
+ }
580
+ catch (err) {
581
+ return {
582
+ package: "claude-code-swarm",
583
+ success: false,
584
+ message: formatError(err),
585
+ };
586
+ }
587
+ }
390
588
  async function initClaudeSwarmProject(ctx) {
391
589
  try {
392
590
  const targetDir = ctx.usePrefix
@@ -410,6 +608,11 @@ async function initClaudeSwarmProject(ctx) {
410
608
  sync: "off",
411
609
  },
412
610
  };
611
+ // Apply per-package config overrides
612
+ const overrides = ctx.packageConfigs?.["claude-code-swarm"]?.values;
613
+ if (overrides) {
614
+ applyNestedOverrides(defaultConfig, overrides);
615
+ }
413
616
  writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
414
617
  }
415
618
  // Write .gitignore for tmp/
@@ -428,6 +631,31 @@ async function initClaudeSwarmProject(ctx) {
428
631
  }
429
632
  }
430
633
  // ─── Helpers ─────────────────────────────────────────────────────────────────
634
+ /**
635
+ * Apply dotted-key overrides to a nested config object.
636
+ * e.g., { "hybrid.vectorWeight": 0.8 } sets config.hybrid.vectorWeight = 0.8
637
+ */
638
+ function applyNestedOverrides(config, overrides) {
639
+ for (const [key, value] of Object.entries(overrides)) {
640
+ const parts = key.split(".");
641
+ let target = config;
642
+ for (let i = 0; i < parts.length - 1; i++) {
643
+ if (typeof target[parts[i]] !== "object" ||
644
+ target[parts[i]] === null) {
645
+ target[parts[i]] = {};
646
+ }
647
+ target = target[parts[i]];
648
+ }
649
+ const finalKey = parts[parts.length - 1];
650
+ // Coerce numeric strings to numbers for config values
651
+ if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value)) {
652
+ target[finalKey] = parseFloat(value);
653
+ }
654
+ else {
655
+ target[finalKey] = value;
656
+ }
657
+ }
658
+ }
431
659
  /** Check if a directory exists and contains at least one file (recursively) */
432
660
  function hasContent(dir) {
433
661
  if (!existsSync(dir))
@@ -446,6 +674,33 @@ function hasContent(dir) {
446
674
  return false;
447
675
  }
448
676
  }
677
+ /**
678
+ * Remove root-level .gitattributes created by older opentasks versions.
679
+ * Newer versions write .gitattributes inside the opentasks directory.
680
+ */
681
+ function cleanupGitattributes(cwd) {
682
+ const attrPath = join(cwd, ".gitattributes");
683
+ if (!existsSync(attrPath))
684
+ return;
685
+ try {
686
+ const content = readFileSync(attrPath, "utf-8");
687
+ const cleaned = content
688
+ .split("\n")
689
+ .filter((line) => !line.includes("merge=opentasks") &&
690
+ !line.includes("OpenTasks merge driver"))
691
+ .join("\n")
692
+ .trim();
693
+ if (!cleaned) {
694
+ unlinkSync(attrPath);
695
+ }
696
+ else {
697
+ writeFileSync(attrPath, cleaned + "\n");
698
+ }
699
+ }
700
+ catch {
701
+ // Non-fatal
702
+ }
703
+ }
449
704
  function getProjectName(cwd) {
450
705
  const pkgPath = join(cwd, "package.json");
451
706
  if (existsSync(pkgPath)) {