@lightupai/polaris 0.0.31 → 0.0.33
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/README.md +1 -0
- package/package.json +1 -1
- package/skills/polaris/SKILL.md +6 -2
- package/src/client/client.ts +52 -3
- package/src/daemon/daemon.ts +144 -1
- package/src/web/pages.ts +122 -55
- package/tests/web.test.ts +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ tests/ Test suite (bun test)
|
|
|
119
119
|
- [ ] Reconciliation and recovery — `polaris recover` command that diffs the daemon JSONL log against the DB, backfills missing events, and posts an abridged recovery summary to Slack as a thread reply at the correct timeline position
|
|
120
120
|
- [ ] CD pipeline for Hetzner — auto-deploy to production on merge to master (SSH + docker compose up), similar to the npm publish job
|
|
121
121
|
- [ ] Auto-update local skill/hooks — locally installed skill and hook files go stale when the repo changes. `polaris install` fixes it but there's no staleness detection or auto-update mechanism
|
|
122
|
+
- [ ] Update available indicator — daemon periodically checks npm for newer version, caches the result. Status line shows "update available" when stale. `polaris update` command installs the latest version and rewrites skill/hooks.
|
|
122
123
|
- [ ] Slack channel name collision — if a channel name was previously deleted, Slack reserves it. Bridge should handle `name_taken` by trying a prefix/suffix (e.g., `p-project-name`)
|
|
123
124
|
|
|
124
125
|
## Development
|
package/package.json
CHANGED
package/skills/polaris/SKILL.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: polaris
|
|
3
3
|
description: Connect to a Polaris multiplayer collaboration session
|
|
4
|
-
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
|
|
5
|
-
argument-hint: [join #channel | rename <new-name> | disconnect | (no args for status)]
|
|
4
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_backfill
|
|
5
|
+
argument-hint: [join #channel | backfill [duration] | rename <new-name> | disconnect | (no args for status)]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Polaris — Multiplayer Collaboration
|
|
@@ -22,6 +22,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
22
22
|
1. Call `polaris_rename` with the new name
|
|
23
23
|
2. Report the result
|
|
24
24
|
|
|
25
|
+
**`/polaris backfill [duration]`** — Recover lost events:
|
|
26
|
+
1. Call `polaris_backfill` with the optional duration (e.g., `2h`, `30m`)
|
|
27
|
+
2. Report how many events were recovered
|
|
28
|
+
|
|
25
29
|
**`/polaris disconnect`** — Disconnect:
|
|
26
30
|
1. Call `polaris_disconnect`
|
|
27
31
|
2. Confirm disconnection
|
package/src/client/client.ts
CHANGED
|
@@ -53,12 +53,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
53
53
|
inputSchema: {
|
|
54
54
|
type: "object" as const,
|
|
55
55
|
properties: {
|
|
56
|
-
channel: { type: "string", description: "Channel name (e.g., #polaris-dev
|
|
56
|
+
channel: { type: "string", description: "Channel name (e.g., #polaris-dev). Omit to list available channels." },
|
|
57
57
|
user: { type: "string", description: "Your participant ID (e.g., user:manu)" },
|
|
58
58
|
session: { type: "string", description: "Session name (optional — auto-generated if omitted)" },
|
|
59
59
|
agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" },
|
|
60
60
|
},
|
|
61
|
-
required: ["
|
|
61
|
+
required: ["user"],
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
{
|
|
@@ -110,6 +110,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
110
110
|
required: ["session"],
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: "polaris_backfill",
|
|
115
|
+
description: "Recover lost events from local daemon logs. Use when events were lost due to disconnection or API downtime.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object" as const,
|
|
118
|
+
properties: {
|
|
119
|
+
duration: { type: "string", description: "Time range to backfill (e.g., '2h', '30m', '1d'). Auto-detects if omitted." },
|
|
120
|
+
from: { type: "string", description: "ISO timestamp to backfill from. Overrides duration." },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
113
124
|
],
|
|
114
125
|
}));
|
|
115
126
|
|
|
@@ -117,7 +128,23 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
117
128
|
const { name, arguments: args } = req.params;
|
|
118
129
|
|
|
119
130
|
if (name === "polaris_connect") {
|
|
120
|
-
const { channel, user, session, agent } = args as { channel
|
|
131
|
+
const { channel, user, session, agent } = args as { channel?: string; user: string; session?: string; agent?: string };
|
|
132
|
+
|
|
133
|
+
// If no channel specified, list available channels
|
|
134
|
+
if (!channel) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await daemonGet("/channels");
|
|
137
|
+
if (res.ok) {
|
|
138
|
+
const body = await res.json() as { channels: string[] };
|
|
139
|
+
if (body.channels.length === 0) {
|
|
140
|
+
return { content: [{ type: "text", text: "No channels found. Create one by joining: `/polaris join #channel-name`" }] };
|
|
141
|
+
}
|
|
142
|
+
return { content: [{ type: "text", text: `Available channels:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #channel-name` }] };
|
|
143
|
+
}
|
|
144
|
+
} catch { /* fall through */ }
|
|
145
|
+
return { content: [{ type: "text", text: "Specify a channel: `/polaris join #channel-name`" }] };
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
const project = channel.replace(/^#/, ""); // strip leading # if present
|
|
122
149
|
try {
|
|
123
150
|
const res = await daemonPost("/connect", {
|
|
@@ -228,6 +255,28 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
228
255
|
}
|
|
229
256
|
}
|
|
230
257
|
|
|
258
|
+
if (name === "polaris_backfill") {
|
|
259
|
+
if (!currentProject) {
|
|
260
|
+
return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
|
|
261
|
+
}
|
|
262
|
+
const { duration, from } = (args ?? {}) as { duration?: string; from?: string };
|
|
263
|
+
try {
|
|
264
|
+
const res = await daemonPost("/backfill", {
|
|
265
|
+
ccSessionId: CC_SESSION_ID,
|
|
266
|
+
...(duration ? { duration } : {}),
|
|
267
|
+
...(from ? { from } : {}),
|
|
268
|
+
});
|
|
269
|
+
const body = await res.json() as { recovered: number; source: string; gaps: string[] };
|
|
270
|
+
if (res.ok) {
|
|
271
|
+
const gapInfo = body.gaps.length > 0 ? `\nGaps: ${body.gaps.join(", ")}` : "";
|
|
272
|
+
return { content: [{ type: "text", text: `Backfill complete: ${body.recovered} events recovered from ${body.source}.${gapInfo}` }] };
|
|
273
|
+
}
|
|
274
|
+
return { content: [{ type: "text", text: `Backfill failed: ${(body as unknown as { error?: string }).error ?? "unknown error"}` }] };
|
|
275
|
+
} catch {
|
|
276
|
+
return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
231
280
|
throw new Error(`Unknown tool: ${name}`);
|
|
232
281
|
});
|
|
233
282
|
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -154,6 +154,114 @@ async function logEvent(endpoint: string, payload: unknown, response?: { status:
|
|
|
154
154
|
} catch { /* best-effort — don't break the request */ }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// --- Session history (persistent connect/disconnect log for backfill) ---
|
|
158
|
+
|
|
159
|
+
const SESSION_HISTORY_FILE = join(homedir(), ".polaris", "session-history.jsonl");
|
|
160
|
+
|
|
161
|
+
async function logSessionChange(action: "connect" | "disconnect", ccSessionId: string, project: string, session: string, user: string, agent: string): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
await ensureLogDir();
|
|
164
|
+
const entry = JSON.stringify({ t: new Date().toISOString(), action, ccSessionId, project, session, user, agent });
|
|
165
|
+
await appendFile(SESSION_HISTORY_FILE, entry + "\n");
|
|
166
|
+
} catch { /* best-effort */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Backfill: recover lost events from daemon log ---
|
|
170
|
+
|
|
171
|
+
async function backfill(mapping: SessionMapping, duration?: string, from?: string): Promise<{ recovered: number; source: string; gaps: string[] }> {
|
|
172
|
+
const now = new Date();
|
|
173
|
+
let since: Date;
|
|
174
|
+
|
|
175
|
+
if (from) {
|
|
176
|
+
since = new Date(from);
|
|
177
|
+
} else if (duration) {
|
|
178
|
+
const match = duration.match(/^(\d+)(h|m|d)$/);
|
|
179
|
+
if (!match) return { recovered: 0, source: "none", gaps: ["invalid duration format"] };
|
|
180
|
+
const val = parseInt(match[1]);
|
|
181
|
+
const unit = match[2];
|
|
182
|
+
since = new Date(now.getTime() - val * (unit === "h" ? 3600000 : unit === "m" ? 60000 : 86400000));
|
|
183
|
+
} else {
|
|
184
|
+
// Auto-detect: query API for most recent event, backfill from there
|
|
185
|
+
try {
|
|
186
|
+
const serviceUrl = getServiceUrl();
|
|
187
|
+
const res = await fetch(
|
|
188
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/messages`,
|
|
189
|
+
{ headers: await authHeaders() }
|
|
190
|
+
);
|
|
191
|
+
if (res.ok) {
|
|
192
|
+
const events = (await res.json()) as Array<{ timestamp: string }>;
|
|
193
|
+
if (events.length > 0) {
|
|
194
|
+
since = new Date(events[events.length - 1].timestamp);
|
|
195
|
+
} else {
|
|
196
|
+
since = new Date(now.getTime() - 3600000); // default 1h
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
since = new Date(now.getTime() - 3600000);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
since = new Date(now.getTime() - 3600000);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Read daemon log files covering the time range
|
|
207
|
+
const logEntries: Array<{ t: string; endpoint: string; payload: Record<string, unknown> }> = [];
|
|
208
|
+
const startDate = since.toISOString().slice(0, 10);
|
|
209
|
+
const endDate = now.toISOString().slice(0, 10);
|
|
210
|
+
|
|
211
|
+
// Collect all dates in range
|
|
212
|
+
const dates: string[] = [];
|
|
213
|
+
const d = new Date(startDate);
|
|
214
|
+
while (d.toISOString().slice(0, 10) <= endDate) {
|
|
215
|
+
dates.push(d.toISOString().slice(0, 10));
|
|
216
|
+
d.setDate(d.getDate() + 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const date of dates) {
|
|
220
|
+
const logFile = join(LOG_DIR, `daemon-${date}.jsonl`);
|
|
221
|
+
try {
|
|
222
|
+
const content = await readFile(logFile, "utf-8");
|
|
223
|
+
for (const line of content.split("\n")) {
|
|
224
|
+
if (!line.trim()) continue;
|
|
225
|
+
try {
|
|
226
|
+
const entry = JSON.parse(line);
|
|
227
|
+
if (entry.endpoint !== "/events") continue;
|
|
228
|
+
if (new Date(entry.t) <= since) continue;
|
|
229
|
+
if (new Date(entry.t) > now) continue;
|
|
230
|
+
logEntries.push(entry);
|
|
231
|
+
} catch { /* skip malformed lines */ }
|
|
232
|
+
}
|
|
233
|
+
} catch { /* log file doesn't exist for this date — not an error */ }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (logEntries.length === 0) {
|
|
237
|
+
return { recovered: 0, source: "daemon_log", gaps: [`no log entries found since ${since.toISOString()}`] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Replay events to the API
|
|
241
|
+
let recovered = 0;
|
|
242
|
+
const serviceUrl = getServiceUrl();
|
|
243
|
+
const headers = await authHeaders();
|
|
244
|
+
|
|
245
|
+
for (const entry of logEntries) {
|
|
246
|
+
const hookEvent = entry.payload.hook_event_name as string | undefined;
|
|
247
|
+
const sender = hookEvent === "UserPromptSubmit" ? mapping.user : mapping.agent;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch(
|
|
251
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers,
|
|
255
|
+
body: JSON.stringify({ sender, payload: entry.payload }),
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
if (res.ok) recovered++;
|
|
259
|
+
} catch { /* skip failed replays */ }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { recovered, source: "daemon_log", gaps: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
157
265
|
// --- HTTP Server ---
|
|
158
266
|
|
|
159
267
|
function json(data: unknown, status = 200): Response {
|
|
@@ -294,6 +402,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
294
402
|
|
|
295
403
|
// Connect to cloud WebSocket
|
|
296
404
|
connectCloudWs(mapping);
|
|
405
|
+
await logSessionChange("connect", mapping.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
|
|
297
406
|
|
|
298
407
|
return json({
|
|
299
408
|
status: "connected",
|
|
@@ -314,7 +423,8 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
314
423
|
if (!body.ccSessionId) return error("ccSessionId required", 400);
|
|
315
424
|
disconnectCloudWs(body.ccSessionId);
|
|
316
425
|
const mapping = sessions.get(body.ccSessionId);
|
|
317
|
-
if (mapping) {
|
|
426
|
+
if (mapping && mapping.project) {
|
|
427
|
+
await logSessionChange("disconnect", body.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
|
|
318
428
|
mapping.project = "";
|
|
319
429
|
mapping.session = "";
|
|
320
430
|
mapping.user = "";
|
|
@@ -541,6 +651,39 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
541
651
|
return json(data);
|
|
542
652
|
}
|
|
543
653
|
|
|
654
|
+
// GET /channels — list available channels/projects
|
|
655
|
+
if (method === "GET" && pathname === "/channels") {
|
|
656
|
+
try {
|
|
657
|
+
const serviceUrl = getServiceUrl();
|
|
658
|
+
const res = await fetch(`${serviceUrl}/projects`, {
|
|
659
|
+
headers: await authHeaders(),
|
|
660
|
+
});
|
|
661
|
+
if (!res.ok) return error("Failed to fetch channels", res.status);
|
|
662
|
+
const projects = (await res.json()) as Array<{ name: string; slack_channel_name?: string }>;
|
|
663
|
+
const channels = projects
|
|
664
|
+
.filter((p) => p.name !== "_system")
|
|
665
|
+
.map((p) => `#${p.slack_channel_name || p.name}`);
|
|
666
|
+
return json({ channels });
|
|
667
|
+
} catch {
|
|
668
|
+
return error("API unreachable", 503);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// POST /backfill — recover lost events from daemon log
|
|
673
|
+
if (method === "POST" && pathname === "/backfill") {
|
|
674
|
+
try {
|
|
675
|
+
const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string };
|
|
676
|
+
if (!body.ccSessionId) return error("ccSessionId required", 400);
|
|
677
|
+
const mapping = sessions.get(body.ccSessionId);
|
|
678
|
+
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
679
|
+
|
|
680
|
+
const result = await backfill(mapping, body.duration, body.from);
|
|
681
|
+
return json(result);
|
|
682
|
+
} catch {
|
|
683
|
+
return error("Invalid JSON", 400);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
544
687
|
return error("Not found", 404);
|
|
545
688
|
},
|
|
546
689
|
});
|
package/src/web/pages.ts
CHANGED
|
@@ -6,51 +6,108 @@ import type { Org } from "../service/db";
|
|
|
6
6
|
export function renderLandingPage(): string {
|
|
7
7
|
return `
|
|
8
8
|
${nav()}
|
|
9
|
-
<div class="max-w-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
<div class="max-w-3xl mx-auto px-6">
|
|
10
|
+
|
|
11
|
+
<!-- Hero -->
|
|
12
|
+
<div class="pt-24 pb-16">
|
|
13
|
+
<h1 class="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
14
|
+
Multiplayer collaboration<br>for Claude Code
|
|
13
15
|
</h1>
|
|
14
|
-
<p class="mt-
|
|
15
|
-
|
|
16
|
+
<p class="mt-4 text-lg text-gray-500 max-w-xl">
|
|
17
|
+
Your teammates see what your agent is doing. They can jump in from Slack and steer it in real time.
|
|
16
18
|
</p>
|
|
17
|
-
<div class="mt-
|
|
18
|
-
<a href="
|
|
19
|
-
|
|
20
|
-
Sign up with Google
|
|
21
|
-
</a>
|
|
22
|
-
<a href="/login" class="px-6 py-3 text-sm font-semibold text-gray-500 hover:text-gray-700 transition">Already have an account? Sign in</a>
|
|
19
|
+
<div class="mt-8 flex items-center gap-4">
|
|
20
|
+
<a href="#get-started" class="px-5 py-2.5 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition">Get started</a>
|
|
21
|
+
<a href="https://github.com/anthropics/polaris" class="text-sm font-medium text-gray-500 hover:text-gray-700 transition">GitHub</a>
|
|
23
22
|
</div>
|
|
24
|
-
</div>
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
<div>
|
|
28
|
-
<div class="
|
|
29
|
-
<
|
|
24
|
+
<!-- Terminal demo -->
|
|
25
|
+
<div class="mt-12 bg-gray-900 rounded-xl overflow-hidden shadow-2xl">
|
|
26
|
+
<div class="flex items-center gap-1.5 px-4 py-3 bg-gray-800">
|
|
27
|
+
<div class="w-3 h-3 rounded-full bg-red-500/80"></div>
|
|
28
|
+
<div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
|
|
29
|
+
<div class="w-3 h-3 rounded-full bg-green-500/80"></div>
|
|
30
|
+
<span class="ml-3 text-xs text-gray-500 font-mono">claude code</span>
|
|
30
31
|
</div>
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
<div class="px-5 py-4 font-mono text-sm leading-relaxed space-y-3">
|
|
33
|
+
<div>
|
|
34
|
+
<span class="text-green-400">$</span> <span class="text-gray-300">npm install -g @lightupai/polaris</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<span class="text-green-400">$</span> <span class="text-gray-300">polaris</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="text-gray-500"> Hooks installed. MCP server registered. Logged in as manu@acme.dev</div>
|
|
40
|
+
<div class="border-t border-gray-700 pt-3">
|
|
41
|
+
<span class="text-polaris-400">></span> <span class="text-gray-300">/polaris join #webapp</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="text-gray-500"> Connected to webapp/s-4f2a as user:manu</div>
|
|
44
|
+
<div class="border-t border-gray-700 pt-3">
|
|
45
|
+
<span class="text-polaris-400">></span> <span class="text-gray-300">implement the auth middleware using RS256</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="text-gray-400"> I'll create src/middleware/auth.ts with RS256 JWT verification...</div>
|
|
48
|
+
<div class="mt-1 pl-4 border-l-2 border-yellow-500/50">
|
|
49
|
+
<span class="text-yellow-400 text-xs">priya via slack</span>
|
|
50
|
+
<span class="text-gray-400"> — make sure to add rate limiting on that endpoint</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="text-gray-400"> Good call. Adding rate limiting middleware before deploying...</div>
|
|
37
53
|
</div>
|
|
38
|
-
<h3 class="text-sm font-semibold text-gray-900">Context injection</h3>
|
|
39
|
-
<p class="mt-2 text-sm text-gray-600">Teammates inject expertise directly into your agent session from Slack, WhatsApp, or any floor.</p>
|
|
40
54
|
</div>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- How it works -->
|
|
58
|
+
<div class="py-16 border-t border-gray-200">
|
|
59
|
+
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">How it works</h2>
|
|
60
|
+
<div class="mt-8 space-y-8">
|
|
61
|
+
<div class="flex gap-4">
|
|
62
|
+
<div class="w-8 h-8 rounded-lg bg-gray-900 flex items-center justify-center text-white text-sm font-bold shrink-0">1</div>
|
|
63
|
+
<div>
|
|
64
|
+
<h3 class="font-semibold text-gray-900">Install</h3>
|
|
65
|
+
<p class="mt-1 text-sm text-gray-500"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-gray-700 text-xs">npm install -g @lightupai/polaris && polaris</code> sets up hooks, MCP server, and authenticates your team.</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="flex gap-4">
|
|
69
|
+
<div class="w-8 h-8 rounded-lg bg-gray-900 flex items-center justify-center text-white text-sm font-bold shrink-0">2</div>
|
|
70
|
+
<div>
|
|
71
|
+
<h3 class="font-semibold text-gray-900">Connect</h3>
|
|
72
|
+
<p class="mt-1 text-sm text-gray-500"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-gray-700 text-xs">/polaris join #your-channel</code> links your Claude Code session to your team's Slack channel.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="flex gap-4">
|
|
76
|
+
<div class="w-8 h-8 rounded-lg bg-gray-900 flex items-center justify-center text-white text-sm font-bold shrink-0">3</div>
|
|
77
|
+
<div>
|
|
78
|
+
<h3 class="font-semibold text-gray-900">Collaborate</h3>
|
|
79
|
+
<p class="mt-1 text-sm text-gray-500">Every prompt and response streams to Slack. Teammates reply there and their messages appear inline in your agent session.</p>
|
|
80
|
+
</div>
|
|
44
81
|
</div>
|
|
45
|
-
<h3 class="text-sm font-semibold text-gray-900">Multiplayer</h3>
|
|
46
|
-
<p class="mt-2 text-sm text-gray-600">Multiple drivers, concurrent sessions, seamless handoffs. Humans and AI agents as first-class participants.</p>
|
|
47
82
|
</div>
|
|
48
83
|
</div>
|
|
49
84
|
|
|
85
|
+
<!-- Why Polaris -->
|
|
50
86
|
<div class="py-16 border-t border-gray-200">
|
|
51
|
-
<h2 class="text-
|
|
52
|
-
<
|
|
87
|
+
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">Why Polaris</h2>
|
|
88
|
+
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
89
|
+
<div class="border border-gray-200 rounded-lg p-5">
|
|
90
|
+
<h3 class="font-semibold text-gray-900">No context switching</h3>
|
|
91
|
+
<p class="mt-2 text-sm text-gray-500">Teammate messages arrive directly in your coding session. No tab switching, no copy-pasting, no "hey can you check Slack."</p>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="border border-gray-200 rounded-lg p-5">
|
|
94
|
+
<h3 class="font-semibold text-gray-900">Full session visibility</h3>
|
|
95
|
+
<p class="mt-2 text-sm text-gray-500">Every prompt, tool call, and response is captured and streamed to your team. Anyone can see what's happening and jump in.</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="border border-gray-200 rounded-lg p-5">
|
|
98
|
+
<h3 class="font-semibold text-gray-900">Human + AI multiplayer</h3>
|
|
99
|
+
<p class="mt-2 text-sm text-gray-500">Multiple developers, multiple agents, concurrent sessions. Humans and AI are first-class participants on the same floor.</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="border border-gray-200 rounded-lg p-5">
|
|
102
|
+
<h3 class="font-semibold text-gray-900">Two-minute setup</h3>
|
|
103
|
+
<p class="mt-2 text-sm text-gray-500">One npm install, one command. No config files, no Docker, no infrastructure. Works with your existing Slack workspace.</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
53
107
|
|
|
108
|
+
<!-- Slack demo -->
|
|
109
|
+
<div class="py-16 border-t border-gray-200">
|
|
110
|
+
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">What your team sees in Slack</h2>
|
|
54
111
|
<div class="mt-8 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden">
|
|
55
112
|
<div class="flex">
|
|
56
113
|
<div class="w-14 bg-[#4A154B] shrink-0 flex flex-col items-center py-3 gap-3">
|
|
@@ -58,9 +115,6 @@ export function renderLandingPage(): string {
|
|
|
58
115
|
<div class="w-8 h-8 rounded-lg bg-white/10 flex items-center justify-center">
|
|
59
116
|
<svg class="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
|
60
117
|
</div>
|
|
61
|
-
<div class="w-8 h-8 rounded-lg bg-white/10 flex items-center justify-center">
|
|
62
|
-
<svg class="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
|
63
|
-
</div>
|
|
64
118
|
</div>
|
|
65
119
|
<div class="flex-1">
|
|
66
120
|
<div class="border-b border-gray-200 px-4 py-2.5 flex items-center justify-between">
|
|
@@ -107,33 +161,46 @@ export function renderLandingPage(): string {
|
|
|
107
161
|
<p class="text-gray-700">Good point from Priya. Switching to RS256 and updating the key config...</p>
|
|
108
162
|
</div>
|
|
109
163
|
</div>
|
|
110
|
-
<div class="flex gap-3">
|
|
111
|
-
<div class="w-8 h-8 rounded-md bg-purple-600 flex items-center justify-center text-white text-xs font-bold shrink-0 mt-0.5">
|
|
112
|
-
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
113
|
-
</div>
|
|
114
|
-
<div>
|
|
115
|
-
<div class="flex items-baseline gap-2"><span class="text-gray-900 font-bold text-sm">security-bot</span><span class="bg-gray-100 text-gray-600 text-xs px-1.5 py-0.5 rounded">agent</span><span class="text-gray-400 text-xs">→ auth</span><span class="text-gray-400 text-xs">10:34 AM</span></div>
|
|
116
|
-
<p class="text-gray-700">This auth endpoint needs rate limiting before going to production</p>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="flex gap-3">
|
|
120
|
-
<div class="w-8 h-8 rounded-md bg-green-600 flex items-center justify-center text-white text-xs font-bold shrink-0 mt-0.5">AI</div>
|
|
121
|
-
<div>
|
|
122
|
-
<div class="flex items-baseline gap-2"><span class="text-gray-900 font-bold text-sm">Agent</span><span class="text-gray-400 text-xs">→ manu/auth</span><span class="text-gray-400 text-xs">10:34 AM</span></div>
|
|
123
|
-
<p class="text-gray-700">Adding rate limiting middleware to the auth endpoints...</p>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
164
|
</div>
|
|
127
165
|
</div>
|
|
128
166
|
</div>
|
|
129
167
|
</div>
|
|
130
|
-
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Get Started -->
|
|
171
|
+
<div id="get-started" class="py-16 border-t border-gray-200">
|
|
172
|
+
<h2 class="text-2xl font-bold text-gray-900">Get started</h2>
|
|
173
|
+
<div class="mt-6 bg-gray-900 rounded-xl overflow-hidden">
|
|
174
|
+
<div class="flex items-center justify-between px-4 py-2.5 bg-gray-800">
|
|
175
|
+
<span class="text-xs text-gray-500 font-mono">terminal</span>
|
|
176
|
+
<button class="polaris-copy text-gray-500 hover:text-gray-300 transition" data-copy="install-cmd">
|
|
177
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
<pre class="px-5 py-4 font-mono text-sm text-gray-300 leading-relaxed" id="install-cmd"><span class="text-green-400">$</span> npm install -g @lightupai/polaris
|
|
181
|
+
<span class="text-green-400">$</span> polaris</pre>
|
|
182
|
+
</div>
|
|
183
|
+
<p class="mt-4 text-sm text-gray-500">Then in Claude Code:</p>
|
|
184
|
+
<div class="mt-2 bg-gray-900 rounded-xl overflow-hidden">
|
|
185
|
+
<pre class="px-5 py-4 font-mono text-sm text-gray-300 leading-relaxed"><span class="text-polaris-400">></span> /polaris join #your-channel</pre>
|
|
186
|
+
</div>
|
|
187
|
+
<p class="mt-6 text-sm text-gray-500">That's it. Your session is now live to your team.</p>
|
|
188
|
+
<div class="mt-8">
|
|
189
|
+
<a href="/signup" class="inline-flex items-center gap-3 px-5 py-2.5 bg-white border border-gray-300 rounded-lg shadow-sm hover:bg-gray-50 transition text-sm font-medium text-gray-700">
|
|
190
|
+
<svg width="16" height="16" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 01-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/><path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009 18z" fill="#34A853"/><path d="M3.964 10.71A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/><path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 00.957 4.958L3.964 6.29C4.672 4.163 6.656 2.58 9 3.58z" fill="#EA4335"/></svg>
|
|
191
|
+
Sign up with Google
|
|
192
|
+
</a>
|
|
193
|
+
</div>
|
|
131
194
|
</div>
|
|
132
195
|
</div>
|
|
133
196
|
|
|
134
|
-
<footer class="border-t border-gray-200 mt-
|
|
135
|
-
<div class="max-w-
|
|
136
|
-
Polaris
|
|
197
|
+
<footer class="border-t border-gray-200 mt-8">
|
|
198
|
+
<div class="max-w-3xl mx-auto px-6 py-8 flex items-center justify-between text-sm text-gray-400">
|
|
199
|
+
<span>Polaris</span>
|
|
200
|
+
<div class="flex items-center gap-6">
|
|
201
|
+
<a href="https://github.com/anthropics/polaris" class="hover:text-gray-600 transition">GitHub</a>
|
|
202
|
+
<a href="/login" class="hover:text-gray-600 transition">Sign in</a>
|
|
203
|
+
</div>
|
|
137
204
|
</div>
|
|
138
205
|
</footer>`;
|
|
139
206
|
}
|
package/tests/web.test.ts
CHANGED
|
@@ -275,7 +275,7 @@ describe("routes", () => {
|
|
|
275
275
|
const res = await app.request("/");
|
|
276
276
|
expect(res.status).toBe(200);
|
|
277
277
|
const body = await res.text();
|
|
278
|
-
expect(body).toContain("Multiplayer
|
|
278
|
+
expect(body).toContain("Multiplayer collaboration");
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
test("GET /preview returns 200", async () => {
|