opencode-swarm-plugin 0.36.0 → 0.36.1

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 (49) hide show
  1. package/.hive/issues.jsonl +4 -4
  2. package/.hive/memories.jsonl +274 -1
  3. package/.turbo/turbo-build.log +4 -4
  4. package/.turbo/turbo-test.log +307 -307
  5. package/CHANGELOG.md +71 -0
  6. package/bin/swarm.ts +234 -179
  7. package/dist/compaction-hook.d.ts +54 -4
  8. package/dist/compaction-hook.d.ts.map +1 -1
  9. package/dist/eval-capture.d.ts +122 -17
  10. package/dist/eval-capture.d.ts.map +1 -1
  11. package/dist/index.d.ts +1 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1278 -619
  14. package/dist/planning-guardrails.d.ts +121 -0
  15. package/dist/planning-guardrails.d.ts.map +1 -1
  16. package/dist/plugin.d.ts +9 -9
  17. package/dist/plugin.d.ts.map +1 -1
  18. package/dist/plugin.js +1283 -329
  19. package/dist/schemas/task.d.ts +0 -1
  20. package/dist/schemas/task.d.ts.map +1 -1
  21. package/dist/swarm-decompose.d.ts +0 -8
  22. package/dist/swarm-decompose.d.ts.map +1 -1
  23. package/dist/swarm-orchestrate.d.ts.map +1 -1
  24. package/dist/swarm-prompts.d.ts +0 -4
  25. package/dist/swarm-prompts.d.ts.map +1 -1
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +0 -6
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/README.md +38 -0
  30. package/evals/coordinator-session.eval.ts +154 -0
  31. package/evals/fixtures/coordinator-sessions.ts +328 -0
  32. package/evals/lib/data-loader.ts +69 -0
  33. package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
  34. package/evals/scorers/coordinator-discipline.ts +315 -0
  35. package/evals/scorers/index.ts +12 -0
  36. package/examples/plugin-wrapper-template.ts +303 -4
  37. package/package.json +2 -2
  38. package/src/compaction-hook.test.ts +8 -1
  39. package/src/compaction-hook.ts +31 -21
  40. package/src/eval-capture.test.ts +390 -0
  41. package/src/eval-capture.ts +163 -4
  42. package/src/index.ts +68 -1
  43. package/src/planning-guardrails.test.ts +387 -2
  44. package/src/planning-guardrails.ts +289 -0
  45. package/src/plugin.ts +10 -10
  46. package/src/swarm-decompose.ts +20 -0
  47. package/src/swarm-orchestrate.ts +44 -0
  48. package/src/swarm-prompts.ts +20 -0
  49. package/src/swarm-review.ts +41 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # opencode-swarm-plugin
2
2
 
