opencode-swarm-plugin 0.42.9 → 0.43.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 +14 -0
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +79 -0
- package/README.md +296 -6
- package/bin/swarm.test.ts +615 -0
- package/bin/swarm.ts +434 -0
- package/dist/dashboard.d.ts +83 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/error-enrichment.d.ts +49 -0
- package/dist/error-enrichment.d.ts.map +1 -0
- package/dist/export-tools.d.ts +76 -0
- package/dist/export-tools.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/observability-tools.d.ts +2 -2
- package/dist/query-tools.d.ts +59 -0
- package/dist/query-tools.d.ts.map +1 -0
- package/dist/replay-tools.d.ts +28 -0
- package/dist/replay-tools.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/dashboard.test.ts +611 -0
- package/src/dashboard.ts +462 -0
- package/src/error-enrichment.test.ts +403 -0
- package/src/error-enrichment.ts +219 -0
- package/src/export-tools.test.ts +476 -0
- package/src/export-tools.ts +257 -0
- package/src/query-tools.test.ts +636 -0
- package/src/query-tools.ts +324 -0
- package/src/replay-tools.test.ts +496 -0
- package/src/replay-tools.ts +240 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export Tools - Convert Cell Events to Various Formats
|
|
3
|
+
*
|
|
4
|
+
* GREEN PHASE: Minimal implementation to pass tests
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - OTLP (OpenTelemetry Protocol) - for distributed tracing
|
|
8
|
+
* - CSV - for spreadsheet analysis
|
|
9
|
+
* - JSON - for generic data interchange
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CellEvent } from "./schemas/cell-events.js";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// OTLP Export
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OpenTelemetry OTLP span structure
|
|
21
|
+
*/
|
|
22
|
+
interface OTLPSpan {
|
|
23
|
+
traceId: string; // 32 hex chars (16 bytes)
|
|
24
|
+
spanId: string; // 16 hex chars (8 bytes)
|
|
25
|
+
name: string; // event.type
|
|
26
|
+
startTimeUnixNano: string; // timestamp in nanoseconds
|
|
27
|
+
attributes: Array<{
|
|
28
|
+
key: string;
|
|
29
|
+
value: {
|
|
30
|
+
stringValue?: string;
|
|
31
|
+
intValue?: number;
|
|
32
|
+
boolValue?: boolean;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface OTLPOutput {
|
|
38
|
+
resourceSpans: Array<{
|
|
39
|
+
resource: {
|
|
40
|
+
attributes: Array<{
|
|
41
|
+
key: string;
|
|
42
|
+
value: { stringValue: string };
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
scopeSpans: Array<{
|
|
46
|
+
scope: {
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
spans: OTLPSpan[];
|
|
50
|
+
}>;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert string to hex hash of specified length
|
|
56
|
+
*/
|
|
57
|
+
function toHex(input: string, bytes: number): string {
|
|
58
|
+
const hash = createHash("sha256").update(input).digest("hex");
|
|
59
|
+
return hash.slice(0, bytes * 2); // 2 hex chars per byte
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert event payload to OTLP attributes
|
|
64
|
+
*/
|
|
65
|
+
function eventToAttributes(event: CellEvent): OTLPSpan["attributes"] {
|
|
66
|
+
const attrs: OTLPSpan["attributes"] = [];
|
|
67
|
+
|
|
68
|
+
// Map common fields with "cell." prefix
|
|
69
|
+
if ("title" in event && event.title) {
|
|
70
|
+
attrs.push({
|
|
71
|
+
key: "cell.title",
|
|
72
|
+
value: { stringValue: event.title },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ("issue_type" in event && event.issue_type) {
|
|
77
|
+
attrs.push({
|
|
78
|
+
key: "cell.type",
|
|
79
|
+
value: { stringValue: event.issue_type },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ("priority" in event && typeof event.priority === "number") {
|
|
84
|
+
attrs.push({
|
|
85
|
+
key: "cell.priority",
|
|
86
|
+
value: { intValue: event.priority },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ("description" in event && event.description) {
|
|
91
|
+
attrs.push({
|
|
92
|
+
key: "cell.description",
|
|
93
|
+
value: { stringValue: event.description },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add type-specific fields
|
|
98
|
+
if ("from_status" in event && event.from_status) {
|
|
99
|
+
attrs.push({
|
|
100
|
+
key: "cell.from_status",
|
|
101
|
+
value: { stringValue: event.from_status },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ("to_status" in event && event.to_status) {
|
|
106
|
+
attrs.push({
|
|
107
|
+
key: "cell.to_status",
|
|
108
|
+
value: { stringValue: event.to_status },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if ("reason" in event && event.reason) {
|
|
113
|
+
attrs.push({
|
|
114
|
+
key: "cell.reason",
|
|
115
|
+
value: { stringValue: event.reason },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ("duration_ms" in event && typeof event.duration_ms === "number") {
|
|
120
|
+
attrs.push({
|
|
121
|
+
key: "cell.duration_ms",
|
|
122
|
+
value: { intValue: event.duration_ms },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return attrs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Export cell events to OpenTelemetry OTLP format
|
|
131
|
+
*
|
|
132
|
+
* Mapping:
|
|
133
|
+
* - epic_id (from metadata) → trace_id (32 hex chars)
|
|
134
|
+
* - cell_id → span_id (16 hex chars)
|
|
135
|
+
* - timestamp → startTimeUnixNano (nanoseconds as string)
|
|
136
|
+
* - event.type → span.name
|
|
137
|
+
* - event payload → span.attributes
|
|
138
|
+
*/
|
|
139
|
+
export function exportToOTLP(events: CellEvent[]): OTLPOutput {
|
|
140
|
+
const spans: OTLPSpan[] = events.map((event) => {
|
|
141
|
+
// Determine trace_id: epic_id from metadata, fallback to project_key
|
|
142
|
+
const epicId =
|
|
143
|
+
("metadata" in event &&
|
|
144
|
+
event.metadata &&
|
|
145
|
+
typeof event.metadata === "object" &&
|
|
146
|
+
"epic_id" in event.metadata &&
|
|
147
|
+
typeof event.metadata.epic_id === "string"
|
|
148
|
+
? event.metadata.epic_id
|
|
149
|
+
: event.project_key);
|
|
150
|
+
|
|
151
|
+
const traceId = toHex(epicId, 16); // 16 bytes = 32 hex chars
|
|
152
|
+
const spanId = toHex(event.cell_id, 8); // 8 bytes = 16 hex chars
|
|
153
|
+
|
|
154
|
+
// Convert timestamp (ms) to nanoseconds
|
|
155
|
+
const startTimeUnixNano = String(event.timestamp * 1_000_000);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
traceId,
|
|
159
|
+
spanId,
|
|
160
|
+
name: event.type,
|
|
161
|
+
startTimeUnixNano,
|
|
162
|
+
attributes: eventToAttributes(event),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
resourceSpans: [
|
|
168
|
+
{
|
|
169
|
+
resource: {
|
|
170
|
+
attributes: [
|
|
171
|
+
{
|
|
172
|
+
key: "service.name",
|
|
173
|
+
value: { stringValue: "swarm" },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
scopeSpans: [
|
|
178
|
+
{
|
|
179
|
+
scope: {
|
|
180
|
+
name: "swarm",
|
|
181
|
+
},
|
|
182
|
+
spans,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// CSV Export
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Escape CSV field value
|
|
196
|
+
* - Wrap in quotes if contains comma, quote, or newline
|
|
197
|
+
* - Convert JSON escaped quotes (\") to CSV doubled quotes ("")
|
|
198
|
+
* - Double any other interior quotes
|
|
199
|
+
*/
|
|
200
|
+
function escapeCsvField(value: string): string {
|
|
201
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
202
|
+
// First convert JSON-escaped quotes \" to just "
|
|
203
|
+
// Then double all quotes for CSV format
|
|
204
|
+
const unescaped = value.replace(/\\"/g, '"');
|
|
205
|
+
return `"${unescaped.replace(/"/g, '""')}"`;
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Export cell events to CSV format
|
|
212
|
+
*
|
|
213
|
+
* Format:
|
|
214
|
+
* - Headers: id,type,timestamp,project_key,cell_id,payload
|
|
215
|
+
* - Payload: JSON serialization of entire event (minus headers)
|
|
216
|
+
*/
|
|
217
|
+
export function exportToCSV(events: CellEvent[]): string {
|
|
218
|
+
const headers = "id,type,timestamp,project_key,cell_id,payload";
|
|
219
|
+
const rows = [headers];
|
|
220
|
+
|
|
221
|
+
for (const event of events) {
|
|
222
|
+
// Don't escape simple fields that don't need it
|
|
223
|
+
const id = "id" in event && event.id ? String(event.id) : "";
|
|
224
|
+
const type = event.type;
|
|
225
|
+
const timestamp = String(event.timestamp);
|
|
226
|
+
const projectKey = event.project_key;
|
|
227
|
+
const cellId = event.cell_id;
|
|
228
|
+
|
|
229
|
+
// Serialize entire event as JSON for payload column
|
|
230
|
+
const payloadJson = JSON.stringify(event);
|
|
231
|
+
const payload = escapeCsvField(payloadJson);
|
|
232
|
+
|
|
233
|
+
rows.push([id, type, timestamp, projectKey, cellId, payload].join(","));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return `${rows.join("\n")}\n`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// JSON Export
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Export cell events to JSON format
|
|
245
|
+
*
|
|
246
|
+
* Format:
|
|
247
|
+
* - Array of event objects
|
|
248
|
+
* - Pretty-printed with 2-space indentation
|
|
249
|
+
* - Preserves all fields and discriminated union types
|
|
250
|
+
*/
|
|
251
|
+
export function exportToJSON(events: CellEvent[]): string {
|
|
252
|
+
if (events.length === 0) {
|
|
253
|
+
return "[]";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return JSON.stringify(events, null, 2);
|
|
257
|
+
}
|