opencode-swarm-plugin 0.51.0 → 0.54.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/log.test.ts +117 -0
- package/bin/commands/log.ts +362 -0
- package/bin/swarm.ts +53 -2
- package/dist/bin/swarm.js +277625 -42866
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/examples/plugin-wrapper-template.ts +271 -23
- package/dist/index.js +1147 -484
- package/dist/learning.d.ts +30 -0
- package/dist/learning.d.ts.map +1 -1
- package/dist/plugin.js +1147 -155
- package/dist/swarm-insights.d.ts +54 -0
- package/dist/swarm-insights.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.js +1089 -3
- package/dist/swarm-strategies.d.ts.map +1 -1
- package/examples/plugin-wrapper-template.ts +271 -23
- package/package.json +2 -2
- package/dist/tools/ubs/index.d.ts +0 -28
- package/dist/tools/ubs/index.d.ts.map +0 -1
- package/dist/tools/ubs/patterns/stub-patterns.d.ts +0 -43
- package/dist/tools/ubs/patterns/stub-patterns.d.ts.map +0 -1
- package/dist/tools/ubs/scanner.d.ts +0 -49
- package/dist/tools/ubs/scanner.d.ts.map +0 -1
- package/dist/tools/ubs/types.d.ts +0 -46
- package/dist/tools/ubs/types.d.ts.map +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for log command
|
|
3
|
+
*
|
|
4
|
+
* TDD: Write tests first, then implement
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
7
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
const LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
|
|
12
|
+
const TEST_LOG_DIR = join(process.cwd(), "test-logs");
|
|
13
|
+
|
|
14
|
+
describe("log command", () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
// Create test log directory
|
|
17
|
+
if (!existsSync(TEST_LOG_DIR)) {
|
|
18
|
+
mkdirSync(TEST_LOG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create sample log files
|
|
22
|
+
const today = new Date().toISOString().split("T")[0];
|
|
23
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().split("T")[0];
|
|
24
|
+
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(TEST_LOG_DIR, `tools-${today}.log`),
|
|
27
|
+
JSON.stringify({ time: new Date().toISOString(), level: "info", msg: "test tool call", tool: "hive_create" }) + "\n" +
|
|
28
|
+
JSON.stringify({ time: new Date().toISOString(), level: "debug", msg: "another call", tool: "swarm_status" }) + "\n"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(TEST_LOG_DIR, `swarmmail-${today}.log`),
|
|
33
|
+
JSON.stringify({ time: new Date().toISOString(), level: "info", msg: "message sent", to: ["agent"] }) + "\n"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(TEST_LOG_DIR, `errors-${today}.log`),
|
|
38
|
+
JSON.stringify({ time: new Date().toISOString(), level: "error", msg: "something failed", error: "Test error" }) + "\n"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(TEST_LOG_DIR, `tools-${yesterday}.log`),
|
|
43
|
+
JSON.stringify({ time: new Date(Date.now() - 86400000).toISOString(), level: "info", msg: "old log entry" }) + "\n"
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
// Clean up test logs
|
|
49
|
+
if (existsSync(TEST_LOG_DIR)) {
|
|
50
|
+
rmSync(TEST_LOG_DIR, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("log helper functions format log entries correctly", () => {
|
|
55
|
+
// This will be implemented in plugin-wrapper-template.ts
|
|
56
|
+
// For now, we test the expected format
|
|
57
|
+
const entry = {
|
|
58
|
+
time: new Date().toISOString(),
|
|
59
|
+
level: "info",
|
|
60
|
+
msg: "test message",
|
|
61
|
+
tool: "hive_create",
|
|
62
|
+
args: { title: "Test" }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const formatted = JSON.stringify(entry);
|
|
66
|
+
expect(formatted).toContain("time");
|
|
67
|
+
expect(formatted).toContain("level");
|
|
68
|
+
expect(formatted).toContain("msg");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("date-stamped log files use YYYY-MM-DD format", () => {
|
|
72
|
+
const today = new Date().toISOString().split("T")[0];
|
|
73
|
+
expect(today).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("log rotation keeps only recent files", () => {
|
|
77
|
+
// Test that files older than 7 days would be deleted
|
|
78
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000);
|
|
79
|
+
const eightDaysAgo = new Date(Date.now() - 8 * 86400000);
|
|
80
|
+
|
|
81
|
+
const sevenDaysDate = sevenDaysAgo.toISOString().split("T")[0];
|
|
82
|
+
const eightDaysDate = eightDaysAgo.toISOString().split("T")[0];
|
|
83
|
+
|
|
84
|
+
// Files with these dates should be deleted by rotation
|
|
85
|
+
expect(sevenDaysDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
86
|
+
expect(eightDaysDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("swarm log CLI", () => {
|
|
91
|
+
test("shows all logs by default", () => {
|
|
92
|
+
// CLI implementation will be tested via spawn
|
|
93
|
+
// For now, we verify the expected behavior exists
|
|
94
|
+
expect(true).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("filters by log type (tools, swarmmail, errors)", () => {
|
|
98
|
+
// CLI should support: swarm log tools, swarm log swarmmail, swarm log errors
|
|
99
|
+
const logTypes = ["tools", "swarmmail", "errors"];
|
|
100
|
+
expect(logTypes).toContain("tools");
|
|
101
|
+
expect(logTypes).toContain("swarmmail");
|
|
102
|
+
expect(logTypes).toContain("errors");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("filters by time with --since flag", () => {
|
|
106
|
+
// CLI should support: swarm log --since 30s, --since 5m, --since 2h
|
|
107
|
+
const timeUnits = ["s", "m", "h"];
|
|
108
|
+
expect(timeUnits).toContain("s");
|
|
109
|
+
expect(timeUnits).toContain("m");
|
|
110
|
+
expect(timeUnits).toContain("h");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("supports watch mode with --watch flag", () => {
|
|
114
|
+
// CLI should support: swarm log --watch
|
|
115
|
+
expect(true).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Command - View and tail swarm logs
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* swarm log [type] - Show recent logs (all types or specific: tools, swarmmail, errors, compaction)
|
|
6
|
+
* swarm log --since <time> - Show logs since time (e.g., 30s, 5m, 2h, 24h)
|
|
7
|
+
* swarm log --watch - Watch mode (live tail)
|
|
8
|
+
* swarm log --level <level> - Filter by level (info, debug, warn, error)
|
|
9
|
+
* swarm log --limit <n> - Limit output lines (default: 50)
|
|
10
|
+
* swarm log --json - JSON output
|
|
11
|
+
*
|
|
12
|
+
* Log files:
|
|
13
|
+
* ~/.config/swarm-tools/logs/tools-YYYY-MM-DD.log
|
|
14
|
+
* ~/.config/swarm-tools/logs/swarmmail-YYYY-MM-DD.log
|
|
15
|
+
* ~/.config/swarm-tools/logs/errors-YYYY-MM-DD.log
|
|
16
|
+
* ~/.config/swarm-tools/logs/compaction.log (legacy, single file)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as p from "@clack/prompts";
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
|
|
24
|
+
// Color utilities (inline)
|
|
25
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
26
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
27
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
28
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
29
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
30
|
+
const gray = (s: string) => `\x1b[90m${s}\x1b[0m`;
|
|
31
|
+
|
|
32
|
+
const LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
|
|
33
|
+
|
|
34
|
+
interface LogEntry {
|
|
35
|
+
time: string;
|
|
36
|
+
level: string;
|
|
37
|
+
msg: string;
|
|
38
|
+
[key: string]: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface LogOptions {
|
|
42
|
+
type?: string; // tools, swarmmail, errors, compaction
|
|
43
|
+
since?: number; // milliseconds
|
|
44
|
+
watch?: boolean;
|
|
45
|
+
level?: string; // info, debug, warn, error
|
|
46
|
+
limit?: number;
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Main log command handler
|
|
52
|
+
*/
|
|
53
|
+
export async function log() {
|
|
54
|
+
const args = process.argv.slice(3);
|
|
55
|
+
|
|
56
|
+
if (args.includes("--help") || args.includes("help")) {
|
|
57
|
+
showHelp();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const options = parseOptions(args);
|
|
62
|
+
|
|
63
|
+
if (options.watch) {
|
|
64
|
+
await watchLogs(options);
|
|
65
|
+
} else {
|
|
66
|
+
await showLogs(options);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse command-line arguments
|
|
72
|
+
*/
|
|
73
|
+
function parseOptions(args: string[]): LogOptions {
|
|
74
|
+
const options: LogOptions = {
|
|
75
|
+
limit: 50,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < args.length; i++) {
|
|
79
|
+
const arg = args[i];
|
|
80
|
+
|
|
81
|
+
if (arg === "--since" && i + 1 < args.length) {
|
|
82
|
+
options.since = parseSince(args[++i]);
|
|
83
|
+
} else if (arg === "--watch" || arg === "-w") {
|
|
84
|
+
options.watch = true;
|
|
85
|
+
} else if (arg === "--level" && i + 1 < args.length) {
|
|
86
|
+
options.level = args[++i];
|
|
87
|
+
} else if (arg === "--limit" && i + 1 < args.length) {
|
|
88
|
+
options.limit = parseInt(args[++i], 10);
|
|
89
|
+
} else if (arg === "--json") {
|
|
90
|
+
options.json = true;
|
|
91
|
+
} else if (!arg.startsWith("--")) {
|
|
92
|
+
// Positional argument - log type
|
|
93
|
+
options.type = arg;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse --since time string (e.g., "30s", "5m", "2h", "24h")
|
|
102
|
+
*/
|
|
103
|
+
function parseSince(since: string): number {
|
|
104
|
+
const match = since.match(/^(\d+)([smhd])$/);
|
|
105
|
+
if (!match) {
|
|
106
|
+
p.log.error(`Invalid --since format: ${since}. Use: 30s, 5m, 2h, 24h`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [, value, unit] = match;
|
|
111
|
+
const num = parseInt(value, 10);
|
|
112
|
+
|
|
113
|
+
const units: Record<string, number> = {
|
|
114
|
+
s: 1000,
|
|
115
|
+
m: 60 * 1000,
|
|
116
|
+
h: 60 * 60 * 1000,
|
|
117
|
+
d: 24 * 60 * 60 * 1000,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return num * units[unit];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get log files for a specific type
|
|
125
|
+
*/
|
|
126
|
+
function getLogFiles(type?: string): string[] {
|
|
127
|
+
if (!existsSync(LOG_DIR)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const files = readdirSync(LOG_DIR);
|
|
132
|
+
const today = new Date().toISOString().split("T")[0];
|
|
133
|
+
|
|
134
|
+
if (type === "compaction") {
|
|
135
|
+
// Legacy single file
|
|
136
|
+
return files
|
|
137
|
+
.filter((f) => f === "compaction.log")
|
|
138
|
+
.map((f) => join(LOG_DIR, f));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Date-stamped log files
|
|
142
|
+
const pattern = type
|
|
143
|
+
? new RegExp(`^${type}-\\d{4}-\\d{2}-\\d{2}\\.log$`)
|
|
144
|
+
: /^(tools|swarmmail|errors)-\d{4}-\d{2}-\d{2}\.log$/;
|
|
145
|
+
|
|
146
|
+
return files
|
|
147
|
+
.filter((f) => pattern.test(f))
|
|
148
|
+
.map((f) => join(LOG_DIR, f))
|
|
149
|
+
.sort((a, b) => {
|
|
150
|
+
// Sort by modification time (newest first)
|
|
151
|
+
const statA = statSync(a);
|
|
152
|
+
const statB = statSync(b);
|
|
153
|
+
return statB.mtimeMs - statA.mtimeMs;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read and parse log entries from a file
|
|
159
|
+
*/
|
|
160
|
+
function readLogEntries(filePath: string): LogEntry[] {
|
|
161
|
+
if (!existsSync(filePath)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = readFileSync(filePath, "utf-8");
|
|
166
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
167
|
+
|
|
168
|
+
return lines
|
|
169
|
+
.map((line) => {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(line) as LogEntry;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.filter((entry): entry is LogEntry => entry !== null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Filter log entries by options
|
|
181
|
+
*/
|
|
182
|
+
function filterEntries(entries: LogEntry[], options: LogOptions): LogEntry[] {
|
|
183
|
+
let filtered = entries;
|
|
184
|
+
|
|
185
|
+
// Filter by time
|
|
186
|
+
if (options.since) {
|
|
187
|
+
const cutoff = Date.now() - options.since;
|
|
188
|
+
filtered = filtered.filter((e) => new Date(e.time).getTime() >= cutoff);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Filter by level
|
|
192
|
+
if (options.level) {
|
|
193
|
+
filtered = filtered.filter((e) => e.level === options.level);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return filtered;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Format log entry for display
|
|
201
|
+
*/
|
|
202
|
+
function formatEntry(entry: LogEntry): string {
|
|
203
|
+
const time = new Date(entry.time).toLocaleTimeString();
|
|
204
|
+
const level = formatLevel(entry.level);
|
|
205
|
+
const msg = entry.msg;
|
|
206
|
+
|
|
207
|
+
// Extract additional fields (excluding time, level, msg)
|
|
208
|
+
const { time: _, level: __, msg: ___, ...rest } = entry;
|
|
209
|
+
const extra = Object.keys(rest).length > 0 ? dim(JSON.stringify(rest)) : "";
|
|
210
|
+
|
|
211
|
+
return `${gray(time)} ${level} ${msg} ${extra}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Format log level with color
|
|
216
|
+
*/
|
|
217
|
+
function formatLevel(level: string): string {
|
|
218
|
+
switch (level) {
|
|
219
|
+
case "error":
|
|
220
|
+
return red("[ERROR]");
|
|
221
|
+
case "warn":
|
|
222
|
+
return yellow("[WARN] ");
|
|
223
|
+
case "info":
|
|
224
|
+
return green("[INFO] ");
|
|
225
|
+
case "debug":
|
|
226
|
+
return cyan("[DEBUG]");
|
|
227
|
+
default:
|
|
228
|
+
return `[${level}]`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Show logs (non-watch mode)
|
|
234
|
+
*/
|
|
235
|
+
async function showLogs(options: LogOptions) {
|
|
236
|
+
const files = getLogFiles(options.type);
|
|
237
|
+
|
|
238
|
+
if (files.length === 0) {
|
|
239
|
+
if (options.type) {
|
|
240
|
+
p.log.warn(`No logs found for type: ${options.type}`);
|
|
241
|
+
} else {
|
|
242
|
+
p.log.warn("No logs found. Run a swarm command to generate logs.");
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Read all entries from all files
|
|
248
|
+
const allEntries = files.flatMap((f) => readLogEntries(f));
|
|
249
|
+
|
|
250
|
+
// Filter
|
|
251
|
+
const filtered = filterEntries(allEntries, options);
|
|
252
|
+
|
|
253
|
+
// Sort by time (newest last for tail-like output)
|
|
254
|
+
const sorted = filtered.sort(
|
|
255
|
+
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Limit
|
|
259
|
+
const limited =
|
|
260
|
+
options.limit && options.limit > 0
|
|
261
|
+
? sorted.slice(-options.limit)
|
|
262
|
+
: sorted;
|
|
263
|
+
|
|
264
|
+
if (options.json) {
|
|
265
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Pretty output
|
|
270
|
+
if (limited.length === 0) {
|
|
271
|
+
p.log.warn("No matching log entries");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
p.log.message(dim(`Showing ${limited.length} log entries`));
|
|
276
|
+
console.log("");
|
|
277
|
+
|
|
278
|
+
for (const entry of limited) {
|
|
279
|
+
console.log(formatEntry(entry));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Watch logs (live tail)
|
|
285
|
+
*/
|
|
286
|
+
async function watchLogs(options: LogOptions) {
|
|
287
|
+
p.log.info("Watching logs (Ctrl+C to stop)...");
|
|
288
|
+
console.log("");
|
|
289
|
+
|
|
290
|
+
// Track last read position for each file
|
|
291
|
+
const filePositions = new Map<string, number>();
|
|
292
|
+
|
|
293
|
+
// eslint-disable-next-line no-constant-condition
|
|
294
|
+
while (true) {
|
|
295
|
+
const files = getLogFiles(options.type);
|
|
296
|
+
|
|
297
|
+
for (const file of files) {
|
|
298
|
+
const lastPos = filePositions.get(file) ?? 0;
|
|
299
|
+
const content = readFileSync(file, "utf-8");
|
|
300
|
+
|
|
301
|
+
if (content.length > lastPos) {
|
|
302
|
+
const newContent = content.slice(lastPos);
|
|
303
|
+
const newLines = newContent.split("\n").filter((line) => line.trim());
|
|
304
|
+
|
|
305
|
+
for (const line of newLines) {
|
|
306
|
+
try {
|
|
307
|
+
const entry = JSON.parse(line) as LogEntry;
|
|
308
|
+
const filtered = filterEntries([entry], options);
|
|
309
|
+
|
|
310
|
+
if (filtered.length > 0) {
|
|
311
|
+
if (options.json) {
|
|
312
|
+
console.log(JSON.stringify(entry));
|
|
313
|
+
} else {
|
|
314
|
+
console.log(formatEntry(entry));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Skip invalid lines
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
filePositions.set(file, content.length);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Poll interval (500ms)
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Show help
|
|
333
|
+
*/
|
|
334
|
+
function showHelp() {
|
|
335
|
+
console.log(`
|
|
336
|
+
${cyan("swarm log")} - View and tail swarm logs
|
|
337
|
+
|
|
338
|
+
${yellow("USAGE:")}
|
|
339
|
+
swarm log [type] [options]
|
|
340
|
+
|
|
341
|
+
${yellow("TYPES:")}
|
|
342
|
+
tools Tool invocations (hive_*, swarm_*, etc.)
|
|
343
|
+
swarmmail Inter-agent messages
|
|
344
|
+
errors Error logs
|
|
345
|
+
compaction Context compaction events (legacy single file)
|
|
346
|
+
|
|
347
|
+
${yellow("OPTIONS:")}
|
|
348
|
+
--since <time> Show logs since time (e.g., 30s, 5m, 2h, 24h)
|
|
349
|
+
--watch, -w Watch mode (live tail)
|
|
350
|
+
--level <level> Filter by level (info, debug, warn, error)
|
|
351
|
+
--limit <n> Limit output lines (default: 50)
|
|
352
|
+
--json JSON output
|
|
353
|
+
|
|
354
|
+
${yellow("EXAMPLES:")}
|
|
355
|
+
swarm log # Show recent logs (all types)
|
|
356
|
+
swarm log tools # Show tool invocations
|
|
357
|
+
swarm log --since 5m # Show logs from last 5 minutes
|
|
358
|
+
swarm log errors --watch # Live tail error logs
|
|
359
|
+
swarm log --level error --limit 20 # Show last 20 error-level logs
|
|
360
|
+
swarm log compaction --json # Show compaction logs as JSON
|
|
361
|
+
`);
|
|
362
|
+
}
|
package/bin/swarm.ts
CHANGED
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
} from "../src/export-tools.js";
|
|
85
85
|
import { tree } from "./commands/tree.js";
|
|
86
86
|
import { session } from "./commands/session.js";
|
|
87
|
+
import { log } from "./commands/log.js";
|
|
87
88
|
import {
|
|
88
89
|
querySwarmHistory,
|
|
89
90
|
formatSwarmHistory,
|
|
@@ -97,7 +98,7 @@ import {
|
|
|
97
98
|
} from "../src/observability-health.js";
|
|
98
99
|
|
|
99
100
|
// Swarm insights
|
|
100
|
-
import { getRejectionAnalytics } from "../src/swarm-insights.js";
|
|
101
|
+
import { getRejectionAnalytics, getCompactionAnalytics } from "../src/swarm-insights.js";
|
|
101
102
|
|
|
102
103
|
// Eval tools
|
|
103
104
|
import { getPhase, getScoreHistory, recordEvalRun, getEvalHistoryPath } from "../src/eval-history.js";
|
|
@@ -3371,6 +3372,7 @@ ${cyan("Stats & History:")}
|
|
|
3371
3372
|
swarm stats --since 24h Show stats for custom time period
|
|
3372
3373
|
swarm stats --regressions Show eval regressions (>10% score drops)
|
|
3373
3374
|
swarm stats --rejections Show rejection reason analytics
|
|
3375
|
+
swarm stats --compaction-prompts Show compaction prompt analytics (visibility into generated prompts)
|
|
3374
3376
|
swarm stats --json Output as JSON for scripting
|
|
3375
3377
|
swarm o11y Show observability health dashboard (hook coverage, events, sessions)
|
|
3376
3378
|
swarm o11y --since 7d Custom time period for event stats (default: 7 days)
|
|
@@ -5080,6 +5082,7 @@ async function stats() {
|
|
|
5080
5082
|
let format: "text" | "json" = "text";
|
|
5081
5083
|
let showRegressions = false;
|
|
5082
5084
|
let showRejections = false;
|
|
5085
|
+
let showCompactionPrompts = false;
|
|
5083
5086
|
|
|
5084
5087
|
for (let i = 0; i < args.length; i++) {
|
|
5085
5088
|
if (args[i] === "--since" || args[i] === "-s") {
|
|
@@ -5091,6 +5094,8 @@ async function stats() {
|
|
|
5091
5094
|
showRegressions = true;
|
|
5092
5095
|
} else if (args[i] === "--rejections") {
|
|
5093
5096
|
showRejections = true;
|
|
5097
|
+
} else if (args[i] === "--compaction-prompts") {
|
|
5098
|
+
showCompactionPrompts = true;
|
|
5094
5099
|
}
|
|
5095
5100
|
}
|
|
5096
5101
|
|
|
@@ -5280,6 +5285,52 @@ async function stats() {
|
|
|
5280
5285
|
console.log("│" + pad(" No rejections in this period") + "│");
|
|
5281
5286
|
}
|
|
5282
5287
|
|
|
5288
|
+
console.log("└─────────────────────────────────────────────────────────────┘");
|
|
5289
|
+
console.log();
|
|
5290
|
+
}
|
|
5291
|
+
} else if (showCompactionPrompts) {
|
|
5292
|
+
// If --compaction-prompts flag, show compaction analytics
|
|
5293
|
+
const compactionAnalytics = await getCompactionAnalytics(swarmMail);
|
|
5294
|
+
|
|
5295
|
+
if (format === "json") {
|
|
5296
|
+
console.log(JSON.stringify(compactionAnalytics, null, 2));
|
|
5297
|
+
} else {
|
|
5298
|
+
console.log();
|
|
5299
|
+
const boxWidth = 61;
|
|
5300
|
+
const pad = (text: string) => text + " ".repeat(Math.max(0, boxWidth - text.length));
|
|
5301
|
+
|
|
5302
|
+
console.log("┌─────────────────────────────────────────────────────────────┐");
|
|
5303
|
+
console.log("│" + pad(" COMPACTION PROMPT ANALYTICS (all time)") + "│");
|
|
5304
|
+
console.log("├─────────────────────────────────────────────────────────────┤");
|
|
5305
|
+
console.log("│" + pad(" Total Events: " + compactionAnalytics.totalEvents) + "│");
|
|
5306
|
+
console.log("│" + pad(" Success Rate: " + compactionAnalytics.successRate.toFixed(1) + "%") + "│");
|
|
5307
|
+
console.log("│" + pad(" Avg Prompt Size: " + compactionAnalytics.avgPromptSize + " chars") + "│");
|
|
5308
|
+
console.log("│" + pad("") + "│");
|
|
5309
|
+
console.log("│" + pad(" By Type:") + "│");
|
|
5310
|
+
console.log("│" + pad(" ├── Prompts Generated: " + compactionAnalytics.byType.prompt_generated) + "│");
|
|
5311
|
+
console.log("│" + pad(" └── Detections Failed: " + compactionAnalytics.byType.detection_failed) + "│");
|
|
5312
|
+
console.log("│" + pad("") + "│");
|
|
5313
|
+
console.log("│" + pad(" Confidence Distribution:") + "│");
|
|
5314
|
+
console.log("│" + pad(" ├── High: " + compactionAnalytics.byConfidence.high) + "│");
|
|
5315
|
+
console.log("│" + pad(" ├── Medium: " + compactionAnalytics.byConfidence.medium) + "│");
|
|
5316
|
+
console.log("│" + pad(" └── Low: " + compactionAnalytics.byConfidence.low) + "│");
|
|
5317
|
+
|
|
5318
|
+
if (compactionAnalytics.recentPrompts.length > 0) {
|
|
5319
|
+
console.log("│" + pad("") + "│");
|
|
5320
|
+
console.log("│" + pad(" Recent Prompts:") + "│");
|
|
5321
|
+
for (const prompt of compactionAnalytics.recentPrompts.slice(0, 5)) {
|
|
5322
|
+
const timestamp = new Date(prompt.timestamp).toLocaleDateString();
|
|
5323
|
+
const conf = prompt.confidence ? ` (${prompt.confidence})` : "";
|
|
5324
|
+
const line = ` ├── ${timestamp}: ${prompt.length} chars${conf}`;
|
|
5325
|
+
console.log("│" + pad(line) + "│");
|
|
5326
|
+
|
|
5327
|
+
if (prompt.preview) {
|
|
5328
|
+
const previewLine = ` ${prompt.preview.substring(0, 50)}...`;
|
|
5329
|
+
console.log("│" + pad(previewLine) + "│");
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5283
5334
|
console.log("└─────────────────────────────────────────────────────────────┘");
|
|
5284
5335
|
console.log();
|
|
5285
5336
|
}
|
|
@@ -6207,7 +6258,7 @@ switch (command) {
|
|
|
6207
6258
|
break;
|
|
6208
6259
|
case "log":
|
|
6209
6260
|
case "logs":
|
|
6210
|
-
await
|
|
6261
|
+
await log();
|
|
6211
6262
|
break;
|
|
6212
6263
|
case "stats":
|
|
6213
6264
|
await stats();
|