@steadwing/openalerts 0.2.4 → 0.2.5

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.
@@ -0,0 +1,321 @@
1
+ export function sessionInfoToMonitor(info) {
2
+ return {
3
+ key: info.key,
4
+ agentId: info.agentId,
5
+ lastActivityAt: info.lastActivityAt,
6
+ status: "idle",
7
+ spawnedBy: info.spawnedBy,
8
+ messageCount: info.messageCount,
9
+ };
10
+ }
11
+ // ─── Chat Event Parser ───────────────────────────────────────────────────────
12
+ export function chatEventToAction(event) {
13
+ let type = "streaming";
14
+ if (event.state === "final")
15
+ type = "complete";
16
+ else if (event.state === "delta")
17
+ type = "streaming";
18
+ else if (event.state === "aborted")
19
+ type = "aborted";
20
+ else if (event.state === "error")
21
+ type = "error";
22
+ const action = {
23
+ id: `${event.runId}-${event.seq}`,
24
+ runId: event.runId,
25
+ sessionKey: event.sessionKey,
26
+ seq: event.seq,
27
+ type,
28
+ eventType: "chat",
29
+ timestamp: Date.now(),
30
+ };
31
+ if (event.state === "final") {
32
+ if (event.usage) {
33
+ action.inputTokens = event.usage.inputTokens;
34
+ action.outputTokens = event.usage.outputTokens;
35
+ }
36
+ if (event.stopReason) {
37
+ action.stopReason = event.stopReason;
38
+ }
39
+ }
40
+ if (event.message) {
41
+ if (typeof event.message === "string") {
42
+ action.content = event.message;
43
+ }
44
+ else if (typeof event.message === "object") {
45
+ const msg = event.message;
46
+ if (Array.isArray(msg.content)) {
47
+ const texts = [];
48
+ for (const block of msg.content) {
49
+ if (typeof block === "object" && block) {
50
+ const b = block;
51
+ if (b.type === "text" && typeof b.text === "string") {
52
+ texts.push(b.text);
53
+ }
54
+ else if (b.type === "tool_use") {
55
+ action.type = "tool_call";
56
+ action.toolName = String(b.name || "unknown");
57
+ action.toolArgs = b.input;
58
+ }
59
+ else if (b.type === "tool_result") {
60
+ action.type = "tool_result";
61
+ if (typeof b.content === "string") {
62
+ texts.push(b.content);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ if (texts.length > 0) {
68
+ action.content = texts.join("");
69
+ }
70
+ }
71
+ else if (typeof msg.content === "string") {
72
+ action.content = msg.content;
73
+ }
74
+ else if (typeof msg.text === "string") {
75
+ action.content = msg.text;
76
+ }
77
+ }
78
+ }
79
+ if (event.errorMessage) {
80
+ action.content = event.errorMessage;
81
+ }
82
+ return action;
83
+ }
84
+ // ─── Agent Event Parser ──────────────────────────────────────────────────────
85
+ export function agentEventToAction(event) {
86
+ const data = event.data;
87
+ let type = "streaming";
88
+ let content;
89
+ let toolName;
90
+ let toolArgs;
91
+ let startedAt;
92
+ let endedAt;
93
+ if (event.stream === "lifecycle") {
94
+ if (data.phase === "start") {
95
+ type = "start";
96
+ startedAt = typeof data.startedAt === "number" ? data.startedAt : event.ts;
97
+ }
98
+ else if (data.phase === "end") {
99
+ type = "complete";
100
+ endedAt = typeof data.endedAt === "number" ? data.endedAt : event.ts;
101
+ }
102
+ }
103
+ else if (data.type === "tool_use") {
104
+ type = "tool_call";
105
+ toolName = String(data.name || "unknown");
106
+ toolArgs = data.input;
107
+ content = `Tool: ${toolName}`;
108
+ }
109
+ else if (data.type === "tool_result") {
110
+ type = "tool_result";
111
+ content = String(data.content || "");
112
+ }
113
+ else if (data.type === "text" || typeof data.text === "string") {
114
+ type = "streaming";
115
+ content = String(data.text || "");
116
+ }
117
+ return {
118
+ id: `${event.runId}-${event.seq}`,
119
+ runId: event.runId,
120
+ sessionKey: event.sessionKey || event.stream,
121
+ seq: event.seq,
122
+ type,
123
+ eventType: "agent",
124
+ timestamp: event.ts,
125
+ content,
126
+ toolName,
127
+ toolArgs,
128
+ startedAt,
129
+ endedAt,
130
+ };
131
+ }
132
+ // ─── Exec Event Parser ───────────────────────────────────────────────────────
133
+ export function execStartedToEvent(event, seq) {
134
+ const execId = `exec-${event.runId}-${event.pid}`;
135
+ const timestamp = Date.now();
136
+ const id = seq != null ? `${execId}-started-${seq}` : `${execId}-started-${timestamp}`;
137
+ return {
138
+ id,
139
+ execId,
140
+ runId: event.runId,
141
+ pid: event.pid,
142
+ sessionId: event.sessionId,
143
+ eventType: "started",
144
+ command: event.command,
145
+ startedAt: event.startedAt,
146
+ timestamp,
147
+ };
148
+ }
149
+ export function execOutputToEvent(event, seq) {
150
+ const execId = `exec-${event.runId}-${event.pid}`;
151
+ const timestamp = Date.now();
152
+ const id = seq != null ? `${execId}-output-${seq}` : `${execId}-output-${timestamp}`;
153
+ return {
154
+ id,
155
+ execId,
156
+ runId: event.runId,
157
+ pid: event.pid,
158
+ sessionId: event.sessionId,
159
+ eventType: "output",
160
+ stream: event.stream,
161
+ output: event.output,
162
+ timestamp,
163
+ };
164
+ }
165
+ export function execCompletedToEvent(event, seq) {
166
+ const execId = `exec-${event.runId}-${event.pid}`;
167
+ const timestamp = Date.now();
168
+ const id = seq != null
169
+ ? `${execId}-completed-${seq}`
170
+ : `${execId}-completed-${timestamp}`;
171
+ return {
172
+ id,
173
+ execId,
174
+ runId: event.runId,
175
+ pid: event.pid,
176
+ sessionId: event.sessionId,
177
+ eventType: "completed",
178
+ durationMs: event.durationMs,
179
+ exitCode: event.exitCode,
180
+ status: event.status,
181
+ timestamp,
182
+ };
183
+ }
184
+ export function parseGatewayEvent(eventName, payload, seq) {
185
+ if (eventName === "health" || eventName === "tick") {
186
+ return null;
187
+ }
188
+ if (eventName === "chat" && payload) {
189
+ const chatEvent = payload;
190
+ return {
191
+ action: chatEventToAction(chatEvent),
192
+ session: {
193
+ key: chatEvent.sessionKey,
194
+ status: chatEvent.state === "delta" ? "thinking" : "active",
195
+ lastActivityAt: Date.now(),
196
+ },
197
+ };
198
+ }
199
+ if (eventName === "agent" && payload) {
200
+ const agentEvent = payload;
201
+ if (agentEvent.stream === "lifecycle") {
202
+ return {
203
+ action: agentEventToAction(agentEvent),
204
+ session: agentEvent.sessionKey
205
+ ? {
206
+ key: agentEvent.sessionKey,
207
+ status: agentEvent.data?.phase === "start" ? "thinking" : "active",
208
+ lastActivityAt: Date.now(),
209
+ }
210
+ : undefined,
211
+ };
212
+ }
213
+ if (agentEvent.stream === "assistant" &&
214
+ typeof agentEvent.data?.text === "string") {
215
+ return {
216
+ action: agentEventToAction(agentEvent),
217
+ session: agentEvent.sessionKey
218
+ ? {
219
+ key: agentEvent.sessionKey,
220
+ status: "thinking",
221
+ lastActivityAt: Date.now(),
222
+ }
223
+ : undefined,
224
+ };
225
+ }
226
+ if (agentEvent.data?.type === "tool_use" ||
227
+ agentEvent.data?.type === "tool_result") {
228
+ return {
229
+ action: agentEventToAction(agentEvent),
230
+ session: agentEvent.sessionKey
231
+ ? {
232
+ key: agentEvent.sessionKey,
233
+ status: "thinking",
234
+ lastActivityAt: Date.now(),
235
+ }
236
+ : undefined,
237
+ };
238
+ }
239
+ return null;
240
+ }
241
+ if (eventName === "exec.started" && payload) {
242
+ const exec = payload;
243
+ const execEvent = execStartedToEvent(exec, seq);
244
+ return {
245
+ execEvent,
246
+ session: exec.sessionId
247
+ ? {
248
+ key: exec.sessionId,
249
+ status: "thinking",
250
+ lastActivityAt: execEvent.timestamp,
251
+ }
252
+ : undefined,
253
+ };
254
+ }
255
+ if (eventName === "exec.output" && payload) {
256
+ const exec = payload;
257
+ const execEvent = execOutputToEvent(exec, seq);
258
+ return {
259
+ execEvent,
260
+ session: exec.sessionId
261
+ ? {
262
+ key: exec.sessionId,
263
+ lastActivityAt: execEvent.timestamp,
264
+ }
265
+ : undefined,
266
+ };
267
+ }
268
+ if (eventName === "exec.completed" && payload) {
269
+ const exec = payload;
270
+ const execEvent = execCompletedToEvent(exec, seq);
271
+ return {
272
+ execEvent,
273
+ session: exec.sessionId
274
+ ? {
275
+ key: exec.sessionId,
276
+ status: "active",
277
+ lastActivityAt: execEvent.timestamp,
278
+ }
279
+ : undefined,
280
+ };
281
+ }
282
+ return null;
283
+ }
284
+ export function diagnosticUsageToSessionUpdate(event) {
285
+ const sessionKey = event.sessionKey || event.sessionId;
286
+ const now = Date.now();
287
+ const result = {};
288
+ if (sessionKey) {
289
+ const inputTokens = event.usage?.input ?? event.usage?.promptTokens ?? 0;
290
+ const outputTokens = event.usage?.output ?? 0;
291
+ result.session = {
292
+ key: sessionKey,
293
+ lastActivityAt: now,
294
+ status: "active",
295
+ totalInputTokens: inputTokens,
296
+ totalOutputTokens: outputTokens,
297
+ };
298
+ if (typeof event.costUsd === "number") {
299
+ result.session.totalCostUsd = event.costUsd;
300
+ }
301
+ }
302
+ if (typeof event.costUsd === "number" || event.usage) {
303
+ const actionId = `usage-${event.ts}-${event.seq}`;
304
+ result.action = {
305
+ id: actionId,
306
+ runId: actionId,
307
+ sessionKey: sessionKey || "unknown",
308
+ seq: event.seq,
309
+ type: "complete",
310
+ eventType: "system",
311
+ timestamp: event.ts,
312
+ inputTokens: event.usage?.input ?? event.usage?.promptTokens,
313
+ outputTokens: event.usage?.output,
314
+ costUsd: event.costUsd,
315
+ model: event.model,
316
+ provider: event.provider,
317
+ duration: event.durationMs,
318
+ };
319
+ }
320
+ return result;
321
+ }
@@ -0,0 +1,6 @@
1
+ export type { MonitorSession, MonitorActionType, MonitorActionEventType, MonitorAction, MonitorExecEventType, MonitorExecProcessStatus, MonitorExecOutputChunk, MonitorExecEvent, MonitorExecProcess, ChatEvent, AgentEvent, ExecStartedEvent, ExecOutputEvent, ExecCompletedEvent, CollectionStats, DiagnosticUsageEvent, CostUsageTotals, CostUsageSummary, } from "./types.js";
2
+ export { parseSessionKey } from "./types.js";
3
+ export type { ParsedGatewayEvent, SessionInfo, ParsedDiagnosticUsage, } from "./event-parser.js";
4
+ export { sessionInfoToMonitor, chatEventToAction, agentEventToAction, execStartedToEvent, execOutputToEvent, execCompletedToEvent, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./event-parser.js";
5
+ export { CollectionManager } from "./collection-manager.js";
6
+ export { CollectionPersistence } from "./persistence.js";
@@ -0,0 +1,6 @@
1
+ export { parseSessionKey } from "./types.js";
2
+ export { sessionInfoToMonitor, chatEventToAction, agentEventToAction, execStartedToEvent, execOutputToEvent, execCompletedToEvent, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./event-parser.js";
3
+ // Collection Manager
4
+ export { CollectionManager } from "./collection-manager.js";
5
+ // Persistence
6
+ export { CollectionPersistence } from "./persistence.js";
@@ -0,0 +1,25 @@
1
+ import type { MonitorSession, MonitorAction, MonitorExecEvent } from "./types.js";
2
+ export declare class CollectionPersistence {
3
+ private stateDir;
4
+ private dirty;
5
+ private flushTimer;
6
+ private lastSessionsJson;
7
+ private pendingActions;
8
+ private pendingExecs;
9
+ constructor(stateDir: string);
10
+ start(): void;
11
+ stop(): void;
12
+ markDirty(): void;
13
+ saveSessions(sessions: MonitorSession[]): void;
14
+ loadSessions(): MonitorSession[];
15
+ queueAction(action: MonitorAction): void;
16
+ loadActions(): MonitorAction[];
17
+ queueExecEvent(event: MonitorExecEvent): void;
18
+ loadExecEvents(): MonitorExecEvent[];
19
+ flush(): void;
20
+ hydrate(): {
21
+ sessions: MonitorSession[];
22
+ actions: MonitorAction[];
23
+ execEvents: MonitorExecEvent[];
24
+ };
25
+ }
@@ -0,0 +1,213 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ // ─── Constants ───────────────────────────────────────────────────────────────
4
+ const COLLECTIONS_DIR_NAME = "collections";
5
+ const SESSIONS_FILENAME = "sessions.json";
6
+ const ACTIONS_FILENAME = "actions.jsonl";
7
+ const EXECS_FILENAME = "execs.jsonl";
8
+ const MAX_ACTIONS_LINES = 10000;
9
+ const MAX_EXECS_LINES = 20000;
10
+ const FLUSH_INTERVAL_MS = 5000;
11
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
12
+ function resolveDir(stateDir) {
13
+ return path.join(stateDir, COLLECTIONS_DIR_NAME);
14
+ }
15
+ function resolveSessionsPath(stateDir) {
16
+ return path.join(resolveDir(stateDir), SESSIONS_FILENAME);
17
+ }
18
+ function resolveActionsPath(stateDir) {
19
+ return path.join(resolveDir(stateDir), ACTIONS_FILENAME);
20
+ }
21
+ function resolveExecsPath(stateDir) {
22
+ return path.join(resolveDir(stateDir), EXECS_FILENAME);
23
+ }
24
+ function ensureDir(stateDir) {
25
+ const dir = resolveDir(stateDir);
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ }
30
+ function writeAtomic(filePath, content) {
31
+ const tmpPath = filePath + ".tmp";
32
+ try {
33
+ fs.writeFileSync(tmpPath, content, "utf-8");
34
+ fs.renameSync(tmpPath, filePath);
35
+ }
36
+ catch {
37
+ fs.writeFileSync(filePath, content, "utf-8");
38
+ try {
39
+ fs.unlinkSync(tmpPath);
40
+ }
41
+ catch {
42
+ // Ignore cleanup failure
43
+ }
44
+ }
45
+ }
46
+ function appendLine(filePath, line) {
47
+ fs.appendFileSync(filePath, line + "\n", "utf-8");
48
+ }
49
+ function capJsonlFile(filePath, maxLines) {
50
+ if (!fs.existsSync(filePath))
51
+ return;
52
+ try {
53
+ const content = fs.readFileSync(filePath, "utf-8");
54
+ const lines = content.trim().split("\n").filter(Boolean);
55
+ if (lines.length <= maxLines)
56
+ return;
57
+ const kept = lines.slice(-maxLines);
58
+ writeAtomic(filePath, kept.join("\n") + "\n");
59
+ }
60
+ catch {
61
+ // Ignore errors during cap
62
+ }
63
+ }
64
+ // ─── Persistence Class ───────────────────────────────────────────────────────
65
+ export class CollectionPersistence {
66
+ stateDir;
67
+ dirty = false;
68
+ flushTimer = null;
69
+ // Cached data for tracking changes
70
+ lastSessionsJson = "";
71
+ pendingActions = [];
72
+ pendingExecs = [];
73
+ constructor(stateDir) {
74
+ this.stateDir = stateDir;
75
+ }
76
+ start() {
77
+ ensureDir(this.stateDir);
78
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
79
+ }
80
+ stop() {
81
+ if (this.flushTimer) {
82
+ clearInterval(this.flushTimer);
83
+ this.flushTimer = null;
84
+ }
85
+ this.flush();
86
+ }
87
+ markDirty() {
88
+ this.dirty = true;
89
+ }
90
+ // ─── Sessions (JSON rewrite) ──────────────────────────────────────────────
91
+ saveSessions(sessions) {
92
+ const json = JSON.stringify(sessions, null, 2);
93
+ if (json === this.lastSessionsJson)
94
+ return;
95
+ this.lastSessionsJson = json;
96
+ this.dirty = true;
97
+ }
98
+ loadSessions() {
99
+ const filePath = resolveSessionsPath(this.stateDir);
100
+ if (!fs.existsSync(filePath))
101
+ return [];
102
+ try {
103
+ const content = fs.readFileSync(filePath, "utf-8");
104
+ const parsed = JSON.parse(content);
105
+ if (Array.isArray(parsed)) {
106
+ return parsed;
107
+ }
108
+ return [];
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ // ─── Actions (JSONL append) ───────────────────────────────────────────────
115
+ queueAction(action) {
116
+ this.pendingActions.push(action);
117
+ this.dirty = true;
118
+ }
119
+ loadActions() {
120
+ const filePath = resolveActionsPath(this.stateDir);
121
+ if (!fs.existsSync(filePath))
122
+ return [];
123
+ try {
124
+ const content = fs.readFileSync(filePath, "utf-8");
125
+ const lines = content.trim().split("\n").filter(Boolean);
126
+ const actions = [];
127
+ for (const line of lines) {
128
+ try {
129
+ const parsed = JSON.parse(line);
130
+ if (parsed && typeof parsed.id === "string") {
131
+ actions.push(parsed);
132
+ }
133
+ }
134
+ catch {
135
+ // Skip malformed lines
136
+ }
137
+ }
138
+ return actions;
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ }
144
+ // ─── Execs (JSONL append) ─────────────────────────────────────────────────
145
+ queueExecEvent(event) {
146
+ this.pendingExecs.push(event);
147
+ this.dirty = true;
148
+ }
149
+ loadExecEvents() {
150
+ const filePath = resolveExecsPath(this.stateDir);
151
+ if (!fs.existsSync(filePath))
152
+ return [];
153
+ try {
154
+ const content = fs.readFileSync(filePath, "utf-8");
155
+ const lines = content.trim().split("\n").filter(Boolean);
156
+ const events = [];
157
+ for (const line of lines) {
158
+ try {
159
+ const parsed = JSON.parse(line);
160
+ if (parsed && typeof parsed.id === "string") {
161
+ events.push(parsed);
162
+ }
163
+ }
164
+ catch {
165
+ // Skip malformed lines
166
+ }
167
+ }
168
+ return events;
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ }
174
+ // ─── Flush ────────────────────────────────────────────────────────────────
175
+ flush() {
176
+ if (!this.dirty && this.pendingActions.length === 0 && this.pendingExecs.length === 0) {
177
+ return;
178
+ }
179
+ ensureDir(this.stateDir);
180
+ // Write sessions if changed
181
+ if (this.lastSessionsJson) {
182
+ const filePath = resolveSessionsPath(this.stateDir);
183
+ writeAtomic(filePath, this.lastSessionsJson);
184
+ }
185
+ // Append pending actions
186
+ if (this.pendingActions.length > 0) {
187
+ const filePath = resolveActionsPath(this.stateDir);
188
+ for (const action of this.pendingActions) {
189
+ appendLine(filePath, JSON.stringify(action));
190
+ }
191
+ this.pendingActions = [];
192
+ capJsonlFile(filePath, MAX_ACTIONS_LINES);
193
+ }
194
+ // Append pending exec events
195
+ if (this.pendingExecs.length > 0) {
196
+ const filePath = resolveExecsPath(this.stateDir);
197
+ for (const event of this.pendingExecs) {
198
+ appendLine(filePath, JSON.stringify(event));
199
+ }
200
+ this.pendingExecs = [];
201
+ capJsonlFile(filePath, MAX_EXECS_LINES);
202
+ }
203
+ this.dirty = false;
204
+ }
205
+ // ─── Full Hydration ───────────────────────────────────────────────────────
206
+ hydrate() {
207
+ return {
208
+ sessions: this.loadSessions(),
209
+ actions: this.loadActions(),
210
+ execEvents: this.loadExecEvents(),
211
+ };
212
+ }
213
+ }