@glwhappen/web-code 1.32.1 → 1.32.3

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.
Files changed (29) hide show
  1. package/dist/assets/{index-CfT-2Nkf.js → index-B_OAjmCW.js} +270 -265
  2. package/dist/assets/index-CF_baW77.css +32 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/modules/database/index.js +1 -0
  5. package/dist-server/server/modules/database/index.js.map +1 -1
  6. package/dist-server/server/modules/database/migrations.js +3 -1
  7. package/dist-server/server/modules/database/migrations.js.map +1 -1
  8. package/dist-server/server/modules/database/repositories/queued-messages.db.integration.test.js +73 -0
  9. package/dist-server/server/modules/database/repositories/queued-messages.db.integration.test.js.map +1 -0
  10. package/dist-server/server/modules/database/repositories/queued-messages.db.js +80 -0
  11. package/dist-server/server/modules/database/repositories/queued-messages.db.js.map +1 -0
  12. package/dist-server/server/modules/database/schema.js +18 -0
  13. package/dist-server/server/modules/database/schema.js.map +1 -1
  14. package/dist-server/server/modules/projects/index.js +1 -0
  15. package/dist-server/server/modules/projects/index.js.map +1 -1
  16. package/dist-server/server/modules/providers/provider.routes.js +87 -0
  17. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  18. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +1 -1
  19. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  20. package/package.json +1 -1
  21. package/server/modules/database/index.ts +1 -0
  22. package/server/modules/database/migrations.ts +3 -0
  23. package/server/modules/database/repositories/queued-messages.db.integration.test.ts +84 -0
  24. package/server/modules/database/repositories/queued-messages.db.ts +154 -0
  25. package/server/modules/database/schema.ts +19 -0
  26. package/server/modules/projects/index.ts +3 -0
  27. package/server/modules/providers/provider.routes.ts +109 -0
  28. package/server/modules/websocket/services/chat-websocket.service.ts +1 -1
  29. package/dist/assets/index-Ct6oPUQk.css +0 -32
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { getConnection } from '@/modules/database/connection.js';
4
+
5
+ export type QueuedMessageRow = {
6
+ id: string;
7
+ user_id: number;
8
+ session_id: string;
9
+ provider: string;
10
+ content: string;
11
+ permission_mode: string | null;
12
+ model: string | null;
13
+ metadata_json: string | null;
14
+ created_at: string;
15
+ updated_at: string;
16
+ };
17
+
18
+ export type QueuedMessage = {
19
+ id: string;
20
+ userId: number;
21
+ sessionId: string;
22
+ provider: string;
23
+ content: string;
24
+ permissionMode: string | null;
25
+ model: string | null;
26
+ metadata: unknown;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ };
30
+
31
+ type CreateQueuedMessageInput = {
32
+ provider: string;
33
+ content: string;
34
+ permissionMode?: string | null;
35
+ model?: string | null;
36
+ metadata?: unknown;
37
+ };
38
+
39
+ type UpdateQueuedMessageInput = Partial<Pick<CreateQueuedMessageInput, 'content' | 'permissionMode' | 'model' | 'metadata'>>;
40
+
41
+ function serializeMetadata(metadata: unknown): string | null {
42
+ if (metadata === undefined || metadata === null) {
43
+ return null;
44
+ }
45
+ return JSON.stringify(metadata);
46
+ }
47
+
48
+ function parseMetadata(metadataJson: string | null): unknown {
49
+ if (!metadataJson) {
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ return JSON.parse(metadataJson);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function mapQueuedMessage(row: QueuedMessageRow): QueuedMessage {
61
+ return {
62
+ id: row.id,
63
+ userId: row.user_id,
64
+ sessionId: row.session_id,
65
+ provider: row.provider,
66
+ content: row.content,
67
+ permissionMode: row.permission_mode,
68
+ model: row.model,
69
+ metadata: parseMetadata(row.metadata_json),
70
+ createdAt: row.created_at,
71
+ updatedAt: row.updated_at,
72
+ };
73
+ }
74
+
75
+ export const queuedMessagesDb = {
76
+ listBySession(userId: number, sessionId: string): QueuedMessage[] {
77
+ const db = getConnection();
78
+ const rows = db
79
+ .prepare(
80
+ `SELECT id, user_id, session_id, provider, content, permission_mode, model, metadata_json, created_at, updated_at
81
+ FROM queued_messages
82
+ WHERE user_id = ? AND session_id = ?
83
+ ORDER BY rowid ASC`
84
+ )
85
+ .all(userId, sessionId) as QueuedMessageRow[];
86
+
87
+ return rows.map(mapQueuedMessage);
88
+ },
89
+
90
+ create(userId: number, sessionId: string, input: CreateQueuedMessageInput): QueuedMessage {
91
+ const db = getConnection();
92
+ const id = randomUUID();
93
+ db.prepare(
94
+ `INSERT INTO queued_messages (id, user_id, session_id, provider, content, permission_mode, model, metadata_json)
95
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
96
+ ).run(
97
+ id,
98
+ userId,
99
+ sessionId,
100
+ input.provider,
101
+ input.content,
102
+ input.permissionMode ?? null,
103
+ input.model ?? null,
104
+ serializeMetadata(input.metadata),
105
+ );
106
+
107
+ return this.getById(userId, sessionId, id)!;
108
+ },
109
+
110
+ getById(userId: number, sessionId: string, id: string): QueuedMessage | null {
111
+ const db = getConnection();
112
+ const row = db
113
+ .prepare(
114
+ `SELECT id, user_id, session_id, provider, content, permission_mode, model, metadata_json, created_at, updated_at
115
+ FROM queued_messages
116
+ WHERE user_id = ? AND session_id = ? AND id = ?
117
+ LIMIT 1`
118
+ )
119
+ .get(userId, sessionId, id) as QueuedMessageRow | undefined;
120
+
121
+ return row ? mapQueuedMessage(row) : null;
122
+ },
123
+
124
+ update(userId: number, sessionId: string, id: string, input: UpdateQueuedMessageInput): QueuedMessage | null {
125
+ const existing = this.getById(userId, sessionId, id);
126
+ if (!existing) {
127
+ return null;
128
+ }
129
+
130
+ const db = getConnection();
131
+ db.prepare(
132
+ `UPDATE queued_messages
133
+ SET content = ?, permission_mode = ?, model = ?, metadata_json = ?, updated_at = CURRENT_TIMESTAMP
134
+ WHERE user_id = ? AND session_id = ? AND id = ?`
135
+ ).run(
136
+ input.content ?? existing.content,
137
+ input.permissionMode ?? existing.permissionMode,
138
+ input.model ?? existing.model,
139
+ input.metadata === undefined ? serializeMetadata(existing.metadata) : serializeMetadata(input.metadata),
140
+ userId,
141
+ sessionId,
142
+ id,
143
+ );
144
+
145
+ return this.getById(userId, sessionId, id);
146
+ },
147
+
148
+ delete(userId: number, sessionId: string, id: string): boolean {
149
+ const db = getConnection();
150
+ return db
151
+ .prepare('DELETE FROM queued_messages WHERE user_id = ? AND session_id = ? AND id = ?')
152
+ .run(userId, sessionId, id).changes > 0;
153
+ },
154
+ };
@@ -102,6 +102,22 @@ CREATE TABLE IF NOT EXISTS sessions (
102
102
  );
103
103
  `;
104
104
 
105
+ export const QUEUED_MESSAGES_TABLE_SCHEMA_SQL = `
106
+ CREATE TABLE IF NOT EXISTS queued_messages (
107
+ id TEXT PRIMARY KEY NOT NULL,
108
+ user_id INTEGER NOT NULL,
109
+ session_id TEXT NOT NULL,
110
+ provider TEXT NOT NULL,
111
+ content TEXT NOT NULL,
112
+ permission_mode TEXT,
113
+ model TEXT,
114
+ metadata_json TEXT,
115
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
116
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
117
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
118
+ );
119
+ `;
120
+
105
121
  export const LAST_SCANNED_AT_SQL = `
106
122
  CREATE TABLE IF NOT EXISTS scan_state (
107
123
  id INTEGER PRIMARY KEY CHECK (id = 1),
@@ -153,6 +169,9 @@ CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id);
153
169
  -- NOTE: This index is created in migrations after sessions is rebuilt to include project_path.
154
170
  -- Creating it here can fail on upgraded installs where the legacy sessions table has no project_path.
155
171
 
172
+ ${QUEUED_MESSAGES_TABLE_SCHEMA_SQL}
173
+ CREATE INDEX IF NOT EXISTS idx_queued_messages_session ON queued_messages(user_id, session_id, created_at);
174
+
156
175
  ${LAST_SCANNED_AT_SQL}
157
176
 
158
177
  ${APP_CONFIG_TABLE_SCHEMA_SQL}
@@ -1,3 +1,6 @@
1
+ export {
2
+ assertUserOwnsProjectPath,
3
+ } from './services/project-authorization.service.js';
1
4
  export {
2
5
  generateDisplayName,
3
6
  getProjectsWithSessions,
@@ -1,5 +1,6 @@
1
1
  import express, { type Request, type Response } from 'express';
2
2
 
3
+ import { queuedMessagesDb } from '@/modules/database/index.js';
3
4
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
5
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
6
  import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
@@ -52,6 +53,8 @@ const normalizeProviderParam = (value: unknown): string =>
52
53
  readPathParam(value, 'provider').trim().toLowerCase();
53
54
 
54
55
  const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
56
+ const QUEUED_MESSAGE_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
57
+ const MAX_QUEUED_MESSAGE_LENGTH = 100_000;
55
58
 
56
59
  const parseSessionId = (value: unknown): string => {
57
60
  const sessionId = readPathParam(value, 'sessionId').trim();
@@ -65,6 +68,18 @@ const parseSessionId = (value: unknown): string => {
65
68
  return sessionId;
66
69
  };
67
70
 
71
+ const parseQueuedMessageId = (value: unknown): string => {
72
+ const queueId = readPathParam(value, 'queueId').trim();
73
+ if (!QUEUED_MESSAGE_ID_PATTERN.test(queueId)) {
74
+ throw new AppError('Invalid queueId.', {
75
+ code: 'INVALID_QUEUE_ID',
76
+ statusCode: 400,
77
+ });
78
+ }
79
+
80
+ return queueId;
81
+ };
82
+
68
83
  const readOptionalQueryString = (value: unknown): string | undefined => {
69
84
  if (typeof value !== 'string') {
70
85
  return undefined;
@@ -206,6 +221,46 @@ const parseProvider = (value: unknown): LLMProvider => {
206
221
  });
207
222
  };
208
223
 
224
+ const parseQueuedMessagePayload = (payload: unknown) => {
225
+ if (!payload || typeof payload !== 'object') {
226
+ throw new AppError('Request body must be an object.', {
227
+ code: 'INVALID_REQUEST_BODY',
228
+ statusCode: 400,
229
+ });
230
+ }
231
+
232
+ const body = payload as Record<string, unknown>;
233
+ const content = typeof body.content === 'string' ? body.content.trim() : '';
234
+ if (!content) {
235
+ throw new AppError('content is required.', {
236
+ code: 'QUEUED_MESSAGE_CONTENT_REQUIRED',
237
+ statusCode: 400,
238
+ });
239
+ }
240
+ if (content.length > MAX_QUEUED_MESSAGE_LENGTH) {
241
+ throw new AppError('content is too long.', {
242
+ code: 'QUEUED_MESSAGE_CONTENT_TOO_LONG',
243
+ statusCode: 400,
244
+ });
245
+ }
246
+
247
+ const provider = typeof body.provider === 'string' ? body.provider.trim().toLowerCase() : '';
248
+ if (provider !== 'claude' && provider !== 'codex' && provider !== 'cursor' && provider !== 'gemini') {
249
+ throw new AppError('provider is invalid.', {
250
+ code: 'INVALID_PROVIDER',
251
+ statusCode: 400,
252
+ });
253
+ }
254
+
255
+ return {
256
+ content,
257
+ provider,
258
+ permissionMode: typeof body.permissionMode === 'string' ? body.permissionMode : null,
259
+ model: typeof body.model === 'string' ? body.model : null,
260
+ metadata: body.metadata,
261
+ };
262
+ };
263
+
209
264
  const parseSessionRenameSummary = (payload: unknown): string => {
210
265
  if (!payload || typeof payload !== 'object') {
211
266
  throw new AppError('Request body must be an object.', {
@@ -391,6 +446,60 @@ router.put(
391
446
  }),
392
447
  );
393
448
 
449
+ router.get(
450
+ '/sessions/:sessionId/queued-messages',
451
+ asyncHandler(async (req: Request, res: Response) => {
452
+ const userId = getAuthenticatedUserId(req);
453
+ const sessionId = parseSessionId(req.params.sessionId);
454
+ const messages = queuedMessagesDb.listBySession(userId, sessionId);
455
+ res.json(createApiSuccessResponse({ messages }));
456
+ }),
457
+ );
458
+
459
+ router.post(
460
+ '/sessions/:sessionId/queued-messages',
461
+ asyncHandler(async (req: Request, res: Response) => {
462
+ const userId = getAuthenticatedUserId(req);
463
+ const sessionId = parseSessionId(req.params.sessionId);
464
+ const message = queuedMessagesDb.create(userId, sessionId, parseQueuedMessagePayload(req.body));
465
+ res.status(201).json(createApiSuccessResponse({ message }));
466
+ }),
467
+ );
468
+
469
+ router.put(
470
+ '/sessions/:sessionId/queued-messages/:queueId',
471
+ asyncHandler(async (req: Request, res: Response) => {
472
+ const userId = getAuthenticatedUserId(req);
473
+ const sessionId = parseSessionId(req.params.sessionId);
474
+ const queueId = parseQueuedMessageId(req.params.queueId);
475
+ const message = queuedMessagesDb.update(userId, sessionId, queueId, parseQueuedMessagePayload(req.body));
476
+ if (!message) {
477
+ throw new AppError('Queued message not found.', {
478
+ code: 'QUEUED_MESSAGE_NOT_FOUND',
479
+ statusCode: 404,
480
+ });
481
+ }
482
+ res.json(createApiSuccessResponse({ message }));
483
+ }),
484
+ );
485
+
486
+ router.delete(
487
+ '/sessions/:sessionId/queued-messages/:queueId',
488
+ asyncHandler(async (req: Request, res: Response) => {
489
+ const userId = getAuthenticatedUserId(req);
490
+ const sessionId = parseSessionId(req.params.sessionId);
491
+ const queueId = parseQueuedMessageId(req.params.queueId);
492
+ const deleted = queuedMessagesDb.delete(userId, sessionId, queueId);
493
+ if (!deleted) {
494
+ throw new AppError('Queued message not found.', {
495
+ code: 'QUEUED_MESSAGE_NOT_FOUND',
496
+ statusCode: 404,
497
+ });
498
+ }
499
+ res.json(createApiSuccessResponse({ deleted: true }));
500
+ }),
501
+ );
502
+
394
503
  router.get(
395
504
  '/sessions/:sessionId/messages',
396
505
  asyncHandler(async (req: Request, res: Response) => {
@@ -1,6 +1,6 @@
1
1
  import type { WebSocket } from 'ws';
2
2
 
3
- import { assertUserOwnsProjectPath } from '@/modules/projects/services/project-authorization.service.js';
3
+ import { assertUserOwnsProjectPath } from '@/modules/projects/index.js';
4
4
  import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
5
5
  import { WebSocketWriter } from '@/modules/websocket/services/websocket-writer.service.js';
6
6
  import type {