opencode-swarm-plugin 0.32.0 → 0.34.0

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 (55) hide show
  1. package/.hive/issues.jsonl +12 -0
  2. package/.hive/memories.jsonl +255 -1
  3. package/.turbo/turbo-build.log +9 -10
  4. package/.turbo/turbo-test.log +343 -337
  5. package/CHANGELOG.md +358 -0
  6. package/README.md +152 -179
  7. package/bin/swarm.test.ts +303 -1
  8. package/bin/swarm.ts +473 -16
  9. package/dist/compaction-hook.d.ts +1 -1
  10. package/dist/compaction-hook.d.ts.map +1 -1
  11. package/dist/index.d.ts +112 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +12380 -131
  14. package/dist/logger.d.ts +34 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/observability-tools.d.ts +116 -0
  17. package/dist/observability-tools.d.ts.map +1 -0
  18. package/dist/plugin.js +12254 -119
  19. package/dist/skills.d.ts.map +1 -1
  20. package/dist/swarm-orchestrate.d.ts +105 -0
  21. package/dist/swarm-orchestrate.d.ts.map +1 -1
  22. package/dist/swarm-prompts.d.ts +113 -2
  23. package/dist/swarm-prompts.d.ts.map +1 -1
  24. package/dist/swarm-research.d.ts +127 -0
  25. package/dist/swarm-research.d.ts.map +1 -0
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +73 -1
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/compaction-resumption.eval.ts +289 -0
  30. package/evals/coordinator-behavior.eval.ts +307 -0
  31. package/evals/fixtures/compaction-cases.ts +350 -0
  32. package/evals/scorers/compaction-scorers.ts +305 -0
  33. package/evals/scorers/index.ts +12 -0
  34. package/examples/plugin-wrapper-template.ts +297 -8
  35. package/package.json +6 -2
  36. package/src/compaction-hook.test.ts +617 -1
  37. package/src/compaction-hook.ts +291 -18
  38. package/src/index.ts +54 -1
  39. package/src/logger.test.ts +189 -0
  40. package/src/logger.ts +135 -0
  41. package/src/observability-tools.test.ts +346 -0
  42. package/src/observability-tools.ts +594 -0
  43. package/src/skills.integration.test.ts +137 -1
  44. package/src/skills.test.ts +42 -1
  45. package/src/skills.ts +8 -4
  46. package/src/swarm-orchestrate.test.ts +123 -0
  47. package/src/swarm-orchestrate.ts +183 -0
  48. package/src/swarm-prompts.test.ts +553 -1
  49. package/src/swarm-prompts.ts +406 -4
  50. package/src/swarm-research.integration.test.ts +544 -0
  51. package/src/swarm-research.test.ts +698 -0
  52. package/src/swarm-research.ts +472 -0
  53. package/src/swarm-review.test.ts +177 -0
  54. package/src/swarm-review.ts +12 -47
  55. package/src/swarm.ts +6 -3
package/bin/swarm.ts CHANGED
@@ -1153,9 +1153,17 @@ const result2 = await Task(subagent_type="swarm/worker", prompt="<from above>")
1153
1153
  4. **SEND FEEDBACK** - Approve or request changes
1154
1154
  \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\`
1155
1155
 
1156
- If approved: Close cell, spawn next worker
1157
- If needs_changes: Worker retries (max 3 attempts)
1158
- If 3 failures: Mark blocked, escalate to human
1156
+ **If approved:**
1157
+ - Close cell, spawn next worker
1158
+
1159
+ **If needs_changes:**
1160
+ - \`swarm_review_feedback\` returns \`retry_context\` (NOT sends message - worker is dead)
1161
+ - Generate retry prompt: \`swarm_spawn_retry(retry_context)\`
1162
+ - Spawn NEW worker with Task() using retry prompt
1163
+ - Max 3 attempts before marking task blocked
1164
+
1165
+ **If 3 failures:**
1166
+ - Mark task blocked, escalate to human
1159
1167
 
1160
1168
  5. **ONLY THEN** - Spawn next worker or complete
1161
1169
 
@@ -1322,6 +1330,233 @@ hive_update(id="<bead-id>", status="blocked")
1322
1330
  Begin by reading your full prompt and executing Step 1.
1323
1331
  `;
