@lightupai/polaris 0.0.5 → 0.0.6

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/service/db.ts CHANGED
@@ -54,24 +54,47 @@ export async function createDb(connectionString?: string): Promise<Sql> {
54
54
  )
55
55
  `;
56
56
 
57
+ // Migrate: if projects table exists without `id` column, drop and recreate
58
+ const [{ exists: hasId }] = await sql`
59
+ SELECT EXISTS (
60
+ SELECT 1 FROM information_schema.columns
61
+ WHERE table_name = 'projects' AND column_name = 'id'
62
+ ) as exists
63
+ `;
64
+ if (!hasId) {
65
+ // Check if the old table exists at all
66
+ const [{ exists: hasTable }] = await sql`
67
+ SELECT EXISTS (
68
+ SELECT 1 FROM information_schema.tables WHERE table_name = 'projects'
69
+ ) as exists
70
+ `;
71
+ if (hasTable) {
72
+ await sql`DROP TABLE IF EXISTS events`;
73
+ await sql`DROP TABLE IF EXISTS sessions`;
74
+ await sql`DROP TABLE IF EXISTS projects`;
75
+ }
76
+ }
77
+
57
78
  await sql`
58
79
  CREATE TABLE IF NOT EXISTS projects (
59
- name TEXT NOT NULL,
80
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
60
81
  org_id TEXT NOT NULL REFERENCES orgs(id),
82
+ name TEXT NOT NULL,
83
+ slack_channel_id TEXT,
84
+ slack_channel_name TEXT,
61
85
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
62
- PRIMARY KEY (org_id, name)
86
+ UNIQUE (org_id, name)
63
87
  )
64
88
  `;
65
89
 
66
90
  await sql`
67
91
  CREATE TABLE IF NOT EXISTS sessions (
68
92
  name TEXT NOT NULL,
69
- project TEXT NOT NULL,
93
+ project_id UUID NOT NULL REFERENCES projects(id),
70
94
  org_id TEXT NOT NULL,
71
95
  driver TEXT,
72
96
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
73
- PRIMARY KEY (org_id, project, name),
74
- FOREIGN KEY (org_id, project) REFERENCES projects(org_id, name)
97
+ PRIMARY KEY (project_id, name)
75
98
  )
76
99
  `;
77
100
 
@@ -79,7 +102,7 @@ export async function createDb(connectionString?: string): Promise<Sql> {
79
102
  CREATE TABLE IF NOT EXISTS events (
80
103
  id UUID PRIMARY KEY,
81
104
  org_id TEXT NOT NULL,
82
- project TEXT NOT NULL,
105
+ project_id UUID NOT NULL REFERENCES projects(id),
83
106
  session TEXT NOT NULL,
84
107
  timestamp TIMESTAMPTZ NOT NULL,
85
108
  source TEXT NOT NULL,
@@ -89,11 +112,11 @@ export async function createDb(connectionString?: string): Promise<Sql> {
89
112
  `;
90
113
 
91
114
  await sql`
92
- CREATE INDEX IF NOT EXISTS idx_events_project ON events(org_id, project, timestamp)
115
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(project_id, timestamp)
93
116
  `;
94
117
 
95
118
  await sql`
96
- CREATE INDEX IF NOT EXISTS idx_events_session ON events(org_id, project, session, timestamp)
119
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(project_id, session, timestamp)
97
120
  `;
98
121
 
99
122
  return sql;
