@lightupai/polaris 0.0.101 → 0.0.102

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.102",
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",
@@ -192,6 +192,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
192
192
  properties: {
193
193
  duration: { type: "string", description: "Time range to backfill (e.g., '2h', '30m', '1d'). Auto-detects if omitted." },
194
194
  from: { type: "string", description: "ISO timestamp to backfill from. Overrides duration." },
195
+ dry_run: { type: "boolean", description: "If true, report how many events would be sent without actually sending them." },
195
196
  },
196
197
  },
197
198
  },
@@ -397,16 +398,20 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
397
398
  if (!currentProject) {
398
399
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
399
400
  }
400
- const { duration, from } = (args ?? {}) as { duration?: string; from?: string };
401
+ const { duration, from, dry_run } = (args ?? {}) as { duration?: string; from?: string; dry_run?: boolean };
401
402
  try {
402
403
  const res = await daemonPost("/backfill", {
403
404
  ccSessionId: CC_SESSION_ID,
404
405
  ...(duration ? { duration } : {}),
405
406
  ...(from ? { from } : {}),
407
+ ...(dry_run ? { dry_run: true } : {}),
406
408
  });
407
- const body = await res.json() as { recovered: number; source: string; gaps: string[] };
409
+ const body = await res.json() as { recovered: number; would_recover?: number; source: string; gaps: string[] };
408
410
  if (res.ok) {
409
411
  const gapInfo = body.gaps.length > 0 ? `\nGaps: ${body.gaps.join(", ")}` : "";
412
+ if (dry_run) {
413
+ return { content: [{ type: "text", text: `Dry run: ${body.would_recover ?? 0} events in daemon log would be sent.${gapInfo}` }] };
414
+ }
410
415
  return { content: [{ type: "text", text: `Backfill complete: ${body.recovered} events recovered from ${body.source}.${gapInfo}` }] };
411
416
  }
412
417
  return { content: [{ type: "text", text: `Backfill failed: ${(body as unknown as { error?: string }).error ?? "unknown error"}` }] };
@@ -460,6 +465,34 @@ async function main() {
460
465
  }
461
466
  }
462
467
 
468
+ // Heartbeat: check every 60s whether the daemon still has our session mapped.
469
+ // If not (dropped WS, daemon restart, compaction), silently re-connect so
470
+ // activity reaches the channel without a manual /polaris join.
471
+ setInterval(async () => {
472
+ if (!currentProject) return; // not connected — nothing to heartbeat
473
+ try {
474
+ const res = await daemonGet(`/status/${CC_SESSION_ID}`);
475
+ const body = await res.json() as { connected?: boolean; project?: string; session?: string };
476
+ if (body.connected) return; // all good
477
+ // Daemon lost the mapping — attempt to reconnect to the same session
478
+ const reconnectRes = await daemonPost("/connect", {
479
+ ccSessionId: CC_SESSION_ID,
480
+ project: currentProject,
481
+ session: currentSession,
482
+ user: currentUser,
483
+ profile: currentProfile,
484
+ });
485
+ if (reconnectRes.ok) {
486
+ console.error(`Polaris: session dropped; auto-reconnected to #${currentProject}/${currentSession}`);
487
+ } else {
488
+ console.error(`Polaris: session dropped and reconnect failed. Run /polaris to check status.`);
489
+ currentProject = "";
490
+ currentSession = "";
491
+ currentUser = "";
492
+ }
493
+ } catch { /* daemon unreachable — will retry next heartbeat */ }
494
+ }, 60_000);
495
+
463
496
  // inject delivery is handled via the UserPromptSubmit hook (see daemon injectQueues); claude/channel push deferred
464
497
 
465
498
  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);
@@ -929,12 +940,12 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
929
940
  // POST /backfill — recover lost events from daemon log
930
941
  if (method === "POST" && pathname === "/backfill") {
931
942
  try {
932
- const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string };
943
+ const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string; dry_run?: boolean };
933
944
  if (!body.ccSessionId) return error("ccSessionId required", 400);
934
945
  const mapping = sessions.get(body.ccSessionId);
935
946
  if (!mapping || !mapping.project) return error("Not connected", 400);
936
947
 
937
- const result = await backfill(mapping, body.duration, body.from);
948
+ const result = await backfill(mapping, body.duration, body.from, body.dry_run ?? false);
938
949
  return json(result);
939
950
  } catch {
940
951
  return error("Invalid JSON", 400);