@lovelybunch/api 1.0.65 → 1.0.67
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/dist/routes/api/v1/events/events.test.d.ts +4 -0
- package/dist/routes/api/v1/events/events.test.js +289 -0
- package/dist/routes/api/v1/events/index.d.ts +7 -0
- package/dist/routes/api/v1/events/index.js +19 -0
- package/dist/routes/api/v1/events/purge/route.d.ts +19 -0
- package/dist/routes/api/v1/events/purge/route.js +62 -0
- package/dist/routes/api/v1/events/route.d.ts +30 -0
- package/dist/routes/api/v1/events/route.js +109 -0
- package/dist/routes/api/v1/events/status/route.d.ts +20 -0
- package/dist/routes/api/v1/events/status/route.js +53 -0
- package/dist/routes/api/v1/events/stream/route.d.ts +9 -0
- package/dist/routes/api/v1/events/stream/route.js +132 -0
- package/dist/routes/api/v1/git/index.js +6 -0
- package/dist/routes/api/v1/init/index.d.ts +1 -0
- package/dist/routes/api/v1/init/index.js +1 -0
- package/dist/routes/api/v1/init/route.d.ts +3 -0
- package/dist/routes/api/v1/init/route.js +129 -0
- package/dist/routes/api/v1/onboard/index.d.ts +3 -0
- package/dist/routes/api/v1/onboard/index.js +8 -0
- package/dist/routes/api/v1/onboard/route.d.ts +13 -0
- package/dist/routes/api/v1/onboard/route.js +311 -0
- package/dist/routes/api/v1/onboarding/check/index.d.ts +3 -0
- package/dist/routes/api/v1/onboarding/check/index.js +5 -0
- package/dist/routes/api/v1/onboarding/check/route.d.ts +12 -0
- package/dist/routes/api/v1/onboarding/check/route.js +24 -0
- package/dist/routes/api/v1/onboarding/index.d.ts +1 -0
- package/dist/routes/api/v1/onboarding/index.js +1 -0
- package/dist/routes/api/v1/onboarding/route.d.ts +3 -0
- package/dist/routes/api/v1/onboarding/route.js +158 -0
- package/dist/routes/api/v1/proposals/[id]/route.js +57 -0
- package/dist/routes/api/v1/proposals/route.js +18 -0
- package/dist/server-with-static.js +62 -0
- package/dist/server.js +63 -0
- package/package.json +4 -4
- package/static/assets/{index-BBRzjrXH.js → index-aLGL6jN0.js} +33 -33
- package/static/index.html +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Events API routes
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll } from "vitest";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { getLogger, resetLogger } from "@lovelybunch/core/logging";
|
|
10
|
+
import events from "./index.js";
|
|
11
|
+
describe("Events API Routes", () => {
|
|
12
|
+
let app;
|
|
13
|
+
let testDir;
|
|
14
|
+
let logsDir;
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
// Create the Hono app with routes
|
|
17
|
+
app = new Hono();
|
|
18
|
+
app.route("/api/v1/events", events);
|
|
19
|
+
});
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
// Create a temporary directory for tests
|
|
22
|
+
testDir = path.join(os.tmpdir(), `coconut-events-test-${Date.now()}`);
|
|
23
|
+
logsDir = path.join(testDir, "logs");
|
|
24
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
25
|
+
// Reset the logger and create a new one with test directory
|
|
26
|
+
resetLogger();
|
|
27
|
+
getLogger({
|
|
28
|
+
coconutId: "test-coconut",
|
|
29
|
+
logsDir: logsDir,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
// Clean up logger and test directory
|
|
34
|
+
const logger = getLogger();
|
|
35
|
+
await logger.close();
|
|
36
|
+
resetLogger();
|
|
37
|
+
try {
|
|
38
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
// Ignore cleanup errors
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
describe("POST /api/v1/events", () => {
|
|
45
|
+
it("should enqueue a single event", async () => {
|
|
46
|
+
const event = {
|
|
47
|
+
kind: "proposal.create",
|
|
48
|
+
actor: "human:test@example.com",
|
|
49
|
+
subject: "proposal:cp-123",
|
|
50
|
+
tags: ["proposal"],
|
|
51
|
+
payload: { intent: "Test proposal" },
|
|
52
|
+
};
|
|
53
|
+
const res = await app.request("/api/v1/events", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify(event),
|
|
57
|
+
});
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
expect(data.accepted).toBe(1);
|
|
61
|
+
expect(data.last_seq).toBeGreaterThan(0);
|
|
62
|
+
// Verify event was written to file
|
|
63
|
+
await getLogger().flush();
|
|
64
|
+
const logFile = path.join(logsDir, "events-current.jsonl");
|
|
65
|
+
const content = await fs.readFile(logFile, "utf-8");
|
|
66
|
+
const lines = content.trim().split("\n");
|
|
67
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
68
|
+
const loggedEvent = JSON.parse(lines[lines.length - 1]);
|
|
69
|
+
expect(loggedEvent.kind).toBe("proposal.create");
|
|
70
|
+
expect(loggedEvent.actor).toBe("human:test@example.com");
|
|
71
|
+
});
|
|
72
|
+
it("should enqueue multiple events", async () => {
|
|
73
|
+
const events = [
|
|
74
|
+
{
|
|
75
|
+
kind: "proposal.create",
|
|
76
|
+
actor: "human:test@example.com",
|
|
77
|
+
subject: "proposal:cp-1",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: "proposal.update",
|
|
81
|
+
actor: "human:test@example.com",
|
|
82
|
+
subject: "proposal:cp-2",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: "proposal.status.change",
|
|
86
|
+
actor: "human:test@example.com",
|
|
87
|
+
subject: "proposal:cp-3",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
const res = await app.request("/api/v1/events", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify(events),
|
|
94
|
+
});
|
|
95
|
+
expect(res.status).toBe(200);
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
expect(data.accepted).toBe(3);
|
|
98
|
+
expect(data.last_seq).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("GET /api/v1/events", () => {
|
|
102
|
+
beforeEach(async () => {
|
|
103
|
+
// Pre-populate with some events
|
|
104
|
+
const logger = getLogger();
|
|
105
|
+
for (let i = 1; i <= 10; i++) {
|
|
106
|
+
logger.log({
|
|
107
|
+
kind: "proposal.create",
|
|
108
|
+
actor: "human:test@example.com",
|
|
109
|
+
subject: `proposal:cp-${i}`,
|
|
110
|
+
payload: { index: i },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
await logger.flush();
|
|
114
|
+
});
|
|
115
|
+
it("should fetch events from current file", async () => {
|
|
116
|
+
const res = await app.request("/api/v1/events");
|
|
117
|
+
expect(res.status).toBe(200);
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
expect(data.items).toBeDefined();
|
|
120
|
+
expect(Array.isArray(data.items)).toBe(true);
|
|
121
|
+
expect(data.items.length).toBe(10);
|
|
122
|
+
expect(data.next_since_seq).toBeGreaterThan(0);
|
|
123
|
+
expect(data.eof).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("should support pagination with since_seq", async () => {
|
|
126
|
+
const res1 = await app.request("/api/v1/events?limit=5");
|
|
127
|
+
expect(res1.status).toBe(200);
|
|
128
|
+
const data1 = await res1.json();
|
|
129
|
+
expect(data1.items.length).toBe(5);
|
|
130
|
+
expect(data1.eof).toBe(false);
|
|
131
|
+
const nextSeq = data1.next_since_seq;
|
|
132
|
+
const res2 = await app.request(`/api/v1/events?since_seq=${nextSeq}&limit=5`);
|
|
133
|
+
expect(res2.status).toBe(200);
|
|
134
|
+
const data2 = await res2.json();
|
|
135
|
+
expect(data2.items.length).toBeGreaterThan(0);
|
|
136
|
+
});
|
|
137
|
+
it("should respect limit parameter", async () => {
|
|
138
|
+
const res = await app.request("/api/v1/events?limit=3");
|
|
139
|
+
expect(res.status).toBe(200);
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
expect(data.items.length).toBe(3);
|
|
142
|
+
expect(data.eof).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
it("should return empty array when no events exist", async () => {
|
|
145
|
+
// Clean up existing events
|
|
146
|
+
await getLogger().close();
|
|
147
|
+
resetLogger();
|
|
148
|
+
await fs.rm(logsDir, { recursive: true, force: true });
|
|
149
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
150
|
+
getLogger({
|
|
151
|
+
coconutId: "test-coconut",
|
|
152
|
+
logsDir: logsDir,
|
|
153
|
+
});
|
|
154
|
+
const res = await app.request("/api/v1/events");
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
expect(data.items).toEqual([]);
|
|
158
|
+
expect(data.next_since_seq).toBe(0);
|
|
159
|
+
expect(data.eof).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("GET /api/v1/events/status", () => {
|
|
163
|
+
beforeEach(async () => {
|
|
164
|
+
// Pre-populate with some events
|
|
165
|
+
const logger = getLogger();
|
|
166
|
+
for (let i = 1; i <= 5; i++) {
|
|
167
|
+
logger.log({
|
|
168
|
+
kind: "proposal.create",
|
|
169
|
+
subject: `proposal:cp-${i}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
await logger.flush();
|
|
173
|
+
});
|
|
174
|
+
it("should return logging system status", async () => {
|
|
175
|
+
const res = await app.request("/api/v1/events/status");
|
|
176
|
+
expect(res.status).toBe(200);
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
expect(data.currentFile).toBe("events-current.jsonl");
|
|
179
|
+
expect(data.sizeBytes).toBeGreaterThan(0);
|
|
180
|
+
expect(data.lastSeq).toBe(5);
|
|
181
|
+
expect(data.rotateBytes).toBe(128 * 1024 * 1024);
|
|
182
|
+
expect(data.logsDir).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
it("should handle missing log file gracefully", async () => {
|
|
185
|
+
// Clean up existing events
|
|
186
|
+
await getLogger().close();
|
|
187
|
+
resetLogger();
|
|
188
|
+
await fs.rm(logsDir, { recursive: true, force: true });
|
|
189
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
190
|
+
getLogger({
|
|
191
|
+
coconutId: "test-coconut",
|
|
192
|
+
logsDir: logsDir,
|
|
193
|
+
});
|
|
194
|
+
const res = await app.request("/api/v1/events/status");
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
expect(data.currentFile).toBeNull();
|
|
198
|
+
expect(data.sizeBytes).toBe(0);
|
|
199
|
+
expect(data.lastSeq).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe("POST /api/v1/events/purge", () => {
|
|
203
|
+
beforeEach(async () => {
|
|
204
|
+
// Create some rotated files with different timestamps
|
|
205
|
+
const oldDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
|
|
206
|
+
const recentDate = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000); // 1 day ago
|
|
207
|
+
await fs.writeFile(path.join(logsDir, "events-20250101-120000.jsonl"), "");
|
|
208
|
+
await fs.utimes(path.join(logsDir, "events-20250101-120000.jsonl"), oldDate, oldDate);
|
|
209
|
+
await fs.writeFile(path.join(logsDir, "events-20250115-120000.jsonl"), "");
|
|
210
|
+
await fs.utimes(path.join(logsDir, "events-20250115-120000.jsonl"), recentDate, recentDate);
|
|
211
|
+
});
|
|
212
|
+
it("should delete old rotated files", async () => {
|
|
213
|
+
const beforeIso = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(); // 3 days ago
|
|
214
|
+
const res = await app.request("/api/v1/events/purge", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ beforeIso }),
|
|
218
|
+
});
|
|
219
|
+
expect(res.status).toBe(200);
|
|
220
|
+
const data = await res.json();
|
|
221
|
+
expect(data.deleted).toBe(1);
|
|
222
|
+
// Verify old file was deleted and recent file remains
|
|
223
|
+
const files = await fs.readdir(logsDir);
|
|
224
|
+
expect(files).toContain("events-20250115-120000.jsonl");
|
|
225
|
+
expect(files).not.toContain("events-20250101-120000.jsonl");
|
|
226
|
+
});
|
|
227
|
+
it("should never delete current file", async () => {
|
|
228
|
+
const logger = getLogger();
|
|
229
|
+
logger.log({ kind: "proposal.create", subject: "proposal:cp-1" });
|
|
230
|
+
await logger.flush();
|
|
231
|
+
const futureDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // tomorrow
|
|
232
|
+
const res = await app.request("/api/v1/events/purge", {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ beforeIso: futureDate }),
|
|
236
|
+
});
|
|
237
|
+
expect(res.status).toBe(200);
|
|
238
|
+
// Current file should still exist
|
|
239
|
+
const currentFile = path.join(logsDir, "events-current.jsonl");
|
|
240
|
+
const exists = await fs
|
|
241
|
+
.access(currentFile)
|
|
242
|
+
.then(() => true)
|
|
243
|
+
.catch(() => false);
|
|
244
|
+
expect(exists).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
it("should return error for missing beforeIso", async () => {
|
|
247
|
+
const res = await app.request("/api/v1/events/purge", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "Content-Type": "application/json" },
|
|
250
|
+
body: JSON.stringify({}),
|
|
251
|
+
});
|
|
252
|
+
expect(res.status).toBe(400);
|
|
253
|
+
const data = await res.json();
|
|
254
|
+
expect(data.error).toBe("beforeIso is required");
|
|
255
|
+
});
|
|
256
|
+
it("should return error for invalid ISO date", async () => {
|
|
257
|
+
const res = await app.request("/api/v1/events/purge", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
body: JSON.stringify({ beforeIso: "not-a-date" }),
|
|
261
|
+
});
|
|
262
|
+
expect(res.status).toBe(400);
|
|
263
|
+
const data = await res.json();
|
|
264
|
+
expect(data.error).toBe("Invalid ISO date");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
describe("GET /api/v1/events/stream", () => {
|
|
268
|
+
it("should stream events via SSE", async () => {
|
|
269
|
+
// Pre-populate with some events
|
|
270
|
+
const logger = getLogger();
|
|
271
|
+
for (let i = 1; i <= 3; i++) {
|
|
272
|
+
logger.log({
|
|
273
|
+
kind: "proposal.create",
|
|
274
|
+
subject: `proposal:cp-${i}`,
|
|
275
|
+
payload: { index: i },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
await logger.flush();
|
|
279
|
+
const res = await app.request("/api/v1/events/stream");
|
|
280
|
+
expect(res.status).toBe(200);
|
|
281
|
+
expect(res.headers.get("content-type")).toContain("text/event-stream");
|
|
282
|
+
expect(res.headers.get("cache-control")).toContain("no-cache");
|
|
283
|
+
expect(res.headers.get("connection")).toContain("keep-alive");
|
|
284
|
+
// Note: Full SSE testing would require streaming the response body,
|
|
285
|
+
// which is complex in this test environment. This test validates
|
|
286
|
+
// that the endpoint returns the correct headers for SSE.
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events API routes index
|
|
3
|
+
* Routes for Coconut activity logging events ingestion, retrieval, and streaming
|
|
4
|
+
*/
|
|
5
|
+
import { Hono } from 'hono';
|
|
6
|
+
declare const events: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
7
|
+
export default events;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events API routes index
|
|
3
|
+
* Routes for Coconut activity logging events ingestion, retrieval, and streaming
|
|
4
|
+
*/
|
|
5
|
+
import { Hono } from 'hono';
|
|
6
|
+
import { GET, POST } from './route.js';
|
|
7
|
+
import { GET as getStream } from './stream/route.js';
|
|
8
|
+
import { GET as getStatus } from './status/route.js';
|
|
9
|
+
import { POST as postPurge } from './purge/route.js';
|
|
10
|
+
const events = new Hono();
|
|
11
|
+
// Main events endpoints
|
|
12
|
+
events.get('/', GET); // GET /api/v1/events?since_seq=&limit= - Fetch paged events
|
|
13
|
+
events.post('/', POST); // POST /api/v1/events - Enqueue events
|
|
14
|
+
// Streaming endpoint
|
|
15
|
+
events.get('/stream', getStream); // GET /api/v1/events/stream?since_seq= - SSE streaming
|
|
16
|
+
// Status and management endpoints
|
|
17
|
+
events.get('/status', getStatus); // GET /api/v1/events/status - System diagnostics
|
|
18
|
+
events.post('/purge', postPurge); // POST /api/v1/events/purge - Delete old rotated files
|
|
19
|
+
export default events;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events purge endpoint
|
|
3
|
+
*/
|
|
4
|
+
import { Context } from "hono";
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/v1/events/purge
|
|
7
|
+
* Delete rotated files older than a timestamp
|
|
8
|
+
* Note: The current file is never deleted
|
|
9
|
+
*/
|
|
10
|
+
export declare function POST(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
11
|
+
error: string;
|
|
12
|
+
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
13
|
+
error: string;
|
|
14
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
15
|
+
deleted: number;
|
|
16
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
17
|
+
error: string;
|
|
18
|
+
message: any;
|
|
19
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events purge endpoint
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { findGaitDirectory } from "../../../../../lib/gait-path.js";
|
|
7
|
+
/**
|
|
8
|
+
* Get the events directory path
|
|
9
|
+
*/
|
|
10
|
+
async function getEventsDir() {
|
|
11
|
+
const gaitDir = await findGaitDirectory();
|
|
12
|
+
if (!gaitDir)
|
|
13
|
+
return null;
|
|
14
|
+
return path.join(gaitDir, "logs");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* POST /api/v1/events/purge
|
|
18
|
+
* Delete rotated files older than a timestamp
|
|
19
|
+
* Note: The current file is never deleted
|
|
20
|
+
*/
|
|
21
|
+
export async function POST(c) {
|
|
22
|
+
try {
|
|
23
|
+
const eventsDir = await getEventsDir();
|
|
24
|
+
if (!eventsDir) {
|
|
25
|
+
return c.json({ error: "Events directory not found" }, 404);
|
|
26
|
+
}
|
|
27
|
+
const body = await c.req.json();
|
|
28
|
+
const beforeIso = body.beforeIso;
|
|
29
|
+
if (!beforeIso) {
|
|
30
|
+
return c.json({ error: "beforeIso is required" }, 400);
|
|
31
|
+
}
|
|
32
|
+
const beforeDate = new Date(beforeIso);
|
|
33
|
+
if (isNaN(beforeDate.getTime())) {
|
|
34
|
+
return c.json({ error: "Invalid ISO date" }, 400);
|
|
35
|
+
}
|
|
36
|
+
// Read all files in the events directory
|
|
37
|
+
const files = await fs.readdir(eventsDir);
|
|
38
|
+
let deleted = 0;
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
// Only process rotated event files (not events-current.jsonl or .seq)
|
|
41
|
+
if (!file.startsWith("events-") || file === "events-current.jsonl") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const filePath = path.join(eventsDir, file);
|
|
45
|
+
const stats = await fs.stat(filePath);
|
|
46
|
+
// Check if file is older than the threshold
|
|
47
|
+
if (stats.mtime < beforeDate) {
|
|
48
|
+
await fs.unlink(filePath);
|
|
49
|
+
deleted++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return c.json({
|
|
53
|
+
deleted,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return c.json({
|
|
58
|
+
error: "Failed to purge events",
|
|
59
|
+
message: error.message,
|
|
60
|
+
}, 500);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events API routes for Coconut activity logging
|
|
3
|
+
* Provides endpoints for logging, querying, and streaming events
|
|
4
|
+
*/
|
|
5
|
+
import { Context } from "hono";
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/v1/events
|
|
8
|
+
* Enqueue one or many events
|
|
9
|
+
*/
|
|
10
|
+
export declare function POST(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
11
|
+
accepted: number;
|
|
12
|
+
last_seq: number;
|
|
13
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
14
|
+
error: string;
|
|
15
|
+
message: any;
|
|
16
|
+
}, 500, "json">)>;
|
|
17
|
+
/**
|
|
18
|
+
* GET /api/v1/events?since_seq=&limit=
|
|
19
|
+
* Fetch a page of events from the current file
|
|
20
|
+
*/
|
|
21
|
+
export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
22
|
+
error: string;
|
|
23
|
+
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
24
|
+
items: any[];
|
|
25
|
+
next_since_seq: number;
|
|
26
|
+
eof: boolean;
|
|
27
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
28
|
+
error: string;
|
|
29
|
+
message: any;
|
|
30
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events API routes for Coconut activity logging
|
|
3
|
+
* Provides endpoints for logging, querying, and streaming events
|
|
4
|
+
*/
|
|
5
|
+
import { getLogger } from "@lovelybunch/core/logging";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import * as fsSync from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { findGaitDirectory } from "../../../../lib/gait-path.js";
|
|
10
|
+
import readline from "readline";
|
|
11
|
+
/**
|
|
12
|
+
* Get the events directory path
|
|
13
|
+
*/
|
|
14
|
+
async function getEventsDir() {
|
|
15
|
+
const gaitDir = await findGaitDirectory();
|
|
16
|
+
if (!gaitDir)
|
|
17
|
+
return null;
|
|
18
|
+
return path.join(gaitDir, "logs");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* POST /api/v1/events
|
|
22
|
+
* Enqueue one or many events
|
|
23
|
+
*/
|
|
24
|
+
export async function POST(c) {
|
|
25
|
+
try {
|
|
26
|
+
const body = await c.req.json();
|
|
27
|
+
const logger = getLogger();
|
|
28
|
+
// Handle array of events or single event
|
|
29
|
+
const events = Array.isArray(body) ? body : [body];
|
|
30
|
+
let lastSeq = 0;
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
logger.log(event);
|
|
33
|
+
lastSeq = logger.getSeq();
|
|
34
|
+
}
|
|
35
|
+
return c.json({
|
|
36
|
+
accepted: events.length,
|
|
37
|
+
last_seq: lastSeq,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return c.json({
|
|
42
|
+
error: "Failed to enqueue events",
|
|
43
|
+
message: error.message,
|
|
44
|
+
}, 500);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* GET /api/v1/events?since_seq=&limit=
|
|
49
|
+
* Fetch a page of events from the current file
|
|
50
|
+
*/
|
|
51
|
+
export async function GET(c) {
|
|
52
|
+
try {
|
|
53
|
+
const eventsDir = await getEventsDir();
|
|
54
|
+
if (!eventsDir) {
|
|
55
|
+
return c.json({ error: "Events directory not found" }, 404);
|
|
56
|
+
}
|
|
57
|
+
const url = new URL(c.req.url);
|
|
58
|
+
const sinceSeq = parseInt(url.searchParams.get("since_seq") || "0", 10);
|
|
59
|
+
const limit = Math.min(parseInt(url.searchParams.get("limit") || "1000", 10), 5000);
|
|
60
|
+
const currentFile = path.join(eventsDir, "events-current.jsonl");
|
|
61
|
+
// Check if file exists
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(currentFile);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return c.json({
|
|
67
|
+
items: [],
|
|
68
|
+
next_since_seq: 0,
|
|
69
|
+
eof: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Read events from file
|
|
73
|
+
const items = [];
|
|
74
|
+
const fileStream = fsSync.createReadStream(currentFile);
|
|
75
|
+
const rl = readline.createInterface({
|
|
76
|
+
input: fileStream,
|
|
77
|
+
crlfDelay: Infinity,
|
|
78
|
+
});
|
|
79
|
+
let lastSeq = sinceSeq;
|
|
80
|
+
let count = 0;
|
|
81
|
+
for await (const line of rl) {
|
|
82
|
+
if (!line.trim())
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
const event = JSON.parse(line);
|
|
86
|
+
if (event.seq > sinceSeq && count < limit) {
|
|
87
|
+
items.push(event);
|
|
88
|
+
count++;
|
|
89
|
+
}
|
|
90
|
+
lastSeq = Math.max(lastSeq, event.seq);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Skip malformed lines
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return c.json({
|
|
98
|
+
items,
|
|
99
|
+
next_since_seq: lastSeq,
|
|
100
|
+
eof: count < limit,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return c.json({
|
|
105
|
+
error: "Failed to fetch events",
|
|
106
|
+
message: error.message,
|
|
107
|
+
}, 500);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events status endpoint
|
|
3
|
+
*/
|
|
4
|
+
import { Context } from "hono";
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/v1/events/status
|
|
7
|
+
* Get logging system status and configuration
|
|
8
|
+
*/
|
|
9
|
+
export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
10
|
+
error: string;
|
|
11
|
+
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
12
|
+
currentFile: string;
|
|
13
|
+
sizeBytes: number;
|
|
14
|
+
lastSeq: number;
|
|
15
|
+
rotateBytes: number;
|
|
16
|
+
logsDir: string;
|
|
17
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
18
|
+
error: string;
|
|
19
|
+
message: any;
|
|
20
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events status endpoint
|
|
3
|
+
*/
|
|
4
|
+
import { getLogger } from "@lovelybunch/core/logging";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { findGaitDirectory } from "../../../../../lib/gait-path.js";
|
|
8
|
+
/**
|
|
9
|
+
* Get the events directory path
|
|
10
|
+
*/
|
|
11
|
+
async function getEventsDir() {
|
|
12
|
+
const gaitDir = await findGaitDirectory();
|
|
13
|
+
if (!gaitDir)
|
|
14
|
+
return null;
|
|
15
|
+
return path.join(gaitDir, "logs");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* GET /api/v1/events/status
|
|
19
|
+
* Get logging system status and configuration
|
|
20
|
+
*/
|
|
21
|
+
export async function GET(c) {
|
|
22
|
+
try {
|
|
23
|
+
const eventsDir = await getEventsDir();
|
|
24
|
+
if (!eventsDir) {
|
|
25
|
+
return c.json({ error: "Events directory not found" }, 404);
|
|
26
|
+
}
|
|
27
|
+
const logger = getLogger();
|
|
28
|
+
const currentFile = path.join(eventsDir, "events-current.jsonl");
|
|
29
|
+
let sizeBytes = 0;
|
|
30
|
+
let exists = false;
|
|
31
|
+
try {
|
|
32
|
+
const stats = await fs.stat(currentFile);
|
|
33
|
+
sizeBytes = stats.size;
|
|
34
|
+
exists = true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// File doesn't exist yet
|
|
38
|
+
}
|
|
39
|
+
return c.json({
|
|
40
|
+
currentFile: exists ? "events-current.jsonl" : null,
|
|
41
|
+
sizeBytes,
|
|
42
|
+
lastSeq: logger.getSeq(),
|
|
43
|
+
rotateBytes: 128 * 1024 * 1024, // 128MB default
|
|
44
|
+
logsDir: eventsDir,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return c.json({
|
|
49
|
+
error: "Failed to get status",
|
|
50
|
+
message: error.message,
|
|
51
|
+
}, 500);
|
|
52
|
+
}
|
|
53
|
+
}
|