@lightupai/polaris 0.0.101 → 0.0.103

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.101",
3
+ "version": "0.0.103",
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",
@@ -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 polaris_team
5
- argument-hint: [join #channel | tag [name] | 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_rename_session polaris_backfill polaris_team
5
+ argument-hint: [join #channel [session-name] | tag [name] | backfill [duration] | rename <new-name> | rename-session <name> | disconnect | (no args for status)]
6
6
  ---
7
7
 
8
8
  ## Polaris — Multiplayer Collaboration
@@ -13,8 +13,8 @@ Manage your connection to a Polaris collaboration session.
13
13
 
14
14
  Based on the arguments provided, do ONE of the following:
15
15
 
16
- **`/polaris join #channel-name`** — Connect to an existing project, or create a new one:
17
- 1. Call `polaris_connect` with the given channel and user identity `user:manu.bansal`
16
+ **`/polaris join #channel-name [session-name]`** — Connect to an existing project, or create a new one:
17
+ 1. Call `polaris_connect` with the given channel, user identity `user:manu.bansal`, and optional `session` name (e.g. `auth-feature`, `bugfix-154`)
18
18
  2. If the response is `project_not_found`:
19
19
  - Show the list of existing projects from the response
20
20
  - Ask the user: "Project '#channel-name' doesn't exist. Create it? [y/N]"
@@ -22,10 +22,14 @@ Based on the arguments provided, do ONE of the following:
22
22
  - If no: stop and show the existing project list so they can pick one
23
23
  3. Report the connection status including the session name
24
24
 
25
- **`/polaris rename <new-name>`** — Rename the current channel:
25
+ **`/polaris rename <new-name>`** — Rename the current **project** (and its Slack channel):
26
26
  1. Call `polaris_rename` with the new name
27
27
  2. Report the result
28
28
 
29
+ **`/polaris rename-session <new-name>`** — Rename this **session thread** only (not the project):
30
+ 1. Call `polaris_rename_session` with the new name
31
+ 2. Report the result
32
+
29
33
  **`/polaris tag [name]`** — Tag a teammate on Slack:
30
34
  1. Call `polaris_team` to get the team list with Slack identities
31
35
  2. If a name was given, find the matching member. If no name, present a numbered list for the user to pick.
package/src/cli/cli.ts CHANGED
@@ -428,8 +428,8 @@ async function install(participantId?: string) {
428
428
  const skillContent = `---
429
429
  name: polaris
430
430
  description: Connect to a Polaris multiplayer collaboration session
431
- allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
432
- argument-hint: [join #channel | rename <new-name> | disconnect | (no args for status)]
431
+ allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session
432
+ argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | (no args for status)]
433
433
  ---
434
434
 
435
435
  ## Polaris — Multiplayer Collaboration
@@ -440,15 +440,19 @@ Manage your connection to a Polaris collaboration session.
440
440
 
441
441
  Based on the arguments provided, do ONE of the following:
442
442
 
443
- **\`/polaris join #channel-name\`** — Connect to a channel:
444
- 1. Call \`polaris_connect\` with the given project and user identity ${identity}
445
- 2. A session name is auto-generated
443
+ **\`/polaris join #channel-name [session-name]\`** — Connect to a channel:
444
+ 1. Call \`polaris_connect\` with the given project, user identity ${identity}, and optional \`session\` name (e.g. \`auth-feature\`, \`bugfix-154\`)
445
+ 2. A session name is auto-generated if not provided
446
446
  3. Report the connection status including the session name
447
447
 
448
- **\`/polaris rename <new-name>\`** — Rename the current project:
448
+ **\`/polaris rename <new-name>\`** — Rename the current **project** (and its Slack channel):
449
449
  1. Call \`polaris_rename\` with the new name
450
450
  2. Report the result
451
451
 
452
+ **\`/polaris rename-session <new-name>\`** — Rename this **session thread** only (not the project):
453
+ 1. Call \`polaris_rename_session\` with the new name
454
+ 2. Report the result
455
+
452
456
  **\`/polaris disconnect\`** — Disconnect:
453
457
  1. Call \`polaris_disconnect\`
454
458
  2. Confirm disconnection
@@ -567,8 +571,8 @@ async function login(appUrl: string, profileName?: string, loginHint?: string) {
567
571
  const skillContent = `---
568
572
  name: polaris
569
573
  description: Connect to a Polaris multiplayer collaboration session
570
- allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
571
- argument-hint: [join #channel | rename <new-name> | disconnect | (no args for status)]
574
+ allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session
575
+ argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | (no args for status)]
572
576
  ---
573
577
 
574
578
  ## Polaris — Multiplayer Collaboration
@@ -579,15 +583,19 @@ Manage your connection to a Polaris collaboration session.
579
583
 
580
584
  Based on the arguments provided, do ONE of the following:
581
585
 
582
- **\`/polaris join #channel-name\`** — Connect to a channel:
583
- 1. Call \`polaris_connect\` with the given project and user identity ${identity}
584
- 2. A session name is auto-generated
586
+ **\`/polaris join #channel-name [session-name]\`** — Connect to a channel:
587
+ 1. Call \`polaris_connect\` with the given project, user identity ${identity}, and optional \`session\` name (e.g. \`auth-feature\`, \`bugfix-154\`)
588
+ 2. A session name is auto-generated if not provided
585
589
  3. Report the connection status including the session name
586
590
 
587
- **\`/polaris rename <new-name>\`** — Rename the current project:
591
+ **\`/polaris rename <new-name>\`** — Rename the current **project** (and its Slack channel):
588
592
  1. Call \`polaris_rename\` with the new name
589
593
  2. Report the result
590
594
 
595
+ **\`/polaris rename-session <new-name>\`** — Rename this **session thread** only (not the project):
596
+ 1. Call \`polaris_rename_session\` with the new name
597
+ 2. Report the result
598
+
591
599
  **\`/polaris disconnect\`** — Disconnect:
592
600
  1. Call \`polaris_disconnect\`
593
601
  2. Confirm disconnection
@@ -156,7 +156,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
156
156
  },
157
157
  {
158
158
  name: "polaris_rename",
159
- description: "Rename the current project. Also renames the linked Slack channel if a floor is connected.",
159
+ description: "Rename the current PROJECT (and its linked Slack channel). To rename just this session thread, use polaris_rename_session instead.",
160
160
  inputSchema: {
161
161
  type: "object" as const,
162
162
  properties: {
@@ -165,6 +165,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
165
165
  required: ["name"],
166
166
  },
167
167
  },
168
+ {
169
+ name: "polaris_rename_session",
170
+ description: "Rename this session thread (e.g. from 's-3f95' to 'auth-feature'). Does not affect the project or other sessions.",
171
+ inputSchema: {
172
+ type: "object" as const,
173
+ properties: {
174
+ name: { type: "string", description: "New session name (e.g. 'auth-feature', 'bugfix-154')" },
175
+ },
176
+ required: ["name"],
177
+ },
178
+ },
168
179
  {
169
180
  name: "polaris_context",
170
181
  description: "Fetch activity from a sibling session in this project. Use this to see what other drivers have been doing.",
@@ -192,6 +203,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
192
203
  properties: {
193
204
  duration: { type: "string", description: "Time range to backfill (e.g., '2h', '30m', '1d'). Auto-detects if omitted." },
194
205
  from: { type: "string", description: "ISO timestamp to backfill from. Overrides duration." },
206
+ dry_run: { type: "boolean", description: "If true, report how many events would be sent without actually sending them." },
195
207
  },
196
208
  },
197
209
  },
@@ -339,6 +351,26 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
339
351
  }
340
352
  }
341
353
 
354
+ if (name === "polaris_rename_session") {
355
+ if (!currentProject) {
356
+ return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
357
+ }
358
+ const newName = (args as { name: string }).name;
359
+ try {
360
+ const res = await daemonPost("/rename-session", { ccSessionId: CC_SESSION_ID, newName });
361
+ const body = await res.json() as { status?: string; oldName?: string; newName?: string; error?: string };
362
+ if (res.ok) {
363
+ const oldName = currentSession;
364
+ currentSession = newName;
365
+ await saveSessionState(currentProject, currentSession, currentUser, currentProfile);
366
+ return { content: [{ type: "text", text: `Renamed session "${oldName}" to "${newName}".` }] };
367
+ }
368
+ return { content: [{ type: "text", text: `Failed to rename session: ${body.error ?? "unknown error"}` }] };
369
+ } catch {
370
+ return { content: [{ type: "text", text: "Failed to rename session — is the Polaris daemon running?" }] };
371
+ }
372
+ }
373
+
342
374
  if (name === "polaris_context") {
343
375
  if (!currentProject) {
344
376
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
@@ -397,16 +429,20 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
397
429
  if (!currentProject) {
398
430
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
399
431
  }
400
- const { duration, from } = (args ?? {}) as { duration?: string; from?: string };
432
+ const { duration, from, dry_run } = (args ?? {}) as { duration?: string; from?: string; dry_run?: boolean };
401
433
  try {
402
434
  const res = await daemonPost("/backfill", {
403
435
  ccSessionId: CC_SESSION_ID,
404
436
  ...(duration ? { duration } : {}),
405
437
  ...(from ? { from } : {}),
438
+ ...(dry_run ? { dry_run: true } : {}),
406
439
  });
407
- const body = await res.json() as { recovered: number; source: string; gaps: string[] };
440
+ const body = await res.json() as { recovered: number; would_recover?: number; source: string; gaps: string[] };
408
441
  if (res.ok) {
409
442
  const gapInfo = body.gaps.length > 0 ? `\nGaps: ${body.gaps.join(", ")}` : "";
443
+ if (dry_run) {
444
+ return { content: [{ type: "text", text: `Dry run: ${body.would_recover ?? 0} events in daemon log would be sent.${gapInfo}` }] };
445
+ }
410
446
  return { content: [{ type: "text", text: `Backfill complete: ${body.recovered} events recovered from ${body.source}.${gapInfo}` }] };
411
447
  }
412
448
  return { content: [{ type: "text", text: `Backfill failed: ${(body as unknown as { error?: string }).error ?? "unknown error"}` }] };
@@ -460,6 +496,34 @@ async function main() {
460
496
  }
461
497
  }
462
498
 
499
+ // Heartbeat: check every 60s whether the daemon still has our session mapped.
500
+ // If not (dropped WS, daemon restart, compaction), silently re-connect so
501
+ // activity reaches the channel without a manual /polaris join.
502
+ setInterval(async () => {
503
+ if (!currentProject) return; // not connected — nothing to heartbeat
504
+ try {
505
+ const res = await daemonGet(`/status/${CC_SESSION_ID}`);
506
+ const body = await res.json() as { connected?: boolean; project?: string; session?: string };
507
+ if (body.connected) return; // all good
508
+ // Daemon lost the mapping — attempt to reconnect to the same session
509
+ const reconnectRes = await daemonPost("/connect", {
510
+ ccSessionId: CC_SESSION_ID,
511
+ project: currentProject,
512
+ session: currentSession,
513
+ user: currentUser,
514
+ profile: currentProfile,
515
+ });
516
+ if (reconnectRes.ok) {
517
+ console.error(`Polaris: session dropped; auto-reconnected to #${currentProject}/${currentSession}`);
518
+ } else {
519
+ console.error(`Polaris: session dropped and reconnect failed. Run /polaris to check status.`);
520
+ currentProject = "";
521
+ currentSession = "";
522
+ currentUser = "";
523
+ }
524
+ } catch { /* daemon unreachable — will retry next heartbeat */ }
525
+ }, 60_000);
526
+
463
527
  // inject delivery is handled via the UserPromptSubmit hook (see daemon injectQueues); claude/channel push deferred
