opencode-swarm-plugin 0.36.0 → 0.37.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/.hive/issues.jsonl +16 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +318 -318
- package/CHANGELOG.md +113 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +413 -179
- package/dist/compaction-hook.d.ts +54 -4
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +122 -17
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1278 -619
- package/dist/planning-guardrails.d.ts +121 -0
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.d.ts +9 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1283 -329
- package/dist/schemas/task.d.ts +0 -1
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +0 -8
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +0 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -6
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +38 -0
- package/evals/coordinator-session.eval.ts +154 -0
- package/evals/fixtures/coordinator-sessions.ts +328 -0
- package/evals/lib/data-loader.ts +69 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
- package/evals/scorers/coordinator-discipline.ts +315 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +303 -4
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +8 -1
- package/src/compaction-hook.ts +31 -21
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +163 -4
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/index.ts +68 -1
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/swarm-decompose.test.ts +195 -0
- package/src/swarm-decompose.ts +72 -1
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.integration.test.ts +24 -29
- package/src/swarm-review.ts +41 -0
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
|
|
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
|
-
|
|
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
|
|
2103
|
-
|
|
2104
|
-
|
|
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
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2167
|
+
if (p.isCancel(selectedCoordinator)) {
|
|
2168
|
+
p.cancel("Setup cancelled");
|
|
2169
|
+
process.exit(0);
|
|
2170
|
+
}
|
|
2171
|
+
coordinatorModel = selectedCoordinator;
|
|
2152
2172
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2215
|
+
if (p.isCancel(selectedWorker)) {
|
|
2216
|
+
p.cancel("Setup cancelled");
|
|
2217
|
+
process.exit(0);
|
|
2218
|
+
}
|
|
2219
|
+
workerModel = selectedWorker;
|
|
2199
2220
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
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
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2292
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,12 +2714,15 @@ async function help() {
|
|
|
2708
2714
|
console.log(magenta(" " + getRandomMessage()));
|
|
2709
2715
|
console.log(`
|
|
2710
2716
|
${cyan("Commands:")}
|
|
2711
|
-
swarm setup
|
|
2712
|
-
|
|
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
|
|
2716
2724
|
swarm migrate Migrate PGlite database to libSQL
|
|
2725
|
+
swarm cells List or get cells from database (replaces 'swarm tool hive_query')
|
|
2717
2726
|
swarm log View swarm logs with filtering
|
|
2718
2727
|
swarm update Update to latest version
|
|
2719
2728
|
swarm version Show version and banner
|
|
@@ -2725,6 +2734,14 @@ ${cyan("Tool Execution:")}
|
|
|
2725
2734
|
swarm tool <name> Execute tool with no args
|
|
2726
2735
|
swarm tool <name> --json '<args>' Execute tool with JSON args
|
|
2727
2736
|
|
|
2737
|
+
${cyan("Cell Management:")}
|
|
2738
|
+
swarm cells List cells from database (default: 20 most recent)
|
|
2739
|
+
swarm cells <id> Get single cell by ID or partial hash
|
|
2740
|
+
swarm cells --status <status> Filter by status (open, in_progress, closed, blocked)
|
|
2741
|
+
swarm cells --type <type> Filter by type (task, bug, feature, epic, chore)
|
|
2742
|
+
swarm cells --ready Show next ready (unblocked) cell
|
|
2743
|
+
swarm cells --json Raw JSON output (array, no wrapper)
|
|
2744
|
+
|
|
2728
2745
|
${cyan("Log Viewing:")}
|
|
2729
2746
|
swarm log Tail recent logs (last 50 lines)
|
|
2730
2747
|
swarm log <module> Filter by module (e.g., compaction)
|
|
@@ -3101,17 +3118,43 @@ interface LogLine {
|
|
|
3101
3118
|
time: string;
|
|
3102
3119
|
module: string;
|
|
3103
3120
|
msg: string;
|
|
3121
|
+
data?: Record<string, unknown>; // Extra structured data
|
|
3104
3122
|
}
|
|
3105
3123
|
|
|
3106
|
-
function parseLogLine(line: string): LogLine | null {
|
|
3124
|
+
function parseLogLine(line: string, sourceFile?: string): LogLine | null {
|
|
3107
3125
|
try {
|
|
3108
3126
|
const parsed = JSON.parse(line);
|
|
3109
|
-
if (
|
|
3127
|
+
if (parsed.time && parsed.msg) {
|
|
3128
|
+
// Handle both pino format (level: number) and plugin wrapper format (level: string)
|
|
3129
|
+
let level: number;
|
|
3130
|
+
if (typeof parsed.level === "number") {
|
|
3131
|
+
level = parsed.level;
|
|
3132
|
+
} else if (typeof parsed.level === "string") {
|
|
3133
|
+
level = levelNameToNumber(parsed.level);
|
|
3134
|
+
} else {
|
|
3135
|
+
level = 30; // default to info
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// Derive module from: explicit field, or source filename (e.g., "compaction.log" -> "compaction")
|
|
3139
|
+
let module = parsed.module;
|
|
3140
|
+
if (!module && sourceFile) {
|
|
3141
|
+
// Extract module from filename: "compaction.log" -> "compaction", "swarm.1log" -> "swarm"
|
|
3142
|
+
const match = sourceFile.match(/([^/]+?)(?:\.\d+)?\.?log$/);
|
|
3143
|
+
if (match) {
|
|
3144
|
+
module = match[1];
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// Extract extra data (everything except core fields)
|
|
3149
|
+
const { level: _l, time: _t, module: _m, msg: _msg, ...extraData } = parsed;
|
|
3150
|
+
const hasExtraData = Object.keys(extraData).length > 0;
|
|
3151
|
+
|
|
3110
3152
|
return {
|
|
3111
|
-
level
|
|
3153
|
+
level,
|
|
3112
3154
|
time: parsed.time,
|
|
3113
|
-
module:
|
|
3155
|
+
module: module || "unknown",
|
|
3114
3156
|
msg: parsed.msg,
|
|
3157
|
+
data: hasExtraData ? extraData : undefined,
|
|
3115
3158
|
};
|
|
3116
3159
|
}
|
|
3117
3160
|
} catch {
|
|
@@ -3164,36 +3207,218 @@ function parseDuration(duration: string): number | null {
|
|
|
3164
3207
|
return value * multipliers[unit];
|
|
3165
3208
|
}
|
|
3166
3209
|
|
|
3167
|
-
function formatLogLine(log: LogLine, useColor = true): string {
|
|
3210
|
+
function formatLogLine(log: LogLine, useColor = true, verbose = false): string {
|
|
3168
3211
|
const timestamp = new Date(log.time).toLocaleTimeString();
|
|
3169
3212
|
const levelName = levelToName(log.level);
|
|
3170
3213
|
const module = log.module.padEnd(12);
|
|
3171
3214
|
const levelStr = useColor ? levelToColor(log.level)(levelName) : levelName;
|
|
3172
3215
|
|
|
3173
|
-
|
|
3216
|
+
let output = `${timestamp} ${levelStr} ${module} ${log.msg}`;
|
|
3217
|
+
|
|
3218
|
+
// In verbose mode, pretty print the structured data
|
|
3219
|
+
if (verbose && log.data) {
|
|
3220
|
+
output += `\n${dim(JSON.stringify(log.data, null, 2))}`;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
return output;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
interface LogEntry {
|
|
3227
|
+
line: string;
|
|
3228
|
+
file: string;
|
|
3174
3229
|
}
|
|
3175
3230
|
|
|
3176
|
-
function readLogFiles(dir: string):
|
|
3231
|
+
function readLogFiles(dir: string): LogEntry[] {
|
|
3177
3232
|
if (!existsSync(dir)) return [];
|
|
3178
3233
|
|
|
3179
3234
|
const allFiles = readdirSync(dir);
|
|
3235
|
+
// Match both pino-roll format (*.1log, *.2log) AND plain *.log files
|
|
3180
3236
|
const logFiles = allFiles
|
|
3181
|
-
.filter((f: string) => /\.\d+log$/.test(f))
|
|
3237
|
+
.filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f))
|
|
3182
3238
|
.sort()
|
|
3183
3239
|
.map((f: string) => join(dir, f));
|
|
3184
3240
|
|
|
3185
|
-
const
|
|
3241
|
+
const entries: LogEntry[] = [];
|
|
3186
3242
|
for (const file of logFiles) {
|
|
3187
3243
|
try {
|
|
3188
3244
|
const content = readFileSync(file, "utf-8");
|
|
3189
3245
|
const fileLines = content.split("\n").filter((line: string) => line.trim());
|
|
3190
|
-
|
|
3246
|
+
for (const line of fileLines) {
|
|
3247
|
+
entries.push({ line, file });
|
|
3248
|
+
}
|
|
3191
3249
|
} catch {
|
|
3192
3250
|
// Skip unreadable files
|
|
3193
3251
|
}
|
|
3194
3252
|
}
|
|
3195
3253
|
|
|
3196
|
-
return
|
|
3254
|
+
return entries;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
/**
|
|
3258
|
+
* Format cells as table output
|
|
3259
|
+
*/
|
|
3260
|
+
function formatCellsTable(cells: Array<{
|
|
3261
|
+
id: string;
|
|
3262
|
+
title: string;
|
|
3263
|
+
status: string;
|
|
3264
|
+
priority: number;
|
|
3265
|
+
}>): string {
|
|
3266
|
+
if (cells.length === 0) {
|
|
3267
|
+
return "No cells found";
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
const rows = cells.map(c => ({
|
|
3271
|
+
id: c.id,
|
|
3272
|
+
title: c.title.length > 50 ? c.title.slice(0, 47) + "..." : c.title,
|
|
3273
|
+
status: c.status,
|
|
3274
|
+
priority: String(c.priority),
|
|
3275
|
+
}));
|
|
3276
|
+
|
|
3277
|
+
// Calculate column widths
|
|
3278
|
+
const widths = {
|
|
3279
|
+
id: Math.max(2, ...rows.map(r => r.id.length)),
|
|
3280
|
+
title: Math.max(5, ...rows.map(r => r.title.length)),
|
|
3281
|
+
status: Math.max(6, ...rows.map(r => r.status.length)),
|
|
3282
|
+
priority: Math.max(8, ...rows.map(r => r.priority.length)),
|
|
3283
|
+
};
|
|
3284
|
+
|
|
3285
|
+
// Build header
|
|
3286
|
+
const header = [
|
|
3287
|
+
"ID".padEnd(widths.id),
|
|
3288
|
+
"TITLE".padEnd(widths.title),
|
|
3289
|
+
"STATUS".padEnd(widths.status),
|
|
3290
|
+
"PRIORITY".padEnd(widths.priority),
|
|
3291
|
+
].join(" ");
|
|
3292
|
+
|
|
3293
|
+
const separator = "-".repeat(header.length);
|
|
3294
|
+
|
|
3295
|
+
// Build rows
|
|
3296
|
+
const bodyRows = rows.map(r =>
|
|
3297
|
+
[
|
|
3298
|
+
r.id.padEnd(widths.id),
|
|
3299
|
+
r.title.padEnd(widths.title),
|
|
3300
|
+
r.status.padEnd(widths.status),
|
|
3301
|
+
r.priority.padEnd(widths.priority),
|
|
3302
|
+
].join(" ")
|
|
3303
|
+
);
|
|
3304
|
+
|
|
3305
|
+
return [header, separator, ...bodyRows].join("\n");
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* List or get cells from database
|
|
3310
|
+
*/
|
|
3311
|
+
async function cells() {
|
|
3312
|
+
const args = process.argv.slice(3);
|
|
3313
|
+
|
|
3314
|
+
// Parse arguments
|
|
3315
|
+
let cellId: string | null = null;
|
|
3316
|
+
let statusFilter: string | null = null;
|
|
3317
|
+
let typeFilter: string | null = null;
|
|
3318
|
+
let readyOnly = false;
|
|
3319
|
+
let jsonOutput = false;
|
|
3320
|
+
|
|
3321
|
+
for (let i = 0; i < args.length; i++) {
|
|
3322
|
+
const arg = args[i];
|
|
3323
|
+
|
|
3324
|
+
if (arg === "--status" && i + 1 < args.length) {
|
|
3325
|
+
statusFilter = args[++i];
|
|
3326
|
+
if (!["open", "in_progress", "closed", "blocked"].includes(statusFilter)) {
|
|
3327
|
+
p.log.error(`Invalid status: ${statusFilter}`);
|
|
3328
|
+
p.log.message(dim(" Valid statuses: open, in_progress, closed, blocked"));
|
|
3329
|
+
process.exit(1);
|
|
3330
|
+
}
|
|
3331
|
+
} else if (arg === "--type" && i + 1 < args.length) {
|
|
3332
|
+
typeFilter = args[++i];
|
|
3333
|
+
if (!["task", "bug", "feature", "epic", "chore"].includes(typeFilter)) {
|
|
3334
|
+
p.log.error(`Invalid type: ${typeFilter}`);
|
|
3335
|
+
p.log.message(dim(" Valid types: task, bug, feature, epic, chore"));
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
} else if (arg === "--ready") {
|
|
3339
|
+
readyOnly = true;
|
|
3340
|
+
} else if (arg === "--json") {
|
|
3341
|
+
jsonOutput = true;
|
|
3342
|
+
} else if (!arg.startsWith("--") && !arg.startsWith("-")) {
|
|
3343
|
+
// Positional arg = cell ID (full or partial)
|
|
3344
|
+
cellId = arg;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// Get adapter using swarm-mail
|
|
3349
|
+
const projectPath = process.cwd();
|
|
3350
|
+
const { getSwarmMailLibSQL, createHiveAdapter, resolvePartialId } = await import("swarm-mail");
|
|
3351
|
+
|
|
3352
|
+
try {
|
|
3353
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
3354
|
+
const db = await swarmMail.getDatabase();
|
|
3355
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
3356
|
+
|
|
3357
|
+
// Run migrations to ensure schema exists
|
|
3358
|
+
await adapter.runMigrations();
|
|
3359
|
+
|
|
3360
|
+
// If cell ID provided, get single cell
|
|
3361
|
+
if (cellId) {
|
|
3362
|
+
// Resolve partial ID to full ID
|
|
3363
|
+
const fullId = await resolvePartialId(adapter, projectPath, cellId) || cellId;
|
|
3364
|
+
const cell = await adapter.getCell(projectPath, fullId);
|
|
3365
|
+
|
|
3366
|
+
if (!cell) {
|
|
3367
|
+
p.log.error(`Cell not found: ${cellId}`);
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
if (jsonOutput) {
|
|
3372
|
+
console.log(JSON.stringify([cell], null, 2));
|
|
3373
|
+
} else {
|
|
3374
|
+
const table = formatCellsTable([{
|
|
3375
|
+
id: cell.id,
|
|
3376
|
+
title: cell.title,
|
|
3377
|
+
status: cell.status,
|
|
3378
|
+
priority: cell.priority,
|
|
3379
|
+
}]);
|
|
3380
|
+
console.log(table);
|
|
3381
|
+
}
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// Otherwise query cells
|
|
3386
|
+
let cells: Array<{ id: string; title: string; status: string; priority: number }>;
|
|
3387
|
+
|
|
3388
|
+
if (readyOnly) {
|
|
3389
|
+
const readyCell = await adapter.getNextReadyCell(projectPath);
|
|
3390
|
+
cells = readyCell ? [{
|
|
3391
|
+
id: readyCell.id,
|
|
3392
|
+
title: readyCell.title,
|
|
3393
|
+
status: readyCell.status,
|
|
3394
|
+
priority: readyCell.priority,
|
|
3395
|
+
}] : [];
|
|
3396
|
+
} else {
|
|
3397
|
+
const queriedCells = await adapter.queryCells(projectPath, {
|
|
3398
|
+
status: statusFilter as any || undefined,
|
|
3399
|
+
type: typeFilter as any || undefined,
|
|
3400
|
+
limit: 20,
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
cells = queriedCells.map(c => ({
|
|
3404
|
+
id: c.id,
|
|
3405
|
+
title: c.title,
|
|
3406
|
+
status: c.status,
|
|
3407
|
+
priority: c.priority,
|
|
3408
|
+
}));
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
if (jsonOutput) {
|
|
3412
|
+
console.log(JSON.stringify(cells, null, 2));
|
|
3413
|
+
} else {
|
|
3414
|
+
const table = formatCellsTable(cells);
|
|
3415
|
+
console.log(table);
|
|
3416
|
+
}
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3419
|
+
p.log.error(`Failed to query cells: ${message}`);
|
|
3420
|
+
process.exit(1);
|
|
3421
|
+
}
|
|
3197
3422
|
}
|
|
3198
3423
|
|
|
3199
3424
|
async function logs() {
|
|
@@ -3207,6 +3432,7 @@ async function logs() {
|
|
|
3207
3432
|
let limit = 50;
|
|
3208
3433
|
let watchMode = false;
|
|
3209
3434
|
let pollInterval = 1000; // 1 second default
|
|
3435
|
+
let verbose = false;
|
|
3210
3436
|
|
|
3211
3437
|
for (let i = 0; i < args.length; i++) {
|
|
3212
3438
|
const arg = args[i];
|
|
@@ -3231,6 +3457,8 @@ async function logs() {
|
|
|
3231
3457
|
}
|
|
3232
3458
|
} else if (arg === "--watch" || arg === "-w") {
|
|
3233
3459
|
watchMode = true;
|
|
3460
|
+
} else if (arg === "--verbose" || arg === "-v") {
|
|
3461
|
+
verbose = true;
|
|
3234
3462
|
} else if (arg === "--interval" && i + 1 < args.length) {
|
|
3235
3463
|
pollInterval = parseInt(args[++i], 10);
|
|
3236
3464
|
if (isNaN(pollInterval) || pollInterval < 100) {
|
|
@@ -3290,7 +3518,7 @@ async function logs() {
|
|
|
3290
3518
|
// Initialize positions from current file sizes
|
|
3291
3519
|
const initializePositions = () => {
|
|
3292
3520
|
if (!existsSync(logsDir)) return;
|
|
3293
|
-
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
|
|
3521
|
+
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
|
|
3294
3522
|
for (const file of files) {
|
|
3295
3523
|
const filePath = join(logsDir, file);
|
|
3296
3524
|
try {
|
|
@@ -3327,14 +3555,14 @@ async function logs() {
|
|
|
3327
3555
|
};
|
|
3328
3556
|
|
|
3329
3557
|
// Print initial logs (last N lines)
|
|
3330
|
-
const
|
|
3331
|
-
let logs: LogLine[] =
|
|
3332
|
-
.map(parseLogLine)
|
|
3558
|
+
const rawEntries = readLogFiles(logsDir);
|
|
3559
|
+
let logs: LogLine[] = rawEntries
|
|
3560
|
+
.map(entry => parseLogLine(entry.line, entry.file))
|
|
3333
3561
|
.filter((log): log is LogLine => log !== null);
|
|
3334
3562
|
logs = filterLogs(logs).slice(-limit);
|
|
3335
3563
|
|
|
3336
3564
|
for (const log of logs) {
|
|
3337
|
-
console.log(formatLogLine(log));
|
|
3565
|
+
console.log(formatLogLine(log, true, verbose));
|
|
3338
3566
|
}
|
|
3339
3567
|
|
|
3340
3568
|
// Initialize positions after printing initial logs
|
|
@@ -3344,18 +3572,18 @@ async function logs() {
|
|
|
3344
3572
|
const pollForNewLogs = () => {
|
|
3345
3573
|
if (!existsSync(logsDir)) return;
|
|
3346
3574
|
|
|
3347
|
-
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
|
|
3575
|
+
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
|
|
3348
3576
|
|
|
3349
3577
|
for (const file of files) {
|
|
3350
3578
|
const filePath = join(logsDir, file);
|
|
3351
3579
|
const newLines = readNewLines(filePath);
|
|
3352
3580
|
|
|
3353
3581
|
for (const line of newLines) {
|
|
3354
|
-
const parsed = parseLogLine(line);
|
|
3582
|
+
const parsed = parseLogLine(line, filePath);
|
|
3355
3583
|
if (parsed) {
|
|
3356
3584
|
const filtered = filterLogs([parsed]);
|
|
3357
3585
|
if (filtered.length > 0) {
|
|
3358
|
-
console.log(formatLogLine(filtered[0]));
|
|
3586
|
+
console.log(formatLogLine(filtered[0], true, verbose));
|
|
3359
3587
|
}
|
|
3360
3588
|
}
|
|
3361
3589
|
}
|
|
@@ -3381,11 +3609,11 @@ async function logs() {
|
|
|
3381
3609
|
}
|
|
3382
3610
|
|
|
3383
3611
|
// Non-watch mode - one-shot output
|
|
3384
|
-
const
|
|
3612
|
+
const rawEntries = readLogFiles(logsDir);
|
|
3385
3613
|
|
|
3386
3614
|
// Parse and filter
|
|
3387
|
-
let logs: LogLine[] =
|
|
3388
|
-
.map(parseLogLine)
|
|
3615
|
+
let logs: LogLine[] = rawEntries
|
|
3616
|
+
.map(entry => parseLogLine(entry.line, entry.file))
|
|
3389
3617
|
.filter((log): log is LogLine => log !== null);
|
|
3390
3618
|
|
|
3391
3619
|
logs = filterLogs(logs);
|
|
@@ -3410,7 +3638,7 @@ async function logs() {
|
|
|
3410
3638
|
console.log();
|
|
3411
3639
|
|
|
3412
3640
|
for (const log of logs) {
|
|
3413
|
-
console.log(formatLogLine(log));
|
|
3641
|
+
console.log(formatLogLine(log, true, verbose));
|
|
3414
3642
|
}
|
|
3415
3643
|
console.log();
|
|
3416
3644
|
}
|
|
@@ -3522,9 +3750,12 @@ async function db() {
|
|
|
3522
3750
|
const command = process.argv[2];
|
|
3523
3751
|
|
|
3524
3752
|
switch (command) {
|
|
3525
|
-
case "setup":
|
|
3526
|
-
|
|
3753
|
+
case "setup": {
|
|
3754
|
+
const reinstallFlag = process.argv.includes("--reinstall") || process.argv.includes("-r");
|
|
3755
|
+
const yesFlag = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3756
|
+
await setup(reinstallFlag || yesFlag, yesFlag);
|
|
3527
3757
|
break;
|
|
3758
|
+
}
|
|
3528
3759
|
case "doctor":
|
|
3529
3760
|
await doctor();
|
|
3530
3761
|
break;
|
|
@@ -3559,6 +3790,9 @@ switch (command) {
|
|
|
3559
3790
|
case "db":
|
|
3560
3791
|
await db();
|
|
3561
3792
|
break;
|
|
3793
|
+
case "cells":
|
|
3794
|
+
await cells();
|
|
3795
|
+
break;
|
|
3562
3796
|
case "log":
|
|
3563
3797
|
case "logs":
|
|
3564
3798
|
await logs();
|