opencode-swarm-plugin 0.49.0 → 0.51.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/bin/commands/session.ts +377 -0
- package/bin/commands/tree.test.ts +71 -0
- package/bin/commands/tree.ts +131 -0
- package/bin/swarm.ts +25 -0
- package/dist/bin/swarm.js +110588 -100200
- package/dist/hive.d.ts +48 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/hive.js +59 -1
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35762 -31723
- package/dist/plugin.js +35396 -31359
- package/dist/swarm-adversarial-review.d.ts +104 -0
- package/dist/swarm-adversarial-review.d.ts.map +1 -0
- package/dist/swarm-insights.d.ts +23 -0
- package/dist/swarm-insights.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-prompts.js +22992 -19079
- package/dist/swarm.d.ts +15 -1
- package/dist/swarm.d.ts.map +1 -1
- package/dist/tools/ubs/index.d.ts +28 -0
- package/dist/tools/ubs/index.d.ts.map +1 -0
- package/dist/tools/ubs/patterns/stub-patterns.d.ts +43 -0
- package/dist/tools/ubs/patterns/stub-patterns.d.ts.map +1 -0
- package/dist/tools/ubs/scanner.d.ts +49 -0
- package/dist/tools/ubs/scanner.d.ts.map +1 -0
- package/dist/tools/ubs/types.d.ts +46 -0
- package/dist/tools/ubs/types.d.ts.map +1 -0
- package/dist/utils/tree-renderer.d.ts +61 -0
- package/dist/utils/tree-renderer.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Command - Chainlink-inspired session management
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* swarm session start [--cell <id>] [--json]
|
|
6
|
+
* swarm session end [--notes "..."] [--json]
|
|
7
|
+
* swarm session status [--json]
|
|
8
|
+
* swarm session history [--limit <n>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* Inspired by: https://github.com/dollspace-gay/chainlink
|
|
11
|
+
* Credit: @dollspace-gay for the session handoff pattern
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as p from "@clack/prompts";
|
|
15
|
+
import {
|
|
16
|
+
getSwarmMailLibSQL,
|
|
17
|
+
createHiveAdapter,
|
|
18
|
+
} from "swarm-mail";
|
|
19
|
+
|
|
20
|
+
// Color utilities (inline, same as swarm.ts)
|
|
21
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
22
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
23
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
24
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Main session command handler
|
|
28
|
+
*/
|
|
29
|
+
export async function session() {
|
|
30
|
+
const args = process.argv.slice(3);
|
|
31
|
+
const subcommand = args[0];
|
|
32
|
+
|
|
33
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
34
|
+
showHelp();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
switch (subcommand) {
|
|
39
|
+
case "start":
|
|
40
|
+
await startSession(args.slice(1));
|
|
41
|
+
break;
|
|
42
|
+
case "end":
|
|
43
|
+
await endSession(args.slice(1));
|
|
44
|
+
break;
|
|
45
|
+
case "status":
|
|
46
|
+
await sessionStatus(args.slice(1));
|
|
47
|
+
break;
|
|
48
|
+
case "history":
|
|
49
|
+
await sessionHistory(args.slice(1));
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
p.log.error(`Unknown session subcommand: ${subcommand}`);
|
|
53
|
+
showHelp();
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start a new session
|
|
60
|
+
*/
|
|
61
|
+
async function startSession(args: string[]) {
|
|
62
|
+
// Parse arguments
|
|
63
|
+
let cellId: string | null = null;
|
|
64
|
+
let jsonOutput = false;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
68
|
+
|
|
69
|
+
if (arg === "--cell" && i + 1 < args.length) {
|
|
70
|
+
cellId = args[++i];
|
|
71
|
+
} else if (arg === "--json") {
|
|
72
|
+
jsonOutput = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const projectPath = process.cwd();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
80
|
+
const db = await swarmMail.getDatabase();
|
|
81
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
82
|
+
|
|
83
|
+
// Run migrations to ensure schema exists
|
|
84
|
+
await adapter.runMigrations();
|
|
85
|
+
|
|
86
|
+
// Start session
|
|
87
|
+
const session = await adapter.startSession(projectPath, {
|
|
88
|
+
active_cell_id: cellId || undefined,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (jsonOutput) {
|
|
92
|
+
console.log(JSON.stringify(session, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pretty output
|
|
97
|
+
p.log.success(green(`Session started: ${session.id}`));
|
|
98
|
+
|
|
99
|
+
if (session.active_cell_id) {
|
|
100
|
+
p.log.message(dim(` Active cell: ${session.active_cell_id}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (session.previous_handoff_notes) {
|
|
104
|
+
p.log.message(
|
|
105
|
+
`\n${yellow("Previous session notes:")}
|
|
106
|
+
${dim(session.previous_handoff_notes)}`,
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
p.log.message(dim("\n (No previous session notes)"));
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
p.log.error(`Failed to start session: ${message}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* End the current session
|
|
120
|
+
*/
|
|
121
|
+
async function endSession(args: string[]) {
|
|
122
|
+
// Parse arguments
|
|
123
|
+
let notes: string | null = null;
|
|
124
|
+
let jsonOutput = false;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < args.length; i++) {
|
|
127
|
+
const arg = args[i];
|
|
128
|
+
|
|
129
|
+
if (arg === "--notes" && i + 1 < args.length) {
|
|
130
|
+
notes = args[++i];
|
|
131
|
+
} else if (arg === "--json") {
|
|
132
|
+
jsonOutput = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const projectPath = process.cwd();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
140
|
+
const db = await swarmMail.getDatabase();
|
|
141
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
142
|
+
|
|
143
|
+
// Run migrations
|
|
144
|
+
await adapter.runMigrations();
|
|
145
|
+
|
|
146
|
+
// Get current session
|
|
147
|
+
const currentSession = await adapter.getCurrentSession(projectPath);
|
|
148
|
+
|
|
149
|
+
if (!currentSession) {
|
|
150
|
+
p.log.error("No active session to end");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// End session
|
|
155
|
+
const endedSession = await adapter.endSession(
|
|
156
|
+
projectPath,
|
|
157
|
+
currentSession.id,
|
|
158
|
+
{
|
|
159
|
+
handoff_notes: notes || undefined,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (jsonOutput) {
|
|
164
|
+
console.log(JSON.stringify(endedSession, null, 2));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pretty output
|
|
169
|
+
p.log.success(green(`Session ended: ${endedSession.id}`));
|
|
170
|
+
|
|
171
|
+
if (endedSession.handoff_notes) {
|
|
172
|
+
p.log.message(
|
|
173
|
+
dim(` Handoff notes saved for next session:
|
|
174
|
+
${endedSession.handoff_notes}`),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const duration = endedSession.ended_at! - endedSession.started_at;
|
|
179
|
+
const durationStr = formatDuration(duration);
|
|
180
|
+
p.log.message(dim(` Duration: ${durationStr}`));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
p.log.error(`Failed to end session: ${message}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Show current session status
|
|
190
|
+
*/
|
|
191
|
+
async function sessionStatus(args: string[]) {
|
|
192
|
+
const jsonOutput = args.includes("--json");
|
|
193
|
+
const projectPath = process.cwd();
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
197
|
+
const db = await swarmMail.getDatabase();
|
|
198
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
199
|
+
|
|
200
|
+
// Run migrations
|
|
201
|
+
await adapter.runMigrations();
|
|
202
|
+
|
|
203
|
+
// Get current session
|
|
204
|
+
const currentSession = await adapter.getCurrentSession(projectPath);
|
|
205
|
+
|
|
206
|
+
if (!currentSession) {
|
|
207
|
+
if (jsonOutput) {
|
|
208
|
+
console.log(JSON.stringify({ active: false }, null, 2));
|
|
209
|
+
} else {
|
|
210
|
+
p.log.message("No active session");
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (jsonOutput) {
|
|
216
|
+
console.log(
|
|
217
|
+
JSON.stringify({ active: true, session: currentSession }, null, 2),
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Pretty output
|
|
223
|
+
p.log.message(green("Active session:"));
|
|
224
|
+
p.log.message(dim(` ID: ${currentSession.id}`));
|
|
225
|
+
p.log.message(
|
|
226
|
+
dim(` Started: ${new Date(currentSession.started_at).toLocaleString()}`),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (currentSession.active_cell_id) {
|
|
230
|
+
p.log.message(dim(` Active cell: ${currentSession.active_cell_id}`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const elapsed = Date.now() - currentSession.started_at;
|
|
234
|
+
p.log.message(dim(` Elapsed: ${formatDuration(elapsed)}`));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
p.log.error(`Failed to get session status: ${message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Show session history
|
|
244
|
+
*/
|
|
245
|
+
async function sessionHistory(args: string[]) {
|
|
246
|
+
// Parse arguments
|
|
247
|
+
let limit = 10;
|
|
248
|
+
let jsonOutput = false;
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < args.length; i++) {
|
|
251
|
+
const arg = args[i];
|
|
252
|
+
|
|
253
|
+
if (arg === "--limit" && i + 1 < args.length) {
|
|
254
|
+
limit = parseInt(args[++i], 10);
|
|
255
|
+
if (isNaN(limit) || limit <= 0) {
|
|
256
|
+
p.log.error(`Invalid limit: ${args[i]}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
} else if (arg === "--json") {
|
|
260
|
+
jsonOutput = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const projectPath = process.cwd();
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
268
|
+
const db = await swarmMail.getDatabase();
|
|
269
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
270
|
+
|
|
271
|
+
// Run migrations
|
|
272
|
+
await adapter.runMigrations();
|
|
273
|
+
|
|
274
|
+
// Get history
|
|
275
|
+
const sessions = await adapter.getSessionHistory(projectPath, { limit });
|
|
276
|
+
|
|
277
|
+
if (jsonOutput) {
|
|
278
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Pretty output
|
|
283
|
+
if (sessions.length === 0) {
|
|
284
|
+
p.log.message("No session history");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
p.log.message(cyan(`Recent sessions (${sessions.length}):\n`));
|
|
289
|
+
|
|
290
|
+
for (const session of sessions) {
|
|
291
|
+
const startedAt = new Date(session.started_at).toLocaleString();
|
|
292
|
+
const status = session.ended_at ? "ended" : green("active");
|
|
293
|
+
|
|
294
|
+
p.log.message(` ${dim(`#${session.id}`)} - ${status}`);
|
|
295
|
+
p.log.message(dim(` Started: ${startedAt}`));
|
|
296
|
+
|
|
297
|
+
if (session.ended_at) {
|
|
298
|
+
const duration = session.ended_at - session.started_at;
|
|
299
|
+
p.log.message(dim(` Duration: ${formatDuration(duration)}`));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (session.active_cell_id) {
|
|
303
|
+
p.log.message(dim(` Cell: ${session.active_cell_id}`));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (session.handoff_notes) {
|
|
307
|
+
p.log.message(dim(` Notes: ${session.handoff_notes.slice(0, 60)}...`));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
p.log.message(""); // Blank line
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
314
|
+
p.log.error(`Failed to get session history: ${message}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Format duration in human-readable format
|
|
321
|
+
*/
|
|
322
|
+
function formatDuration(ms: number): string {
|
|
323
|
+
const seconds = Math.floor(ms / 1000);
|
|
324
|
+
const minutes = Math.floor(seconds / 60);
|
|
325
|
+
const hours = Math.floor(minutes / 60);
|
|
326
|
+
const days = Math.floor(hours / 24);
|
|
327
|
+
|
|
328
|
+
if (days > 0) {
|
|
329
|
+
return `${days}d ${hours % 24}h`;
|
|
330
|
+
}
|
|
331
|
+
if (hours > 0) {
|
|
332
|
+
return `${hours}h ${minutes % 60}m`;
|
|
333
|
+
}
|
|
334
|
+
if (minutes > 0) {
|
|
335
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
336
|
+
}
|
|
337
|
+
return `${seconds}s`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Show help
|
|
342
|
+
*/
|
|
343
|
+
function showHelp() {
|
|
344
|
+
console.log(`
|
|
345
|
+
${cyan("swarm session")} - Manage work sessions with handoff notes
|
|
346
|
+
|
|
347
|
+
${cyan("Commands:")}
|
|
348
|
+
swarm session start [--cell <id>] Start a new session
|
|
349
|
+
swarm session end [--notes "..."] End current session with optional handoff notes
|
|
350
|
+
swarm session status Show current session info
|
|
351
|
+
swarm session history [--limit n] Show session history (default: 10)
|
|
352
|
+
|
|
353
|
+
${cyan("Options:")}
|
|
354
|
+
--json Output as JSON
|
|
355
|
+
--cell <id> Set active cell when starting session
|
|
356
|
+
--notes "..." Save handoff notes for next session
|
|
357
|
+
--limit <n> Limit history results
|
|
358
|
+
|
|
359
|
+
${cyan("Examples:")}
|
|
360
|
+
${dim("# Start session")}
|
|
361
|
+
swarm session start
|
|
362
|
+
|
|
363
|
+
${dim("# Start session with active cell")}
|
|
364
|
+
swarm session start --cell opencode-swarm-monorepo-lf2p4u-mk2uv4j7u3o
|
|
365
|
+
|
|
366
|
+
${dim("# End session with handoff notes")}
|
|
367
|
+
swarm session end --notes "Completed auth flow. Next: add error handling"
|
|
368
|
+
|
|
369
|
+
${dim("# Check current session")}
|
|
370
|
+
swarm session status
|
|
371
|
+
|
|
372
|
+
${dim("# View recent sessions")}
|
|
373
|
+
swarm session history --limit 5
|
|
374
|
+
|
|
375
|
+
${dim("Inspired by Chainlink: https://github.com/dollspace-gay/chainlink")}
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for tree command
|
|
3
|
+
*
|
|
4
|
+
* Integration tests for CLI command that renders cell hierarchies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
|
8
|
+
import { createInMemorySwarmMailLibSQL, createHiveAdapter } from "swarm-mail";
|
|
9
|
+
import type { SwarmMailAdapter } from "swarm-mail";
|
|
10
|
+
|
|
11
|
+
describe("tree command integration", () => {
|
|
12
|
+
let swarmMail: SwarmMailAdapter;
|
|
13
|
+
const projectPath = "/tmp/test-tree-project";
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
swarmMail = await createInMemorySwarmMailLibSQL("test-tree");
|
|
17
|
+
const db = await swarmMail.getDatabase();
|
|
18
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
19
|
+
await adapter.runMigrations();
|
|
20
|
+
|
|
21
|
+
// Create test data: epic with children
|
|
22
|
+
const epic = await adapter.createCell(projectPath, {
|
|
23
|
+
title: "Test Epic",
|
|
24
|
+
type: "epic",
|
|
25
|
+
priority: 0,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await adapter.createCell(projectPath, {
|
|
29
|
+
title: "Task 1",
|
|
30
|
+
type: "task",
|
|
31
|
+
priority: 1,
|
|
32
|
+
parent_id: epic.id,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await adapter.createCell(projectPath, {
|
|
36
|
+
title: "Task 2",
|
|
37
|
+
type: "task",
|
|
38
|
+
priority: 2,
|
|
39
|
+
parent_id: epic.id,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create standalone task
|
|
43
|
+
await adapter.createCell(projectPath, {
|
|
44
|
+
title: "Standalone Task",
|
|
45
|
+
type: "task",
|
|
46
|
+
priority: 1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await swarmMail.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("queries cells and builds tree structure", async () => {
|
|
55
|
+
const db = await swarmMail.getDatabase();
|
|
56
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
57
|
+
|
|
58
|
+
const cells = await adapter.queryCells(projectPath, {
|
|
59
|
+
limit: 100,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
63
|
+
|
|
64
|
+
// Verify we have epic and children
|
|
65
|
+
const epic = cells.find((c) => c.type === "epic");
|
|
66
|
+
expect(epic).toBeDefined();
|
|
67
|
+
|
|
68
|
+
const children = cells.filter((c) => c.parent_id === epic?.id);
|
|
69
|
+
expect(children.length).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tree command for visualizing cell hierarchies
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Chainlink's tree visualization.
|
|
5
|
+
* Credit: https://github.com/dollspace-gay/chainlink
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* swarm tree - Show all cells as tree
|
|
9
|
+
* swarm tree --status open - Filter by status
|
|
10
|
+
* swarm tree --epic <id> - Show specific epic subtree
|
|
11
|
+
* swarm tree --json - JSON output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as p from "@clack/prompts";
|
|
15
|
+
import { getSwarmMailLibSQL, createHiveAdapter } from "swarm-mail";
|
|
16
|
+
import type { Cell } from "swarm-mail";
|
|
17
|
+
import {
|
|
18
|
+
buildTreeStructure,
|
|
19
|
+
renderTree,
|
|
20
|
+
} from "../../src/utils/tree-renderer.js";
|
|
21
|
+
|
|
22
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
23
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
24
|
+
|
|
25
|
+
export interface TreeOptions {
|
|
26
|
+
status?: string;
|
|
27
|
+
epic?: string;
|
|
28
|
+
json?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse tree command arguments
|
|
33
|
+
*/
|
|
34
|
+
export function parseTreeArgs(args: string[]): TreeOptions {
|
|
35
|
+
const options: TreeOptions = {};
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
|
|
40
|
+
if (arg === "--status" && i + 1 < args.length) {
|
|
41
|
+
options.status = args[++i];
|
|
42
|
+
} else if (arg === "--epic" && i + 1 < args.length) {
|
|
43
|
+
options.epic = args[++i];
|
|
44
|
+
} else if (arg === "--json") {
|
|
45
|
+
options.json = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute tree command
|
|
54
|
+
*/
|
|
55
|
+
export async function tree(args: string[] = []) {
|
|
56
|
+
const options = parseTreeArgs(args);
|
|
57
|
+
|
|
58
|
+
const projectPath = process.cwd();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
62
|
+
const db = await swarmMail.getDatabase();
|
|
63
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
64
|
+
|
|
65
|
+
// Run migrations to ensure schema exists
|
|
66
|
+
await adapter.runMigrations();
|
|
67
|
+
|
|
68
|
+
// Query cells with filters
|
|
69
|
+
let cells: Cell[];
|
|
70
|
+
|
|
71
|
+
if (options.epic) {
|
|
72
|
+
// Get epic and its descendants
|
|
73
|
+
const epic = await adapter.getCell(projectPath, options.epic);
|
|
74
|
+
if (!epic) {
|
|
75
|
+
p.log.error(`Epic not found: ${options.epic}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get all cells that are children of this epic
|
|
80
|
+
const allCells = await adapter.queryCells(projectPath, {
|
|
81
|
+
limit: 1000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Filter to epic and its descendants
|
|
85
|
+
cells = [epic];
|
|
86
|
+
const childIds = new Set([epic.id]);
|
|
87
|
+
|
|
88
|
+
// Iteratively find all descendants
|
|
89
|
+
let foundNew = true;
|
|
90
|
+
while (foundNew) {
|
|
91
|
+
foundNew = false;
|
|
92
|
+
for (const cell of allCells) {
|
|
93
|
+
if (
|
|
94
|
+
cell.parent_id &&
|
|
95
|
+
childIds.has(cell.parent_id) &&
|
|
96
|
+
!childIds.has(cell.id)
|
|
97
|
+
) {
|
|
98
|
+
cells.push(cell);
|
|
99
|
+
childIds.add(cell.id);
|
|
100
|
+
foundNew = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Get all cells
|
|
106
|
+
cells = await adapter.queryCells(projectPath, {
|
|
107
|
+
status: options.status as any,
|
|
108
|
+
limit: 1000,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (cells.length === 0) {
|
|
113
|
+
p.log.message(dim("No cells found"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Output
|
|
118
|
+
if (options.json) {
|
|
119
|
+
const tree = buildTreeStructure(cells);
|
|
120
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
121
|
+
} else {
|
|
122
|
+
const tree = buildTreeStructure(cells);
|
|
123
|
+
const output = renderTree(tree);
|
|
124
|
+
console.log(output);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
p.log.error(`Failed to render tree: ${message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/bin/swarm.ts
CHANGED
|
@@ -82,6 +82,8 @@ import {
|
|
|
82
82
|
exportToCSV,
|
|
83
83
|
exportToJSON,
|
|
84
84
|
} from "../src/export-tools.js";
|
|
85
|
+
import { tree } from "./commands/tree.js";
|
|
86
|
+
import { session } from "./commands/session.js";
|
|
85
87
|
import {
|
|
86
88
|
querySwarmHistory,
|
|
87
89
|
formatSwarmHistory,
|
|
@@ -3283,6 +3285,11 @@ async function exportEvents() {
|
|
|
3283
3285
|
}
|
|
3284
3286
|
}
|
|
3285
3287
|
|
|
3288
|
+
async function treeCommand() {
|
|
3289
|
+
const args = process.argv.slice(3);
|
|
3290
|
+
await tree(args);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3286
3293
|
async function help() {
|
|
3287
3294
|
console.log(yellow(BANNER));
|
|
3288
3295
|
console.log(dim(" " + TAGLINE + " v" + VERSION));
|
|
@@ -3313,6 +3320,8 @@ ${cyan("Commands:")}
|
|
|
3313
3320
|
swarm dashboard Live terminal UI with worker status (--epic, --refresh)
|
|
3314
3321
|
swarm replay Event replay with timing (--speed, --type, --agent, --since, --until)
|
|
3315
3322
|
swarm export Export events (--format otlp/csv/json, --epic, --output)
|
|
3323
|
+
swarm tree Visualize cell hierarchy as ASCII tree (--status, --epic, --json)
|
|
3324
|
+
swarm session Manage work sessions with handoff notes (start, end, status, history)
|
|
3316
3325
|
swarm update Update to latest version
|
|
3317
3326
|
swarm version Show version and banner
|
|
3318
3327
|
swarm tool Execute a tool (for plugin wrapper)
|
|
@@ -3394,6 +3403,16 @@ ${cyan("Observability Commands:")}
|
|
|
3394
3403
|
swarm export --format csv Export as CSV
|
|
3395
3404
|
swarm export --epic <id> Export specific epic only
|
|
3396
3405
|
swarm export --output <file> Write to file instead of stdout
|
|
3406
|
+
swarm tree Show all cells as tree
|
|
3407
|
+
swarm tree --status open Show only open cells
|
|
3408
|
+
swarm tree --epic <id> Show specific epic subtree
|
|
3409
|
+
swarm tree --json Output as JSON
|
|
3410
|
+
|
|
3411
|
+
${cyan("Session Management (Chainlink-inspired):")}
|
|
3412
|
+
swarm session start [--cell <id>] Start new session (shows previous handoff notes)
|
|
3413
|
+
swarm session end [--notes "..."] End session with handoff notes for next session
|
|
3414
|
+
swarm session status Show current session info
|
|
3415
|
+
swarm session history [--limit n] Show session history (default: 10)
|
|
3397
3416
|
|
|
3398
3417
|
${cyan("Usage in OpenCode:")}
|
|
3399
3418
|
/swarm "Add user authentication with OAuth"
|
|
@@ -6220,6 +6239,12 @@ switch (command) {
|
|
|
6220
6239
|
case "export":
|
|
6221
6240
|
await exportEvents();
|
|
6222
6241
|
break;
|
|
6242
|
+
case "tree":
|
|
6243
|
+
await treeCommand();
|
|
6244
|
+
break;
|
|
6245
|
+
case "session":
|
|
6246
|
+
await session();
|
|
6247
|
+
break;
|
|
6223
6248
|
case "version":
|
|
6224
6249
|
case "--version":
|
|
6225
6250
|
case "-v":
|