@@ -170,17 +193,59 @@ export async function upsertUser(sql: Sql, id: string, email: string, name: stri
170
193
 
171
194
  export async function createProject(sql: Sql, orgId: string, name: string): Promise<Project> {
172
195
  const [row] = await sql`
173
- INSERT INTO projects (name, org_id) VALUES (${name}, ${orgId}) RETURNING name, created_at
196
+ INSERT INTO projects (org_id, name) VALUES (${orgId}, ${name})
197
+ RETURNING id, name, slack_channel_id, slack_channel_name, created_at
198
+ `;
199
+ return {
200
+ id: row.id,
201
+ name: row.name,
202
+ slack_channel_id: row.slack_channel_id ?? null,
203
+ slack_channel_name: row.slack_channel_name ?? null,
204
+ created_at: row.created_at.toISOString(),
205
+ };
206
+ }
207
+
208
+ export async function renameProject(sql: Sql, orgId: string, oldName: string, newName: string): Promise<void> {
209
+ await sql`
210
+ UPDATE projects SET name = ${newName}, slack_channel_name = ${newName}
211
+ WHERE org_id = ${orgId} AND name = ${oldName}
212
+ `;
213
+ }
214
+
215
+ export async function setProjectSlackChannel(sql: Sql, orgId: string, projectName: string, channelId: string, channelName?: string): Promise<void> {
216
+ await sql`
217
+ UPDATE projects SET slack_channel_id = ${channelId}, slack_channel_name = ${channelName ?? null}
218
+ WHERE org_id = ${orgId} AND name = ${projectName}
219
+ `;
220
+ }
221
+
222
+ export async function listProjects(sql: Sql, orgId: string): Promise<Project[]> {
223
+ const rows = await sql`
224
+ SELECT id, name, slack_channel_id, slack_channel_name, created_at
225
+ FROM projects WHERE org_id = ${orgId} ORDER BY created_at ASC
174
226
  `;
175
- return { name: row.name, created_at: row.created_at.toISOString() };
227
+ return rows.map((r) => ({
228
+ id: r.id,
229
+ name: r.name,
230
+ slack_channel_id: r.slack_channel_id ?? null,
231
+ slack_channel_name: r.slack_channel_name ?? null,
232
+ created_at: r.created_at.toISOString(),
233
+ }));
176
234
  }
177
235
 
178
236
  export async function getProject(sql: Sql, orgId: string, name: string): Promise<Project | null> {
179
237
  const [row] = await sql`
180
- SELECT name, created_at FROM projects WHERE org_id = ${orgId} AND name = ${name}
238
+ SELECT id, name, slack_channel_id, slack_channel_name, created_at
239
+ FROM projects WHERE org_id = ${orgId} AND name = ${name}
181
240
  `;
182
241
  if (!row) return null;
183
- return { name: row.name, created_at: row.created_at.toISOString() };
242
+ return {
243
+ id: row.id,
244
+ name: row.name,
245
+ slack_channel_id: row.slack_channel_id ?? null,
246
+ slack_channel_name: row.slack_channel_name ?? null,
247
+ created_at: row.created_at.toISOString(),
248
+ };
184
249
  }
185
250
 
186
251
  // --- Sessions (org-scoped) ---
@@ -193,13 +258,15 @@ export async function createSession(
193
258
  driver: ParticipantId | null
194
259
  ): Promise<Session> {
195
260
  const [row] = await sql`
196
- INSERT INTO sessions (name, project, org_id, driver)
197
- VALUES (${name}, ${project}, ${orgId}, ${driver})
198
- RETURNING name, project, driver, created_at
261
+ INSERT INTO sessions (name, project_id, org_id, driver)
262
+ SELECT ${name}, p.id, ${orgId}, ${driver}
263
+ FROM projects p WHERE p.org_id = ${orgId} AND p.name = ${project}
264
+ RETURNING name, driver, created_at
199
265
  `;
266
+ if (!row) throw new Error(`Project not found: ${project}`);
200
267
  return {
201
268
  name: row.name,
202
- project: row.project,
269
+ project,
203
270
  driver: row.driver,
204
271
  created_at: row.created_at.toISOString(),
205
272
  };
@@ -207,8 +274,10 @@ export async function createSession(
207
274
 
208
275
  export async function getSession(sql: Sql, orgId: string, project: string, name: string): Promise<Session | null> {
209
276
  const [row] = await sql`
210
- SELECT name, project, driver, created_at FROM sessions
211
- WHERE org_id = ${orgId} AND project = ${project} AND name = ${name}
277
+ SELECT s.name, p.name as project, s.driver, s.created_at
278
+ FROM sessions s
279
+ JOIN projects p ON s.project_id = p.id
280
+ WHERE s.org_id = ${orgId} AND p.name = ${project} AND s.name = ${name}
212
281
  `;
213
282
  if (!row) return null;
214
283
  return {
@@ -219,17 +288,58 @@ export async function getSession(sql: Sql, orgId: string, project: string, name:
219
288
  };
220
289
  }
221
290
 
291
+ export async function listSessions(sql: Sql, orgId: string, project?: string): Promise<Session[]> {
292
+ const rows = project
293
+ ? await sql`
294
+ SELECT s.name, p.name as project, s.driver, s.created_at
295
+ FROM sessions s
296
+ JOIN projects p ON s.project_id = p.id
297
+ WHERE s.org_id = ${orgId} AND p.name = ${project}
298
+ ORDER BY s.created_at ASC
299
+ `
300
+ : await sql`
301
+ SELECT s.name, p.name as project, s.driver, s.created_at
302
+ FROM sessions s
303
+ JOIN projects p ON s.project_id = p.id
304
+ WHERE s.org_id = ${orgId}
305
+ ORDER BY s.created_at ASC
306
+ `;
307
+ return rows.map((row) => ({
308
+ name: row.name,
309
+ project: row.project,
310
+ driver: row.driver,
311
+ created_at: row.created_at.toISOString(),
312
+ }));
313
+ }
314
+
315
+ export async function getSessionPromptCounts(sql: Sql, orgId: string): Promise<Map<string, number>> {
316
+ const rows = await sql`
317
+ SELECT p.name as project, e.session, count(*)::int as count
318
+ FROM events e
319
+ JOIN projects p ON e.project_id = p.id
320
+ WHERE e.org_id = ${orgId} AND e.payload->>'hook_event_name' = 'UserPromptSubmit'
321
+ GROUP BY p.name, e.session
322
+ `;
323
+ const counts = new Map<string, number>();
324
+ for (const row of rows) {
325
+ counts.set(`${row.project}/${row.session}`, row.count);
326
+ }
327
+ return counts;
328
+ }
329
+
222
330
  export async function setDriver(sql: Sql, orgId: string, project: string, session: string, driver: ParticipantId): Promise<void> {
223
331
  await sql`
224
332
  UPDATE sessions SET driver = ${driver}
225
- WHERE org_id = ${orgId} AND project = ${project} AND name = ${session}
333
+ WHERE org_id = ${orgId} AND name = ${session}
334
+ AND project_id = (SELECT id FROM projects WHERE org_id = ${orgId} AND name = ${project})
226
335
  `;
227
336
  }
228
337
 
229
338
  export async function clearDriver(sql: Sql, orgId: string, project: string, session: string): Promise<void> {
230
339
  await sql`
231
340
  UPDATE sessions SET driver = NULL
232
- WHERE org_id = ${orgId} AND project = ${project} AND name = ${session}
341
+ WHERE org_id = ${orgId} AND name = ${session}
342
+ AND project_id = (SELECT id FROM projects WHERE org_id = ${orgId} AND name = ${project})
233
343
  `;
234
344
  }
235
345
 
@@ -237,8 +347,9 @@ export async function clearDriver(sql: Sql, orgId: string, project: string, sess
237
347
 
238
348
  export async function pushEvent(sql: Sql, orgId: string, event: PolarisEvent): Promise<void> {
239
349
  await sql`
240
- INSERT INTO events (id, org_id, project, session, timestamp, source, sender, payload)
241
- VALUES (${event.id}, ${orgId}, ${event.project}, ${event.session}, ${event.timestamp}, ${event.source}, ${event.sender}, ${sql.json(event.payload)})
350
+ INSERT INTO events (id, org_id, project_id, session, timestamp, source, sender, payload)
351
+ SELECT ${event.id}, ${orgId}, p.id, ${event.session}, ${event.timestamp}, ${event.source}, ${event.sender}, ${sql.json(event.payload)}
352
+ FROM projects p WHERE p.org_id = ${orgId} AND p.name = ${event.project}
242
353
  `;
243
354
  }
244
355
 
@@ -264,24 +375,44 @@ function rowToEvent(row: {
264
375
 
265
376
  export async function getProjectEvents(sql: Sql, orgId: string, project: string): Promise<PolarisEvent[]> {
266
377
  const rows = await sql`
267
- SELECT id, project, session, timestamp, source, sender, payload
268
- FROM events WHERE org_id = ${orgId} AND project = ${project} ORDER BY timestamp ASC
378
+ SELECT e.id, p.name as project, e.session, e.timestamp, e.source, e.sender, e.payload
379
+ FROM events e
380
+ JOIN projects p ON e.project_id = p.id
381
+ WHERE e.org_id = ${orgId} AND p.name = ${project}
382
+ ORDER BY e.timestamp ASC
269
383
  `;
270
384
  return rows.map(rowToEvent);
271
385
  }
272
386
 
273
387
  export async function getSessionEvents(sql: Sql, orgId: string, project: string, session: string): Promise<PolarisEvent[]> {
274
388
  const rows = await sql`
275
- SELECT id, project, session, timestamp, source, sender, payload
276
- FROM events WHERE org_id = ${orgId} AND project = ${project} AND session = ${session} ORDER BY timestamp ASC
389
+ SELECT e.id, p.name as project, e.session, e.timestamp, e.source, e.sender, e.payload
390
+ FROM events e
391
+ JOIN projects p ON e.project_id = p.id
392
+ WHERE e.org_id = ${orgId} AND p.name = ${project} AND e.session = ${session}
393
+ ORDER BY e.timestamp ASC
394
+ `;
395
+ return rows.map(rowToEvent);
396
+ }
397
+
398
+ export async function getOrgEventsSince(sql: Sql, orgId: string, since: string): Promise<PolarisEvent[]> {
399
+ const rows = await sql`
400
+ SELECT e.id, p.name as project, e.session, e.timestamp, e.source, e.sender, e.payload
401
+ FROM events e
402
+ JOIN projects p ON e.project_id = p.id
403
+ WHERE e.org_id = ${orgId} AND e.timestamp > ${since}
404
+ ORDER BY e.timestamp ASC
277
405
  `;
278
406
  return rows.map(rowToEvent);
279
407
  }
280
408
 
281
409
  export async function getEventsSince(sql: Sql, orgId: string, project: string, since: string): Promise<PolarisEvent[]> {
282
410
  const rows = await sql`
283
- SELECT id, project, session, timestamp, source, sender, payload
284
- FROM events WHERE org_id = ${orgId} AND project = ${project} AND timestamp > ${since} ORDER BY timestamp ASC
411
+ SELECT e.id, p.name as project, e.session, e.timestamp, e.source, e.sender, e.payload
412
+ FROM events e
413
+ JOIN projects p ON e.project_id = p.id
414
+ WHERE e.org_id = ${orgId} AND p.name = ${project} AND e.timestamp > ${since}
415
+ ORDER BY e.timestamp ASC
285
416
  `;
286
417
  return rows.map(rowToEvent);
287
418
  }
@@ -6,7 +6,9 @@ import {
6
6
  createOrg,
7
7
  getOrg,
8
8
  createProject,
9
+ listProjects,
9
10
  getProject,
11
+ renameProject,
10
12
  createSession,
11
13
  getSession,
12
14
  setDriver,
@@ -213,6 +215,11 @@ export async function startServer(opts: {
213
215
 
214
216
  // --- Project endpoints ---
215
217
 
218
+ if (method === "GET" && pathname === "/projects") {
219
+ const projects = await listProjects(sql, orgId);
220
+ return json(projects);
221
+ }
222
+
216
223
  params = matchRoute(method, pathname, "/projects", "POST");
217
224
  if (params) {
218
225
  const body = await jsonBody(req);
@@ -233,6 +240,37 @@ export async function startServer(opts: {
233
240
  return json(project);
234
241
  }
235
242
 
243
+ params = matchRoute(method, pathname, "/projects/:proj/rename", "POST");
244
+ if (params) {
245
+ const project = await getProject(sql, orgId, params.proj);
246
+ if (!project) return error("Project not found", 404);
247
+ const body = await req.json() as { name?: string };
248
+ if (!body.name) return error("name is required", 400);
249
+ const existing = await getProject(sql, orgId, body.name);
250
+ if (existing) return error("A project with that name already exists", 409);
251
+ await renameProject(sql, orgId, params.proj, body.name);
252
+
253
+ // Rename Slack channel if one is linked
254
+ if (project.slack_channel_id) {
255
+ try {
256
+ const org = await getOrg(sql, orgId);
257
+ if (org?.slack_bot_token) {
258
+ const { WebClient } = await import("@slack/web-api");
259
+ const web = new WebClient(org.slack_bot_token);
260
+ await web.conversations.rename({
261
+ channel: project.slack_channel_id,
262
+ name: body.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 80),
263
+ });
264
+ }
265
+ } catch (e) {
266
+ // Non-fatal — DB rename succeeded, Slack rename is best-effort
267
+ console.error("[server] Slack channel rename failed:", e);
268
+ }
269
+ }
270
+
271
+ return json({ status: "renamed", oldName: params.proj, newName: body.name });
272
+ }
273
+
236
274
  params = matchRoute(method, pathname, "/projects/:proj/messages", "GET");
237
275
  if (params) {
238
276
  const project = await getProject(sql, orgId, params.proj);
@@ -312,6 +350,15 @@ export async function startServer(opts: {
312
350
  broadcastEvent(event);
313
351
  broadcastSse(event);
314
352
 
353
+ // Notify web dashboard on all events (best-effort, different process)
354
+ const authHeader = req.headers.get("Authorization");
355
+ if (authHeader) {
356
+ fetch(`http://${process.env.WEB_HOST ?? "localhost"}:${Number(process.env.WEB_PORT ?? 3000)}/api/notify-dashboard`, {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json", Authorization: authHeader },
359
+ }).catch(() => {});
360
+ }
361
+
315
362
  // Forward _system events to Slack
316
363
  if (params.proj === "_system") {
317
364
  const text = (parsed.data.payload as { stop_response?: string; prompt?: string }).stop_response