@lightupai/polaris 0.0.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.
@@ -0,0 +1,275 @@
1
+ import type { Server, ServerWebSocket } from "bun";
2
+
3
+ // --- Session registry ---
4
+
5
+ interface SessionMapping {
6
+ ccSessionId: string;
7
+ project: string;
8
+ session: string;
9
+ user: string;
10
+ ws: WebSocket | null;
11
+ }
12
+
13
+ const sessions = new Map<string, SessionMapping>(); // keyed by ccSessionId
14
+
15
+ // IPC callbacks for MCP servers to receive advisor messages
16
+ const mcpCallbacks = new Map<string, (event: unknown) => void>(); // keyed by ccSessionId
17
+
18
+ function getServiceUrl(): string {
19
+ return process.env.POLARIS_SERVICE_URL ?? "http://localhost:4321";
20
+ }
21
+
22
+ // --- Cloud WebSocket management ---
23
+
24
+ function connectCloudWs(mapping: SessionMapping) {
25
+ const serviceUrl = getServiceUrl();
26
+ const wsUrl = serviceUrl.replace(/^http/, "ws");
27
+ const ws = new WebSocket(`${wsUrl}/projects/${mapping.project}/sessions/${mapping.session}/ws`);
28
+
29
+ ws.onmessage = (event) => {
30
+ try {
31
+ const data = JSON.parse(event.data as string);
32
+ // Forward inject events to the registered MCP callback
33
+ if (data.source === "inject") {
34
+ const callback = mcpCallbacks.get(mapping.ccSessionId);
35
+ if (callback) callback(data);
36
+ }
37
+ } catch {
38
+ // Ignore malformed messages
39
+ }
40
+ };
41
+
42
+ ws.onclose = () => {
43
+ // Reconnect if still registered
44
+ if (sessions.has(mapping.ccSessionId) && sessions.get(mapping.ccSessionId)!.project === mapping.project) {
45
+ setTimeout(() => {
46
+ if (sessions.has(mapping.ccSessionId)) {
47
+ connectCloudWs(sessions.get(mapping.ccSessionId)!);
48
+ }
49
+ }, 3000);
50
+ }
51
+ };
52
+
53
+ ws.onerror = () => {
54
+ // Will trigger onclose
55
+ };
56
+
57
+ mapping.ws = ws;
58
+ }
59
+
60
+ function disconnectCloudWs(ccSessionId: string) {
61
+ const mapping = sessions.get(ccSessionId);
62
+ if (mapping?.ws) {
63
+ mapping.ws.close();
64
+ mapping.ws = null;
65
+ }
66
+ }
67
+
68
+ // --- HTTP Server ---
69
+
70
+ function json(data: unknown, status = 200): Response {
71
+ return new Response(JSON.stringify(data), {
72
+ status,
73
+ headers: { "Content-Type": "application/json" },
74
+ });
75
+ }
76
+
77
+ function error(message: string, status: number): Response {
78
+ return json({ error: message }, status);
79
+ }
80
+
81
+ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 4321)): {
82
+ server: Server;
83
+ sessions: Map<string, SessionMapping>;
84
+ mcpCallbacks: Map<string, (event: unknown) => void>;
85
+ stop: () => void;
86
+ } {
87
+ const server = Bun.serve({
88
+ port,
89
+ hostname: "127.0.0.1",
90
+
91
+ async fetch(req) {
92
+ const url = new URL(req.url);
93
+ const { pathname } = url;
94
+ const method = req.method;
95
+
96
+ // POST /register — MCP server registers its CC session
97
+ if (method === "POST" && pathname === "/register") {
98
+ try {
99
+ const body = (await req.json()) as { ccSessionId: string };
100
+ if (!body.ccSessionId) return error("ccSessionId required", 400);
101
+ // Register without a session mapping yet — that comes from /connect
102
+ if (!sessions.has(body.ccSessionId)) {
103
+ sessions.set(body.ccSessionId, {
104
+ ccSessionId: body.ccSessionId,
105
+ project: "",
106
+ session: "",
107
+ user: "",
108
+ ws: null,
109
+ });
110
+ }
111
+ return json({ status: "registered", ccSessionId: body.ccSessionId });
112
+ } catch {
113
+ return error("Invalid JSON", 400);
114
+ }
115
+ }
116
+
117
+ // POST /connect — bind a CC session to a polaris project/session
118
+ if (method === "POST" && pathname === "/connect") {
119
+ try {
120
+ const body = (await req.json()) as {
121
+ ccSessionId: string;
122
+ project: string;
123
+ session: string;
124
+ user: string;
125
+ };
126
+ if (!body.ccSessionId || !body.project || !body.session || !body.user) {
127
+ return error("ccSessionId, project, session, and user are required", 400);
128
+ }
129
+
130
+ // Disconnect existing cloud WS if switching sessions
131
+ disconnectCloudWs(body.ccSessionId);
132
+
133
+ const mapping: SessionMapping = {
134
+ ccSessionId: body.ccSessionId,
135
+ project: body.project,
136
+ session: body.session,
137
+ user: body.user,
138
+ ws: null,
139
+ };
140
+ sessions.set(body.ccSessionId, mapping);
141
+
142
+ // Ensure the project exists on the cloud service (create if not)
143
+ const serviceUrl = getServiceUrl();
144
+ await fetch(`${serviceUrl}/projects`, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ name: body.project }),
148
+ }); // Ignore 409 (already exists)
149
+
150
+ // Ensure the session exists (create if not, claim driver)
151
+ const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify({ name: body.session, driver: body.user }),
155
+ });
156
+ if (!sessionRes.ok && sessionRes.status !== 409) {
157
+ const err = await sessionRes.text();
158
+ return error(`Failed to create session: ${err}`, 500);
159
+ }
160
+
161
+ // If session already existed, try to claim driver
162
+ if (sessionRes.status === 409) {
163
+ await fetch(`${serviceUrl}/projects/${body.project}/sessions/${body.session}/driver`, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ driver: body.user }),
167
+ }); // Ignore errors (might already be driver)
168
+ }
169
+
170
+ // Connect to cloud WebSocket
171
+ connectCloudWs(mapping);
172
+
173
+ return json({
174
+ status: "connected",
175
+ project: body.project,
176
+ session: body.session,
177
+ user: body.user,
178
+ });
179
+ } catch {
180
+ return error("Invalid JSON", 400);
181
+ }
182
+ }
183
+
184
+ // POST /disconnect — unbind a CC session
185
+ if (method === "POST" && pathname === "/disconnect") {
186
+ try {
187
+ const body = (await req.json()) as { ccSessionId: string };
188
+ if (!body.ccSessionId) return error("ccSessionId required", 400);
189
+ disconnectCloudWs(body.ccSessionId);
190
+ const mapping = sessions.get(body.ccSessionId);
191
+ if (mapping) {
192
+ mapping.project = "";
193
+ mapping.session = "";
194
+ mapping.user = "";
195
+ }
196
+ return json({ status: "disconnected" });
197
+ } catch {
198
+ return error("Invalid JSON", 400);
199
+ }
200
+ }
201
+
202
+ // POST /events — hook events arrive here, routed by session_id in the payload
203
+ if (method === "POST" && pathname === "/events") {
204
+ try {
205
+ const body = (await req.json()) as { session_id?: string; [key: string]: unknown };
206
+ const ccSessionId = body.session_id;
207
+ if (!ccSessionId) return error("session_id required in hook payload", 400);
208
+
209
+ const mapping = sessions.get(ccSessionId);
210
+ if (!mapping || !mapping.project) {
211
+ // Session not connected to polaris — silently discard
212
+ return json({ status: "not_connected" });
213
+ }
214
+
215
+ // Relay to cloud service
216
+ const serviceUrl = getServiceUrl();
217
+ const res = await fetch(
218
+ `${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
219
+ {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
222
+ body: JSON.stringify({ sender: mapping.user, payload: body }),
223
+ }
224
+ );
225
+
226
+ if (!res.ok) {
227
+ const err = await res.text();
228
+ return new Response(err, { status: res.status });
229
+ }
230
+ return json({ status: "relayed" });
231
+ } catch {
232
+ return error("Invalid JSON", 400);
233
+ }
234
+ }
235
+
236
+ // GET /status/:ccSessionId — status line queries this
237
+ if (method === "GET" && pathname.startsWith("/status/")) {
238
+ const ccSessionId = pathname.slice("/status/".length);
239
+ const mapping = sessions.get(ccSessionId);
240
+ if (!mapping || !mapping.project) {
241
+ return json({ connected: false });
242
+ }
243
+ return json({
244
+ connected: true,
245
+ project: mapping.project,
246
+ session: mapping.session,
247
+ user: mapping.user,
248
+ });
249
+ }
250
+
251
+ // GET /status — daemon health + all active sessions
252
+ if (method === "GET" && pathname === "/status") {
253
+ const active = Array.from(sessions.values())
254
+ .filter((m) => m.project)
255
+ .map((m) => ({
256
+ ccSessionId: m.ccSessionId,
257
+ project: m.project,
258
+ session: m.session,
259
+ user: m.user,
260
+ }));
261
+ return json({ ok: true, version: "0.0.1", sessions: active });
262
+ }
263
+
264
+ return error("Not found", 404);
265
+ },
266
+ });
267
+
268
+ return { server, sessions, mcpCallbacks, stop: () => server.stop(true) };
269
+ }
270
+
271
+ // --- Run if executed directly ---
272
+ if (import.meta.main) {
273
+ const { server } = startDaemon();
274
+ console.error(`Polaris daemon listening on http://127.0.0.1:${server.port}`);
275
+ }
@@ -0,0 +1,45 @@
1
+ import * as jose from "jose";
2
+
3
+ const JWT_SECRET = new TextEncoder().encode(process.env.POLARIS_JWT_SECRET ?? "polaris-dev-secret-change-in-prod");
4
+ const JWT_ISSUER = "polaris";
5
+ const JWT_EXPIRY = "30d";
6
+
7
+ export interface TokenPayload {
8
+ sub: string; // user ID
9
+ email: string;
10
+ name: string;
11
+ org_id: string;
12
+ participant_id: string;
13
+ }
14
+
15
+ export async function createToken(payload: TokenPayload): Promise<string> {
16
+ return new jose.SignJWT({
17
+ email: payload.email,
18
+ name: payload.name,
19
+ org_id: payload.org_id,
20
+ participant_id: payload.participant_id,
21
+ })
22
+ .setProtectedHeader({ alg: "HS256" })
23
+ .setIssuedAt()
24
+ .setIssuer(JWT_ISSUER)
25
+ .setSubject(payload.sub)
26
+ .setExpirationTime(JWT_EXPIRY)
27
+ .sign(JWT_SECRET);
28
+ }
29
+
30
+ export async function verifyToken(token: string): Promise<TokenPayload | null> {
31
+ try {
32
+ const { payload } = await jose.jwtVerify(token, JWT_SECRET, {
33
+ issuer: JWT_ISSUER,
34
+ });
35
+ return {
36
+ sub: payload.sub!,
37
+ email: payload.email as string,
38
+ name: payload.name as string,
39
+ org_id: payload.org_id as string,
40
+ participant_id: payload.participant_id as string,
41
+ };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
@@ -0,0 +1,275 @@
1
+ import postgres from "postgres";
2
+ import type { PolarisEvent, Project, Session, ParticipantId } from "../types";
3
+
4
+ export type Sql = postgres.Sql;
5
+
6
+ // --- Types ---
7
+
8
+ export interface Org {
9
+ id: string;
10
+ name: string;
11
+ domain: string | null;
12
+ slack_team_id: string | null;
13
+ slack_bot_token: string | null;
14
+ slack_system_channel_id: string | null;
15
+ created_at: string;
16
+ }
17
+
18
+ export interface User {
19
+ id: string;
20
+ email: string;
21
+ name: string;
22
+ org_id: string;
23
+ participant_id: string;
24
+ created_at: string;
25
+ }
26
+
27
+ // --- Schema ---
28
+
29
+ export async function createDb(connectionString?: string): Promise<Sql> {
30
+ const sql = postgres(connectionString ?? process.env.DATABASE_URL ?? "postgres://polaris:polaris@localhost:5432/polaris");
31
+
32
+ await sql`
33
+ CREATE TABLE IF NOT EXISTS orgs (
34
+ id TEXT PRIMARY KEY,
35
+ name TEXT NOT NULL,
36
+ domain TEXT,
37
+ slack_team_id TEXT,
38
+ slack_bot_token TEXT,
39
+ slack_system_channel_id TEXT,
40
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
41
+ )
42
+ `;
43
+
44
+ await sql`
45
+ CREATE TABLE IF NOT EXISTS users (
46
+ id TEXT PRIMARY KEY,
47
+ email TEXT NOT NULL UNIQUE,
48
+ name TEXT NOT NULL,
49
+ org_id TEXT NOT NULL REFERENCES orgs(id),
50
+ participant_id TEXT NOT NULL,
51
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
52
+ )
53
+ `;
54
+
55
+ await sql`
56
+ CREATE TABLE IF NOT EXISTS projects (
57
+ name TEXT NOT NULL,
58
+ org_id TEXT NOT NULL REFERENCES orgs(id),
59
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
60
+ PRIMARY KEY (org_id, name)
61
+ )
62
+ `;
63
+
64
+ await sql`
65
+ CREATE TABLE IF NOT EXISTS sessions (
66
+ name TEXT NOT NULL,
67
+ project TEXT NOT NULL,
68
+ org_id TEXT NOT NULL,
69
+ driver TEXT,
70
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
71
+ PRIMARY KEY (org_id, project, name),
72
+ FOREIGN KEY (org_id, project) REFERENCES projects(org_id, name)
73
+ )
74
+ `;
75
+
76
+ await sql`
77
+ CREATE TABLE IF NOT EXISTS events (
78
+ id UUID PRIMARY KEY,
79
+ org_id TEXT NOT NULL,
80
+ project TEXT NOT NULL,
81
+ session TEXT NOT NULL,
82
+ timestamp TIMESTAMPTZ NOT NULL,
83
+ source TEXT NOT NULL,
84
+ sender TEXT NOT NULL,
85
+ payload JSONB NOT NULL
86
+ )
87
+ `;
88
+
89
+ await sql`
90
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(org_id, project, timestamp)
91
+ `;
92
+
93
+ await sql`
94
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(org_id, project, session, timestamp)
95
+ `;
96
+
97
+ return sql;
98
+ }
99
+
100
+ // --- Orgs ---
101
+
102
+ export async function createOrg(sql: Sql, id: string, name: string, domain?: string): Promise<Org> {
103
+ const [row] = await sql`
104
+ INSERT INTO orgs (id, name, domain) VALUES (${id}, ${name}, ${domain ?? null})
105
+ RETURNING *
106
+ `;
107
+ return { ...row, created_at: row.created_at.toISOString() } as Org;
108
+ }
109
+
110
+ export async function getOrg(sql: Sql, id: string): Promise<Org | null> {
111
+ const [row] = await sql`SELECT * FROM orgs WHERE id = ${id}`;
112
+ if (!row) return null;
113
+ return { ...row, created_at: row.created_at.toISOString() } as Org;
114
+ }
115
+
116
+ export async function getOrgByDomain(sql: Sql, domain: string): Promise<Org | null> {
117
+ const [row] = await sql`SELECT * FROM orgs WHERE domain = ${domain}`;
118
+ if (!row) return null;
119
+ return { ...row, created_at: row.created_at.toISOString() } as Org;
120
+ }
121
+
122
+ export async function setOrgSlack(sql: Sql, orgId: string, teamId: string, botToken: string, systemChannelId?: string): Promise<void> {
123
+ await sql`UPDATE orgs SET slack_team_id = ${teamId}, slack_bot_token = ${botToken}, slack_system_channel_id = ${systemChannelId ?? null} WHERE id = ${orgId}`;
124
+ }
125
+
126
+ // --- Users ---
127
+
128
+ export async function createUser(sql: Sql, id: string, email: string, name: string, orgId: string, participantId: string): Promise<User> {
129
+ const [row] = await sql`
130
+ INSERT INTO users (id, email, name, org_id, participant_id) VALUES (${id}, ${email}, ${name}, ${orgId}, ${participantId})
131
+ RETURNING *
132
+ `;
133
+ return { ...row, created_at: row.created_at.toISOString() } as User;
134
+ }
135
+
136
+ export async function getUser(sql: Sql, id: string): Promise<User | null> {
137
+ const [row] = await sql`SELECT * FROM users WHERE id = ${id}`;
138
+ if (!row) return null;
139
+ return { ...row, created_at: row.created_at.toISOString() } as User;
140
+ }
141
+
142
+ export async function getUserByEmail(sql: Sql, email: string): Promise<User | null> {
143
+ const [row] = await sql`SELECT * FROM users WHERE email = ${email}`;
144
+ if (!row) return null;
145
+ return { ...row, created_at: row.created_at.toISOString() } as User;
146
+ }
147
+
148
+ export async function upsertUser(sql: Sql, id: string, email: string, name: string, orgId: string, participantId: string): Promise<User> {
149
+ const [row] = await sql`
150
+ INSERT INTO users (id, email, name, org_id, participant_id) VALUES (${id}, ${email}, ${name}, ${orgId}, ${participantId})
151
+ ON CONFLICT (email) DO UPDATE SET name = ${name}, org_id = ${orgId}, participant_id = ${participantId}
152
+ RETURNING *
153
+ `;
154
+ return { ...row, created_at: row.created_at.toISOString() } as User;
155
+ }
156
+
157
+ // --- Projects (org-scoped) ---
158
+
159
+ export async function createProject(sql: Sql, orgId: string, name: string): Promise<Project> {
160
+ const [row] = await sql`
161
+ INSERT INTO projects (name, org_id) VALUES (${name}, ${orgId}) RETURNING name, created_at
162
+ `;
163
+ return { name: row.name, created_at: row.created_at.toISOString() };
164
+ }
165
+
166
+ export async function getProject(sql: Sql, orgId: string, name: string): Promise<Project | null> {
167
+ const [row] = await sql`
168
+ SELECT name, created_at FROM projects WHERE org_id = ${orgId} AND name = ${name}
169
+ `;
170
+ if (!row) return null;
171
+ return { name: row.name, created_at: row.created_at.toISOString() };
172
+ }
173
+
174
+ // --- Sessions (org-scoped) ---
175
+
176
+ export async function createSession(
177
+ sql: Sql,
178
+ orgId: string,
179
+ project: string,
180
+ name: string,
181
+ driver: ParticipantId | null
182
+ ): Promise<Session> {
183
+ const [row] = await sql`
184
+ INSERT INTO sessions (name, project, org_id, driver)
185
+ VALUES (${name}, ${project}, ${orgId}, ${driver})
186
+ RETURNING name, project, driver, created_at
187
+ `;
188
+ return {
189
+ name: row.name,
190
+ project: row.project,
191
+ driver: row.driver,
192
+ created_at: row.created_at.toISOString(),
193
+ };
194
+ }
195
+
196
+ export async function getSession(sql: Sql, orgId: string, project: string, name: string): Promise<Session | null> {
197
+ const [row] = await sql`
198
+ SELECT name, project, driver, created_at FROM sessions
199
+ WHERE org_id = ${orgId} AND project = ${project} AND name = ${name}
200
+ `;
201
+ if (!row) return null;
202
+ return {
203
+ name: row.name,
204
+ project: row.project,
205
+ driver: row.driver,
206
+ created_at: row.created_at.toISOString(),
207
+ };
208
+ }
209
+
210
+ export async function setDriver(sql: Sql, orgId: string, project: string, session: string, driver: ParticipantId): Promise<void> {
211
+ await sql`
212
+ UPDATE sessions SET driver = ${driver}
213
+ WHERE org_id = ${orgId} AND project = ${project} AND name = ${session}
214
+ `;
215
+ }
216
+
217
+ export async function clearDriver(sql: Sql, orgId: string, project: string, session: string): Promise<void> {
218
+ await sql`
219
+ UPDATE sessions SET driver = NULL
220
+ WHERE org_id = ${orgId} AND project = ${project} AND name = ${session}
221
+ `;
222
+ }
223
+
224
+ // --- Events (org-scoped) ---
225
+
226
+ export async function pushEvent(sql: Sql, orgId: string, event: PolarisEvent): Promise<void> {
227
+ await sql`
228
+ INSERT INTO events (id, org_id, project, session, timestamp, source, sender, payload)
229
+ VALUES (${event.id}, ${orgId}, ${event.project}, ${event.session}, ${event.timestamp}, ${event.source}, ${event.sender}, ${sql.json(event.payload)})
230
+ `;
231
+ }
232
+
233
+ function rowToEvent(row: {
234
+ id: string;
235
+ project: string;
236
+ session: string;
237
+ timestamp: Date;
238
+ source: string;
239
+ sender: string;
240
+ payload: unknown;
241
+ }): PolarisEvent {
242
+ return {
243
+ id: row.id,
244
+ project: row.project,
245
+ session: row.session,
246
+ timestamp: row.timestamp.toISOString(),
247
+ source: row.source as PolarisEvent["source"],
248
+ sender: row.sender as ParticipantId,
249
+ payload: row.payload as PolarisEvent["payload"],
250
+ };
251
+ }
252
+
253
+ export async function getProjectEvents(sql: Sql, orgId: string, project: string): Promise<PolarisEvent[]> {
254
+ const rows = await sql`
255
+ SELECT id, project, session, timestamp, source, sender, payload
256
+ FROM events WHERE org_id = ${orgId} AND project = ${project} ORDER BY timestamp ASC
257
+ `;
258
+ return rows.map(rowToEvent);
259
+ }
260
+
261
+ export async function getSessionEvents(sql: Sql, orgId: string, project: string, session: string): Promise<PolarisEvent[]> {
262
+ const rows = await sql`
263
+ SELECT id, project, session, timestamp, source, sender, payload
264
+ FROM events WHERE org_id = ${orgId} AND project = ${project} AND session = ${session} ORDER BY timestamp ASC
265
+ `;
266
+ return rows.map(rowToEvent);
267
+ }
268
+
269
+ export async function getEventsSince(sql: Sql, orgId: string, project: string, since: string): Promise<PolarisEvent[]> {
270
+ const rows = await sql`
271
+ SELECT id, project, session, timestamp, source, sender, payload
272
+ FROM events WHERE org_id = ${orgId} AND project = ${project} AND timestamp > ${since} ORDER BY timestamp ASC
273
+ `;
274
+ return rows.map(rowToEvent);
275
+ }