@castlekit/castle 0.3.0 → 0.3.1
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/LICENSE +21 -0
- package/install.sh +20 -1
- package/package.json +17 -2
- package/src/app/api/openclaw/agents/route.ts +7 -1
- package/src/app/api/openclaw/chat/channels/route.ts +6 -3
- package/src/app/api/openclaw/chat/route.ts +17 -6
- package/src/app/api/openclaw/chat/search/route.ts +2 -1
- package/src/app/api/openclaw/config/route.ts +2 -0
- package/src/app/api/openclaw/events/route.ts +23 -8
- package/src/app/api/openclaw/ping/route.ts +5 -0
- package/src/app/api/openclaw/session/context/route.ts +163 -0
- package/src/app/api/openclaw/session/status/route.ts +179 -11
- package/src/app/api/openclaw/sessions/route.ts +2 -0
- package/src/app/chat/[channelId]/page.tsx +115 -35
- package/src/app/globals.css +10 -0
- package/src/app/page.tsx +10 -8
- package/src/components/chat/chat-input.tsx +23 -5
- package/src/components/chat/message-bubble.tsx +29 -13
- package/src/components/chat/message-list.tsx +238 -80
- package/src/components/chat/session-stats-panel.tsx +391 -86
- package/src/components/providers/search-provider.tsx +33 -4
- package/src/lib/db/index.ts +12 -2
- package/src/lib/db/queries.ts +199 -72
- package/src/lib/db/schema.ts +4 -0
- package/src/lib/gateway-connection.ts +24 -3
- package/src/lib/hooks/use-chat.ts +219 -241
- package/src/lib/hooks/use-compaction-events.ts +132 -0
- package/src/lib/hooks/use-context-boundary.ts +82 -0
- package/src/lib/hooks/use-openclaw.ts +44 -57
- package/src/lib/hooks/use-search.ts +1 -0
- package/src/lib/hooks/use-session-stats.ts +4 -1
- package/src/lib/sse-singleton.ts +184 -0
- package/src/lib/types/chat.ts +22 -6
- package/src/lib/db/__tests__/queries.test.ts +0 -318
- package/vitest.config.ts +0 -13
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import Database from "better-sqlite3";
|
|
3
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
4
|
-
import { mkdtempSync, rmSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { tmpdir } from "os";
|
|
7
|
-
import * as schema from "../schema";
|
|
8
|
-
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Test-specific DB setup (in-memory or temp file)
|
|
11
|
-
// ============================================================================
|
|
12
|
-
|
|
13
|
-
let tmpDir: string;
|
|
14
|
-
|
|
15
|
-
const TABLE_SQL = `
|
|
16
|
-
CREATE TABLE IF NOT EXISTS channels (
|
|
17
|
-
id TEXT PRIMARY KEY,
|
|
18
|
-
name TEXT NOT NULL,
|
|
19
|
-
default_agent_id TEXT NOT NULL,
|
|
20
|
-
created_at INTEGER NOT NULL,
|
|
21
|
-
updated_at INTEGER
|
|
22
|
-
);
|
|
23
|
-
CREATE TABLE IF NOT EXISTS channel_agents (
|
|
24
|
-
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
25
|
-
agent_id TEXT NOT NULL,
|
|
26
|
-
PRIMARY KEY (channel_id, agent_id)
|
|
27
|
-
);
|
|
28
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
29
|
-
id TEXT PRIMARY KEY,
|
|
30
|
-
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
31
|
-
session_key TEXT,
|
|
32
|
-
started_at INTEGER NOT NULL,
|
|
33
|
-
ended_at INTEGER,
|
|
34
|
-
summary TEXT,
|
|
35
|
-
total_input_tokens INTEGER DEFAULT 0,
|
|
36
|
-
total_output_tokens INTEGER DEFAULT 0
|
|
37
|
-
);
|
|
38
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
39
|
-
id TEXT PRIMARY KEY,
|
|
40
|
-
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
41
|
-
session_id TEXT REFERENCES sessions(id),
|
|
42
|
-
sender_type TEXT NOT NULL,
|
|
43
|
-
sender_id TEXT NOT NULL,
|
|
44
|
-
sender_name TEXT,
|
|
45
|
-
content TEXT NOT NULL DEFAULT '',
|
|
46
|
-
status TEXT NOT NULL DEFAULT 'complete',
|
|
47
|
-
mentioned_agent_id TEXT,
|
|
48
|
-
run_id TEXT,
|
|
49
|
-
session_key TEXT,
|
|
50
|
-
input_tokens INTEGER,
|
|
51
|
-
output_tokens INTEGER,
|
|
52
|
-
created_at INTEGER NOT NULL
|
|
53
|
-
);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS idx_messages_run_id ON messages(run_id);
|
|
56
|
-
CREATE TABLE IF NOT EXISTS message_attachments (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
59
|
-
attachment_type TEXT NOT NULL,
|
|
60
|
-
file_path TEXT NOT NULL,
|
|
61
|
-
mime_type TEXT,
|
|
62
|
-
file_size INTEGER,
|
|
63
|
-
original_name TEXT,
|
|
64
|
-
created_at INTEGER NOT NULL
|
|
65
|
-
);
|
|
66
|
-
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
67
|
-
id TEXT PRIMARY KEY,
|
|
68
|
-
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
69
|
-
agent_id TEXT,
|
|
70
|
-
emoji TEXT NOT NULL,
|
|
71
|
-
emoji_char TEXT NOT NULL,
|
|
72
|
-
created_at INTEGER NOT NULL
|
|
73
|
-
);
|
|
74
|
-
`;
|
|
75
|
-
|
|
76
|
-
const FTS5_SQL = `
|
|
77
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts
|
|
78
|
-
USING fts5(content, tokenize='unicode61');
|
|
79
|
-
|
|
80
|
-
CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
|
|
81
|
-
INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
82
|
-
END;
|
|
83
|
-
CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
|
|
84
|
-
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', OLD.rowid, OLD.content);
|
|
85
|
-
END;
|
|
86
|
-
CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE OF content ON messages BEGIN
|
|
87
|
-
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', OLD.rowid, OLD.content);
|
|
88
|
-
INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
89
|
-
END;
|
|
90
|
-
`;
|
|
91
|
-
|
|
92
|
-
function createTestDb() {
|
|
93
|
-
tmpDir = mkdtempSync(join(tmpdir(), "castle-test-"));
|
|
94
|
-
const dbPath = join(tmpDir, "test.db");
|
|
95
|
-
const sqlite = new Database(dbPath);
|
|
96
|
-
sqlite.pragma("journal_mode = WAL");
|
|
97
|
-
sqlite.pragma("foreign_keys = ON");
|
|
98
|
-
sqlite.exec(TABLE_SQL);
|
|
99
|
-
sqlite.exec(FTS5_SQL);
|
|
100
|
-
return { db: drizzle(sqlite, { schema }), sqlite, dbPath };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// Tests
|
|
105
|
-
// ============================================================================
|
|
106
|
-
|
|
107
|
-
describe("Database Queries", () => {
|
|
108
|
-
let db: ReturnType<typeof drizzle>;
|
|
109
|
-
let sqlite: Database.Database;
|
|
110
|
-
|
|
111
|
-
beforeAll(() => {
|
|
112
|
-
const setup = createTestDb();
|
|
113
|
-
db = setup.db;
|
|
114
|
-
sqlite = setup.sqlite;
|
|
115
|
-
|
|
116
|
-
// Override the global DB for our query functions
|
|
117
|
-
const DB_KEY = "__castle_db__";
|
|
118
|
-
(globalThis as Record<string, unknown>)[DB_KEY] = db;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
afterAll(() => {
|
|
122
|
-
sqlite.close();
|
|
123
|
-
try { rmSync(tmpDir, { recursive: true }); } catch { /* ignore */ }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ---- Channels ----
|
|
127
|
-
|
|
128
|
-
describe("Channels", () => {
|
|
129
|
-
it("should create and retrieve a channel", async () => {
|
|
130
|
-
const { createChannel, getChannel, getChannels } = await import("../queries");
|
|
131
|
-
|
|
132
|
-
const channel = createChannel("Test Channel", "agent-1", ["agent-1", "agent-2"]);
|
|
133
|
-
expect(channel.name).toBe("Test Channel");
|
|
134
|
-
expect(channel.defaultAgentId).toBe("agent-1");
|
|
135
|
-
expect(channel.agents).toContain("agent-1");
|
|
136
|
-
expect(channel.agents).toContain("agent-2");
|
|
137
|
-
|
|
138
|
-
const fetched = getChannel(channel.id);
|
|
139
|
-
expect(fetched).not.toBeNull();
|
|
140
|
-
expect(fetched!.name).toBe("Test Channel");
|
|
141
|
-
|
|
142
|
-
const all = getChannels();
|
|
143
|
-
expect(all.length).toBeGreaterThanOrEqual(1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("should update a channel name", async () => {
|
|
147
|
-
const { createChannel, updateChannel, getChannel } = await import("../queries");
|
|
148
|
-
|
|
149
|
-
const channel = createChannel("Original", "agent-1");
|
|
150
|
-
updateChannel(channel.id, { name: "Updated" });
|
|
151
|
-
|
|
152
|
-
const fetched = getChannel(channel.id);
|
|
153
|
-
expect(fetched!.name).toBe("Updated");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("should delete a channel", async () => {
|
|
157
|
-
const { createChannel, deleteChannel, getChannel } = await import("../queries");
|
|
158
|
-
|
|
159
|
-
const channel = createChannel("To Delete", "agent-1");
|
|
160
|
-
const deleted = deleteChannel(channel.id);
|
|
161
|
-
expect(deleted).toBe(true);
|
|
162
|
-
|
|
163
|
-
const fetched = getChannel(channel.id);
|
|
164
|
-
expect(fetched).toBeNull();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// ---- Messages ----
|
|
169
|
-
|
|
170
|
-
describe("Messages", () => {
|
|
171
|
-
it("should create and retrieve messages", async () => {
|
|
172
|
-
const { createChannel, createMessage, getMessagesByChannel } = await import("../queries");
|
|
173
|
-
|
|
174
|
-
const channel = createChannel("Msg Test", "agent-1");
|
|
175
|
-
|
|
176
|
-
createMessage({
|
|
177
|
-
channelId: channel.id,
|
|
178
|
-
senderType: "user",
|
|
179
|
-
senderId: "local-user",
|
|
180
|
-
senderName: "You",
|
|
181
|
-
content: "Hello there",
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
createMessage({
|
|
185
|
-
channelId: channel.id,
|
|
186
|
-
senderType: "agent",
|
|
187
|
-
senderId: "agent-1",
|
|
188
|
-
senderName: "Agent",
|
|
189
|
-
content: "Hi! How can I help?",
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const messages = getMessagesByChannel(channel.id);
|
|
193
|
-
expect(messages.length).toBe(2);
|
|
194
|
-
expect(messages[0].content).toBe("Hello there");
|
|
195
|
-
expect(messages[1].content).toBe("Hi! How can I help?");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("should support pagination with before cursor", async () => {
|
|
199
|
-
const { createChannel, createMessage, getMessagesByChannel } = await import("../queries");
|
|
200
|
-
|
|
201
|
-
const channel = createChannel("Pagination Test", "agent-1");
|
|
202
|
-
|
|
203
|
-
// Create 5 messages with slight time gaps
|
|
204
|
-
const ids: string[] = [];
|
|
205
|
-
for (let i = 0; i < 5; i++) {
|
|
206
|
-
const msg = createMessage({
|
|
207
|
-
channelId: channel.id,
|
|
208
|
-
senderType: "user",
|
|
209
|
-
senderId: "local-user",
|
|
210
|
-
content: `Message ${i}`,
|
|
211
|
-
});
|
|
212
|
-
ids.push(msg.id);
|
|
213
|
-
// Small delay to ensure different timestamps
|
|
214
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Get latest 3
|
|
218
|
-
const latest = getMessagesByChannel(channel.id, 3);
|
|
219
|
-
expect(latest.length).toBe(3);
|
|
220
|
-
|
|
221
|
-
// Get messages before the oldest of the latest
|
|
222
|
-
const older = getMessagesByChannel(channel.id, 3, latest[0].id);
|
|
223
|
-
expect(older.length).toBe(2);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("should update message status", async () => {
|
|
227
|
-
const { createChannel, createMessage, updateMessage, getMessagesByChannel } = await import("../queries");
|
|
228
|
-
|
|
229
|
-
const channel = createChannel("Status Test", "agent-1");
|
|
230
|
-
const msg = createMessage({
|
|
231
|
-
channelId: channel.id,
|
|
232
|
-
senderType: "agent",
|
|
233
|
-
senderId: "agent-1",
|
|
234
|
-
content: "Partial response...",
|
|
235
|
-
status: "complete",
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
updateMessage(msg.id, { status: "interrupted" });
|
|
239
|
-
|
|
240
|
-
const messages = getMessagesByChannel(channel.id);
|
|
241
|
-
expect(messages[0].status).toBe("interrupted");
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// ---- Search (FTS5) ----
|
|
246
|
-
|
|
247
|
-
describe("Search (FTS5)", () => {
|
|
248
|
-
it("should find messages by content", async () => {
|
|
249
|
-
const { createChannel, createMessage, searchMessages } = await import("../queries");
|
|
250
|
-
|
|
251
|
-
const channel = createChannel("Search Test", "agent-1");
|
|
252
|
-
|
|
253
|
-
createMessage({
|
|
254
|
-
channelId: channel.id,
|
|
255
|
-
senderType: "user",
|
|
256
|
-
senderId: "local-user",
|
|
257
|
-
content: "The quick brown fox jumps over the lazy dog",
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
createMessage({
|
|
261
|
-
channelId: channel.id,
|
|
262
|
-
senderType: "agent",
|
|
263
|
-
senderId: "agent-1",
|
|
264
|
-
content: "Here is some completely different text about cats",
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const results = searchMessages("brown fox");
|
|
268
|
-
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
269
|
-
expect(results[0].content).toContain("brown fox");
|
|
270
|
-
|
|
271
|
-
const noResults = searchMessages("nonexistent_xyz_term");
|
|
272
|
-
expect(noResults.length).toBe(0);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("should reject overly long queries", async () => {
|
|
276
|
-
const { searchMessages } = await import("../queries");
|
|
277
|
-
const longQuery = "a".repeat(501);
|
|
278
|
-
const results = searchMessages(longQuery);
|
|
279
|
-
expect(results).toEqual([]);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// ---- Sessions ----
|
|
284
|
-
|
|
285
|
-
describe("Sessions", () => {
|
|
286
|
-
it("should create and retrieve sessions", async () => {
|
|
287
|
-
const { createChannel, createSession, getSessionsByChannel, getLatestSessionKey } = await import("../queries");
|
|
288
|
-
|
|
289
|
-
const channel = createChannel("Session Test", "agent-1");
|
|
290
|
-
|
|
291
|
-
const session = createSession({
|
|
292
|
-
channelId: channel.id,
|
|
293
|
-
sessionKey: "sk_test_123",
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
expect(session.sessionKey).toBe("sk_test_123");
|
|
297
|
-
|
|
298
|
-
const sessions = getSessionsByChannel(channel.id);
|
|
299
|
-
expect(sessions.length).toBe(1);
|
|
300
|
-
|
|
301
|
-
const key = getLatestSessionKey(channel.id);
|
|
302
|
-
expect(key).toBe("sk_test_123");
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// ---- Storage Stats ----
|
|
307
|
-
|
|
308
|
-
describe("Storage Stats", () => {
|
|
309
|
-
it("should return correct counts", async () => {
|
|
310
|
-
const { getStorageStats } = await import("../queries");
|
|
311
|
-
|
|
312
|
-
const stats = getStorageStats();
|
|
313
|
-
expect(stats.messages).toBeGreaterThan(0);
|
|
314
|
-
expect(stats.channels).toBeGreaterThan(0);
|
|
315
|
-
expect(typeof stats.totalAttachmentBytes).toBe("number");
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
});
|
package/vitest.config.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
test: {
|
|
6
|
-
globals: true,
|
|
7
|
-
environment: "node",
|
|
8
|
-
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
|
9
|
-
alias: {
|
|
10
|
-
"@": resolve(__dirname, "src"),
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
});
|