@lightupai/polaris 0.0.78 → 0.0.80

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.
@@ -1,11 +1,15 @@
1
1
  name: CD
2
2
 
3
- # Continuous deployment: on every push (merge) to master, SSH to the prod
4
- # host and run deploy.sh (git pull + docker compose build/up).
3
+ # Continuous deployment: on every push (merge) to master:
4
+ # 1. SSH to the prod host and run deploy.sh (git pull + docker compose build/up).
5
+ # 2. Publish @lightupai/polaris to npm if the version in package.json has been
6
+ # bumped beyond what is already published (i.e. only when there are client-side
7
+ # changes that warrant a release).
5
8
  #
6
9
  # Required repository secrets (Settings → Secrets and variables → Actions):
7
10
  # DEPLOY_HOST — hostname or IP of the prod box (e.g. polaris.lightup.ai)
8
11
  # DEPLOY_SSH_KEY — private SSH key (OpenSSH format) for the deploy user
12
+ # NPM_TOKEN — npm Automation token (bypasses OTP; create at npmjs.com)
9
13
  #
10
14
  # Optional repository secrets (deploy.sh has sensible defaults):
11
15
  # DEPLOY_USER — SSH user on the prod box (default: deploy)
@@ -75,3 +79,34 @@ jobs:
75
79
  DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
76
80
  DEPLOY_DIR: ${{ secrets.DEPLOY_DIR }}
77
81
  run: ./deploy.sh
82
+
83
+ publish-npm:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+
88
+ - uses: actions/setup-node@v4
89
+ with:
90
+ node-version: '20'
91
+ registry-url: 'https://registry.npmjs.org'
92
+
93
+ - name: Check if version needs publishing
94
+ id: version-check
95
+ run: |
96
+ LOCAL=$(node -p "require('./package.json').version")
97
+ PUBLISHED=$(npm view @lightupai/polaris version 2>/dev/null || echo "0.0.0")
98
+ echo "local=$LOCAL" >> "$GITHUB_OUTPUT"
99
+ echo "published=$PUBLISHED" >> "$GITHUB_OUTPUT"
100
+ if [ "$LOCAL" != "$PUBLISHED" ]; then
101
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
102
+ echo "Version $LOCAL differs from published $PUBLISHED — will publish."
103
+ else
104
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
105
+ echo "Version $LOCAL already published — skipping npm publish."
106
+ fi
107
+
108
+ - name: Publish to npm
109
+ if: steps.version-check.outputs.should_publish == 'true'
110
+ env:
111
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
112
+ run: npm publish --access public
@@ -46,6 +46,7 @@ services:
46
46
  SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET}
47
47
  SLACK_REDIRECT_URI: https://app.withpolaris.ai/slack/callback
48
48
  SIGNUP_SLACK_BOT_TOKEN: ${SIGNUP_SLACK_BOT_TOKEN:-}
49
+ POLARIS_API_URL: http://api:4321
49
50
  depends_on:
50
51
  postgres:
51
52
  condition: service_healthy
@@ -10,9 +10,9 @@ A user who signs up should experience a smooth path from landing page to their f
10
10
 
11
11
  1. Choose a plan (or default to free)
12
12
  2. Sign up with Google
13
- 3. Connect their Slack workspace
13
+ 3. Connect their Slack workspace *(optional — can be skipped and done later)*
14
14
  4. Install the CLI
15
- 5. Join a channel and start their first session
15
+ 5. Start their first session (join a channel if Slack is connected)
16
16
 
17
17
  Each step should feel intentional, not accidental. The user should never be left wondering "what do I do next?"
18
18
 
@@ -34,17 +34,18 @@ After Google OAuth completes, the callback handler should route based on the use
34
34
  |---|---|
35
35
  | Existing user (login) | Dashboard |
36
36
  | New user, existing org | Dashboard (org already set up) |
37
- | New user, new org, no Slack | Onboarding: Connect Slack |
37
+ | New user, new org, no Slack | Onboarding: Connect Slack (with skip option) |
38
38
  | New user, new org, Slack connected | Onboarding: Install CLI |
