cue-console 0.1.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/src/lib/db.ts ADDED
@@ -0,0 +1,581 @@
1
+ import Database from "better-sqlite3";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import type { UserResponse, Group } from "./types";
5
+
6
+ const DB_PATH = join(homedir(), ".cue", "cue.db");
7
+
8
+ let db: Database.Database | null = null;
9
+
10
+ export function getDb(): Database.Database {
11
+ if (!db) {
12
+ db = new Database(DB_PATH);
13
+ db.pragma("journal_mode = WAL");
14
+ initTables();
15
+ }
16
+ return db;
17
+ }
18
+
19
+ function initTables() {
20
+ const database = db!;
21
+
22
+ database.exec(`
23
+ CREATE TABLE IF NOT EXISTS agent_profiles (
24
+ agent_id TEXT PRIMARY KEY,
25
+ display_name TEXT NOT NULL,
26
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
27
+ )
28
+ `);
29
+
30
+ database.exec(`
31
+ CREATE TABLE IF NOT EXISTS cue_requests (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ request_id TEXT UNIQUE,
34
+ agent_id TEXT NOT NULL,
35
+ prompt TEXT NOT NULL,
36
+ payload TEXT,
37
+ status TEXT NOT NULL,
38
+ created_at DATETIME NOT NULL,
39
+ updated_at DATETIME NOT NULL
40
+ )
41
+ `);
42
+
43
+ database.exec(`
44
+ CREATE TABLE IF NOT EXISTS cue_responses (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ request_id TEXT UNIQUE,
47
+ response_json TEXT NOT NULL,
48
+ cancelled INTEGER NOT NULL,
49
+ created_at DATETIME NOT NULL,
50
+ FOREIGN KEY (request_id) REFERENCES cue_requests(request_id)
51
+ )
52
+ `);
53
+
54
+ // Groups table
55
+ database.exec(`
56
+ CREATE TABLE IF NOT EXISTS groups (
57
+ id TEXT PRIMARY KEY,
58
+ name TEXT NOT NULL,
59
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
60
+ )
61
+ `);
62
+
63
+ // Group members table
64
+ database.exec(`
65
+ CREATE TABLE IF NOT EXISTS group_members (
66
+ group_id TEXT NOT NULL,
67
+ agent_name TEXT NOT NULL,
68
+ joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
69
+ PRIMARY KEY (group_id, agent_name),
70
+ FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
71
+ )
72
+ `);
73
+ }
74
+
75
+ export function getAgentDisplayName(agentId: string): string | undefined {
76
+ const row = getDb()
77
+ .prepare(`SELECT display_name FROM agent_profiles WHERE agent_id = ?`)
78
+ .get(agentId) as { display_name: string } | undefined;
79
+ return row?.display_name;
80
+ }
81
+
82
+ export function upsertAgentDisplayName(agentId: string, displayName: string): void {
83
+ const clean = displayName.trim();
84
+ if (!clean) return;
85
+ getDb()
86
+ .prepare(
87
+ `INSERT INTO agent_profiles (agent_id, display_name, updated_at)
88
+ VALUES (?, ?, datetime('now'))
89
+ ON CONFLICT(agent_id) DO UPDATE SET
90
+ display_name = excluded.display_name,
91
+ updated_at = datetime('now')`
92
+ )
93
+ .run(agentId, clean);
94
+ }
95
+
96
+ export function getAgentDisplayNames(agentIds: string[]): Record<string, string> {
97
+ const unique = Array.from(new Set(agentIds.filter(Boolean)));
98
+ if (unique.length === 0) return {};
99
+ const placeholders = unique.map(() => "?").join(",");
100
+ const rows = getDb()
101
+ .prepare(
102
+ `SELECT agent_id, display_name
103
+ FROM agent_profiles
104
+ WHERE agent_id IN (${placeholders})`
105
+ )
106
+ .all(...unique) as Array<{ agent_id: string; display_name: string }>;
107
+ const map: Record<string, string> = {};
108
+ for (const r of rows) map[r.agent_id] = r.display_name;
109
+ return map;
110
+ }
111
+
112
+ // Type definitions
113
+ export interface CueRequest {
114
+ id: number;
115
+ request_id: string;
116
+ agent_id: string;
117
+ prompt: string;
118
+ payload: string | null;
119
+ status: "PENDING" | "COMPLETED" | "CANCELLED";
120
+ created_at: string;
121
+ updated_at: string;
122
+ }
123
+
124
+ export interface CueResponse {
125
+ id: number;
126
+ request_id: string;
127
+ response_json: string;
128
+ cancelled: boolean;
129
+ created_at: string;
130
+ }
131
+
132
+ export type AgentTimelineItem =
133
+ | {
134
+ item_type: "request";
135
+ time: string;
136
+ request: CueRequest;
137
+ }
138
+ | {
139
+ item_type: "response";
140
+ time: string;
141
+ response: CueResponse;
142
+ request_id: string;
143
+ };
144
+
145
+ // UserResponse, Group, GroupMember are imported from types.ts
146
+ export type { UserResponse, Group, GroupMember } from "./types";
147
+
148
+ // Query functions
149
+ export function getPendingRequests(): CueRequest[] {
150
+ return getDb()
151
+ .prepare(
152
+ `SELECT * FROM cue_requests
153
+ WHERE status = 'PENDING'
154
+ ORDER BY created_at DESC`
155
+ )
156
+ .all() as CueRequest[];
157
+ }
158
+
159
+ export function getRequestsByAgent(agentId: string): CueRequest[] {
160
+ return getDb()
161
+ .prepare(
162
+ `SELECT * FROM cue_requests
163
+ WHERE agent_id = ?
164
+ ORDER BY created_at ASC
165
+ LIMIT 50`
166
+ )
167
+ .all(agentId) as CueRequest[];
168
+ }
169
+
170
+ export function getResponsesByAgent(agentId: string): CueResponse[] {
171
+ return getDb()
172
+ .prepare(
173
+ `SELECT r.* FROM cue_responses r
174
+ JOIN cue_requests req ON r.request_id = req.request_id
175
+ WHERE req.agent_id = ?
176
+ ORDER BY r.created_at ASC
177
+ LIMIT 50`
178
+ )
179
+ .all(agentId) as CueResponse[];
180
+ }
181
+
182
+ export function getAgentLastResponse(agentId: string): CueResponse | undefined {
183
+ return getDb()
184
+ .prepare(
185
+ `SELECT r.* FROM cue_responses r
186
+ JOIN cue_requests req ON r.request_id = req.request_id
187
+ WHERE req.agent_id = ?
188
+ ORDER BY r.created_at DESC
189
+ LIMIT 1`
190
+ )
191
+ .get(agentId) as CueResponse | undefined;
192
+ }
193
+
194
+ export function getAgentTimeline(
195
+ agentId: string,
196
+ before: string | null,
197
+ limit: number
198
+ ): { items: AgentTimelineItem[]; nextCursor: string | null } {
199
+ const rows = getDb()
200
+ .prepare(
201
+ `SELECT * FROM (
202
+ SELECT
203
+ 'request' AS item_type,
204
+ req.created_at AS time,
205
+ req.request_id AS request_id,
206
+ req.id AS req_id,
207
+ req.agent_id AS agent_id,
208
+ req.prompt AS prompt,
209
+ req.payload AS payload,
210
+ req.status AS status,
211
+ req.created_at AS req_created_at,
212
+ req.updated_at AS req_updated_at,
213
+ NULL AS resp_id,
214
+ NULL AS response_json,
215
+ NULL AS cancelled,
216
+ NULL AS resp_created_at
217
+ FROM cue_requests req
218
+ WHERE req.agent_id = ?
219
+
220
+ UNION ALL
221
+
222
+ SELECT
223
+ 'response' AS item_type,
224
+ r.created_at AS time,
225
+ r.request_id AS request_id,
226
+ NULL AS req_id,
227
+ NULL AS agent_id,
228
+ NULL AS prompt,
229
+ NULL AS payload,
230
+ NULL AS status,
231
+ NULL AS req_created_at,
232
+ NULL AS req_updated_at,
233
+ r.id AS resp_id,
234
+ r.response_json AS response_json,
235
+ r.cancelled AS cancelled,
236
+ r.created_at AS resp_created_at
237
+ FROM cue_responses r
238
+ JOIN cue_requests req2 ON r.request_id = req2.request_id
239
+ WHERE req2.agent_id = ?
240
+ )
241
+ WHERE (? IS NULL OR time < ?)
242
+ ORDER BY time DESC
243
+ LIMIT ?`
244
+ )
245
+ .all(agentId, agentId, before, before, limit) as Array<
246
+ | {
247
+ item_type: "request";
248
+ time: string;
249
+ request_id: string;
250
+ req_id: number;
251
+ agent_id: string;
252
+ prompt: string;
253
+ payload: string | null;
254
+ status: CueRequest["status"];
255
+ req_created_at: string;
256
+ req_updated_at: string;
257
+ }
258
+ | {
259
+ item_type: "response";
260
+ time: string;
261
+ request_id: string;
262
+ resp_id: number;
263
+ response_json: string;
264
+ cancelled: 0 | 1;
265
+ resp_created_at: string;
266
+ }
267
+ >;
268
+
269
+ const items: AgentTimelineItem[] = rows.map((row) => {
270
+ if (row.item_type === "request") {
271
+ return {
272
+ item_type: "request",
273
+ time: row.time,
274
+ request: {
275
+ id: row.req_id,
276
+ request_id: row.request_id,
277
+ agent_id: row.agent_id,
278
+ prompt: row.prompt,
279
+ payload: row.payload,
280
+ status: row.status,
281
+ created_at: row.req_created_at,
282
+ updated_at: row.req_updated_at,
283
+ },
284
+ };
285
+ }
286
+ return {
287
+ item_type: "response",
288
+ time: row.time,
289
+ request_id: row.request_id,
290
+ response: {
291
+ id: row.resp_id,
292
+ request_id: row.request_id,
293
+ response_json: row.response_json,
294
+ cancelled: row.cancelled === 1,
295
+ created_at: row.resp_created_at,
296
+ },
297
+ };
298
+ });
299
+
300
+ const nextCursor = items.length > 0 ? items[items.length - 1].time : null;
301
+ return { items, nextCursor };
302
+ }
303
+
304
+ export function getAllAgents(): string[] {
305
+ const results = getDb()
306
+ .prepare(
307
+ `SELECT agent_id, MAX(created_at) as last_time FROM cue_requests
308
+ WHERE agent_id != ''
309
+ GROUP BY agent_id
310
+ ORDER BY last_time DESC`
311
+ )
312
+ .all() as { agent_id: string }[];
313
+ return results.map((r) => r.agent_id);
314
+ }
315
+
316
+ export function getAgentLastRequest(
317
+ agentId: string
318
+ ): CueRequest | undefined {
319
+ return getDb()
320
+ .prepare(
321
+ `SELECT * FROM cue_requests
322
+ WHERE agent_id = ?
323
+ ORDER BY created_at DESC
324
+ LIMIT 1`
325
+ )
326
+ .get(agentId) as CueRequest | undefined;
327
+ }
328
+
329
+ export function getPendingCountByAgent(agentId: string): number {
330
+ const result = getDb()
331
+ .prepare(
332
+ `SELECT COUNT(*) as count FROM cue_requests
333
+ WHERE agent_id = ? AND status = 'PENDING'`
334
+ )
335
+ .get(agentId) as { count: number };
336
+ return result.count;
337
+ }
338
+
339
+ export function sendResponse(
340
+ requestId: string,
341
+ response: UserResponse,
342
+ cancelled: boolean = false
343
+ ): void {
344
+ const db = getDb();
345
+
346
+ // Insert response
347
+ db.prepare(
348
+ `INSERT OR IGNORE INTO cue_responses (request_id, response_json, cancelled, created_at)
349
+ VALUES (?, ?, ?, datetime('now'))`
350
+ ).run(requestId, JSON.stringify(response), cancelled ? 1 : 0);
351
+
352
+ // Update request status
353
+ db.prepare(
354
+ `UPDATE cue_requests
355
+ SET status = ?
356
+ WHERE request_id = ?`
357
+ ).run(cancelled ? "CANCELLED" : "COMPLETED", requestId);
358
+ }
359
+
360
+ // Group-related functions
361
+ export function createGroup(id: string, name: string): void {
362
+ getDb()
363
+ .prepare(`INSERT INTO groups (id, name) VALUES (?, ?)`)
364
+ .run(id, name);
365
+ }
366
+
367
+ export function getAllGroups(): Group[] {
368
+ return getDb()
369
+ .prepare(`SELECT * FROM groups ORDER BY created_at DESC`)
370
+ .all() as Group[];
371
+ }
372
+
373
+ export function getGroupMembers(groupId: string): string[] {
374
+ const results = getDb()
375
+ .prepare(`SELECT agent_name FROM group_members WHERE group_id = ?`)
376
+ .all(groupId) as { agent_name: string }[];
377
+ return results.map((r) => r.agent_name);
378
+ }
379
+
380
+ export function addGroupMember(groupId: string, agentName: string): void {
381
+ getDb()
382
+ .prepare(
383
+ `INSERT OR IGNORE INTO group_members (group_id, agent_name) VALUES (?, ?)`
384
+ )
385
+ .run(groupId, agentName);
386
+ }
387
+
388
+ export function removeGroupMember(groupId: string, agentName: string): void {
389
+ getDb()
390
+ .prepare(`DELETE FROM group_members WHERE group_id = ? AND agent_name = ?`)
391
+ .run(groupId, agentName);
392
+ }
393
+
394
+ export function deleteGroup(groupId: string): void {
395
+ getDb().prepare(`DELETE FROM groups WHERE id = ?`).run(groupId);
396
+ }
397
+
398
+ export function updateGroupName(groupId: string, name: string): void {
399
+ const clean = name.trim();
400
+ if (!clean) return;
401
+ getDb().prepare(`UPDATE groups SET name = ? WHERE id = ?`).run(clean, groupId);
402
+ }
403
+
404
+ export function getGroupPendingCount(groupId: string): number {
405
+ const members = getGroupMembers(groupId);
406
+ if (members.length === 0) return 0;
407
+
408
+ const placeholders = members.map(() => "?").join(",");
409
+ const result = getDb()
410
+ .prepare(
411
+ `SELECT COUNT(*) as count FROM cue_requests
412
+ WHERE agent_id IN (${placeholders}) AND status = 'PENDING'`
413
+ )
414
+ .get(...members) as { count: number };
415
+ return result.count;
416
+ }
417
+
418
+ export function getGroupPendingRequests(groupId: string): CueRequest[] {
419
+ const members = getGroupMembers(groupId);
420
+ if (members.length === 0) return [];
421
+
422
+ const placeholders = members.map(() => "?").join(",");
423
+ return getDb()
424
+ .prepare(
425
+ `SELECT * FROM cue_requests
426
+ WHERE agent_id IN (${placeholders}) AND status = 'PENDING'
427
+ ORDER BY created_at ASC`
428
+ )
429
+ .all(...members) as CueRequest[];
430
+ }
431
+
432
+ export function getGroupLastRequest(groupId: string): CueRequest | undefined {
433
+ const members = getGroupMembers(groupId);
434
+ if (members.length === 0) return undefined;
435
+
436
+ const placeholders = members.map(() => "?").join(",");
437
+ return getDb()
438
+ .prepare(
439
+ `SELECT * FROM cue_requests
440
+ WHERE agent_id IN (${placeholders})
441
+ ORDER BY created_at DESC
442
+ LIMIT 1`
443
+ )
444
+ .get(...members) as CueRequest | undefined;
445
+ }
446
+
447
+ export function getGroupLastResponse(groupId: string): CueResponse | undefined {
448
+ const members = getGroupMembers(groupId);
449
+ if (members.length === 0) return undefined;
450
+
451
+ const placeholders = members.map(() => "?").join(",");
452
+ return getDb()
453
+ .prepare(
454
+ `SELECT r.* FROM cue_responses r
455
+ JOIN cue_requests req ON r.request_id = req.request_id
456
+ WHERE req.agent_id IN (${placeholders})
457
+ ORDER BY r.created_at DESC
458
+ LIMIT 1`
459
+ )
460
+ .get(...members) as CueResponse | undefined;
461
+ }
462
+
463
+ export function getGroupTimeline(
464
+ groupId: string,
465
+ before: string | null,
466
+ limit: number
467
+ ): { items: AgentTimelineItem[]; nextCursor: string | null } {
468
+ const members = getGroupMembers(groupId);
469
+ if (members.length === 0) return { items: [], nextCursor: null };
470
+
471
+ const placeholders = members.map(() => "?").join(",");
472
+ const query = `SELECT * FROM (
473
+ SELECT
474
+ 'request' AS item_type,
475
+ req.created_at AS time,
476
+ req.request_id AS request_id,
477
+ req.id AS req_id,
478
+ req.agent_id AS agent_id,
479
+ req.prompt AS prompt,
480
+ req.payload AS payload,
481
+ req.status AS status,
482
+ req.created_at AS req_created_at,
483
+ req.updated_at AS req_updated_at,
484
+ NULL AS resp_id,
485
+ NULL AS response_json,
486
+ NULL AS cancelled,
487
+ NULL AS resp_created_at
488
+ FROM cue_requests req
489
+ WHERE req.agent_id IN (${placeholders})
490
+
491
+ UNION ALL
492
+
493
+ SELECT
494
+ 'response' AS item_type,
495
+ r.created_at AS time,
496
+ r.request_id AS request_id,
497
+ NULL AS req_id,
498
+ NULL AS agent_id,
499
+ NULL AS prompt,
500
+ NULL AS payload,
501
+ NULL AS status,
502
+ NULL AS req_created_at,
503
+ NULL AS req_updated_at,
504
+ r.id AS resp_id,
505
+ r.response_json AS response_json,
506
+ r.cancelled AS cancelled,
507
+ r.created_at AS resp_created_at
508
+ FROM cue_responses r
509
+ JOIN cue_requests req2 ON r.request_id = req2.request_id
510
+ WHERE req2.agent_id IN (${placeholders})
511
+ )
512
+ WHERE (? IS NULL OR time < ?)
513
+ ORDER BY time DESC
514
+ LIMIT ?`;
515
+
516
+ const rows = getDb()
517
+ .prepare(query)
518
+ .all(
519
+ ...members,
520
+ ...members,
521
+ before,
522
+ before,
523
+ limit
524
+ ) as Array<
525
+ | {
526
+ item_type: "request";
527
+ time: string;
528
+ request_id: string;
529
+ req_id: number;
530
+ agent_id: string;
531
+ prompt: string;
532
+ payload: string | null;
533
+ status: CueRequest["status"];
534
+ req_created_at: string;
535
+ req_updated_at: string;
536
+ }
537
+ | {
538
+ item_type: "response";
539
+ time: string;
540
+ request_id: string;
541
+ resp_id: number;
542
+ response_json: string;
543
+ cancelled: 0 | 1;
544
+ resp_created_at: string;
545
+ }
546
+ >;
547
+
548
+ const items: AgentTimelineItem[] = rows.map((row) => {
549
+ if (row.item_type === "request") {
550
+ return {
551
+ item_type: "request",
552
+ time: row.time,
553
+ request: {
554
+ id: row.req_id,
555
+ request_id: row.request_id,
556
+ agent_id: row.agent_id,
557
+ prompt: row.prompt,
558
+ payload: row.payload,
559
+ status: row.status,
560
+ created_at: row.req_created_at,
561
+ updated_at: row.req_updated_at,
562
+ },
563
+ };
564
+ }
565
+ return {
566
+ item_type: "response",
567
+ time: row.time,
568
+ request_id: row.request_id,
569
+ response: {
570
+ id: row.resp_id,
571
+ request_id: row.request_id,
572
+ response_json: row.response_json,
573
+ cancelled: row.cancelled === 1,
574
+ created_at: row.resp_created_at,
575
+ },
576
+ };
577
+ });
578
+
579
+ const nextCursor = items.length > 0 ? items[items.length - 1].time : null;
580
+ return { items, nextCursor };
581
+ }
@@ -0,0 +1,89 @@
1
+ /* tslint:disable */
2
+ /**
3
+ /* This file was automatically generated from pydantic models by running pydantic2ts.
4
+ /* Do not modify it by hand - just update the pydantic models and then re-run the script
5
+ */
6
+
7
+ /**
8
+ * Request status (SQLModel stores enum names in uppercase)
9
+ */
10
+ export type RequestStatus = "PENDING" | "COMPLETED" | "CANCELLED";
11
+
12
+ /**
13
+ * Request from MCP -> client (cue-hub / simulator)
14
+ */
15
+ export interface CueRequest {
16
+ id?: number | null;
17
+ request_id: string;
18
+ agent_id?: string;
19
+ prompt: string;
20
+ payload?: string | null;
21
+ status?: RequestStatus;
22
+ created_at?: string;
23
+ updated_at?: string;
24
+ }
25
+ /**
26
+ * Response from client (cue-hub / simulator) -> MCP
27
+ */
28
+ export interface CueResponse {
29
+ id?: number | null;
30
+ request_id: string;
31
+ response_json: string;
32
+ cancelled?: boolean;
33
+ created_at?: string;
34
+ }
35
+ /**
36
+ * Image content
37
+ */
38
+ export interface ImageContent {
39
+ mime_type: string;
40
+ base64_data: string;
41
+ }
42
+
43
+ export interface Mention {
44
+ userId: string;
45
+ start: number;
46
+ length: number;
47
+ display: string;
48
+ }
49
+ /**
50
+ * User response
51
+ */
52
+ export interface UserResponse {
53
+ text?: string;
54
+ images?: ImageContent[];
55
+ mentions?: Mention[];
56
+ }
57
+
58
+ // ============ Frontend extension types ============
59
+
60
+ /**
61
+ * Group
62
+ */
63
+ export interface Group {
64
+ id: string;
65
+ name: string;
66
+ created_at: string;
67
+ }
68
+
69
+ /**
70
+ * Group member
71
+ */
72
+ export interface GroupMember {
73
+ group_id: string;
74
+ agent_name: string;
75
+ joined_at: string;
76
+ }
77
+
78
+ /**
79
+ * Conversation list item
80
+ */
81
+ export interface ConversationItem {
82
+ id: string;
83
+ name: string;
84
+ displayName: string;
85
+ type: "agent" | "group";
86
+ lastMessage?: string;
87
+ lastTime?: string;
88
+ pendingCount: number;
89
+ }