@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/.env.example +17 -0
- package/.github/workflows/ci.yml +38 -0
- package/.mcp.json +3 -3
- package/Makefile +20 -3
- package/README.md +124 -0
- package/bun.lock +289 -0
- package/deploy.sh +18 -0
- package/docker/Caddyfile +7 -0
- package/docker/Dockerfile +13 -0
- package/docker/bridge-entrypoint.sh +17 -0
- package/docker-compose.prod.yml +85 -0
- package/docs/deploy-hetzner.md +99 -0
- package/hooks/capture-stop.sh +6 -0
- package/hooks/capture-stop.ts +122 -0
- package/hooks/capture.sh +1 -1
- package/hooks/statusline.sh +22 -11
- package/package.json +3 -1
- package/skills/polaris/SKILL.md +6 -2
- package/src/bridge-discover-org.ts +5 -0
- package/src/cli/cli.ts +401 -160
- package/src/client/client.ts +37 -24
- package/src/daemon/daemon.ts +250 -8
- package/src/service/db.ts +159 -28
- package/src/service/server.ts +47 -0
- package/src/slack/bridge.ts +399 -0
- package/src/slack/format.ts +115 -0
- package/src/types.ts +7 -1
- package/src/web/app.ts +40 -10
- package/src/web/layout.ts +16 -2
- package/src/web/views.ts +63 -77
- package/tests/bridge.test.ts +205 -0
- package/tests/client.test.ts +3 -13
- package/tests/daemon.test.ts +5 -14
- package/tests/e2e.test.ts +4 -13
- package/tests/format.test.ts +103 -0
- package/tests/helpers.ts +71 -0
- package/tests/service.test.ts +2 -13
- package/tests/types.test.ts +2 -2
- package/tests/web.test.ts +17 -31
package/src/client/client.ts
CHANGED
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
// --- Configuration ---
|
|
9
9
|
|
|
10
|
-
const DAEMON_URL = process.env.POLARIS_DAEMON_URL ?? "http://127.0.0.1:
|
|
11
|
-
const SERVICE_URL = process.env.POLARIS_SERVICE_URL ?? "http://localhost:4321";
|
|
10
|
+
const DAEMON_URL = process.env.POLARIS_DAEMON_URL ?? "http://127.0.0.1:4322";
|
|
12
11
|
|
|
13
12
|
// Generate a stable session ID for this MCP server instance
|
|
14
13
|
const CC_SESSION_ID = process.env.POLARIS_CC_SESSION_ID ?? crypto.randomUUID();
|
|
@@ -27,12 +26,6 @@ async function daemonGet(path: string): Promise<Response> {
|
|
|
27
26
|
return fetch(`${DAEMON_URL}${path}`);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
// --- Cloud service (direct, for context queries) ---
|
|
31
|
-
|
|
32
|
-
async function serviceGet(path: string): Promise<Response> {
|
|
33
|
-
return fetch(`${SERVICE_URL}${path}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
// --- Current connection state ---
|
|
37
30
|
|
|
38
31
|
let currentProject = "";
|
|
@@ -94,6 +87,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
94
87
|
required: ["message"],
|
|
95
88
|
},
|
|
96
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: "polaris_rename",
|
|
92
|
+
description: "Rename the current project. Also renames the Slack channel.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object" as const,
|
|
95
|
+
properties: {
|
|
96
|
+
name: { type: "string", description: "New project name" },
|
|
97
|
+
},
|
|
98
|
+
required: ["name"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
97
101
|
{
|
|
98
102
|
name: "polaris_context",
|
|
99
103
|
description: "Fetch activity from a sibling session in this project. Use this to see what other drivers have been doing.",
|
|
@@ -164,24 +168,33 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
164
168
|
}
|
|
165
169
|
const message = (args as { message: string }).message;
|
|
166
170
|
try {
|
|
167
|
-
const res = await
|
|
168
|
-
method: "POST",
|
|
169
|
-
headers: { "Content-Type": "application/json" },
|
|
170
|
-
body: JSON.stringify({
|
|
171
|
-
sender: currentUser,
|
|
172
|
-
payload: {
|
|
173
|
-
hook_event_name: "Stop",
|
|
174
|
-
session_id: CC_SESSION_ID,
|
|
175
|
-
stop_response: message,
|
|
176
|
-
},
|
|
177
|
-
}),
|
|
178
|
-
});
|
|
171
|
+
const res = await daemonPost("/reply", { ccSessionId: CC_SESSION_ID, message });
|
|
179
172
|
if (res.ok) {
|
|
180
173
|
return { content: [{ type: "text", text: "Reply sent to the floor." }] };
|
|
181
174
|
}
|
|
182
|
-
|
|
175
|
+
const body = await res.json();
|
|
176
|
+
return { content: [{ type: "text", text: `Failed to send reply: ${(body as { error?: string }).error ?? res.status}` }] };
|
|
177
|
+
} catch {
|
|
178
|
+
return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (name === "polaris_rename") {
|
|
183
|
+
if (!currentProject) {
|
|
184
|
+
return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
|
|
185
|
+
}
|
|
186
|
+
const newName = (args as { name: string }).name;
|
|
187
|
+
try {
|
|
188
|
+
const res = await daemonPost("/rename", { oldName: currentProject, newName });
|
|
189
|
+
const body = await res.json();
|
|
190
|
+
if (res.ok) {
|
|
191
|
+
const oldName = currentProject;
|
|
192
|
+
currentProject = newName;
|
|
193
|
+
return { content: [{ type: "text", text: `Renamed project "${oldName}" to "${newName}".` }] };
|
|
194
|
+
}
|
|
195
|
+
return { content: [{ type: "text", text: `Failed to rename: ${(body as { error?: string }).error ?? "unknown error"}` }] };
|
|
183
196
|
} catch {
|
|
184
|
-
return { content: [{ type: "text", text: "Failed to
|
|
197
|
+
return { content: [{ type: "text", text: "Failed to rename — is the Polaris daemon running?" }] };
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
|
|
@@ -191,7 +204,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
191
204
|
}
|
|
192
205
|
const targetSession = (args as { session: string }).session;
|
|
193
206
|
try {
|
|
194
|
-
const res = await
|
|
207
|
+
const res = await daemonGet(`/context/${CC_SESSION_ID}/${targetSession}`);
|
|
195
208
|
if (!res.ok) {
|
|
196
209
|
return { content: [{ type: "text", text: `Could not fetch session "${targetSession}": ${res.status}` }] };
|
|
197
210
|
}
|
|
@@ -208,7 +221,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
208
221
|
.join("\n");
|
|
209
222
|
return { content: [{ type: "text", text: summary || "(no activity yet)" }] };
|
|
210
223
|
} catch {
|
|
211
|
-
return { content: [{ type: "text", text: "Failed to reach the
|
|
224
|
+
return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
+
import { readFile, appendFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
2
5
|
|
|
3
6
|
// --- Session registry ---
|
|
4
7
|
|
|
@@ -7,6 +10,7 @@ interface SessionMapping {
|
|
|
7
10
|
project: string;
|
|
8
11
|
session: string;
|
|
9
12
|
user: string;
|
|
13
|
+
slackChannel?: string;
|
|
10
14
|
ws: WebSocket | null;
|
|
11
15
|
}
|
|
12
16
|
|
|
@@ -15,8 +19,67 @@ const sessions = new Map<string, SessionMapping>(); // keyed by ccSessionId
|
|
|
15
19
|
// IPC callbacks for MCP servers to receive advisor messages
|
|
16
20
|
const mcpCallbacks = new Map<string, (event: unknown) => void>(); // keyed by ccSessionId
|
|
17
21
|
|
|
22
|
+
// --- Config resolution (env var > config.json > legacy credentials.json > defaults) ---
|
|
23
|
+
|
|
24
|
+
interface PolarisConfig {
|
|
25
|
+
active: string;
|
|
26
|
+
profiles: Record<string, { api: string; token: string; [key: string]: unknown }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let cachedConfig: PolarisConfig | null | undefined = undefined;
|
|
30
|
+
async function loadConfig(): Promise<PolarisConfig | null> {
|
|
31
|
+
if (cachedConfig !== undefined) return cachedConfig;
|
|
32
|
+
try {
|
|
33
|
+
const configPath = join(homedir(), ".polaris", "config.json");
|
|
34
|
+
cachedConfig = JSON.parse(await readFile(configPath, "utf-8"));
|
|
35
|
+
return cachedConfig;
|
|
36
|
+
} catch {
|
|
37
|
+
cachedConfig = null;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
function getServiceUrl(): string {
|
|
19
|
-
|
|
43
|
+
// 1. Env var override (Makefile uses this for local dev)
|
|
44
|
+
if (process.env.POLARIS_SERVICE_URL) return process.env.POLARIS_SERVICE_URL;
|
|
45
|
+
// 2. Active profile (read synchronously from cache — loaded at startup)
|
|
46
|
+
if (cachedConfig?.active && cachedConfig.profiles[cachedConfig.active]) {
|
|
47
|
+
return cachedConfig.profiles[cachedConfig.active].api;
|
|
48
|
+
}
|
|
49
|
+
// 3. Fallback
|
|
50
|
+
return "https://api.polaris.lightup.ai";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let cachedToken: string | null | undefined = undefined;
|
|
54
|
+
async function getAuthToken(): Promise<string | null> {
|
|
55
|
+
if (cachedToken !== undefined) return cachedToken;
|
|
56
|
+
// 1. Env var (for testing). Empty string means "no auth".
|
|
57
|
+
if (process.env.POLARIS_AUTH_TOKEN !== undefined) {
|
|
58
|
+
cachedToken = process.env.POLARIS_AUTH_TOKEN || null;
|
|
59
|
+
return cachedToken;
|
|
60
|
+
}
|
|
61
|
+
// 2. Active profile in config.json
|
|
62
|
+
const config = await loadConfig();
|
|
63
|
+
if (config?.active && config.profiles[config.active]?.token) {
|
|
64
|
+
cachedToken = config.profiles[config.active].token;
|
|
65
|
+
return cachedToken;
|
|
66
|
+
}
|
|
67
|
+
// 3. Legacy credentials.json
|
|
68
|
+
try {
|
|
69
|
+
const credsPath = join(homedir(), ".polaris", "credentials.json");
|
|
70
|
+
const creds = JSON.parse(await readFile(credsPath, "utf-8"));
|
|
71
|
+
cachedToken = creds.token ?? null;
|
|
72
|
+
return cachedToken;
|
|
73
|
+
} catch {
|
|
74
|
+
cachedToken = null;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function authHeaders(): Promise<Record<string, string>> {
|
|
80
|
+
const token = await getAuthToken();
|
|
81
|
+
if (token) return { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
|
|
82
|
+
return { "Content-Type": "application/json" };
|
|
20
83
|
}
|
|
21
84
|
|
|
22
85
|
// --- Cloud WebSocket management ---
|
|
@@ -65,6 +128,26 @@ function disconnectCloudWs(ccSessionId: string) {
|
|
|
65
128
|
}
|
|
66
129
|
}
|
|
67
130
|
|
|
131
|
+
// --- Local event log (JSONL) for manual recovery ---
|
|
132
|
+
|
|
133
|
+
const LOG_DIR = join(homedir(), ".polaris", "logs");
|
|
134
|
+
let logReady: Promise<void> | null = null;
|
|
135
|
+
|
|
136
|
+
function ensureLogDir(): Promise<void> {
|
|
137
|
+
if (!logReady) logReady = mkdir(LOG_DIR, { recursive: true }).then(() => {});
|
|
138
|
+
return logReady;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function logEvent(endpoint: string, payload: unknown, response?: { status: number; body?: unknown }): Promise<void> {
|
|
142
|
+
try {
|
|
143
|
+
await ensureLogDir();
|
|
144
|
+
const entry: Record<string, unknown> = { t: new Date().toISOString(), endpoint, payload };
|
|
145
|
+
if (response) entry.response = response;
|
|
146
|
+
const file = join(LOG_DIR, `daemon-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
147
|
+
await appendFile(file, JSON.stringify(entry) + "\n");
|
|
148
|
+
} catch { /* best-effort — don't break the request */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
68
151
|
// --- HTTP Server ---
|
|
69
152
|
|
|
70
153
|
function json(data: unknown, status = 200): Response {
|
|
@@ -123,6 +206,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
123
206
|
session: string;
|
|
124
207
|
user: string;
|
|
125
208
|
};
|
|
209
|
+
await logEvent("/connect", body);
|
|
126
210
|
if (!body.ccSessionId || !body.project || !body.session || !body.user) {
|
|
127
211
|
return error("ccSessionId, project, session, and user are required", 400);
|
|
128
212
|
}
|
|
@@ -143,18 +227,19 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
143
227
|
const serviceUrl = getServiceUrl();
|
|
144
228
|
await fetch(`${serviceUrl}/projects`, {
|
|
145
229
|
method: "POST",
|
|
146
|
-
headers:
|
|
230
|
+
headers: await authHeaders(),
|
|
147
231
|
body: JSON.stringify({ name: body.project }),
|
|
148
232
|
}); // Ignore 409 (already exists)
|
|
149
233
|
|
|
150
234
|
// Ensure the session exists (create if not, claim driver)
|
|
151
235
|
const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
|
|
152
236
|
method: "POST",
|
|
153
|
-
headers:
|
|
237
|
+
headers: await authHeaders(),
|
|
154
238
|
body: JSON.stringify({ name: body.session, driver: body.user }),
|
|
155
239
|
});
|
|
156
240
|
if (!sessionRes.ok && sessionRes.status !== 409) {
|
|
157
241
|
const err = await sessionRes.text();
|
|
242
|
+
await logEvent("/connect", body, { status: sessionRes.status, body: err });
|
|
158
243
|
return error(`Failed to create session: ${err}`, 500);
|
|
159
244
|
}
|
|
160
245
|
|
|
@@ -162,11 +247,22 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
162
247
|
if (sessionRes.status === 409) {
|
|
163
248
|
await fetch(`${serviceUrl}/projects/${body.project}/sessions/${body.session}/driver`, {
|
|
164
249
|
method: "POST",
|
|
165
|
-
headers:
|
|
250
|
+
headers: await authHeaders(),
|
|
166
251
|
body: JSON.stringify({ driver: body.user }),
|
|
167
252
|
}); // Ignore errors (might already be driver)
|
|
168
253
|
}
|
|
169
254
|
|
|
255
|
+
// Fetch Slack channel name for status display
|
|
256
|
+
try {
|
|
257
|
+
const projRes = await fetch(`${serviceUrl}/projects/${body.project}`, {
|
|
258
|
+
headers: await authHeaders(),
|
|
259
|
+
});
|
|
260
|
+
if (projRes.ok) {
|
|
261
|
+
const projData = await projRes.json() as { slack_channel_name?: string };
|
|
262
|
+
mapping.slackChannel = projData.slack_channel_name ?? undefined;
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
|
|
170
266
|
// Connect to cloud WebSocket
|
|
171
267
|
connectCloudWs(mapping);
|
|
172
268
|
|
|
@@ -199,17 +295,42 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
199
295
|
}
|
|
200
296
|
}
|
|
201
297
|
|
|
298
|
+
// POST /disconnect-all — disconnect all sessions (for testing)
|
|
299
|
+
if (method === "POST" && pathname === "/disconnect-all") {
|
|
300
|
+
for (const [id, mapping] of sessions) {
|
|
301
|
+
disconnectCloudWs(id);
|
|
302
|
+
mapping.project = "";
|
|
303
|
+
mapping.session = "";
|
|
304
|
+
mapping.user = "";
|
|
305
|
+
}
|
|
306
|
+
sessions.clear();
|
|
307
|
+
return json({ status: "all_disconnected" });
|
|
308
|
+
}
|
|
309
|
+
|
|
202
310
|
// POST /events — hook events arrive here, routed by session_id in the payload
|
|
203
311
|
if (method === "POST" && pathname === "/events") {
|
|
204
312
|
try {
|
|
205
313
|
const body = (await req.json()) as { session_id?: string; [key: string]: unknown };
|
|
314
|
+
await logEvent("/events", body);
|
|
206
315
|
const ccSessionId = body.session_id;
|
|
207
316
|
if (!ccSessionId) return error("session_id required in hook payload", 400);
|
|
208
317
|
|
|
209
|
-
|
|
318
|
+
let mapping = sessions.get(ccSessionId);
|
|
210
319
|
if (!mapping || !mapping.project) {
|
|
211
|
-
//
|
|
212
|
-
|
|
320
|
+
// CC session_id doesn't match any registered MCP client.
|
|
321
|
+
// Try to find a connected session to route to (the MCP client
|
|
322
|
+
// generates its own UUID, which differs from CC's session_id).
|
|
323
|
+
const connectedSessions = Array.from(sessions.values()).filter((m) => m.project);
|
|
324
|
+
if (connectedSessions.length === 1) {
|
|
325
|
+
// Only one active session — route to it and remember the mapping
|
|
326
|
+
mapping = connectedSessions[0];
|
|
327
|
+
sessions.set(ccSessionId, { ...mapping, ccSessionId, slackChannel: undefined });
|
|
328
|
+
} else if (connectedSessions.length > 1) {
|
|
329
|
+
// Multiple sessions — can't determine which one. Discard.
|
|
330
|
+
return json({ status: "ambiguous" });
|
|
331
|
+
} else {
|
|
332
|
+
return json({ status: "not_connected" });
|
|
333
|
+
}
|
|
213
334
|
}
|
|
214
335
|
|
|
215
336
|
// Relay to cloud service
|
|
@@ -218,13 +339,14 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
218
339
|
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
219
340
|
{
|
|
220
341
|
method: "POST",
|
|
221
|
-
headers:
|
|
342
|
+
headers: await authHeaders(),
|
|
222
343
|
body: JSON.stringify({ sender: mapping.user, payload: body }),
|
|
223
344
|
}
|
|
224
345
|
);
|
|
225
346
|
|
|
226
347
|
if (!res.ok) {
|
|
227
348
|
const err = await res.text();
|
|
349
|
+
await logEvent("/events", body, { status: res.status, body: err });
|
|
228
350
|
return new Response(err, { status: res.status });
|
|
229
351
|
}
|
|
230
352
|
return json({ status: "relayed" });
|
|
@@ -233,6 +355,55 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
233
355
|
}
|
|
234
356
|
}
|
|
235
357
|
|
|
358
|
+
// POST /rename — rename a project (proxies to cloud API, updates local state)
|
|
359
|
+
if (method === "POST" && pathname === "/rename") {
|
|
360
|
+
try {
|
|
361
|
+
const body = (await req.json()) as { oldName: string; newName: string };
|
|
362
|
+
if (!body.oldName || !body.newName) return error("oldName and newName required", 400);
|
|
363
|
+
|
|
364
|
+
// Call cloud API to rename in DB
|
|
365
|
+
const serviceUrl = getServiceUrl();
|
|
366
|
+
const res = await fetch(`${serviceUrl}/projects/${body.oldName}/rename`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: await authHeaders(),
|
|
369
|
+
body: JSON.stringify({ name: body.newName }),
|
|
370
|
+
});
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
const err = await res.text();
|
|
373
|
+
return new Response(err, { status: res.status });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update in-memory sessions
|
|
377
|
+
for (const m of sessions.values()) {
|
|
378
|
+
if (m.project === body.oldName) {
|
|
379
|
+
m.project = body.newName;
|
|
380
|
+
m.slackChannel = body.newName;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return json({ status: "renamed", oldName: body.oldName, newName: body.newName });
|
|
385
|
+
} catch {
|
|
386
|
+
return error("Invalid JSON", 400);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// POST /channel-update — bridge pushes channel rename notifications
|
|
391
|
+
if (method === "POST" && pathname === "/channel-update") {
|
|
392
|
+
try {
|
|
393
|
+
const body = (await req.json()) as { project: string; slackChannel: string };
|
|
394
|
+
if (!body.project || !body.slackChannel) return error("project and slackChannel required", 400);
|
|
395
|
+
// Update all sessions for this project
|
|
396
|
+
for (const m of sessions.values()) {
|
|
397
|
+
if (m.project === body.project) {
|
|
398
|
+
m.slackChannel = body.slackChannel;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return json({ status: "updated" });
|
|
402
|
+
} catch {
|
|
403
|
+
return error("Invalid JSON", 400);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
236
407
|
// GET /status/:ccSessionId — status line queries this
|
|
237
408
|
if (method === "GET" && pathname.startsWith("/status/")) {
|
|
238
409
|
const ccSessionId = pathname.slice("/status/".length);
|
|
@@ -240,11 +411,22 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
240
411
|
if (!mapping || !mapping.project) {
|
|
241
412
|
return json({ connected: false });
|
|
242
413
|
}
|
|
414
|
+
// Resolve slackChannel from any session in the same project
|
|
415
|
+
let slackChannel = mapping.slackChannel ?? null;
|
|
416
|
+
if (!slackChannel) {
|
|
417
|
+
for (const m of sessions.values()) {
|
|
418
|
+
if (m.project === mapping.project && m.slackChannel) {
|
|
419
|
+
slackChannel = m.slackChannel;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
243
424
|
return json({
|
|
244
425
|
connected: true,
|
|
245
426
|
project: mapping.project,
|
|
246
427
|
session: mapping.session,
|
|
247
428
|
user: mapping.user,
|
|
429
|
+
slackChannel,
|
|
248
430
|
});
|
|
249
431
|
}
|
|
250
432
|
|
|
@@ -261,6 +443,63 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
261
443
|
return json({ ok: true, version: "0.0.1", sessions: active });
|
|
262
444
|
}
|
|
263
445
|
|
|
446
|
+
// POST /reply — proxy a reply event to the cloud API
|
|
447
|
+
if (method === "POST" && pathname === "/reply") {
|
|
448
|
+
try {
|
|
449
|
+
const body = (await req.json()) as { ccSessionId: string; message: string };
|
|
450
|
+
await logEvent("/reply", body);
|
|
451
|
+
if (!body.ccSessionId || !body.message) return error("ccSessionId and message required", 400);
|
|
452
|
+
const mapping = sessions.get(body.ccSessionId);
|
|
453
|
+
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
454
|
+
|
|
455
|
+
const serviceUrl = getServiceUrl();
|
|
456
|
+
const res = await fetch(
|
|
457
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
458
|
+
{
|
|
459
|
+
method: "POST",
|
|
460
|
+
headers: await authHeaders(),
|
|
461
|
+
body: JSON.stringify({
|
|
462
|
+
sender: mapping.user,
|
|
463
|
+
payload: {
|
|
464
|
+
hook_event_name: "Stop",
|
|
465
|
+
session_id: body.ccSessionId,
|
|
466
|
+
stop_response: body.message,
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
if (!res.ok) {
|
|
472
|
+
const err = await res.text();
|
|
473
|
+
await logEvent("/reply", body, { status: res.status, body: err });
|
|
474
|
+
return new Response(err, { status: res.status });
|
|
475
|
+
}
|
|
476
|
+
return json({ status: "sent" });
|
|
477
|
+
} catch {
|
|
478
|
+
return error("Invalid JSON", 400);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// GET /context/:ccSessionId/:session — proxy context fetch from cloud API
|
|
483
|
+
if (method === "GET" && pathname.match(/^\/context\/[^/]+\/[^/]+$/)) {
|
|
484
|
+
const parts = pathname.split("/");
|
|
485
|
+
const ccSessionId = parts[2];
|
|
486
|
+
const targetSession = parts[3];
|
|
487
|
+
const mapping = sessions.get(ccSessionId);
|
|
488
|
+
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
489
|
+
|
|
490
|
+
const serviceUrl = getServiceUrl();
|
|
491
|
+
const res = await fetch(
|
|
492
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${targetSession}/messages`,
|
|
493
|
+
{ headers: await authHeaders() }
|
|
494
|
+
);
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
const err = await res.text();
|
|
497
|
+
return new Response(err, { status: res.status });
|
|
498
|
+
}
|
|
499
|
+
const data = await res.json();
|
|
500
|
+
return json(data);
|
|
501
|
+
}
|
|
502
|
+
|
|
264
503
|
return error("Not found", 404);
|
|
265
504
|
},
|
|
266
505
|
});
|
|
@@ -270,6 +509,9 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
270
509
|
|
|
271
510
|
// --- Run if executed directly ---
|
|
272
511
|
if (import.meta.main) {
|
|
512
|
+
// Load config before starting so getServiceUrl() has the active profile
|
|
513
|
+
await loadConfig();
|
|
273
514
|
const { server } = startDaemon();
|
|
274
515
|
console.error(`Polaris daemon listening on http://127.0.0.1:${server.port}`);
|
|
516
|
+
console.error(` API endpoint: ${getServiceUrl()}`);
|
|
275
517
|
}
|