464
528
 
465
529
  const transport = new StdioServerTransport();
@@ -15,6 +15,7 @@ interface SessionMapping {
15
15
  slackChannel?: string;
16
16
  ws: WebSocket | null;
17
17
  pendingMapping?: boolean; // true until the first hook event from this shell claims the mapping
18
+ closingIntentionally?: boolean; // set before deliberate ws.close() so onclose can distinguish drops
18
19
  }
19
20
 
20
21
  function generateSessionName(): string {
@@ -164,14 +165,18 @@ function connectCloudWs(mapping: SessionMapping) {
164
165
  };
165
166
 
166
167
  ws.onclose = () => {
167
- // Reconnect if still registered
168
- if (sessions.has(mapping.ccSessionId) && sessions.get(mapping.ccSessionId)!.project === mapping.project) {
168
+ const m = sessions.get(mapping.ccSessionId);
169
+ if (m && m.project === mapping.project && mapping.project !== "" && !m.closingIntentionally) {
170
+ // Unexpected cloud WS drop — log it so the user has a record
171
+ void logSessionChange("ws-drop", mapping.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
169
172
  setTimeout(() => {
170
- if (sessions.has(mapping.ccSessionId)) {
171
- connectCloudWs(sessions.get(mapping.ccSessionId)!);
173
+ const current = sessions.get(mapping.ccSessionId);
174
+ if (current && current.project) {
175
+ connectCloudWs(current);
172
176
  }
173
177
  }, 3000);
174
178
  }
179
+ if (m) m.closingIntentionally = false;
175
180
  };
176
181
 
177
182
  ws.onerror = () => {
@@ -184,6 +189,7 @@ function connectCloudWs(mapping: SessionMapping) {
184
189
  function disconnectCloudWs(ccSessionId: string) {
185
190
  const mapping = sessions.get(ccSessionId);
186
191
  if (mapping?.ws) {
192
+ mapping.closingIntentionally = true;
187
193
  mapping.ws.close();
188
194
  mapping.ws = null;
189
195
  }
@@ -310,7 +316,7 @@ async function scanOutbox(): Promise<void> {
310
316
 
311
317
  const SESSION_HISTORY_FILE = join(homedir(), ".polaris", "session-history.jsonl");
312
318
 
313
- async function logSessionChange(action: "connect" | "disconnect", ccSessionId: string, project: string, session: string, user: string, agent: string): Promise<void> {
319
+ async function logSessionChange(action: "connect" | "disconnect" | "ws-drop", ccSessionId: string, project: string, session: string, user: string, agent: string): Promise<void> {
314
320
  try {
315
321
  await ensureLogDir();
316
322
  const entry = JSON.stringify({ t: new Date().toISOString(), action, ccSessionId, project, session, user, agent });
@@ -320,7 +326,7 @@ async function logSessionChange(action: "connect" | "disconnect", ccSessionId: s
320
326
 
321
327
  // --- Backfill: recover lost events from daemon log ---
322
328
 
323
- async function backfill(mapping: SessionMapping, duration?: string, from?: string): Promise<{ recovered: number; source: string; gaps: string[] }> {
329
+ async function backfill(mapping: SessionMapping, duration?: string, from?: string, dryRun = false): Promise<{ recovered: number; would_recover?: number; source: string; gaps: string[] }> {
324
330
  const now = new Date();
325
331
  let since: Date;
326
332
 
@@ -389,6 +395,11 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
389
395
  return { recovered: 0, source: "daemon_log", gaps: [`no log entries found since ${since.toISOString()}`] };
390
396
  }
391
397
 
398
+ // Dry-run: report how many events would be sent without actually sending
399
+ if (dryRun) {
400
+ return { recovered: 0, would_recover: logEntries.length, source: "daemon_log", gaps: [] };
401
+ }
402
+
392
403
  // Replay events to the API
393
404
  let recovered = 0;
394
405
  const serviceUrl = getServiceUrlForProfile(mapping.profile);
@@ -766,6 +777,36 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
766
777
  }
767
778
  }
768
779
 
780
+ // POST /rename-session — rename the current session (not the project)
781
+ if (method === "POST" && pathname === "/rename-session") {
782
+ try {
783
+ const body = (await req.json()) as { ccSessionId: string; newName: string };
784
+ if (!body.ccSessionId || !body.newName) return error("ccSessionId and newName required", 400);
785
+ const mapping = sessions.get(body.ccSessionId);
786
+ if (!mapping || !mapping.project) return error("Not connected", 400);
787
+
788
+ const serviceUrl = getServiceUrlForProfile(mapping.profile);
789
+ const res = await fetch(
790
+ `${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/rename`,
791
+ {
792
+ method: "POST",
793
+ headers: await authHeadersForProfile(mapping.profile),
794
+ body: JSON.stringify({ name: body.newName }),
795
+ }
796
+ );
797
+ if (!res.ok) {
798
+ const err = await res.text();
799
+ return new Response(err, { status: res.status });
800
+ }
801
+
802
+ const oldName = mapping.session;
803
+ mapping.session = body.newName;
804
+ return json({ status: "renamed", oldName, newName: body.newName });
805
+ } catch {
806
+ return error("Invalid JSON", 400);
807
+ }
808
+ }
809
+
769
810
  // POST /channel-update — bridge pushes channel rename notifications
770
811
  if (method === "POST" && pathname === "/channel-update") {
771
812
  try {
@@ -929,12 +970,12 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
929
970
  // POST /backfill — recover lost events from daemon log
930
971
  if (method === "POST" && pathname === "/backfill") {
931
972
  try {
932
- const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string };
973
+ const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string; dry_run?: boolean };
933
974
  if (!body.ccSessionId) return error("ccSessionId required", 400);
934
975
  const mapping = sessions.get(body.ccSessionId);
935
976
  if (!mapping || !mapping.project) return error("Not connected", 400);
936
977
 
937
- const result = await backfill(mapping, body.duration, body.from);
978
+ const result = await backfill(mapping, body.duration, body.from, body.dry_run ?? false);
938
979
  return json(result);
939
980
  } catch {
940
981
  return error("Invalid JSON", 400);
package/src/service/db.ts CHANGED
@@ -311,6 +311,15 @@ export async function createProject(sql: Sql, orgId: string, name: string): Prom
311
311
  return rowToProject(row);
312
312
  }
313
313
 
314
+ export async function renameSession(sql: Sql, orgId: string, project: string, oldName: string, newName: string): Promise<void> {
315
+ const result = await sql`
316
+ UPDATE sessions SET name = ${newName}
317
+ WHERE org_id = ${orgId} AND name = ${oldName}
318
+ AND project_id = (SELECT id FROM projects WHERE org_id = ${orgId} AND name = ${project})
319
+ `;
320
+ if (result.count === 0) throw new Error(`Session not found: ${project}/${oldName}`);
321
+ }
322
+
314
323
  export async function renameProject(sql: Sql, orgId: string, oldName: string, newName: string): Promise<void> {
315
324
  await sql`
316
325
  UPDATE projects SET name = ${newName}, slack_channel_name = ${newName}
@@ -9,6 +9,7 @@ import {
9
9
  listProjects,
10
10
  getProject,
11
11
  renameProject,
12
+ renameSession,
12
13
  createSession,
13
14
  getSession,
14
15
  setDriver,
@@ -378,6 +379,18 @@ export async function startServer(opts: {
378
379
  return json({ status: "renamed", oldName: params.proj, newName: body.name });
379
380
  }
380
381
 
382
+ params = matchRoute(method, pathname, "/projects/:proj/sessions/:sess/rename", "POST");
383
+ if (params) {
384
+ const body = await req.json() as { name?: string };
385
+ if (!body.name) return error("name is required", 400);
386
+ try {
387
+ await renameSession(sql, orgId, params.proj, params.sess, body.name);
388
+ } catch (e) {
389
+ return error((e as Error).message, 404);
390
+ }
391
+ return json({ status: "renamed", oldName: params.sess, newName: body.name });
392
+ }
393
+
381
394
  // --- Project ACL endpoints (visibility & members) ---
382
395
 
383
396
  params = matchRoute(method, pathname, "/projects/:proj/visibility", "POST");