39
39
 
40
40
  ### Onboarding Flow
41
41
 
42
42
  A dedicated onboarding page replaces the empty dashboard for new orgs. Three steps, shown as a checklist:
43
43
 
44
- **Step 1: Connect Slack**
45
- - Large "Add to Slack" button
46
- - Explain: "Polaris uses Slack as the collaboration layer. Connect your workspace to get started."
47
- - After connecting, auto-advance to Step 2
44
+ **Step 1: Connect Slack** *(optional)*
45
+ - Large "Connect Slack" button
46
+ - Explain: "Share sessions with your team in real time. Connect Slack to publish your session log to a channel."
47
+ - "Skip for now" link lets users proceed without connecting. Skip state is stored in a browser cookie (`polaris_floor_skipped`).
48
+ - After connecting (or skipping), advance to Step 2
48
49
 
49
50
  **Step 2: Install the CLI**
50
51
  - Show `npx @lightupai/polaris` with a copy button
@@ -120,8 +121,8 @@ When a free-tier org approaches or hits their limit:
120
121
  - [ ] **Store selected plan on org record**
121
122
  Add a `plan` column to the orgs table (free/team/enterprise, default 'free'). Populate from the `?plan` query param during signup. Show current plan on the dashboard.
122
123
 
123
- - [ ] **Prompt Slack connection immediately after signup**
124
- For new orgs without Slack connected, route to setup view with prominent "Add to Slack" button instead of empty dashboard. Ensure the flow from signup → setup is seamless.
124
+ - [x] **Prompt Slack connection immediately after signup**
125
+ Setup view shows Floor as step 1 with a "Connect Slack" button. Users can skip via "Skip for now" (cookie-based) and proceed to install the CLI and start sessions without Slack. Floor can be connected at any time from the setup or active dashboard.
125
126
 
126
127
  - [ ] **Post-signup onboarding flow**
127
128
  After a brand new org signup, redirect to onboarding page (not dashboard). Three steps: Connect Slack → Install CLI → Join a channel. Repurpose existing `renderWelcomePage`/`renderSetupView`. Auto-advance when each step is completed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "description": "Record and stream Claude Code sessions to Slack. Like Gong for AI coding — capture every prompt, response, and tool call. Multiplayer collaboration across AI coding agents.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -71,7 +71,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
