opencode-swarm-plugin 0.48.0 → 0.49.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.
- package/README.md +10 -7
- package/bin/swarm-setup-consolidate.test.ts +84 -0
- package/bin/swarm.test.ts +170 -0
- package/bin/swarm.ts +300 -2
- package/bin/test-setup-manual.md +67 -0
- package/dist/bin/swarm.js +1055 -188
- package/dist/coordinator-guard.d.ts +79 -0
- package/dist/coordinator-guard.d.ts.map +1 -0
- package/dist/examples/plugin-wrapper-template.ts +13 -2
- package/dist/hive.d.ts.map +1 -1
- package/dist/hive.js +5 -4
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -18
- package/dist/memory.d.ts.map +1 -1
- package/dist/plugin.js +383 -18
- package/dist/query-tools.d.ts +5 -0
- package/dist/query-tools.d.ts.map +1 -1
- package/dist/schemas/cell.d.ts +12 -0
- package/dist/schemas/cell.d.ts.map +1 -1
- package/dist/swarm-insights.d.ts +158 -0
- package/dist/swarm-insights.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts +4 -4
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-prompts.js +345 -18
- package/dist/swarm-strategies.d.ts +1 -1
- package/dist/swarm.d.ts +2 -2
- package/examples/plugin-wrapper-template.ts +13 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -505,15 +505,18 @@ See **[evals/README.md](./evals/README.md)** for full architecture, scorer detai
|
|
|
505
505
|
### Setup & Configuration
|
|
506
506
|
|
|
507
507
|
```bash
|
|
508
|
-
swarm setup
|
|
509
|
-
swarm
|
|
510
|
-
swarm
|
|
511
|
-
swarm
|
|
512
|
-
swarm
|
|
513
|
-
swarm
|
|
514
|
-
swarm
|
|
508
|
+
swarm setup # Interactive installer for all dependencies
|
|
509
|
+
swarm setup -y # Non-interactive mode (auto-migrate stray databases)
|
|
510
|
+
swarm doctor # Check dependency health (CASS, UBS, Ollama)
|
|
511
|
+
swarm init # Initialize hive in current project
|
|
512
|
+
swarm config # Show config file paths
|
|
513
|
+
swarm update # Update swarm plugin and bundled skills
|
|
514
|
+
swarm migrate # Migrate from legacy PGLite to libSQL
|
|
515
|
+
swarm version # Show version info
|
|
515
516
|
```
|
|
516
517
|
|
|
518
|
+
**Database Consolidation**: `swarm setup` automatically detects and migrates stray databases (`.opencode/swarm.db`, `.hive/swarm-mail.db`, nested package databases) to the global database at `~/.config/swarm-tools/swarm.db`. Use `-y` flag to migrate without prompting.
|
|
519
|
+
|
|
517
520
|
### Observability Commands
|
|
518
521
|
|
|
519
522
|
**swarm query** - SQL analytics with presets
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Test: swarm setup database consolidation
|
|
4
|
+
*
|
|
5
|
+
* Tests that the swarm setup command can detect and migrate stray databases
|
|
6
|
+
* when the -y flag is provided.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
10
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
|
|
15
|
+
describe("swarm setup -y database consolidation", () => {
|
|
16
|
+
let testDir: string;
|
|
17
|
+
let projectPath: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Create a temporary project directory
|
|
21
|
+
testDir = join(tmpdir(), `swarm-setup-test-${Date.now()}`);
|
|
22
|
+
projectPath = join(testDir, "test-project");
|
|
23
|
+
mkdirSync(projectPath, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
// Clean up
|
|
28
|
+
if (existsSync(testDir)) {
|
|
29
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("consolidateDatabases can be imported from swarm-mail", async () => {
|
|
34
|
+
// Verify the import works
|
|
35
|
+
const { consolidateDatabases } = await import("swarm-mail");
|
|
36
|
+
expect(typeof consolidateDatabases).toBe("function");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("getGlobalDbPath can be imported from swarm-mail", async () => {
|
|
40
|
+
// Verify the import works
|
|
41
|
+
const { getGlobalDbPath } = await import("swarm-mail");
|
|
42
|
+
expect(typeof getGlobalDbPath).toBe("function");
|
|
43
|
+
|
|
44
|
+
// Verify it returns a path
|
|
45
|
+
const path = getGlobalDbPath();
|
|
46
|
+
expect(typeof path).toBe("string");
|
|
47
|
+
expect(path.length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("consolidateDatabases respects yes flag", async () => {
|
|
51
|
+
const { consolidateDatabases, getGlobalDbPath } = await import("swarm-mail");
|
|
52
|
+
const globalDbPath = getGlobalDbPath();
|
|
53
|
+
|
|
54
|
+
// Call with yes: true (non-interactive mode)
|
|
55
|
+
const report = await consolidateDatabases(projectPath, globalDbPath, {
|
|
56
|
+
yes: true,
|
|
57
|
+
interactive: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Should return a report object with correct structure
|
|
61
|
+
expect(report).toBeDefined();
|
|
62
|
+
expect(typeof report.straysFound).toBe("number");
|
|
63
|
+
expect(typeof report.straysMigrated).toBe("number");
|
|
64
|
+
expect(typeof report.totalRowsMigrated).toBe("number");
|
|
65
|
+
expect(Array.isArray(report.migrations)).toBe(true);
|
|
66
|
+
expect(Array.isArray(report.errors)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("consolidateDatabases can run in non-interactive mode", async () => {
|
|
70
|
+
const { consolidateDatabases, getGlobalDbPath } = await import("swarm-mail");
|
|
71
|
+
const globalDbPath = getGlobalDbPath();
|
|
72
|
+
|
|
73
|
+
// Call with interactive: false (non-interactive mode, same as -y)
|
|
74
|
+
const report = await consolidateDatabases(projectPath, globalDbPath, {
|
|
75
|
+
yes: false,
|
|
76
|
+
interactive: false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Should return a report object without prompting
|
|
80
|
+
expect(report).toBeDefined();
|
|
81
|
+
expect(typeof report.straysFound).toBe("number");
|
|
82
|
+
expect(Array.isArray(report.migrations)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/bin/swarm.test.ts
CHANGED
|
@@ -2132,3 +2132,173 @@ describe("swarm export", () => {
|
|
|
2132
2132
|
});
|
|
2133
2133
|
});
|
|
2134
2134
|
|
|
2135
|
+
// ============================================================================
|
|
2136
|
+
// DB Repair Command Tests (TDD)
|
|
2137
|
+
// ============================================================================
|
|
2138
|
+
|
|
2139
|
+
describe("swarm db repair", () => {
|
|
2140
|
+
let testDir: string;
|
|
2141
|
+
|
|
2142
|
+
beforeEach(() => {
|
|
2143
|
+
testDir = join(tmpdir(), `swarm-db-repair-test-${Date.now()}`);
|
|
2144
|
+
mkdirSync(testDir, { recursive: true });
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
afterEach(() => {
|
|
2148
|
+
if (existsSync(testDir)) {
|
|
2149
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
describe("parseRepairArgs", () => {
|
|
2154
|
+
function parseRepairArgs(args: string[]): { dryRun: boolean } {
|
|
2155
|
+
let dryRun = false;
|
|
2156
|
+
|
|
2157
|
+
for (const arg of args) {
|
|
2158
|
+
if (arg === "--dry-run") {
|
|
2159
|
+
dryRun = true;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
return { dryRun };
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
test("parses --dry-run flag", () => {
|
|
2167
|
+
const result = parseRepairArgs(["--dry-run"]);
|
|
2168
|
+
expect(result.dryRun).toBe(true);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test("defaults to dryRun=false", () => {
|
|
2172
|
+
const result = parseRepairArgs([]);
|
|
2173
|
+
expect(result.dryRun).toBe(false);
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
describe("executeRepair", () => {
|
|
2178
|
+
/**
|
|
2179
|
+
* Execute repair operation
|
|
2180
|
+
* @param dryRun - If true, only count what would be deleted
|
|
2181
|
+
* @returns Summary of changes
|
|
2182
|
+
*/
|
|
2183
|
+
async function executeRepair(dryRun: boolean): Promise<{
|
|
2184
|
+
nullBeads: number;
|
|
2185
|
+
orphanedRecipients: number;
|
|
2186
|
+
messagesWithoutRecipients: number;
|
|
2187
|
+
expiredReservations: number;
|
|
2188
|
+
}> {
|
|
2189
|
+
// Mock implementation for testing
|
|
2190
|
+
// Real implementation will use getSwarmMailLibSQL()
|
|
2191
|
+
return {
|
|
2192
|
+
nullBeads: dryRun ? 427 : 0,
|
|
2193
|
+
orphanedRecipients: dryRun ? 208 : 0,
|
|
2194
|
+
messagesWithoutRecipients: dryRun ? 72 : 0,
|
|
2195
|
+
expiredReservations: dryRun ? 213 : 0,
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
test("dry run returns counts without deleting", async () => {
|
|
2200
|
+
const result = await executeRepair(true);
|
|
2201
|
+
|
|
2202
|
+
expect(result.nullBeads).toBeGreaterThanOrEqual(0);
|
|
2203
|
+
expect(result.orphanedRecipients).toBeGreaterThanOrEqual(0);
|
|
2204
|
+
expect(result.messagesWithoutRecipients).toBeGreaterThanOrEqual(0);
|
|
2205
|
+
expect(result.expiredReservations).toBeGreaterThanOrEqual(0);
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
test("actual repair returns zero after cleanup", async () => {
|
|
2209
|
+
const result = await executeRepair(false);
|
|
2210
|
+
|
|
2211
|
+
// After cleanup, all counts should be zero (mock behavior)
|
|
2212
|
+
expect(result.nullBeads).toBe(0);
|
|
2213
|
+
expect(result.orphanedRecipients).toBe(0);
|
|
2214
|
+
expect(result.messagesWithoutRecipients).toBe(0);
|
|
2215
|
+
expect(result.expiredReservations).toBe(0);
|
|
2216
|
+
});
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
describe("formatRepairSummary", () => {
|
|
2220
|
+
function formatRepairSummary(
|
|
2221
|
+
counts: {
|
|
2222
|
+
nullBeads: number;
|
|
2223
|
+
orphanedRecipients: number;
|
|
2224
|
+
messagesWithoutRecipients: number;
|
|
2225
|
+
expiredReservations: number;
|
|
2226
|
+
},
|
|
2227
|
+
dryRun: boolean,
|
|
2228
|
+
): string {
|
|
2229
|
+
const lines: string[] = [];
|
|
2230
|
+
|
|
2231
|
+
if (dryRun) {
|
|
2232
|
+
lines.push("DRY RUN - No changes made");
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
lines.push("Would delete:");
|
|
2235
|
+
} else {
|
|
2236
|
+
lines.push("Deleted:");
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
lines.push(` - ${counts.nullBeads} beads with NULL IDs`);
|
|
2240
|
+
lines.push(` - ${counts.orphanedRecipients} orphaned message_recipients`);
|
|
2241
|
+
lines.push(` - ${counts.messagesWithoutRecipients} messages without recipients`);
|
|
2242
|
+
lines.push(` - ${counts.expiredReservations} expired unreleased reservations`);
|
|
2243
|
+
|
|
2244
|
+
const total =
|
|
2245
|
+
counts.nullBeads +
|
|
2246
|
+
counts.orphanedRecipients +
|
|
2247
|
+
counts.messagesWithoutRecipients +
|
|
2248
|
+
counts.expiredReservations;
|
|
2249
|
+
|
|
2250
|
+
lines.push("");
|
|
2251
|
+
lines.push(`Total: ${total} records ${dryRun ? "would be cleaned" : "cleaned"}`);
|
|
2252
|
+
|
|
2253
|
+
return lines.join("\n");
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
test("formats dry run summary", () => {
|
|
2257
|
+
const counts = {
|
|
2258
|
+
nullBeads: 427,
|
|
2259
|
+
orphanedRecipients: 208,
|
|
2260
|
+
messagesWithoutRecipients: 72,
|
|
2261
|
+
expiredReservations: 213,
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
const result = formatRepairSummary(counts, true);
|
|
2265
|
+
|
|
2266
|
+
expect(result).toContain("DRY RUN");
|
|
2267
|
+
expect(result).toContain("Would delete:");
|
|
2268
|
+
expect(result).toContain("427 beads with NULL IDs");
|
|
2269
|
+
expect(result).toContain("208 orphaned message_recipients");
|
|
2270
|
+
expect(result).toContain("72 messages without recipients");
|
|
2271
|
+
expect(result).toContain("213 expired unreleased reservations");
|
|
2272
|
+
expect(result).toContain("Total: 920 records would be cleaned");
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
test("formats actual repair summary", () => {
|
|
2276
|
+
const counts = {
|
|
2277
|
+
nullBeads: 427,
|
|
2278
|
+
orphanedRecipients: 208,
|
|
2279
|
+
messagesWithoutRecipients: 72,
|
|
2280
|
+
expiredReservations: 213,
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
const result = formatRepairSummary(counts, false);
|
|
2284
|
+
|
|
2285
|
+
expect(result).not.toContain("DRY RUN");
|
|
2286
|
+
expect(result).toContain("Deleted:");
|
|
2287
|
+
expect(result).toContain("Total: 920 records cleaned");
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
test("handles zero counts", () => {
|
|
2291
|
+
const counts = {
|
|
2292
|
+
nullBeads: 0,
|
|
2293
|
+
orphanedRecipients: 0,
|
|
2294
|
+
messagesWithoutRecipients: 0,
|
|
2295
|
+
expiredReservations: 0,
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
const result = formatRepairSummary(counts, false);
|
|
2299
|
+
|
|
2300
|
+
expect(result).toContain("Total: 0 records cleaned");
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
});
|
|
2304
|
+
|
package/bin/swarm.ts
CHANGED
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
resolvePartialId,
|
|
51
51
|
createDurableStreamAdapter,
|
|
52
52
|
createDurableStreamServer,
|
|
53
|
+
consolidateDatabases,
|
|
54
|
+
getGlobalDbPath,
|
|
53
55
|
} from "swarm-mail";
|
|
54
56
|
import { execSync, spawn } from "child_process";
|
|
55
57
|
import { tmpdir } from "os";
|
|
@@ -92,6 +94,9 @@ import {
|
|
|
92
94
|
formatHealthDashboard,
|
|
93
95
|
} from "../src/observability-health.js";
|
|
94
96
|
|
|
97
|
+
// Swarm insights
|
|
98
|
+
import { getRejectionAnalytics } from "../src/swarm-insights.js";
|
|
99
|
+
|
|
95
100
|
// Eval tools
|
|
96
101
|
import { getPhase, getScoreHistory, recordEvalRun, getEvalHistoryPath } from "../src/eval-history.js";
|
|
97
102
|
import { DEFAULT_THRESHOLDS, checkGate } from "../src/eval-gates.js";
|
|
@@ -102,8 +107,11 @@ import { detectRegressions } from "../src/regression-detection.js";
|
|
|
102
107
|
import { allTools } from "../src/index.js";
|
|
103
108
|
|
|
104
109
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
105
|
-
// When
|
|
106
|
-
|
|
110
|
+
// When running from bin/swarm.ts, go up one level to find package.json
|
|
111
|
+
// When bundled to dist/bin/swarm.js, go up two levels
|
|
112
|
+
const pkgPath = existsSync(join(__dirname, "..", "package.json"))
|
|
113
|
+
? join(__dirname, "..", "package.json")
|
|
114
|
+
: join(__dirname, "..", "..", "package.json");
|
|
107
115
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
108
116
|
const VERSION: string = pkg.version;
|
|
109
117
|
|
|
@@ -2163,6 +2171,85 @@ async function setup(forceReinstall = false, nonInteractive = false) {
|
|
|
2163
2171
|
p.log.message(dim(' No OpenCode config found (skipping MCP check)'));
|
|
2164
2172
|
}
|
|
2165
2173
|
|
|
2174
|
+
// Check for stray databases and consolidate to global database
|
|
2175
|
+
p.log.step("Checking for stray databases...");
|
|
2176
|
+
const globalDbPath = getGlobalDbPath();
|
|
2177
|
+
|
|
2178
|
+
try {
|
|
2179
|
+
const report = await consolidateDatabases(cwd, globalDbPath, {
|
|
2180
|
+
yes: nonInteractive,
|
|
2181
|
+
interactive: !nonInteractive,
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
if (report.straysFound > 0) {
|
|
2185
|
+
if (report.totalRowsMigrated > 0) {
|
|
2186
|
+
p.log.success(
|
|
2187
|
+
`Migrated ${report.totalRowsMigrated} records from ${report.straysMigrated} stray database(s)`
|
|
2188
|
+
);
|
|
2189
|
+
for (const migration of report.migrations) {
|
|
2190
|
+
const { migrated, skipped } = migration.result;
|
|
2191
|
+
if (migrated > 0 || skipped > 0) {
|
|
2192
|
+
p.log.message(
|
|
2193
|
+
dim(
|
|
2194
|
+
` ${migration.path}: ${migrated} migrated, ${skipped} skipped`
|
|
2195
|
+
)
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
} else {
|
|
2200
|
+
p.log.message(
|
|
2201
|
+
dim(" All data already in global database (no migration needed)")
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
} else {
|
|
2205
|
+
p.log.message(dim(" No stray databases found"));
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (report.errors.length > 0) {
|
|
2209
|
+
p.log.warn(`${report.errors.length} error(s) during consolidation`);
|
|
2210
|
+
for (const error of report.errors) {
|
|
2211
|
+
p.log.message(dim(` ${error}`));
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
p.log.warn("Database consolidation check failed");
|
|
2216
|
+
if (error instanceof Error) {
|
|
2217
|
+
p.log.message(dim(` ${error.message}`));
|
|
2218
|
+
}
|
|
2219
|
+
// Don't fail setup - this is non-critical
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Run database repair after consolidation
|
|
2223
|
+
p.log.step("Running database integrity check...");
|
|
2224
|
+
try {
|
|
2225
|
+
const repairResult = await runDbRepair({ dryRun: false });
|
|
2226
|
+
|
|
2227
|
+
if (repairResult.totalCleaned === 0) {
|
|
2228
|
+
p.log.success("Database integrity verified - no issues found");
|
|
2229
|
+
} else {
|
|
2230
|
+
p.log.success(`Cleaned ${repairResult.totalCleaned} orphaned/invalid records`);
|
|
2231
|
+
|
|
2232
|
+
if (repairResult.nullBeads > 0) {
|
|
2233
|
+
p.log.message(dim(` - ${repairResult.nullBeads} beads with NULL IDs`));
|
|
2234
|
+
}
|
|
2235
|
+
if (repairResult.orphanedRecipients > 0) {
|
|
2236
|
+
p.log.message(dim(` - ${repairResult.orphanedRecipients} orphaned message recipients`));
|
|
2237
|
+
}
|
|
2238
|
+
if (repairResult.messagesWithoutRecipients > 0) {
|
|
2239
|
+
p.log.message(dim(` - ${repairResult.messagesWithoutRecipients} messages without recipients`));
|
|
2240
|
+
}
|
|
2241
|
+
if (repairResult.expiredReservations > 0) {
|
|
2242
|
+
p.log.message(dim(` - ${repairResult.expiredReservations} expired reservations`));
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
p.log.warn("Database repair check failed (non-critical)");
|
|
2247
|
+
if (error instanceof Error) {
|
|
2248
|
+
p.log.message(dim(` ${error.message}`));
|
|
2249
|
+
}
|
|
2250
|
+
// Don't fail setup - this is non-critical
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2166
2253
|
// Model defaults: opus for coordinator, sonnet for worker, haiku for lite
|
|
2167
2254
|
const DEFAULT_COORDINATOR = "anthropic/claude-opus-4-5";
|
|
2168
2255
|
const DEFAULT_WORKER = "anthropic/claude-sonnet-4-5";
|
|
@@ -3274,6 +3361,7 @@ ${cyan("Stats & History:")}
|
|
|
3274
3361
|
swarm stats Show swarm health metrics powered by swarm-insights (last 7 days)
|
|
3275
3362
|
swarm stats --since 24h Show stats for custom time period
|
|
3276
3363
|
swarm stats --regressions Show eval regressions (>10% score drops)
|
|
3364
|
+
swarm stats --rejections Show rejection reason analytics
|
|
3277
3365
|
swarm stats --json Output as JSON for scripting
|
|
3278
3366
|
swarm o11y Show observability health dashboard (hook coverage, events, sessions)
|
|
3279
3367
|
swarm o11y --since 7d Custom time period for event stats (default: 7 days)
|
|
@@ -4532,7 +4620,178 @@ async function logs() {
|
|
|
4532
4620
|
*
|
|
4533
4621
|
* Helps debug which database is being used and its schema state.
|
|
4534
4622
|
*/
|
|
4623
|
+
/**
|
|
4624
|
+
* Run database repair programmatically
|
|
4625
|
+
* Returns counts of cleaned records
|
|
4626
|
+
*/
|
|
4627
|
+
async function runDbRepair(options: { dryRun: boolean }): Promise<{
|
|
4628
|
+
nullBeads: number;
|
|
4629
|
+
orphanedRecipients: number;
|
|
4630
|
+
messagesWithoutRecipients: number;
|
|
4631
|
+
expiredReservations: number;
|
|
4632
|
+
totalCleaned: number;
|
|
4633
|
+
}> {
|
|
4634
|
+
const { dryRun } = options;
|
|
4635
|
+
const globalDbPath = getGlobalDbPath();
|
|
4636
|
+
const swarmMail = await getSwarmMailLibSQL(globalDbPath);
|
|
4637
|
+
const db = await swarmMail.getDatabase();
|
|
4638
|
+
|
|
4639
|
+
// Count records before cleanup
|
|
4640
|
+
const nullBeadsResult = await db.query<{ count: number }>("SELECT COUNT(*) as count FROM beads WHERE id IS NULL");
|
|
4641
|
+
const nullBeads = Number(nullBeadsResult[0]?.count ?? 0);
|
|
4642
|
+
|
|
4643
|
+
const orphanedRecipientsResult = await db.query<{ count: number }>(
|
|
4644
|
+
"SELECT COUNT(*) as count FROM message_recipients WHERE NOT EXISTS (SELECT 1 FROM agents WHERE agents.name = message_recipients.agent_name)"
|
|
4645
|
+
);
|
|
4646
|
+
const orphanedRecipients = Number(orphanedRecipientsResult[0]?.count ?? 0);
|
|
4647
|
+
|
|
4648
|
+
const messagesWithoutRecipientsResult = await db.query<{ count: number }>(
|
|
4649
|
+
"SELECT COUNT(*) as count FROM messages WHERE NOT EXISTS (SELECT 1 FROM message_recipients WHERE message_recipients.message_id = messages.id)"
|
|
4650
|
+
);
|
|
4651
|
+
const messagesWithoutRecipients = Number(messagesWithoutRecipientsResult[0]?.count ?? 0);
|
|
4652
|
+
|
|
4653
|
+
const expiredReservationsResult = await db.query<{ count: number }>(
|
|
4654
|
+
"SELECT COUNT(*) as count FROM reservations WHERE released_at IS NULL AND expires_at < strftime('%s', 'now') * 1000"
|
|
4655
|
+
);
|
|
4656
|
+
const expiredReservations = Number(expiredReservationsResult[0]?.count ?? 0);
|
|
4657
|
+
|
|
4658
|
+
const totalCleaned = nullBeads + orphanedRecipients + messagesWithoutRecipients + expiredReservations;
|
|
4659
|
+
|
|
4660
|
+
// If dry run or nothing to clean, return early
|
|
4661
|
+
if (dryRun || totalCleaned === 0) {
|
|
4662
|
+
return {
|
|
4663
|
+
nullBeads,
|
|
4664
|
+
orphanedRecipients,
|
|
4665
|
+
messagesWithoutRecipients,
|
|
4666
|
+
expiredReservations,
|
|
4667
|
+
totalCleaned,
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
// Execute cleanup queries
|
|
4672
|
+
if (nullBeads > 0) {
|
|
4673
|
+
await db.query("DELETE FROM beads WHERE id IS NULL");
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
if (orphanedRecipients > 0) {
|
|
4677
|
+
await db.query(
|
|
4678
|
+
"DELETE FROM message_recipients WHERE NOT EXISTS (SELECT 1 FROM agents WHERE agents.name = message_recipients.agent_name)"
|
|
4679
|
+
);
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
if (messagesWithoutRecipients > 0) {
|
|
4683
|
+
await db.query(
|
|
4684
|
+
"DELETE FROM messages WHERE NOT EXISTS (SELECT 1 FROM message_recipients WHERE message_recipients.message_id = messages.id)"
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
if (expiredReservations > 0) {
|
|
4689
|
+
await db.query(
|
|
4690
|
+
"UPDATE reservations SET released_at = strftime('%s', 'now') * 1000 WHERE released_at IS NULL AND expires_at < strftime('%s', 'now') * 1000"
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
return {
|
|
4695
|
+
nullBeads,
|
|
4696
|
+
orphanedRecipients,
|
|
4697
|
+
messagesWithoutRecipients,
|
|
4698
|
+
expiredReservations,
|
|
4699
|
+
totalCleaned,
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
/**
|
|
4704
|
+
* Database repair command (CLI interface)
|
|
4705
|
+
* Executes cleanup SQL to remove orphaned/invalid data
|
|
4706
|
+
*/
|
|
4707
|
+
async function dbRepair() {
|
|
4708
|
+
const args = process.argv.slice(4); // Skip 'swarm', 'db', 'repair'
|
|
4709
|
+
let dryRun = false;
|
|
4710
|
+
|
|
4711
|
+
// Parse --dry-run flag
|
|
4712
|
+
for (const arg of args) {
|
|
4713
|
+
if (arg === "--dry-run") {
|
|
4714
|
+
dryRun = true;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
p.intro(dryRun ? "swarm db repair (DRY RUN)" : "swarm db repair");
|
|
4719
|
+
|
|
4720
|
+
const s = p.spinner();
|
|
4721
|
+
s.start("Analyzing database...");
|
|
4722
|
+
|
|
4723
|
+
try {
|
|
4724
|
+
// Use shared helper for analysis
|
|
4725
|
+
const result = await runDbRepair({ dryRun: true });
|
|
4726
|
+
|
|
4727
|
+
s.stop("Analysis complete");
|
|
4728
|
+
|
|
4729
|
+
// Show counts
|
|
4730
|
+
p.log.step(dryRun ? "Would delete:" : "Deleting:");
|
|
4731
|
+
if (result.nullBeads > 0) {
|
|
4732
|
+
p.log.message(` - ${result.nullBeads} beads with NULL IDs`);
|
|
4733
|
+
}
|
|
4734
|
+
if (result.orphanedRecipients > 0) {
|
|
4735
|
+
p.log.message(` - ${result.orphanedRecipients} orphaned message_recipients`);
|
|
4736
|
+
}
|
|
4737
|
+
if (result.messagesWithoutRecipients > 0) {
|
|
4738
|
+
p.log.message(` - ${result.messagesWithoutRecipients} messages without recipients`);
|
|
4739
|
+
}
|
|
4740
|
+
if (result.expiredReservations > 0) {
|
|
4741
|
+
p.log.message(` - ${result.expiredReservations} expired unreleased reservations`);
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
if (result.totalCleaned === 0) {
|
|
4745
|
+
p.outro(green("✓ Database is clean! No records to delete."));
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
console.log();
|
|
4750
|
+
p.log.message(dim(`Total: ${result.totalCleaned} records ${dryRun ? "would be" : "will be"} cleaned`));
|
|
4751
|
+
console.log();
|
|
4752
|
+
|
|
4753
|
+
// If dry run, stop here
|
|
4754
|
+
if (dryRun) {
|
|
4755
|
+
p.outro(dim("Run without --dry-run to execute cleanup"));
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
// Confirm before actual deletion
|
|
4760
|
+
const confirmed = await p.confirm({
|
|
4761
|
+
message: `Delete ${result.totalCleaned} records?`,
|
|
4762
|
+
initialValue: false,
|
|
4763
|
+
});
|
|
4764
|
+
|
|
4765
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
4766
|
+
p.cancel("Cleanup cancelled");
|
|
4767
|
+
return;
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
// Execute cleanup
|
|
4771
|
+
const cleanupSpinner = p.spinner();
|
|
4772
|
+
cleanupSpinner.start("Cleaning database...");
|
|
4773
|
+
|
|
4774
|
+
await runDbRepair({ dryRun: false });
|
|
4775
|
+
|
|
4776
|
+
cleanupSpinner.stop("Cleanup complete");
|
|
4777
|
+
|
|
4778
|
+
p.outro(green(`✓ Successfully cleaned ${result.totalCleaned} records`));
|
|
4779
|
+
} catch (error) {
|
|
4780
|
+
s.stop("Error");
|
|
4781
|
+
p.log.error(error instanceof Error ? error.message : String(error));
|
|
4782
|
+
process.exit(1);
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4535
4786
|
async function db() {
|
|
4787
|
+
const args = process.argv.slice(3);
|
|
4788
|
+
|
|
4789
|
+
// Check for 'repair' subcommand
|
|
4790
|
+
if (args[0] === "repair") {
|
|
4791
|
+
await dbRepair();
|
|
4792
|
+
return;
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4536
4795
|
const projectPath = process.cwd();
|
|
4537
4796
|
const projectName = basename(projectPath);
|
|
4538
4797
|
const hash = hashLibSQLProjectPath(projectPath);
|
|
@@ -4801,6 +5060,7 @@ async function stats() {
|
|
|
4801
5060
|
let period = "7d"; // default to 7 days
|
|
4802
5061
|
let format: "text" | "json" = "text";
|
|
4803
5062
|
let showRegressions = false;
|
|
5063
|
+
let showRejections = false;
|
|
4804
5064
|
|
|
4805
5065
|
for (let i = 0; i < args.length; i++) {
|
|
4806
5066
|
if (args[i] === "--since" || args[i] === "-s") {
|
|
@@ -4810,6 +5070,8 @@ async function stats() {
|
|
|
4810
5070
|
format = "json";
|
|
4811
5071
|
} else if (args[i] === "--regressions") {
|
|
4812
5072
|
showRegressions = true;
|
|
5073
|
+
} else if (args[i] === "--rejections") {
|
|
5074
|
+
showRejections = true;
|
|
4813
5075
|
}
|
|
4814
5076
|
}
|
|
4815
5077
|
|
|
@@ -4966,6 +5228,42 @@ async function stats() {
|
|
|
4966
5228
|
console.log("✅ No eval regressions detected (>10% threshold)\n");
|
|
4967
5229
|
}
|
|
4968
5230
|
}
|
|
5231
|
+
} else if (showRejections) {
|
|
5232
|
+
// If --rejections flag, show rejection analytics
|
|
5233
|
+
const rejectionAnalytics = await getRejectionAnalytics(swarmMail);
|
|
5234
|
+
|
|
5235
|
+
if (format === "json") {
|
|
5236
|
+
console.log(JSON.stringify(rejectionAnalytics, null, 2));
|
|
5237
|
+
} else {
|
|
5238
|
+
console.log();
|
|
5239
|
+
const boxWidth = 61;
|
|
5240
|
+
const pad = (text: string) => text + " ".repeat(Math.max(0, boxWidth - text.length));
|
|
5241
|
+
|
|
5242
|
+
console.log("┌─────────────────────────────────────────────────────────────┐");
|
|
5243
|
+
console.log("│" + pad(" REJECTION ANALYSIS (last " + period + ")") + "│");
|
|
5244
|
+
console.log("├─────────────────────────────────────────────────────────────┤");
|
|
5245
|
+
console.log("│" + pad(" Total Reviews: " + rejectionAnalytics.totalReviews) + "│");
|
|
5246
|
+
|
|
5247
|
+
const rejectionRate = rejectionAnalytics.totalReviews > 0
|
|
5248
|
+
? (100 - rejectionAnalytics.approvalRate).toFixed(0)
|
|
5249
|
+
: "0";
|
|
5250
|
+
console.log("│" + pad(" Approved: " + rejectionAnalytics.approved + " (" + rejectionAnalytics.approvalRate.toFixed(0) + "%)") + "│");
|
|
5251
|
+
console.log("│" + pad(" Rejected: " + rejectionAnalytics.rejected + " (" + rejectionRate + "%)") + "│");
|
|
5252
|
+
console.log("│" + pad("") + "│");
|
|
5253
|
+
|
|
5254
|
+
if (rejectionAnalytics.topReasons.length > 0) {
|
|
5255
|
+
console.log("│" + pad(" Top Rejection Reasons:") + "│");
|
|
5256
|
+
for (const reason of rejectionAnalytics.topReasons) {
|
|
5257
|
+
const line = " ├── " + reason.category + ": " + reason.count + " (" + reason.percentage.toFixed(0) + "%)";
|
|
5258
|
+
console.log("│" + pad(line) + "│");
|
|
5259
|
+
}
|
|
5260
|
+
} else {
|
|
5261
|
+
console.log("│" + pad(" No rejections in this period") + "│");
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
console.log("└─────────────────────────────────────────────────────────────┘");
|
|
5265
|
+
console.log();
|
|
5266
|
+
}
|
|
4969
5267
|
} else {
|
|
4970
5268
|
// Normal stats output
|
|
4971
5269
|
if (format === "json") {
|