1324
1332
 
1333
+ const getResearcherAgent = (model: string) => `---
1334
+ name: swarm-researcher
1335
+ description: READ-ONLY research agent - discovers tools, fetches docs, stores findings
1336
+ model: ${model}
1337
+ ---
1338
+
1339
+ You are a research agent. Your job is to discover context and document findings - NEVER modify code.
1340
+
1341
+ ## CRITICAL: You Are READ-ONLY
1342
+
1343
+ **YOU DO NOT:**
1344
+ - Edit code files
1345
+ - Run tests
1346
+ - Make commits
1347
+ - Reserve files (you don't edit, so no reservations needed)
1348
+ - Implement features
1349
+
1350
+ **YOU DO:**
1351
+ - Discover available tools (MCP servers, skills, CLI tools)
1352
+ - Read lockfiles to get current package versions
1353
+ - Fetch documentation for those versions
1354
+ - Store findings in semantic-memory (full details)
1355
+ - Broadcast summaries via swarm mail (condensed)
1356
+ - Return structured summary for shared context
1357
+
1358
+ ## Workflow
1359
+
1360
+ ### Step 1: Initialize (MANDATORY FIRST)
1361
+
1362
+ \`\`\`
1363
+ swarmmail_init(project_path="/abs/path/to/project", task_description="Research: <what you're researching>")
1364
+ \`\`\`
1365
+
1366
+ ### Step 2: Discover Available Tools
1367
+
1368
+ **DO NOT assume what tools are installed. Discover them:**
1369
+
1370
+ \`\`\`
1371
+ # Check what skills user has installed
1372
+ skills_list()
1373
+
1374
+ # Check what MCP servers are available (look for context7, pdf-brain, fetch, etc.)
1375
+ # Note: No direct MCP listing tool - infer from task context or ask coordinator
1376
+
1377
+ # Check for CLI tools if relevant (bd, cass, ubs, ollama)
1378
+ # Use Bash tool to check: which <tool-name>
1379
+ \`\`\`
1380
+
1381
+ ### Step 3: Load Relevant Skills
1382
+
1383
+ Based on research task, load appropriate skills:
1384
+
1385
+ \`\`\`
1386
+ skills_use(name="<skill-name>", context="Researching <topic>")
1387
+ \`\`\`
1388
+
1389
+ ### Step 4: Read Lockfiles (if researching dependencies)
1390
+
1391
+ **DO NOT read implementation code.** Only read metadata:
1392
+
1393
+ \`\`\`
1394
+ # For package.json projects
1395
+ read("package.json")
1396
+ read("package-lock.json") or read("bun.lock") or read("pnpm-lock.yaml")
1397
+
1398
+ # For Python
1399
+ read("requirements.txt") or read("pyproject.toml")
1400
+
1401
+ # For Go
1402
+ read("go.mod")
1403
+ \`\`\`
1404
+
1405
+ Extract current version numbers for libraries you need to research.
1406
+
1407
+ ### Step 5: Fetch Documentation
1408
+
1409
+ Use available doc tools to get version-specific docs:
1410
+
1411
+ \`\`\`
1412
+ # If context7 available (check skills_list or task context)
1413
+ # Use it for library docs
1414
+
1415
+ # If pdf-brain available
1416
+ pdf-brain_search(query="<library> <version> <topic>", limit=5)
1417
+
1418
+ # If fetch tool available
1419
+ fetch(url="https://docs.example.com/v2.0/...")
1420
+
1421
+ # If repo-crawl available for OSS libraries
1422
+ repo-crawl_readme(repo="owner/repo")
1423
+ repo-crawl_file(repo="owner/repo", path="docs/...")
1424
+ \`\`\`
1425
+
1426
+ ### Step 6: Store Full Findings in Semantic Memory
1427
+
1428
+ **Store detailed findings for future agents:**
1429
+
1430
+ \`\`\`
1431
+ semantic-memory_store(
1432
+ information="Researched <library> v<version>. Key findings: <detailed notes with examples, gotchas, patterns>",
1433
+ metadata="<library>, <version>, <topic>, research"
1434
+ )
1435
+ \`\`\`
1436
+
1437
+ **Include:**
1438
+ - Library/framework versions discovered
1439
+ - Key API patterns
1440
+ - Breaking changes from previous versions
1441
+ - Common gotchas
1442
+ - Relevant examples
1443
+
1444
+ ### Step 7: Broadcast Condensed Summary via Swarm Mail
1445
+
1446
+ **Send concise summary to coordinator:**
1447
+
1448
+ \`\`\`
1449
+ swarmmail_send(
1450
+ to=["coordinator"],
1451
+ subject="Research Complete: <topic>",
1452
+ body="<3-5 bullet points with key takeaways>",
1453
+ thread_id="<epic-id>"
1454
+ )
1455
+ \`\`\`
1456
+
1457
+ ### Step 8: Return Structured Summary
1458
+
1459
+ **Output format for shared_context:**
1460
+
1461
+ \`\`\`json
1462
+ {
1463
+ "researched": "<topic>",
1464
+ "tools_discovered": ["skill-1", "skill-2", "mcp-server-1"],
1465
+ "versions": {
1466
+ "library-1": "1.2.3",
1467
+ "library-2": "4.5.6"
1468
+ },
1469
+ "key_findings": [
1470
+ "Finding 1 with actionable insight",
1471
+ "Finding 2 with actionable insight",
1472
+ "Finding 3 with actionable insight"
1473
+ ],
1474
+ "relevant_skills": ["skill-to-use-1", "skill-to-use-2"],
1475
+ "stored_in_memory": true
1476
+ }
1477
+ \`\`\`
1478
+
1479
+ ## Tool Discovery Patterns
1480
+
1481
+ ### Skills Discovery
1482
+
1483
+ \`\`\`
1484
+ skills_list()
1485
+ # Returns: Available skills from global, project, bundled sources
1486
+
1487
+ # Load relevant skill for research domain
1488
+ skills_use(name="<skill>", context="Researching <topic>")
1489
+ \`\`\`
1490
+
1491
+ ### MCP Server Detection
1492
+
1493
+ **No direct listing tool.** Infer from:
1494
+ - Task context (coordinator may mention available tools)
1495
+ - Trial: Try calling a tool and catch error if not available
1496
+ - Read OpenCode config if accessible
1497
+
1498
+ ### CLI Tool Detection
1499
+
1500
+ \`\`\`
1501
+ # Check if tool is installed
1502
+ bash("which <tool>", description="Check if <tool> is available")
1503
+
1504
+ # Examples:
1505
+ bash("which cass", description="Check CASS availability")
1506
+ bash("which ubs", description="Check UBS availability")
1507
+ bash("ollama --version", description="Check Ollama availability")
1508
+ \`\`\`
1509
+
1510
+ ## Context Efficiency Rules (MANDATORY)
1511
+
1512
+ **NEVER dump raw documentation.** Always summarize.
1513
+
1514
+ | ❌ Bad (Context Bomb) | ✅ Good (Condensed) |
1515
+ |---------------------|-------------------|
1516
+ | Paste entire API reference | "Library uses hooks API. Key hooks: useQuery, useMutation. Breaking change in v2: callbacks removed." |
1517
+ | Copy full changelog | "v2.0 breaking changes: renamed auth() → authenticate(), dropped IE11 support" |
1518
+ | Include all examples | "Common pattern: async/await with error boundaries (stored full example in semantic-memory)" |
1519
+
1520
+ **Storage Strategy:**
1521
+ - **Semantic Memory**: Full details, examples, code snippets
1522
+ - **Swarm Mail**: 3-5 bullet points only
1523
+ - **Return Value**: Structured JSON summary
1524
+
1525
+ ## When to Use This Agent
1526
+
1527
+ **DO spawn researcher when:**
1528
+ - Task requires understanding current tech stack versions
1529
+ - Need to fetch library/framework documentation
1530
+ - Discovering project conventions from config files
1531
+ - Researching best practices for unfamiliar domain
1532
+
1533
+ **DON'T spawn researcher when:**
1534
+ - Information is already in semantic memory (query first!)
1535
+ - Task doesn't need external docs
1536
+ - Time-sensitive work (research adds latency)
1537
+
1538
+ ## Example Research Tasks
1539
+
1540
+ **"Research Next.js 16 caching APIs"**
1541
+
1542
+ 1. Read package.json → extract Next.js version
1543
+ 2. Use context7 or fetch to get Next.js 16 cache docs
1544
+ 3. Store findings: unstable_cache, revalidatePath, cache patterns
1545
+ 4. Broadcast: "Next.js 16 uses native fetch caching + unstable_cache for functions"
1546
+ 5. Return structured summary with key APIs
1547
+
1548
+ **"Discover available testing tools"**
1549
+
1550
+ 1. Check skills_list for testing-patterns skill
1551
+ 2. Check which jest/vitest/bun (bash tool)
1552
+ 3. Read package.json devDependencies
1553
+ 4. Store findings: test runner, assertion library, coverage tool
1554
+ 5. Broadcast: "Project uses Bun test with happy-dom"
1555
+ 6. Return tool inventory
1556
+
1557
+ Begin by executing Step 1 (swarmmail_init).
1558
+ `;
1559
+
1325
1560
  // ============================================================================