71
71
  inputSchema: {
72
72
  type: "object" as const,
73
73
  properties: {
74
- channel: { type: "string", description: "Channel name (e.g., #polaris-dev). Omit to list available channels." },
74
+ channel: { type: "string", description: "Project workspace to join (e.g., #my-project). Becomes the Slack channel name if a floor is connected. Omit to list existing projects." },
75
75
  user: { type: "string", description: "Your participant ID (e.g., user:manu)" },
76
76
  session: { type: "string", description: "Session name (optional — auto-generated if omitted)" },
77
77
  agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" },
@@ -97,7 +97,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
97
97
  },
98
98
  {
99
99
  name: "polaris_reply",
100
- description: "Send a message to the project floor (visible to all advisors and the Slack/WhatsApp channel).",
100
+ description: "Send a message to the project floor (visible to all advisors, and to the linked Slack channel if a floor is connected).",
101
101
  inputSchema: {
102
102
  type: "object" as const,
103
103
  properties: {
@@ -108,7 +108,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
108
108
  },
109
109
  {
110
110
  name: "polaris_rename",
111
- description: "Rename the current project. Also renames the Slack channel.",
111
+ description: "Rename the current project. Also renames the linked Slack channel if a floor is connected.",
112
112
  inputSchema: {
113
113
  type: "object" as const,
114
114
  properties: {
@@ -184,12 +184,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
184
184
  if (res.ok) {
185
185
  const body = await res.json() as { channels: string[] };
186
186
  if (body.channels.length === 0) {
187
- return { content: [{ type: "text", text: "No channels found. Create one by joining: `/polaris join #channel-name`" }] };
187
+ return { content: [{ type: "text", text: "No projects found. Start one with: `/polaris join #my-project`" }] };
188
188
  }
189
- return { content: [{ type: "text", text: `Available channels:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #channel-name` }] };
189
+ return { content: [{ type: "text", text: `Available projects:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #project-name` }] };
190
190
  }
191
191
  } catch { /* fall through */ }
192
- return { content: [{ type: "text", text: "Specify a channel: `/polaris join #channel-name`" }] };
192
+ return { content: [{ type: "text", text: "Specify a project: `/polaris join #my-project`" }] };
193
193
  }
194
194
 
195
195
  const project = channel.replace(/^#/, ""); // strip leading # if present
@@ -13,7 +13,7 @@ interface SessionMapping {
13
13
  agent: string;
14
14
  slackChannel?: string;
15
15
  ws: WebSocket | null;
16
- pendingMapping?: boolean; // true until a hook event maps the real CC session ID
16
+ pendingMapping?: boolean; // true until the first hook event from this shell claims the mapping
17
17
  }
18
18
 
19
19
  function generateSessionName(): string {
@@ -590,19 +590,18 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
590
590
  // MCP, connected via /connect, or a previously learned alias) —
591
591
  // always routed by that match, regardless of how many other
592
592
  // sessions are connected. A matchable event is never dropped.
593
- // 2. No match + exactly one connected session: route to it and
594
- // remember session_id as an alias of that mapping (the MCP
595
- // client's generated UUID differs from CC's hook session_id),
596
- // so later events route by rule 1 even once more sessions join.
597
- // 3. No match + multiple connected sessions: truly unmatchable —
598
- // drop, but loudly (console warning + JSONL log entry).
599
- // 4. No match + nothing connected: not_connected (existing).
593
+ // 2. No exact match + a pending session exists: the MCP client's
594
+ // generated UUID differs from CC's hook session_id. The first
595
+ // hook event from the shell that ran /connect claims the pending
596
+ // slot and becomes the canonical alias going forward.
597
+ // 3. No match + no pending session: unknown hook — the shell never
598
+ // ran polaris_connect. Always returns not_connected; hooks from
599
+ // unjoined shells are never auto-routed to an existing session.
600
600
  let mapping = sessions.get(ccSessionId);
601
601
  if (!mapping || !mapping.project) {
602
602
  // CC session_id doesn't match any registered MCP client. The MCP
603
603
  // server uses a different UUID than CC's session_id. Match to a
604
- // session with pendingMapping (most recent first); fall back to the
605
- // single-connected-session heuristic when none is pending.
604
+ // session with pendingMapping (most recent first).
606
605
  const pending = Array.from(sessions.values()).filter((m) => m.project && m.pendingMapping);
607
606
  if (pending.length > 0) {
608
607
  // Map the CC session ID to the most recently connected pending session
@@ -618,32 +617,10 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
618
617
  injectQueues.set(mapping.ccSessionId, queue);
619
618
  injectQueues.set(ccSessionId, queue);
620
619
  } else {
621
- // No pending sessions — fall back to the single-session heuristic.
622
- const connectedSessions = Array.from(sessions.values()).filter((m) => m.project);
623
- if (connectedSessions.length === 1) {
624
- // Only one active session — route to it and remember the mapping
625
- mapping = connectedSessions[0];
626
- sessions.set(ccSessionId, { ...mapping, ccSessionId, slackChannel: undefined });
627
- // Share the inject queue (see note above).
628
- const queue = injectQueues.get(mapping.ccSessionId) ?? [];
629
- injectQueues.set(mapping.ccSessionId, queue);
630
- injectQueues.set(ccSessionId, queue);
631
- } else if (connectedSessions.length > 1) {
632
- // Multiple sessions and session_id matches none of them (nor any
633
- // learned alias) — can't determine which one. Drop with a clear
634
- // warning, never silently.
635
- const candidates = connectedSessions.map((m) => `${m.project}/${m.session}`).join(", ");
636
- console.error(
637
- `polaris daemon: dropping ${String(body.hook_event_name ?? "hook")} event — ` +
638
- `session_id ${ccSessionId} matches no known mapping and ${connectedSessions.length} ` +
639
- `sessions are connected (${candidates}). ` +
640
- `Reconnect with polaris_connect in the affected Claude session to re-establish routing.`
641
- );
642
- await logEvent("/events", body, { status: 0, body: `dropped: ambiguous across ${connectedSessions.length} sessions (${candidates})` });
643
- return json({ status: "ambiguous" });
644
- } else {
645
- return json({ status: "not_connected" });
646
- }
620
+ // No pending sessions — this hook's session_id doesn't match any
621
+ // known mapping. Unjoined shells are never auto-routed; the user
622
+ // must run polaris_connect explicitly in this Claude session.
623
+ return json({ status: "not_connected" });
647
624
  }
648
625
  }
649
626
 
package/src/web/app.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Hono } from "hono";
2
+ import { getCookie, setCookie } from "hono/cookie";
2
3
  import { Google } from "arctic";
3
4
  import { createToken, verifyToken } from "../service/auth";
4
5
  import {
@@ -207,8 +208,8 @@ export function createApp(sql: Sql) {
207
208
  // --- Landing page ---
208
209
 
209
210
  app.get("/", (c) => {
210
- return layout(renderLandingPage(), "Polaris — It's like Gong for Claude Code sessions", {
211
- title: "Polaris — It's like Gong for Claude Code sessions",
211
+ return layout(renderLandingPage(), "Polaris by Lightup — It's like Gong for Claude Code sessions", {
212
+ title: "Polaris by Lightup — It's like Gong for Claude Code sessions",
212
213
  description: "Capture every AI coding session. Stream prompts, responses, and tool calls to Slack in real time. Collaborate across agents. Nothing is lost.",
213
214
  canonical: "https://app.withpolaris.ai",
214
215
  });
@@ -384,6 +385,8 @@ export function createApp(sql: Sql) {
384
385
 
385
386
  const hasConnectedSession = allSessions.length > 0;
386
387
 
388
+ const floorSkipped = getCookie(c, "polaris_floor_skipped") === "1";
389
+
387
390
  const ctx = {
388
391
  token,
389
392
  userName: payload.name,
@@ -391,6 +394,7 @@ export function createApp(sql: Sql) {
391
394
  orgSlug: org.slug,
392
395
  email: payload.email,
393
396
  slackConnected: !!org.slack_team_id,
397
+ floorSkipped,
394
398
  cliInstalled,
395
399
  hasConnectedSession,
396
400
  totalPrompts: Array.from(promptCounts.values()).reduce((a, b) => a + b, 0),
@@ -400,9 +404,18 @@ export function createApp(sql: Sql) {
400
404
  };
401
405
 
402
406
  if (hasConnectedSession) {
403
- return layout(renderActiveView(ctx, sessionFixtures, projectFixtures, devices), "Dashboard — Polaris");
407
+ return layout(renderActiveView(ctx, sessionFixtures, projectFixtures, devices), "Dashboard — Polaris by Lightup");
404
408
  }
405
- return layout(renderSetupView(ctx, devices), "Setup — Polaris");
409
+ return layout(renderSetupView(ctx, devices), "Setup — Polaris by Lightup");
410
+ });
411
+
412
+ // --- Skip floor setup ---
413
+
414
+ app.get("/skip/floor", async (c) => {
415
+ const token = c.req.query("token");
416
+ if (!token) return c.redirect("/login");
417
+ setCookie(c, "polaris_floor_skipped", "1", { path: "/", maxAge: 60 * 60 * 24 * 365, httpOnly: true, sameSite: "Lax" });
418
+ return c.redirect(`/dashboard?token=${token}`);
406
419
  });
407
420
 
408
421
  // --- Profile ---
@@ -429,7 +442,7 @@ export function createApp(sql: Sql) {
429
442
  totalPrompts: 0,
430
443
  };
431
444
 
432
- return layout(renderProfileView(ctx, payload.participant_id), "Polaris - Profile");
445
+ return layout(renderProfileView(ctx, payload.participant_id), "Polaris by Lightup - Profile");
433
446
  });
434
447
 
435
448
  // --- Session transcript ---
@@ -610,7 +623,7 @@ export function createApp(sql: Sql) {
610
623
  }
611
624
 
612
625
  const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
613
- return layout(renderDecisionsView(ctx, project, decisions, decisionsError), "Polaris - Decisions");
626
+ return layout(renderDecisionsView(ctx, project, decisions, decisionsError), "Polaris by Lightup - Decisions");
614
627
  });
615
628
 
616
629
  // --- Project settings (visibility + members) ---
@@ -782,14 +795,14 @@ export function createApp(sql: Sql) {
782
795
  }
783
796
 
784
797
  const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
785
- return layout(renderSearchView(ctx, query, results, searchError), "Polaris - Search");
798
+ return layout(renderSearchView(ctx, query, results, searchError), "Polaris by Lightup - Search");
786
799
  });
787
800
 
788
801
  // --- Preview (dev only — all view states on one page) ---
789
802
 
790
803
  app.get("/preview", (c) => {
791
804
  const mockToken = "preview-token";
792
- const base = { token: mockToken, userName: mockUser.name, orgName: mockOrg.name, orgSlug: "lightup-data" as string | null, email: mockUser.email };
805
+ const base = { token: mockToken, userName: mockUser.name, orgName: mockOrg.name, orgSlug: "lightup-data" as string | null, email: mockUser.email, floorSkipped: false };
793
806
 
794
807
  const mockTeam = [{ name: mockUser.name, email: mockUser.email }, { name: "Alice Chen", email: "alice@lightup.ai" }, { name: "Laura Mowry", email: "laura@lightup.ai" }];
795
808
  const mockSenders = ["user:manu.bansal", "user:alice.chen", "user:laura.mowry"];
@@ -799,10 +812,11 @@ export function createApp(sql: Sql) {
799
812
  return { date: d.toISOString().slice(0, 10), sender, count: Math.floor(Math.random() * 12) + 1 };
800
813
  })
801
814
  );
802
- const fresh = { ...base, orgSlug: null, slackConnected: false, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0 };
803
- const slackDone = { ...base, slackConnected: true, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
804
- const cliDone = { ...base, slackConnected: true, cliInstalled: true, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
805
- const allDone = { ...base, slackConnected: true, cliInstalled: true, hasConnectedSession: true, totalPrompts: 127, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
815
+ const fresh = { ...base, orgSlug: null, slackConnected: false, floorSkipped: false, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0 };
816
+ const floorSkipped = { ...base, orgSlug: null, slackConnected: false, floorSkipped: true, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0 };
817
+ const slackDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
818
+ const cliDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: true, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
819
+ const allDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: true, hasConnectedSession: true, totalPrompts: 127, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
806
820
  const teamPlan = { ...fresh, plan: "pro" };
807
821
 
808
822
  return layout(`
@@ -819,6 +833,14 @@ export function createApp(sql: Sql) {
819
833
  </div>
820
834
  </section>
821
835
 
836
+ <section>
837
+ <h2 class="text-lg font-bold text-gray-700 mb-1">Setup: floor skipped</h2>
838
+ <p class="text-sm text-gray-400 mb-4">User clicked "Skip for now" — floor subdued, Devices becomes active.</p>
839
+ <div class="border border-gray-200 rounded-xl overflow-hidden shadow-sm">
840
+ ${renderSetupView(floorSkipped)}
841
+ </div>
842
+ </section>
843
+
822
844
  <section>
823
845
  <h2 class="text-lg font-bold text-gray-700 mb-1">Setup: Slack connected</h2>
824
846
  <p class="text-sm text-gray-400 mb-4">Floor is live, CLI not installed yet.</p>
@@ -899,7 +921,7 @@ export function createApp(sql: Sql) {
899
921
  </section>
900
922
  </div>
901
923
  </div>
902
- `, "Polaris - Preview");
924
+ `, "Polaris by Lightup - Preview");
903
925
  });
904
926
 
905
927
  // --- Slack OAuth ---
package/src/web/layout.ts CHANGED
@@ -14,7 +14,7 @@ export interface SeoOpts {
14
14
  canonical?: string;
15
15
  }
16
16
 
17
- export function layout(body: string, title = "Polaris", seo?: SeoOpts): Response {
17
+ export function layout(body: string, title = "Polaris by Lightup", seo?: SeoOpts): Response {
18
18
  const pageTitle = seo?.title ?? title;
19
19
  const description = seo?.description ?? "It's like Gong for Claude Code sessions. Capture every prompt, response, and tool call. Stream to Slack. Collaborate in real time.";
20
20
  const canonical = seo?.canonical ?? "https://app.withpolaris.ai";
@@ -131,7 +131,10 @@ export function nav(token?: string, opts?: NavOpts): string {
131
131
  return `
132
132
  <nav class="border-b border-gray-200 bg-white">
133
133
  <div class="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
134
- <a href="${token ? `/dashboard?token=${token}` : "/"}" class="text-lg font-bold tracking-tight text-gray-900">Polaris</a>
134
+ <div class="flex items-center gap-1.5">
135
+ <a href="${token ? `/dashboard?token=${token}` : "/"}" class="text-lg font-bold tracking-tight text-gray-900">Polaris</a>
136
+ <span class="text-sm text-gray-400">by <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a></span>
137
+ </div>
135
138
  <div class="flex items-center gap-4">${right}</div>
136
139
  </div>
137
140
  </nav>${bannerHtml}`;
package/src/web/pages.ts CHANGED
@@ -488,7 +488,7 @@ export function renderLandingPage(): string {
488
488
 
489
489
  <footer class="border-t border-gray-200 mt-8">
490
490
  <div class="max-w-3xl mx-auto px-6 py-8 flex items-center justify-between text-sm text-gray-400">
491
- <span>Polaris</span>
491
+ <span>© ${new Date().getFullYear()} <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a>. All rights reserved.</span>
492
492
  <div class="flex items-center gap-6">
493
493
  <a href="https://github.com/lightup-data/polaris/discussions" class="inline-flex items-center gap-1.5 hover:text-gray-600 transition"><svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>Community</a>
494
494
  <a href="/login" class="hover:text-gray-600 transition">Sign in</a>
@@ -505,7 +505,7 @@ export function renderWelcomePage(token: string, name: string, orgName: string,
505
505
  <div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
506
506
  <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
507
507
  </div>
508
- <h1 class="text-2xl font-bold text-gray-900">Welcome to Polaris</h1>
508
+ <h1 class="text-2xl font-bold text-gray-900">Welcome to Polaris <span class="font-normal text-lg text-gray-400">by <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a></span></h1>
509
509
  <p class="mt-2 text-gray-600">${orgName} is ready to go.</p>
510
510
  </div>
511
511
  <div class="mt-8">
package/src/web/views.ts CHANGED
@@ -17,6 +17,7 @@ interface ViewContext {
17
17
  orgSlug: string | null;
18
18
  email: string;
19
19
  slackConnected: boolean;
20
+ floorSkipped: boolean;
20
21
  cliInstalled: boolean;
21
22
  hasConnectedSession: boolean;
22
23
  totalPrompts: number;
@@ -276,26 +277,42 @@ function renderFloorSection(ctx: ViewContext, compact = false, state: StepState
276
277
  </div>`;
277
278
  }
278
279
 
280
+ if (ctx.floorSkipped) {
281
+ return `
282
+ <div class="opacity-50">
283
+ <div class="flex items-baseline gap-2 mb-3">
284
+ <h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor</h2>
285
+ ${statusBadge("Skipped", false)}
286
+ </div>
287
+ <div class="bg-gray-50 border border-gray-200 rounded-lg px-5 py-3 flex items-center justify-between">
288
+ <p class="text-sm text-gray-400">No floor connected.</p>
289
+ <a href="/slack/install?token=${ctx.token}" class="text-xs text-gray-400 hover:text-gray-600 transition">Connect Slack</a>
290
+ </div>
291
+ </div>`;
292
+ }
293
+
279
294
  return sectionWrap(state, `
280
295
  <div>
281
296
  <div class="flex items-baseline gap-2 mb-3">
282
- <h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor</h2>
283
- ${statusBadge("Not connected", false)}
297
+ <h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor <span class="text-[10px] font-normal normal-case text-gray-400">— optional</span></h2>
284
298
  </div>
285
- <div class="bg-white border ${state === "active" ? cardClass("active") : "border-amber-200"} rounded-lg p-5">
299
+ <div class="bg-white border ${state === "active" ? cardClass("active") : "border-gray-200"} rounded-lg p-5">
286
300
  <div class="flex items-center gap-3">
287
301
  <div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center shrink-0">
288
302
  <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
289
303
  </div>
290
304
  <div>
291
- <p class="text-sm font-semibold text-gray-900">Slack</p>
292
- <p class="text-sm text-gray-500 mt-0.5">Connect your Slack workspace to enable the floor for your team.</p>
305
+ <p class="text-sm font-semibold text-gray-900">Connect a floor</p>
306
+ <p class="text-sm text-gray-500 mt-0.5">Share sessions with your team in real time. Connect Slack to publish your session log to a channel.</p>
293
307
  </div>
294
308
  </div>
295
- <a href="/slack/install?token=${ctx.token}" class="mt-3 inline-flex items-center gap-2 px-4 py-2 bg-[#4A154B] text-white text-sm font-medium rounded-lg hover:bg-[#3a1039] transition">
296
- ${slackIcon}
297
- Connect Slack
298
- </a>
309
+ <div class="mt-3 flex items-center gap-4">
310
+ <a href="/slack/install?token=${ctx.token}" class="inline-flex items-center gap-2 px-4 py-2 bg-[#4A154B] text-white text-sm font-medium rounded-lg hover:bg-[#3a1039] transition">
311
+ ${slackIcon}
312
+ Connect Slack
313
+ </a>
314
+ <a href="/skip/floor?token=${ctx.token}" class="text-sm text-gray-400 hover:text-gray-600 transition">Skip for now</a>
315
+ </div>
299
316
  </div>
300
317
  </div>`);
301
318
  }
@@ -402,7 +419,10 @@ function renderProjectsSessionsSection(ctx: ViewContext, sessions: SessionFixtur
402
419
  : "Inside your AI agent (Claude Code, Cursor, etc.), run:"}</p>
403
420
  ${ctx.hasConnectedSession
404
421
  ? ""
405
- : copyBlock("/polaris join #my-channel")}
422
+ : copyBlock("/polaris join #my-project")}
423
+ ${ctx.hasConnectedSession
424
+ ? ""
425
+ : `<p class="text-xs text-gray-400 mt-2">The name becomes your project workspace — and your Slack channel if a floor is connected.</p>`}
406
426
  </div>
407
427
  </div>`);
408
428
  }
@@ -474,7 +494,8 @@ function autoRefreshScript(token: string): string {
474
494
  // Same three sections, but each shows its setup prompt instead of live data.
475
495
 
476
496
  export function renderSetupView(ctx: ViewContext, devices: DeviceFixture[] = []): string {
477
- const nextStep = !ctx.slackConnected ? "floor" : !ctx.cliInstalled ? "devices" : "sessions";
497
+ const floorDone = ctx.slackConnected || ctx.floorSkipped;
498
+ const nextStep = !floorDone ? "floor" : !ctx.cliInstalled ? "devices" : "sessions";
478
499
  const stepState = (step: string): "done" | "active" | "future" => {
479
500
  const order = ["floor", "devices", "sessions"];
480
501
  const nextIdx = order.indexOf(nextStep);