3
+ ## 0.36.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`9c1f3f3`](https://github.com/joelhooks/swarm-tools/commit/9c1f3f3e7204f02c133c4a036fa34e83d8376a8c) Thanks [@joelhooks](https://github.com/joelhooks)! - ## 🐝 Coordinator Discipline: Prohibition-First Enforcement
8
+
9
+ Coordinators kept "just doing it themselves" after compaction. Now they can't.
10
+
11
+ **The Problem:**
12
+ After context compaction, coordinators would ignore their own instructions to "spawn workers for remaining subtasks" and edit files directly. The compaction context was narrative ("do this") rather than prescriptive ("NEVER do that").
13
+
14
+ **The Fix:**
15
+
16
+ ### 1. Prohibition-First Compaction Context
17
+
18
+ The `SWARM_COMPACTION_CONTEXT` now leads with explicit anti-patterns:
19
+
20
+ ```markdown
21
+ ### ⛔ NEVER DO THESE (Coordinator Anti-Patterns)
22
+
23
+ - ❌ **NEVER** use `edit` or `write` tools - SPAWN A WORKER
24
+ - ❌ **NEVER** run tests with `bash` - SPAWN A WORKER
25
+ - ❌ **NEVER** implement features yourself - SPAWN A WORKER
26
+ - ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
27
+ ```
28
+
29
+ ### 2. Runtime Violation Detection
30
+
31
+ `detectCoordinatorViolation()` is now wired up in `tool.execute.before`:
32
+
33
+ - Detects when coordinators call `edit`, `write`, or test commands
34
+ - Emits warnings to help coordinators self-correct
35
+ - Captures VIOLATION events for post-hoc analysis
36
+
37
+ ### 3. Coordinator Context Tracking
38
+
39
+ New functions track when we're in coordinator mode:
40
+
41
+ - `setCoordinatorContext()` - Activated when `hive_create_epic` or `swarm_decompose` is called
42
+ - `isInCoordinatorContext()` - Checks if we're currently coordinating
43
+ - `clearCoordinatorContext()` - Cleared when epic is closed
44
+
45
+ **Why This Matters:**
46
+
47
+ Coordinators that do implementation work burn context, create conflicts, and defeat the purpose of swarm coordination. This fix makes the anti-pattern visible and provides guardrails to prevent it.
48
+
49
+ **Validation:**
50
+
51
+ - Check `~/.config/swarm-tools/sessions/` for VIOLATION events
52
+ - Run `coordinator-behavior.eval.ts` to score coordinator discipline
53
+
54
+ - [`4c23c7a`](https://github.com/joelhooks/swarm-tools/commit/4c23c7a31013bc6537d83a9294b51540056cde93) Thanks [@joelhooks](https://github.com/joelhooks)! - ## Fix Double Hook Registration
55
+
56
+ The compaction hook was firing twice per compaction event because OpenCode's plugin loader
57
+ calls ALL exports as plugin functions. We were exporting `SwarmPlugin` as both:
58
+
59
+ 1. Named export: `export const SwarmPlugin`
60
+ 2. Default export: `export default SwarmPlugin`
61
+
62
+ This caused the plugin to register twice, doubling all hook invocations.
63
+
64
+ **Fix:** Changed to default-only export pattern:
65
+
66
+ - `src/index.ts`: `const SwarmPlugin` (no export keyword)
67
+ - `src/plugin.ts`: `export default SwarmPlugin` (no named re-export)
68
+
69
+ **Impact:** Compaction hooks now fire once. LLM calls during compaction reduced by 50%.
70
+
71
+ - Updated dependencies [[`e0c422d`](https://github.com/joelhooks/swarm-tools/commit/e0c422de3f5e15c117cc0cc655c0b03242245be4), [`43c8c93`](https://github.com/joelhooks/swarm-tools/commit/43c8c93ef90b2f04ce59317192334f69d7c4204e)]:
72
+ - swarm-mail@1.5.1
73
+
3
74
  ## 0.36.0
4
75
 
5
76
  ### Minor Changes
package/bin/swarm.ts CHANGED
@@ -983,7 +983,7 @@ function getPluginWrapper(): string {
983
983
  `[swarm] Could not read plugin template from ${templatePath}, using minimal wrapper`,
984
984
  );
985
985
  return `// Minimal fallback - install opencode-swarm-plugin globally for full functionality
986
- import { SwarmPlugin } from "opencode-swarm-plugin"
986
+ import SwarmPlugin from "opencode-swarm-plugin"
987
987
  export default SwarmPlugin
988
988
  `;
989
989
  }
@@ -1709,7 +1709,7 @@ async function doctor() {
1709
1709
  if (updateInfo) showUpdateNotification(updateInfo);
1710
1710
  }
1711
1711
 
1712
- async function setup() {
1712
+ async function setup(forceReinstall = false, nonInteractive = false) {
1713
1713
  console.clear();
1714
1714
  console.log(yellow(BANNER));
1715
1715
  console.log(getDecoratedBee());
@@ -1783,7 +1783,7 @@ async function setup() {
1783
1783
  legacyWorkerPath,
1784
1784
  ].filter((f) => existsSync(f));
1785
1785
 
1786
- if (existingFiles.length > 0) {
1786
+ if (existingFiles.length > 0 && !forceReinstall) {
1787
1787
  p.log.success("Swarm is already configured!");
1788
1788
  p.log.message(dim(" Found " + existingFiles.length + "/5 config files"));
1789
1789
 
@@ -1903,7 +1903,8 @@ async function setup() {
1903
1903
  p.log.step("Missing " + requiredMissing.length + " required dependencies");
1904
1904
 
1905
1905
  for (const { dep } of requiredMissing) {
1906
- const shouldInstall = await p.confirm({
1906
+ // In non-interactive mode, auto-install required deps
1907
+ const shouldInstall = nonInteractive ? true : await p.confirm({
1907
1908
  message: "Install " + dep.name + "? (" + dep.description + ")",
1908
1909
  initialValue: true,
1909
1910
  });
@@ -1931,8 +1932,8 @@ async function setup() {
1931
1932
  }
1932
1933
  }
1933
1934
 
1934
- // Only prompt for optional deps if there are missing ones
1935
- if (optionalMissing.length > 0) {
1935
+ // Only prompt for optional deps if there are missing ones (skip in non-interactive mode)
1936
+ if (optionalMissing.length > 0 && !nonInteractive) {
1936
1937
  const installable = optionalMissing.filter(
1937
1938
  (r) => r.dep.installType !== "manual",
1938
1939
  );
@@ -2099,135 +2100,157 @@ async function setup() {
2099
2100
  p.log.message(dim(' No OpenCode config found (skipping MCP check)'));
2100
2101
  }
2101
2102
 
2102
- // Model selection
2103
- p.log.step("Configuring swarm agents...");
2104
- p.log.message(dim(" Coordinator handles orchestration, worker executes tasks"));
2103
+ // Model defaults: opus for coordinator, sonnet for worker, haiku for lite
2104
+ const DEFAULT_COORDINATOR = "anthropic/claude-opus-4-5";
2105
+ const DEFAULT_WORKER = "anthropic/claude-sonnet-4-5";
2106
+ const DEFAULT_LITE = "anthropic/claude-haiku-4-5";
2107
+
2108
+ // Model selection (skip if non-interactive)
2109
+ let coordinatorModel: string;
2110
+ let workerModel: string;
2111
+ let liteModel: string;
2112
+
2113
+ if (nonInteractive) {
2114
+ coordinatorModel = DEFAULT_COORDINATOR;
2115
+ workerModel = DEFAULT_WORKER;
2116
+ liteModel = DEFAULT_LITE;
2117
+ p.log.step("Using default models:");
2118
+ p.log.message(dim(` Coordinator: ${coordinatorModel}`));
2119
+ p.log.message(dim(` Worker: ${workerModel}`));
2120
+ p.log.message(dim(` Lite: ${liteModel}`));
2121
+ } else {
2122
+ p.log.step("Configuring swarm agents...");
2123
+ p.log.message(dim(" Coordinator handles orchestration, worker executes tasks"));
2105
2124
 
2106
- const coordinatorModel = await p.select({
2107
- message: "Select coordinator model (for orchestration/planning):",
2108
- options: [
2109
- {
2110
- value: "anthropic/claude-sonnet-4-5",
2111
- label: "Claude Sonnet 4.5",
2112
- hint: "Best balance of speed and capability (recommended)",
2113
- },
2114
- {
2115
- value: "anthropic/claude-haiku-4-5",
2116
- label: "Claude Haiku 4.5",
2117
- hint: "Fast and cost-effective",
2118
- },
2119
- {
2120
- value: "anthropic/claude-opus-4-5",
2121
- label: "Claude Opus 4.5",
2122
- hint: "Most capable, slower",
2123
- },
2124
- {
2125
- value: "openai/gpt-4o",
2126
- label: "GPT-4o",
2127
- hint: "Fast, good for most tasks",
2128
- },
2129
- {
2130
- value: "openai/gpt-4-turbo",
2131
- label: "GPT-4 Turbo",
2132
- hint: "Powerful, more expensive",
2133
- },
2134
- {
2135
- value: "google/gemini-2.0-flash",
2136
- label: "Gemini 2.0 Flash",
2137
- hint: "Fast and capable",
2138
- },
2139
- {
2140
- value: "google/gemini-1.5-pro",
2141
- label: "Gemini 1.5 Pro",
2142
- hint: "More capable",
2143
- },
2144
- ],
2145
- initialValue: "anthropic/claude-sonnet-4-5",
2146
- });
2125
+ const selectedCoordinator = await p.select({
2126
+ message: "Select coordinator model (for orchestration/planning):",
2127
+ options: [
2128
+ {
2129
+ value: "anthropic/claude-opus-4-5",
2130
+ label: "Claude Opus 4.5",
2131
+ hint: "Most capable, best for complex orchestration (recommended)",
2132
+ },
2133
+ {
2134
+ value: "anthropic/claude-sonnet-4-5",
2135
+ label: "Claude Sonnet 4.5",
2136
+ hint: "Good balance of speed and capability",
2137
+ },
2138
+ {
2139
+ value: "anthropic/claude-haiku-4-5",
2140
+ label: "Claude Haiku 4.5",
2141
+ hint: "Fast and cost-effective",
2142
+ },
2143
+ {
2144
+ value: "openai/gpt-4o",
2145
+ label: "GPT-4o",
2146
+ hint: "Fast, good for most tasks",
2147
+ },
2148
+ {
2149
+ value: "openai/gpt-4-turbo",
2150
+ label: "GPT-4 Turbo",
2151
+ hint: "Powerful, more expensive",
2152
+ },
2153
+ {
2154
+ value: "google/gemini-2.0-flash",
2155
+ label: "Gemini 2.0 Flash",
2156
+ hint: "Fast and capable",
2157
+ },
2158
+ {
2159
+ value: "google/gemini-1.5-pro",
2160
+ label: "Gemini 1.5 Pro",
2161
+ hint: "More capable",
2162
+ },
2163
+ ],
2164
+ initialValue: DEFAULT_COORDINATOR,
2165
+ });
2147
2166
 
2148
- if (p.isCancel(coordinatorModel)) {
2149
- p.cancel("Setup cancelled");
2150
- process.exit(0);
2151
- }
2167
+ if (p.isCancel(selectedCoordinator)) {
2168
+ p.cancel("Setup cancelled");
2169
+ process.exit(0);
2170
+ }
2171
+ coordinatorModel = selectedCoordinator;
2152
2172
 
2153
- const workerModel = await p.select({
2154
- message: "Select worker model (for task execution):",
2155
- options: [
2156
- {
2157
- value: "anthropic/claude-haiku-4-5",
2158
- label: "Claude Haiku 4.5",
2159
- hint: "Fast and cost-effective (recommended)",
2160
- },
2161
- {
2162
- value: "anthropic/claude-sonnet-4-5",
2163
- label: "Claude Sonnet 4.5",
2164
- hint: "Best balance of speed and capability",
2165
- },
2166
- {
2167
- value: "anthropic/claude-opus-4-5",
2168
- label: "Claude Opus 4.5",
2169
- hint: "Most capable, slower",
2170
- },
2171
- {
2172
- value: "openai/gpt-4o",
2173
- label: "GPT-4o",
2174
- hint: "Fast, good for most tasks",
2175
- },
2176
- {
2177
- value: "openai/gpt-4-turbo",
2178
- label: "GPT-4 Turbo",
2179
- hint: "Powerful, more expensive",
2180
- },
2181
- {
2182
- value: "google/gemini-2.0-flash",
2183
- label: "Gemini 2.0 Flash",
2184
- hint: "Fast and capable",
2185
- },
2186
- {
2187
- value: "google/gemini-1.5-pro",
2188
- label: "Gemini 1.5 Pro",
2189
- hint: "More capable",
2190
- },
2191
- ],
2192
- initialValue: "anthropic/claude-haiku-4-5",
2193
- });
2173
+ const selectedWorker = await p.select({
2174
+ message: "Select worker model (for task execution):",
2175
+ options: [
2176
+ {
2177
+ value: "anthropic/claude-sonnet-4-5",
2178
+ label: "Claude Sonnet 4.5",
2179
+ hint: "Best balance of speed and capability (recommended)",
2180
+ },
2181
+ {
2182
+ value: "anthropic/claude-haiku-4-5",
2183
+ label: "Claude Haiku 4.5",
2184
+ hint: "Fast and cost-effective",
2185
+ },
2186
+ {
2187
+ value: "anthropic/claude-opus-4-5",
2188
+ label: "Claude Opus 4.5",
2189
+ hint: "Most capable, slower",
2190
+ },
2191
+ {
2192
+ value: "openai/gpt-4o",
2193
+ label: "GPT-4o",
2194
+ hint: "Fast, good for most tasks",
2195
+ },
2196
+ {
2197
+ value: "openai/gpt-4-turbo",
2198
+ label: "GPT-4 Turbo",
2199
+ hint: "Powerful, more expensive",
2200
+ },
2201
+ {
2202
+ value: "google/gemini-2.0-flash",
2203
+ label: "Gemini 2.0 Flash",
2204
+ hint: "Fast and capable",
2205
+ },
2206
+ {
2207
+ value: "google/gemini-1.5-pro",
2208
+ label: "Gemini 1.5 Pro",
2209
+ hint: "More capable",
2210
+ },
2211
+ ],
2212
+ initialValue: DEFAULT_WORKER,
2213
+ });
2194
2214
 
2195
- if (p.isCancel(workerModel)) {
2196
- p.cancel("Setup cancelled");
2197
- process.exit(0);
2198
- }
2215
+ if (p.isCancel(selectedWorker)) {
2216
+ p.cancel("Setup cancelled");
2217
+ process.exit(0);
2218
+ }
2219
+ workerModel = selectedWorker;
2199
2220
 
2200
- // Lite model selection for simple tasks (docs, tests)
2201
- const liteModel = await p.select({
2202
- message: "Select lite model (for docs, tests, simple edits):",
2203
- options: [
2204
- {
2205
- value: "anthropic/claude-haiku-4-5",
2206
- label: "Claude Haiku 4.5",
2207
- hint: "Fast and cost-effective (recommended)",
2208
- },
2209
- {
2210
- value: "anthropic/claude-sonnet-4-5",
2211
- label: "Claude Sonnet 4.5",
2212
- hint: "More capable, slower",
2213
- },
2214
- {
2215
- value: "openai/gpt-4o-mini",
2216
- label: "GPT-4o Mini",
2217
- hint: "Fast and cheap",
2218
- },
2219
- {
2220
- value: "google/gemini-2.0-flash",
2221
- label: "Gemini 2.0 Flash",
2222
- hint: "Fast and capable",
2223
- },
2224
- ],
2225
- initialValue: "anthropic/claude-haiku-4-5",
2226
- });
2221
+ // Lite model selection for simple tasks (docs, tests)
2222
+ const selectedLite = await p.select({
2223
+ message: "Select lite model (for docs, tests, simple edits):",
2224
+ options: [
2225
+ {
2226
+ value: "anthropic/claude-haiku-4-5",
2227
+ label: "Claude Haiku 4.5",
2228
+ hint: "Fast and cost-effective (recommended)",
2229
+ },
2230
+ {
2231
+ value: "anthropic/claude-sonnet-4-5",
2232
+ label: "Claude Sonnet 4.5",
2233
+ hint: "More capable, slower",
2234
+ },
2235
+ {
2236
+ value: "openai/gpt-4o-mini",
2237
+ label: "GPT-4o Mini",
2238
+ hint: "Fast and cheap",
2239
+ },
2240
+ {
2241
+ value: "google/gemini-2.0-flash",
2242
+ label: "Gemini 2.0 Flash",
2243
+ hint: "Fast and capable",
2244
+ },
2245
+ ],
2246
+ initialValue: DEFAULT_LITE,
2247
+ });
2227
2248
 
2228
- if (p.isCancel(liteModel)) {
2229
- p.cancel("Setup cancelled");
2230
- process.exit(0);
2249
+ if (p.isCancel(selectedLite)) {
2250
+ p.cancel("Setup cancelled");
2251
+ process.exit(0);
2252
+ }
2253
+ liteModel = selectedLite;
2231
2254
  }
2232
2255
 
2233
2256
  p.log.success("Selected models:");
@@ -2249,7 +2272,8 @@ async function setup() {
2249
2272
 
2250
2273
  // Write plugin and command files
2251
2274
  p.log.step("Writing configuration files...");
2252
- stats[writeFileWithStatus(pluginPath, getPluginWrapper(), "Plugin")]++;
2275
+ const pluginContent = getPluginWrapper().replace(/__SWARM_LITE_MODEL__/g, liteModel);
2276
+ stats[writeFileWithStatus(pluginPath, pluginContent, "Plugin")]++;
2253
2277
  stats[writeFileWithStatus(commandPath, SWARM_COMMAND, "Command")]++;
2254
2278
 
2255
2279
  // Write nested agent files (swarm/planner.md, swarm/worker.md, swarm/researcher.md)
@@ -2288,21 +2312,8 @@ async function setup() {
2288
2312
  );
2289
2313
 
2290
2314
  if (missingBundled.length > 0 || managedBundled.length > 0) {
2291
- const shouldSync = await p.confirm({
2292
- message:
2293
- "Sync bundled skills into your global skills directory? " +
2294
- (missingBundled.length > 0
2295
- ? `(${missingBundled.length} missing)`
2296
- : "(update managed skills)"),
2297
- initialValue: isReinstall || missingBundled.length > 0,
2298
- });
2299
-
2300
- if (p.isCancel(shouldSync)) {
2301
- p.cancel("Setup cancelled");
2302
- process.exit(0);
2303
- }
2304
-
2305
- if (shouldSync) {
2315
+ // Always sync bundled skills - no prompt needed
2316
+ {
2306
2317
  const syncSpinner = p.spinner();
2307
2318
  syncSpinner.start("Syncing bundled skills...");
2308
2319
  try {
@@ -2339,15 +2350,10 @@ async function setup() {
2339
2350
  }
2340
2351
  }
2341
2352
 
2342
- // Offer to update AGENTS.md with skill awareness
2353
+ // Always update AGENTS.md with skill awareness - no prompt needed
2343
2354
  const agentsPath = join(configDir, "AGENTS.md");
2344
2355
  if (existsSync(agentsPath)) {
2345
- const updateAgents = await p.confirm({
2346
- message: "Update AGENTS.md with skill awareness?",
2347
- initialValue: true,
2348
- });
2349
-
2350
- if (!p.isCancel(updateAgents) && updateAgents) {
2356
+ {
2351
2357
  const s = p.spinner();
2352
2358
  s.start("Updating AGENTS.md...");
2353
2359
 
@@ -2708,8 +2714,10 @@ async function help() {
2708
2714
  console.log(magenta(" " + getRandomMessage()));
2709
2715
  console.log(`
2710
2716
  ${cyan("Commands:")}
2711
- swarm setup Interactive installer - checks and installs dependencies
2712
- swarm doctor Health check - shows status of all dependencies
2717
+ swarm setup Interactive installer - checks and installs dependencies
2718
+ --reinstall, -r Skip prompt, go straight to reinstall
2719
+ --yes, -y Non-interactive with defaults (opus/sonnet/haiku)
2720
+ swarm doctor Health check - shows status of all dependencies
2713
2721
  swarm init Initialize beads in current project
2714
2722
  swarm config Show paths to generated config files
2715
2723
  swarm agents Update AGENTS.md with skill awareness
@@ -3101,17 +3109,43 @@ interface LogLine {
3101
3109
  time: string;
3102
3110
  module: string;
3103
3111
  msg: string;
3112
+ data?: Record<string, unknown>; // Extra structured data
3104
3113
  }
3105
3114
 
3106
- function parseLogLine(line: string): LogLine | null {
3115
+ function parseLogLine(line: string, sourceFile?: string): LogLine | null {
3107
3116
  try {
3108
3117
  const parsed = JSON.parse(line);
3109
- if (typeof parsed.level === "number" && parsed.time && parsed.msg) {
3118
+ if (parsed.time && parsed.msg) {
3119
+ // Handle both pino format (level: number) and plugin wrapper format (level: string)
3120
+ let level: number;
3121
+ if (typeof parsed.level === "number") {
3122
+ level = parsed.level;
3123
+ } else if (typeof parsed.level === "string") {
3124
+ level = levelNameToNumber(parsed.level);
3125
+ } else {
3126
+ level = 30; // default to info
3127
+ }
3128
+
3129
+ // Derive module from: explicit field, or source filename (e.g., "compaction.log" -> "compaction")
3130
+ let module = parsed.module;
3131
+ if (!module && sourceFile) {
3132
+ // Extract module from filename: "compaction.log" -> "compaction", "swarm.1log" -> "swarm"
3133
+ const match = sourceFile.match(/([^/]+?)(?:\.\d+)?\.?log$/);
3134
+ if (match) {
3135
+ module = match[1];
3136
+ }
3137
+ }
3138
+
3139
+ // Extract extra data (everything except core fields)
3140
+ const { level: _l, time: _t, module: _m, msg: _msg, ...extraData } = parsed;
3141
+ const hasExtraData = Object.keys(extraData).length > 0;
3142
+
3110
3143
  return {
3111
- level: parsed.level,
3144
+ level,
3112
3145
  time: parsed.time,
3113
- module: parsed.module || "unknown",
3146
+ module: module || "unknown",
3114
3147
  msg: parsed.msg,
3148
+ data: hasExtraData ? extraData : undefined,
3115
3149
  };
3116
3150
  }
3117
3151
  } catch {
@@ -3164,36 +3198,51 @@ function parseDuration(duration: string): number | null {
3164
3198
  return value * multipliers[unit];
3165
3199
  }
3166
3200
 
3167
- function formatLogLine(log: LogLine, useColor = true): string {
3201
+ function formatLogLine(log: LogLine, useColor = true, verbose = false): string {
3168
3202
  const timestamp = new Date(log.time).toLocaleTimeString();
3169
3203
  const levelName = levelToName(log.level);
3170
3204
  const module = log.module.padEnd(12);
3171
3205
  const levelStr = useColor ? levelToColor(log.level)(levelName) : levelName;
3172
3206
 
3173
- return `${timestamp} ${levelStr} ${module} ${log.msg}`;
3207
+ let output = `${timestamp} ${levelStr} ${module} ${log.msg}`;
3208
+
3209
+ // In verbose mode, pretty print the structured data
3210
+ if (verbose && log.data) {
3211
+ output += `\n${dim(JSON.stringify(log.data, null, 2))}`;
3212
+ }
3213
+
3214
+ return output;
3215
+ }
3216
+
3217
+ interface LogEntry {
3218
+ line: string;
3219
+ file: string;
3174
3220
  }
3175
3221
 
3176
- function readLogFiles(dir: string): string[] {
3222
+ function readLogFiles(dir: string): LogEntry[] {
3177
3223
  if (!existsSync(dir)) return [];
3178
3224
 
3179
3225
  const allFiles = readdirSync(dir);
3226
+ // Match both pino-roll format (*.1log, *.2log) AND plain *.log files
3180
3227
  const logFiles = allFiles
3181
- .filter((f: string) => /\.\d+log$/.test(f))
3228
+ .filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f))
3182
3229
  .sort()
3183
3230
  .map((f: string) => join(dir, f));
3184
3231
 
3185
- const lines: string[] = [];
3232
+ const entries: LogEntry[] = [];
3186
3233
  for (const file of logFiles) {
3187
3234
  try {
3188
3235
  const content = readFileSync(file, "utf-8");
3189
3236
  const fileLines = content.split("\n").filter((line: string) => line.trim());
3190
- lines.push(...fileLines);
3237
+ for (const line of fileLines) {
3238
+ entries.push({ line, file });
3239
+ }
3191
3240
  } catch {
3192
3241
  // Skip unreadable files
3193
3242
  }
3194
3243
  }
3195
3244
 
3196
- return lines;
3245
+ return entries;
3197
3246
  }
3198
3247
 
3199
3248
  async function logs() {
@@ -3207,6 +3256,7 @@ async function logs() {
3207
3256
  let limit = 50;
3208
3257
  let watchMode = false;
3209
3258
  let pollInterval = 1000; // 1 second default
3259
+ let verbose = false;
3210
3260
 
3211
3261
  for (let i = 0; i < args.length; i++) {
3212
3262
  const arg = args[i];
@@ -3231,6 +3281,8 @@ async function logs() {
3231
3281
  }
3232
3282
  } else if (arg === "--watch" || arg === "-w") {
3233
3283
  watchMode = true;
3284
+ } else if (arg === "--verbose" || arg === "-v") {
3285
+ verbose = true;
3234
3286
  } else if (arg === "--interval" && i + 1 < args.length) {
3235
3287
  pollInterval = parseInt(args[++i], 10);
3236
3288
  if (isNaN(pollInterval) || pollInterval < 100) {
@@ -3290,7 +3342,7 @@ async function logs() {
3290
3342
  // Initialize positions from current file sizes
3291
3343
  const initializePositions = () => {
3292
3344
  if (!existsSync(logsDir)) return;
3293
- const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
3345
+ const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
3294
3346
  for (const file of files) {
3295
3347
  const filePath = join(logsDir, file);
3296
3348
  try {
@@ -3327,14 +3379,14 @@ async function logs() {
3327
3379
  };
3328
3380
 
3329
3381
  // Print initial logs (last N lines)
3330
- const rawLines = readLogFiles(logsDir);
3331
- let logs: LogLine[] = rawLines
3332
- .map(parseLogLine)
3382
+ const rawEntries = readLogFiles(logsDir);
3383
+ let logs: LogLine[] = rawEntries
3384
+ .map(entry => parseLogLine(entry.line, entry.file))
3333
3385
  .filter((log): log is LogLine => log !== null);
3334
3386
  logs = filterLogs(logs).slice(-limit);
3335
3387
 
3336
3388
  for (const log of logs) {
3337
- console.log(formatLogLine(log));
3389
+ console.log(formatLogLine(log, true, verbose));
3338
3390
  }
3339
3391
 
3340
3392
  // Initialize positions after printing initial logs
@@ -3344,18 +3396,18 @@ async function logs() {
3344
3396
  const pollForNewLogs = () => {
3345
3397
  if (!existsSync(logsDir)) return;
3346
3398
 
3347
- const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
3399
+ const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
3348
3400
 
3349
3401
  for (const file of files) {
3350
3402
  const filePath = join(logsDir, file);
3351
3403
  const newLines = readNewLines(filePath);
3352
3404
 
3353
3405
  for (const line of newLines) {
3354
- const parsed = parseLogLine(line);
3406
+ const parsed = parseLogLine(line, filePath);
3355
3407
  if (parsed) {
3356
3408
  const filtered = filterLogs([parsed]);
3357
3409
  if (filtered.length > 0) {
3358
- console.log(formatLogLine(filtered[0]));
3410
+ console.log(formatLogLine(filtered[0], true, verbose));
3359
3411
  }
3360
3412
  }
3361
3413
  }
@@ -3381,11 +3433,11 @@ async function logs() {
3381
3433
  }
3382
3434
 
3383
3435
  // Non-watch mode - one-shot output
3384
- const rawLines = readLogFiles(logsDir);
3436
+ const rawEntries = readLogFiles(logsDir);
3385
3437
 
3386
3438
  // Parse and filter
3387
- let logs: LogLine[] = rawLines
3388
- .map(parseLogLine)
3439
+ let logs: LogLine[] = rawEntries
3440
+ .map(entry => parseLogLine(entry.line, entry.file))
3389
3441
  .filter((log): log is LogLine => log !== null);
3390
3442
 
3391
3443
  logs = filterLogs(logs);
@@ -3410,7 +3462,7 @@ async function logs() {
3410
3462
  console.log();
3411
3463
 
3412
3464
  for (const log of logs) {
3413
- console.log(formatLogLine(log));
3465
+ console.log(formatLogLine(log, true, verbose));
3414
3466
  }
3415
3467
  console.log();
3416
3468
  }
@@ -3522,9 +3574,12 @@ async function db() {
3522
3574
  const command = process.argv[2];
3523
3575
 
3524
3576
  switch (command) {
3525
- case "setup":
3526
- await setup();
3577
+ case "setup": {
3578
+ const reinstallFlag = process.argv.includes("--reinstall") || process.argv.includes("-r");
3579
+ const yesFlag = process.argv.includes("--yes") || process.argv.includes("-y");
3580
+ await setup(reinstallFlag || yesFlag, yesFlag);
3527
3581
  break;
3582
+ }
3528
3583
  case "doctor":
3529
3584
  await doctor();
3530
3585
  break;