@lightupai/polaris 0.0.33 → 0.0.35

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,133 @@
1
+ # Design: @Mentions
2
+
3
+ ## Context
4
+
5
+ Users want to tag teammates while working in a coding session — "get Krishna's opinion on this approach." The tag should be a real Slack mention that triggers a notification, not plain text.
6
+
7
+ ## UX Goal
8
+
9
+ As close to native Slack `@mention` as possible. The agent knows the team, resolves names accurately, and the tagged person gets a real notification.
10
+
11
+ ## Design Decisions
12
+
13
+ 1. **Discovery**: Agent has the full team member list available (via `polaris_team` tool). No guessing or fuzzy matching.
14
+ 2. **Resolution**: Agent resolves `@krishna` → Slack user ID inline when constructing the message. The team list is fetched at session start or on demand.
15
+ 3. **Semantics**: Start as a Slack mention (notification only). Extend later to invitations, review requests, tracked consultations.
16
+ 4. **Inbound mentions**: When Slack users @mention other users in messages to sessions, the floor captures the mention semantically (who was tagged, in what context).
17
+
18
+ ## Implementation
19
+
20
+ ### polaris_team tool
21
+
22
+ New MCP tool that returns the org's team members with their Slack user IDs.
23
+
24
+ ```typescript
25
+ {
26
+ name: "polaris_team",
27
+ description: "List team members with their Slack identities. Use this to resolve @mentions.",
28
+ inputSchema: { type: "object", properties: {} },
29
+ }
30
+ ```
31
+
32
+ Response:
33
+ ```json
34
+ {
35
+ "members": [
36
+ { "name": "Krishna Patel", "participant_id": "user:krishna.patel", "slack_id": "U0XXXXXXX", "slack_display": "krishna" },
37
+ { "name": "Tuhin Roy", "participant_id": "user:tuhin.roy", "slack_id": "U0YYYYYYY", "slack_display": "tuhin" },
38
+ { "name": "Manu Bansal", "participant_id": "user:manu.bansal", "slack_id": "UCUHHNJDT", "slack_display": "manu" }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ ### Data source
44
+
45
+ The team list comes from:
46
+ 1. **Slack workspace members** — `users.list` API call, cached by the bridge/API
47
+ 2. **Polaris users table** — users who have signed up, with their participant IDs
48
+
49
+ The API needs a new endpoint: `GET /team` that joins Slack workspace members with Polaris users. The daemon proxies this like other API calls.
50
+
51
+ ### Mention in polaris_reply
52
+
53
+ When the agent calls `polaris_reply` with text containing `<@UXXXXXXX>`, the bridge posts it as-is — Slack renders the mention natively.
54
+
55
+ The agent's flow:
56
+ 1. User says "tag krishna about this auth approach"
57
+ 2. Agent calls `polaris_team` (or uses cached result)
58
+ 3. Finds Krishna → `slack_id: U0XXXXXXX`
59
+ 4. Calls `polaris_reply` with `"<@U0XXXXXXX> what do you think about this auth approach?"`
60
+ 5. Bridge posts to Slack, Krishna gets a notification
61
+
62
+ ### Mention resolution in the bridge (outbound)
63
+
64
+ As a fallback, the bridge can also resolve `@krishna` → `<@U0XXXXXXX>` in message text before posting. This handles cases where the agent or hooks include plain `@name` without resolution.
65
+
66
+ Resolution logic:
67
+ 1. Scan message text for `@word` patterns
68
+ 2. Look up each word against Slack display names (cached)
69
+ 3. Replace with `<@slack_id>` if unique match
70
+ 4. Leave as plain text if no match or ambiguous
71
+
72
+ ### Inbound mention tracking
73
+
74
+ When a Slack message is injected into a session and contains `<@UXXXXXXX>` patterns:
75
+ 1. The bridge resolves the Slack ID to a display name
76
+ 2. The inject event payload includes a `mentions` array: `["user:krishna.patel"]`
77
+ 3. The floor records who was consulted on what
78
+
79
+ This is metadata on the event — no schema change needed (it goes in the JSONB payload).
80
+
81
+ ## Caching
82
+
83
+ The Slack user list is expensive to fetch (paginated API call). Cache it:
84
+ - **Bridge**: on startup and every 30 minutes
85
+ - **API /team endpoint**: cache for 5 minutes
86
+ - **Agent**: fetches once per session via `polaris_team`, uses for all mentions in that session
87
+
88
+ ## Skill update
89
+
90
+ The skill instructions should tell the agent:
91
+ - When the user mentions someone by name, call `polaris_team` to resolve their Slack ID
92
+ - Use `<@slack_id>` format in `polaris_reply` messages
93
+ - If unsure which person, present the matches and ask
94
+
95
+ ## UX Flow (v1)
96
+
97
+ Claude Code CLI's `@` is reserved for file references. Instead, use a conversational `/polaris tag` command:
98
+
99
+ ```
100
+ User: /polaris tag
101
+ Agent: Who would you like to tag?
102
+ 1. Krishna Patel (@krishna)
103
+ 2. Tuhin Roy (@tuhin)
104
+ 3. Laura Mowry (@laura)
105
+ User: 1
106
+ Agent: What message?
107
+ User: what do you think about this auth approach?
108
+ Agent: → Posted to #polaris-dev: @krishna what do you think about this auth approach?
109
+ ```
110
+
111
+ The agent calls `polaris_team` to get the list, presents it, and calls `polaris_reply` with the resolved Slack mention.
112
+
113
+ ### Shorthand (future)
114
+
115
+ Once the flow works, add inline shorthand so the user can skip the interactive steps:
116
+ - `/polaris tag krishna what do you think?` — resolves and posts in one step
117
+ - Custom Claude Code `@` completion provider (feature request to Anthropic)
118
+
119
+ ## Future extensions
120
+
121
+ - **Mention as invitation**: tagging someone could auto-invite them as an advisor to the session
122
+ - **Review request**: `@krishna review this PR` creates a tracked review request
123
+ - **Mention analytics**: dashboard shows who was consulted most, on which projects
124
+ - **Agent-to-person tagging**: named agents like Dean could tag humans when they need input
125
+
126
+ ## Implementation Order
127
+
128
+ 1. `GET /team` API endpoint (join Slack users with Polaris users)
129
+ 2. Daemon `/team` proxy endpoint
130
+ 3. `polaris_team` MCP tool
131
+ 4. Bridge fallback mention resolution (outbound)
132
+ 5. Inbound mention tracking in inject events
133
+ 6. Skill update with mention instructions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "polaris": "bin/polaris",
@@ -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 polaris_backfill
5
- argument-hint: [join #channel | backfill [duration] | 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 polaris_team
5
+ argument-hint: [join #channel | tag [name] | backfill [duration] | rename <new-name> | disconnect | (no args for status)]
6
6
  ---
7
7
 
8
8
  ## Polaris — Multiplayer Collaboration
@@ -22,6 +22,13 @@ 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 tag [name]`** — Tag a teammate on Slack:
26
+ 1. Call `polaris_team` to get the team list with Slack identities
27
+ 2. If a name was given, find the matching member. If no name, present a numbered list for the user to pick.
28
+ 3. Ask the user what message to send (if not already provided)
29
+ 4. Call `polaris_reply` with the message, including the Slack mention in `<@SLACK_ID>` format
30
+ 5. Confirm the message was sent
31
+
25
32
  **`/polaris backfill [duration]`** — Recover lost events:
26
33
  1. Call `polaris_backfill` with the optional duration (e.g., `2h`, `30m`)
27
34
  2. Report how many events were recovered
@@ -110,6 +110,14 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
110
110
  required: ["session"],
111
111
  },
112
112
  },
113
+ {
114
+ name: "polaris_team",
115
+ description: "List team members with their Slack identities. Use this to resolve @mentions before posting to Slack.",
116
+ inputSchema: {
117
+ type: "object" as const,
118
+ properties: {},
119
+ },
120
+ },
113
121
  {
114
122
  name: "polaris_backfill",
115
123
  description: "Recover lost events from local daemon logs. Use when events were lost due to disconnection or API downtime.",
@@ -255,6 +263,25 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
255
263
  }
256
264
  }
257
265
 
266
+ if (name === "polaris_team") {
267
+ try {
268
+ const res = await daemonGet("/team");
269
+ if (res.ok) {
270
+ const body = await res.json() as { members: Array<{ name: string; participant_id: string; slack_id: string | null; slack_display: string | null }> };
271
+ if (body.members.length === 0) {
272
+ return { content: [{ type: "text", text: "No team members found." }] };
273
+ }
274
+ const list = body.members.map((m, i) =>
275
+ ` ${i + 1}. ${m.name} (${m.participant_id})${m.slack_display ? ` — @${m.slack_display}` : ""}${m.slack_id ? ` [${m.slack_id}]` : ""}`
276
+ ).join("\n");
277
+ return { content: [{ type: "text", text: `Team members:\n${list}` }] };
278
+ }
279
+ return { content: [{ type: "text", text: "Failed to fetch team list." }] };
280
+ } catch {
281
+ return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
282
+ }
283
+ }
284
+
258
285
  if (name === "polaris_backfill") {
259
286
  if (!currentProject) {
260
287
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
@@ -669,6 +669,21 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
669
669
  }
670
670
  }
671
671
 