1326
1561
  // Commands
1327
1562
  // ============================================================================
@@ -1533,6 +1768,7 @@ async function setup() {
1533
1768
  const swarmAgentDir = join(agentDir, "swarm");
1534
1769
  const plannerAgentPath = join(swarmAgentDir, "planner.md");
1535
1770
  const workerAgentPath = join(swarmAgentDir, "worker.md");
1771
+ const researcherAgentPath = join(swarmAgentDir, "researcher.md");
1536
1772
  // Legacy flat paths (for detection/cleanup)
1537
1773
  const legacyPlannerPath = join(agentDir, "swarm-planner.md");
1538
1774
  const legacyWorkerPath = join(agentDir, "swarm-worker.md");
@@ -1542,13 +1778,14 @@ async function setup() {
1542
1778
  commandPath,
1543
1779
  plannerAgentPath,
1544
1780
  workerAgentPath,
1781
+ researcherAgentPath,
1545
1782
  legacyPlannerPath,
1546
1783
  legacyWorkerPath,
1547
1784
  ].filter((f) => existsSync(f));
1548
1785
 
1549
1786
  if (existingFiles.length > 0) {
1550
1787
  p.log.success("Swarm is already configured!");
1551
- p.log.message(dim(" Found " + existingFiles.length + "/4 config files"));
1788
+ p.log.message(dim(" Found " + existingFiles.length + "/5 config files"));
1552
1789
 
1553
1790
  const action = await p.select({
1554
1791
  message: "What would you like to do?",
@@ -2015,11 +2252,12 @@ async function setup() {
2015
2252
  stats[writeFileWithStatus(pluginPath, getPluginWrapper(), "Plugin")]++;
2016
2253
  stats[writeFileWithStatus(commandPath, SWARM_COMMAND, "Command")]++;
2017
2254
 
2018
- // Write nested agent files (swarm/planner.md, swarm/worker.md)
2255
+ // Write nested agent files (swarm/planner.md, swarm/worker.md, swarm/researcher.md)
2019
2256
  // This is the format used by Task(subagent_type="swarm/worker")
2020
2257
  p.log.step("Writing agent configuration...");
2021
2258
  stats[writeFileWithStatus(plannerAgentPath, getPlannerAgent(coordinatorModel as string), "Planner agent")]++;
2022
2259
  stats[writeFileWithStatus(workerAgentPath, getWorkerAgent(workerModel as string), "Worker agent")]++;
2260
+ stats[writeFileWithStatus(researcherAgentPath, getResearcherAgent(workerModel as string), "Researcher agent")]++;
2023
2261
 
2024
2262
  // Clean up legacy flat agent files if they exist
2025
2263
  if (existsSync(legacyPlannerPath) || existsSync(legacyWorkerPath)) {
@@ -2320,8 +2558,10 @@ function config() {
2320
2558
  const configDir = join(homedir(), ".config", "opencode");
2321
2559
  const pluginPath = join(configDir, "plugin", "swarm.ts");
2322
2560
  const commandPath = join(configDir, "command", "swarm.md");
2323
- const plannerAgentPath = join(configDir, "agent", "swarm-planner.md");
2324
- const workerAgentPath = join(configDir, "agent", "swarm-worker.md");
2561
+ const swarmAgentDir = join(configDir, "agent", "swarm");
2562
+ const plannerAgentPath = join(swarmAgentDir, "planner.md");
2563
+ const workerAgentPath = join(swarmAgentDir, "worker.md");
2564
+ const researcherAgentPath = join(swarmAgentDir, "researcher.md");
2325
2565
  const globalSkillsPath = join(configDir, "skills");
2326
2566
 
2327
2567
  console.log(yellow(BANNER));
@@ -2333,8 +2573,9 @@ function config() {
2333
2573
  const files = [
2334
2574
  { path: pluginPath, desc: "Plugin loader", emoji: "🔌" },
2335
2575
  { path: commandPath, desc: "/swarm command prompt", emoji: "📜" },
2336
- { path: plannerAgentPath, desc: "@swarm-planner agent", emoji: "🤖" },
2337
- { path: workerAgentPath, desc: "@swarm-worker agent", emoji: "🐝" },
2576
+ { path: plannerAgentPath, desc: "@swarm/planner agent", emoji: "🤖" },
2577
+ { path: workerAgentPath, desc: "@swarm/worker agent", emoji: "🐝" },
2578
+ { path: researcherAgentPath, desc: "@swarm/researcher agent", emoji: "🔬" },
2338
2579
  ];
2339
2580
 
2340
2581
  for (const { path, desc, emoji } of files) {
@@ -2473,6 +2714,7 @@ ${cyan("Commands:")}
2473
2714
  swarm config Show paths to generated config files
2474
2715
  swarm agents Update AGENTS.md with skill awareness
2475
2716
  swarm migrate Migrate PGlite database to libSQL
2717
+ swarm log View swarm logs with filtering
2476
2718
  swarm update Update to latest version
2477
2719
  swarm version Show version and banner
2478
2720
  swarm tool Execute a tool (for plugin wrapper)
@@ -2483,17 +2725,27 @@ ${cyan("Tool Execution:")}
2483
2725
  swarm tool <name> Execute tool with no args
2484
2726
  swarm tool <name> --json '<args>' Execute tool with JSON args
2485
2727
 
2728
+ ${cyan("Log Viewing:")}
2729
+ swarm log Tail recent logs (last 50 lines)
2730
+ swarm log <module> Filter by module (e.g., compaction)
2731
+ swarm log --level <level> Filter by level (trace, debug, info, warn, error, fatal)
2732
+ swarm log --since <duration> Time filter (30s, 5m, 2h, 1d)
2733
+ swarm log --json Raw JSON output for jq
2734
+ swarm log --limit <n> Limit output to n lines (default: 50)
2735
+
2486
2736
  ${cyan("Usage in OpenCode:")}
2487
2737
  /swarm "Add user authentication with OAuth"
2488
- @swarm-planner "Decompose this into parallel tasks"
2489
- @swarm-worker "Execute this specific subtask"
2738
+ @swarm/planner "Decompose this into parallel tasks"
2739
+ @swarm/worker "Execute this specific subtask"
2740
+ @swarm/researcher "Research Next.js caching APIs"
2490
2741
 
2491
2742
  ${cyan("Customization:")}
2492
2743
  Edit the generated files to customize behavior:
2493
- ${dim("~/.config/opencode/command/swarm.md")} - /swarm command prompt
2494
- ${dim("~/.config/opencode/agent/swarm-planner.md")} - @swarm-planner (coordinator)
2495
- ${dim("~/.config/opencode/agent/swarm-worker.md")} - @swarm-worker (fast executor)
2496
- ${dim("~/.config/opencode/plugin/swarm.ts")} - Plugin loader
2744
+ ${dim("~/.config/opencode/command/swarm.md")} - /swarm command prompt
2745
+ ${dim("~/.config/opencode/agent/swarm/planner.md")} - @swarm/planner (coordinator)
2746
+ ${dim("~/.config/opencode/agent/swarm/worker.md")} - @swarm/worker (task executor)
2747
+ ${dim("~/.config/opencode/agent/swarm/researcher.md")} - @swarm/researcher (read-only research)
2748
+ ${dim("~/.config/opencode/plugin/swarm.ts")} - Plugin loader
2497
2749
 
2498
2750
  ${dim("Docs: https://github.com/joelhooks/opencode-swarm-plugin")}
2499
2751
  `);
@@ -2810,7 +3062,7 @@ async function migrate() {
2810
3062
  // Show results
2811
3063
  const showStat = (label: string, stat: { migrated: number; skipped: number; failed: number }) => {
2812
3064
  if (stat.migrated > 0 || stat.skipped > 0 || stat.failed > 0) {
2813
- const parts = [];
3065
+ const parts: string[] = [];
2814
3066
  if (stat.migrated > 0) parts.push(green(`${stat.migrated} migrated`));
2815
3067
  if (stat.skipped > 0) parts.push(dim(`${stat.skipped} skipped`));
2816
3068
  if (stat.failed > 0) parts.push(`\x1b[31m${stat.failed} failed\x1b[0m`);
@@ -2838,6 +3090,207 @@ async function migrate() {
2838
3090
  }
2839
3091
  }
2840
3092
 
3093
+ // ============================================================================
3094
+ // Log Command - View swarm logs with filtering
3095
+ // ============================================================================
3096
+
3097
+ interface LogLine {
3098
+ level: number;
3099
+ time: string;
3100
+ module: string;
3101
+ msg: string;
3102
+ }
3103
+
3104
+ function parseLogLine(line: string): LogLine | null {
3105
+ try {
3106
+ const parsed = JSON.parse(line);
3107
+ if (typeof parsed.level === "number" && parsed.time && parsed.msg) {
3108
+ return {
3109
+ level: parsed.level,
3110
+ time: parsed.time,
3111
+ module: parsed.module || "unknown",
3112
+ msg: parsed.msg,
3113
+ };
3114
+ }
3115
+ } catch {
3116
+ // Invalid JSON
3117
+ }
3118
+ return null;
3119
+ }
3120
+
3121
+ function levelToName(level: number): string {
3122
+ if (level >= 60) return "FATAL";
3123
+ if (level >= 50) return "ERROR";
3124
+ if (level >= 40) return "WARN ";
3125
+ if (level >= 30) return "INFO ";
3126
+ if (level >= 20) return "DEBUG";
3127
+ return "TRACE";
3128
+ }
3129
+
3130
+ function levelToColor(level: number): (s: string) => string {
3131
+ if (level >= 50) return (s: string) => `\x1b[31m${s}\x1b[0m`; // red
3132
+ if (level >= 40) return (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
3133
+ if (level >= 30) return green; // green
3134
+ return dim; // dim for debug/trace
3135
+ }
3136
+
3137
+ function levelNameToNumber(name: string): number {
3138
+ const lower = name.toLowerCase();
3139
+ if (lower === "fatal") return 60;
3140
+ if (lower === "error") return 50;
3141
+ if (lower === "warn") return 40;
3142
+ if (lower === "info") return 30;
3143
+ if (lower === "debug") return 20;
3144
+ if (lower === "trace") return 10;
3145
+ return 30; // default to info
3146
+ }
3147
+
3148
+ function parseDuration(duration: string): number | null {
3149
+ const match = duration.match(/^(\d+)([smhd])$/);
3150
+ if (!match) return null;
3151
+
3152
+ const [, num, unit] = match;
3153
+ const value = parseInt(num, 10);
3154
+
3155
+ const multipliers: Record<string, number> = {
3156
+ s: 1000,
3157
+ m: 60 * 1000,
3158
+ h: 60 * 60 * 1000,
3159
+ d: 24 * 60 * 60 * 1000,
3160
+ };
3161
+
3162
+ return value * multipliers[unit];
3163
+ }
3164
+
3165
+ function formatLogLine(log: LogLine, useColor = true): string {
3166
+ const timestamp = new Date(log.time).toLocaleTimeString();
3167
+ const levelName = levelToName(log.level);
3168
+ const module = log.module.padEnd(12);
3169
+ const levelStr = useColor ? levelToColor(log.level)(levelName) : levelName;
3170
+
3171
+ return `${timestamp} ${levelStr} ${module} ${log.msg}`;
3172
+ }
3173
+
3174
+ function readLogFiles(dir: string): string[] {
3175
+ if (!existsSync(dir)) return [];
3176
+
3177
+ const allFiles = readdirSync(dir);
3178
+ const logFiles = allFiles
3179
+ .filter((f: string) => /\.\d+log$/.test(f))
3180
+ .sort()
3181
+ .map((f: string) => join(dir, f));
3182
+
3183
+ const lines: string[] = [];
3184
+ for (const file of logFiles) {
3185
+ try {
3186
+ const content = readFileSync(file, "utf-8");
3187
+ const fileLines = content.split("\n").filter((line: string) => line.trim());
3188
+ lines.push(...fileLines);
3189
+ } catch {
3190
+ // Skip unreadable files
3191
+ }
3192
+ }
3193
+
3194
+ return lines;
3195
+ }
3196
+
3197
+ async function logs() {
3198
+ const args = process.argv.slice(3);
3199
+
3200
+ // Parse arguments
3201
+ let moduleFilter: string | null = null;
3202
+ let levelFilter: number | null = null;
3203
+ let sinceMs: number | null = null;
3204
+ let jsonOutput = false;
3205
+ let limit = 50;
3206
+
3207
+ for (let i = 0; i < args.length; i++) {
3208
+ const arg = args[i];
3209
+
3210
+ if (arg === "--level" && i + 1 < args.length) {
3211
+ levelFilter = levelNameToNumber(args[++i]);
3212
+ } else if (arg === "--since" && i + 1 < args.length) {
3213
+ const duration = parseDuration(args[++i]);
3214
+ if (duration === null) {
3215
+ p.log.error(`Invalid duration format: ${args[i]}`);
3216
+ p.log.message(dim(" Use format: 30s, 5m, 2h, 1d"));
3217
+ process.exit(1);
3218
+ }
3219
+ sinceMs = duration;
3220
+ } else if (arg === "--json") {
3221
+ jsonOutput = true;
3222
+ } else if (arg === "--limit" && i + 1 < args.length) {
3223
+ limit = parseInt(args[++i], 10);
3224
+ if (isNaN(limit) || limit <= 0) {
3225
+ p.log.error(`Invalid limit: ${args[i]}`);
3226
+ process.exit(1);
3227
+ }
3228
+ } else if (!arg.startsWith("--")) {
3229
+ // Positional arg = module filter
3230
+ moduleFilter = arg;
3231
+ }
3232
+ }
3233
+
3234
+ // Read logs from ~/.config/swarm-tools/logs/
3235
+ const logsDir = join(homedir(), ".config", "swarm-tools", "logs");
3236
+
3237
+ if (!existsSync(logsDir)) {
3238
+ if (!jsonOutput) {
3239
+ p.log.warn("No logs directory found");
3240
+ p.log.message(dim(` Expected: ${logsDir}`));
3241
+ } else {
3242
+ console.log(JSON.stringify({ logs: [] }));
3243
+ }
3244
+ return;
3245
+ }
3246
+
3247
+ const rawLines = readLogFiles(logsDir);
3248
+
3249
+ // Parse and filter
3250
+ let logs: LogLine[] = rawLines
3251
+ .map(parseLogLine)
3252
+ .filter((log): log is LogLine => log !== null);
3253
+
3254
+ // Apply filters
3255
+ if (moduleFilter) {
3256
+ logs = logs.filter((log) => log.module === moduleFilter);
3257
+ }
3258
+
3259
+ if (levelFilter !== null) {
3260
+ logs = logs.filter((log) => log.level >= levelFilter);
3261
+ }
3262
+
3263
+ if (sinceMs !== null) {
3264
+ const cutoffTime = Date.now() - sinceMs;
3265
+ logs = logs.filter((log) => new Date(log.time).getTime() >= cutoffTime);
3266
+ }
3267
+
3268
+ // Apply limit (keep most recent)
3269
+ logs = logs.slice(-limit);
3270
+
3271
+ // Output
3272
+ if (jsonOutput) {
3273
+ console.log(JSON.stringify({ logs }, null, 2));
3274
+ } else {
3275
+ if (logs.length === 0) {
3276
+ p.log.warn("No logs found matching filters");
3277
+ return;
3278
+ }
3279
+
3280
+ console.log(yellow(BANNER));
3281
+ console.log(dim(` Logs (${logs.length} entries)`));
3282
+ if (moduleFilter) console.log(dim(` Module: ${moduleFilter}`));
3283
+ if (levelFilter !== null) console.log(dim(` Level: >=${levelToName(levelFilter)}`));
3284
+ if (sinceMs !== null) console.log(dim(` Since: last ${args[args.indexOf("--since") + 1]}`));
3285
+ console.log();
3286
+
3287
+ for (const log of logs) {
3288
+ console.log(formatLogLine(log));
3289
+ }
3290
+ console.log();
3291
+ }
3292
+ }
3293
+
2841
3294
  // ============================================================================
2842
3295
  // Database Info Command
2843
3296
  // ============================================================================
@@ -2981,6 +3434,10 @@ switch (command) {
2981
3434
  case "db":
2982
3435
  await db();
2983
3436
  break;
3437
+ case "log":
3438
+ case "logs":
3439
+ await logs();
3440
+ break;
2984
3441
  case "version":
2985
3442
  case "--version":
2986
3443
  case "-v":
@@ -67,7 +67,7 @@ export declare const SWARM_DETECTION_FALLBACK = "## \uD83D\uDC1D Swarm Detection
67
67
  * });
68
68
  * ```
69
69
  */
70
- export declare function createCompactionHook(): (_input: {
70
+ export declare function createCompactionHook(): (input: {
71
71
  sessionID: string;
72
72
  }, output: {
73
73
  context: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../src/compaction-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AASH;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,k9DAwDpC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,0nCAiCpC,CAAC;AAoIF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,KAEhC,QAAQ;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC7B,QAAQ;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,KAC5B,OAAO,CAAC,IAAI,CAAC,CAcjB"}
1
+ {"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../src/compaction-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA+BH;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,k9DAwDpC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,0nCAiCpC,CAAC;AAuMF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,KAEhC,OAAO;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC5B,QAAQ;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,KAC5B,OAAO,CAAC,IAAI,CAAC,CAmFjB"}