672
+ // GET /team — list team members with Slack identities
673
+ if (method === "GET" && pathname === "/team") {
674
+ try {
675
+ const serviceUrl = getServiceUrl();
676
+ const res = await fetch(`${serviceUrl}/team`, {
677
+ headers: await authHeaders(),
678
+ });
679
+ if (!res.ok) return error("Failed to fetch team", res.status);
680
+ const data = await res.json();
681
+ return json(data);
682
+ } catch {
683
+ return error("API unreachable", 503);
684
+ }
685
+ }
686
+
672
687
  // POST /backfill — recover lost events from daemon log
673
688
  if (method === "POST" && pathname === "/backfill") {
674
689
  try {
package/src/service/db.ts CHANGED
@@ -189,6 +189,11 @@ export async function upsertUser(sql: Sql, id: string, email: string, name: stri
189
189
  return { ...row, created_at: row.created_at.toISOString() } as User;
190
190
  }
191
191
 
192
+ export async function listUsers(sql: Sql, orgId: string): Promise<User[]> {
193
+ const rows = await sql`SELECT * FROM users WHERE org_id = ${orgId} ORDER BY name ASC`;
194
+ return rows.map((r) => ({ ...r, created_at: r.created_at.toISOString() }) as User);
195
+ }
196
+
192
197
  // --- Projects (org-scoped) ---
193
198
 
194
199
  export async function createProject(sql: Sql, orgId: string, name: string): Promise<Project> {
@@ -444,6 +444,50 @@ export async function startServer(opts: {
444
444
  return json({ status: "claimed", driver: parsed.data.driver, session: params.sess });
445
445
  }
446
446
 
447
+ // GET /team — list org members with Slack identities
448
+ if (method === "GET" && pathname === "/team") {
449
+ const { listUsers, getOrg: getOrgFn } = await import("./db");
450
+ const users = await listUsers(sql, orgId);
451
+ const org = await getOrgFn(sql, orgId);
452
+
453
+ // Resolve Slack user info if bot token available
454
+ let slackMembers: Array<{ id: string; name: string; display_name: string }> = [];
455
+ if (org?.slack_bot_token) {
456
+ try {
457
+ const slackRes = await fetch("https://slack.com/api/users.list?limit=200", {
458
+ headers: { Authorization: `Bearer ${org.slack_bot_token}` },
459
+ });
460
+ if (slackRes.ok) {
461
+ const slackData = (await slackRes.json()) as { members?: Array<{ id: string; real_name?: string; profile?: { display_name?: string }; deleted?: boolean; is_bot?: boolean }> };
462
+ slackMembers = (slackData.members ?? [])
463
+ .filter((m) => !m.deleted && !m.is_bot)
464
+ .map((m) => ({
465
+ id: m.id,
466
+ name: m.real_name ?? "",
467
+ display_name: m.profile?.display_name ?? "",
468
+ }));
469
+ }
470
+ } catch { /* Slack API unavailable */ }
471
+ }
472
+
473
+ // Join Polaris users with Slack members by email match or name match
474
+ const team = users.map((u) => {
475
+ const slack = slackMembers.find((m) =>
476
+ m.name.toLowerCase() === u.name.toLowerCase() ||
477
+ m.display_name.toLowerCase() === u.name.toLowerCase().replace(/\s+/g, ".")
478
+ );
479
+ return {
480
+ name: u.name,
481
+ participant_id: u.participant_id,
482
+ email: u.email,
483
+ slack_id: slack?.id ?? null,
484
+ slack_display: slack?.display_name || slack?.name || null,
485
+ };
486
+ });
487
+
488
+ return json({ members: team });
489
+ }
490
+
447
491
  if (method === "GET" && pathname === "/status") {
448
492
  return json({ ok: true, version: "0.0.1" });
449
493
  }
package/src/web/pages.ts CHANGED
@@ -11,10 +11,11 @@ export function renderLandingPage(): string {
11
11
  <!-- Hero -->
12
12
  <div class="pt-24 pb-16">
13
13
  <h1 class="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
14
- Multiplayer collaboration<br>for Claude Code
14
+ Realtime multiplayer Claude Code<br>sessions and knowledge capture
15
15
  </h1>
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
+ <p class="mt-2 text-sm font-medium text-polaris-600">It's like Gong for AI.</p>
17
+ <p class="mt-4 text-lg text-gray-500 max-w-2xl">
18
+ Move your Claude Code sessions out of private windows and into a shared workspace in Slack. Polaris runs quietly in the background to document your sessions automatically, turning isolated chats into a continuous stream of team intelligence.
18
19
  </p>
19
20
  <div class="mt-8 flex items-center gap-4">
20
21
  <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>
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 collaboration");
278
+ expect(body).toContain("Polaris");
279
279
  });
280
280
 
281
281
  test("GET /preview returns 200", async () => {