@lightupai/polaris 0.0.109 → 0.0.113

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.
@@ -41,7 +41,7 @@ if [ -f "$INSTALLED_PKG" ]; then
41
41
  NOW=$(date +%s 2>/dev/null || echo "0")
42
42
  CACHE_AGE=$(( NOW - CACHE_TS )) 2>/dev/null || CACHE_AGE=9999
43
43
  STALE=false
44
- [ "$CACHE_AGE" -gt 3600 ] 2>/dev/null && STALE=true
44
+ [ "$CACHE_AGE" -gt 300 ] 2>/dev/null && STALE=true
45
45
  else
46
46
  STALE=true
47
47
  fi
@@ -72,11 +72,26 @@ if [ "$CONNECTED" = "true" ]; then
72
72
  if [ -n "$ACCOUNT" ]; then
73
73
  ACCOUNT_TAG="[${ACCOUNT}] "
74
74
  fi
75
- if [ -n "$SLACK" ]; then
76
- echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}${UPDATE_MSG}"
77
- else
78
- echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER})${UPDATE_MSG}"
75
+
76
+ # Liveness indicator: seconds since last delivered event
77
+ RECENCY=""
78
+ LAST_AGE=$(echo "$STATUS" | jq -r '.lastEventAge // ""' 2>/dev/null || echo "")
79
+ if [ -n "$LAST_AGE" ] && [ "$LAST_AGE" != "null" ]; then
80
+ if [ "$LAST_AGE" -lt 60 ] 2>/dev/null; then
81
+ RECENCY=" · just now"
82
+ elif [ "$LAST_AGE" -lt 3600 ] 2>/dev/null; then
83
+ RECENCY=" · $(( LAST_AGE / 60 ))m ago"
84
+ else
85
+ RECENCY=" · $(( LAST_AGE / 3600 ))h ago"
86
+ fi
87
+ fi
88
+
89
+ # Show Slack channel only when it differs from the project name (adds info, avoids duplication)
90
+ SLACK_TAG=""
91
+ if [ -n "$SLACK" ] && [ "$SLACK" != "$PROJECT" ]; then
92
+ SLACK_TAG=" → #${SLACK}"
79
93
  fi
94
+ echo "polaris: ${ACCOUNT_TAG}#${PROJECT}/${SESSION} (${USER})${SLACK_TAG}${RECENCY}${UPDATE_MSG}"
80
95
  else
81
96
  echo "polaris: not connected${UPDATE_MSG}"
82
97
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.109",
3
+ "version": "0.0.113",
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",
package/src/cli/cli.ts CHANGED
@@ -275,7 +275,52 @@ async function removeSystemdService(): Promise<void> {
275
275
  }
276
276
 
277
277
  // Platform dispatcher
278
+ // --- Session state migration ---
279
+ //
280
+ // When upgrading from a pre-0.0.109 daemon (which never wrote daemon-state.json),
281
+ // the new daemon would start with an empty session map and drop every connected
282
+ // session. This function fetches the running daemon's session list via GET /status
283
+ // and writes daemon-state.json before the daemon is stopped, so the new daemon
284
+ // can restore the map and reconnect cloud WebSockets without any user action.
285
+ // Best-effort: failures are silent (daemon may not be running, or already migrated).
286
+ async function migrateSessionState(): Promise<void> {
287
+ const stateFile = join(POLARIS_DIR, "sessions", "daemon-state.json");
288
+ // If a 0.0.109+ daemon already wrote the file, its state is more complete
289
+ // (includes learned hook aliases) — don't overwrite it.
290
+ try { await readFile(stateFile); return; } catch { /* doesn't exist — proceed */ }
291
+
292
+ try {
293
+ const config = await loadConfig();
294
+ const secret = resolveDaemonSecret(config);
295
+ const headers: Record<string, string> = {};
296
+ if (secret) headers["x-polaris-daemon-secret"] = secret;
297
+ const res = await fetch("http://127.0.0.1:4322/status", {
298
+ headers,
299
+ signal: AbortSignal.timeout(2000),
300
+ });
301
+ if (!res.ok) return;
302
+ const data = await res.json() as { sessions?: Array<{ ccSessionId: string; project: string; session: string; user: string }> };
303
+ if (!data.sessions || data.sessions.length === 0) return;
304
+
305
+ const activeProfile = config?.active || "";
306
+ const sessions = data.sessions.map((s) => ({
307
+ ccSessionId: s.ccSessionId,
308
+ project: s.project,
309
+ session: s.session,
310
+ user: s.user,
311
+ agent: "agent:claude",
312
+ profile: activeProfile,
313
+ }));
314
+
315
+ await mkdir(join(POLARIS_DIR, "sessions"), { recursive: true });
316
+ await writeFile(stateFile, JSON.stringify({ sessions }));
317
+ console.log(` ✓ Migrated ${sessions.length} active session(s) to persistent state`);
318
+ } catch { /* best-effort — daemon not running or unreachable */ }
319
+ }
320
+
278
321
  async function installDaemonService(daemonSecret?: string): Promise<void> {
322
+ // Migrate session state from older daemon before it is stopped
323
+ await migrateSessionState();
279
324
  if (process.platform === "darwin") return installLaunchAgent(daemonSecret);
280
325
  if (process.platform === "linux") return installSystemdService(daemonSecret);
281
326
  // Windows and others: no managed service; daemon is started manually
@@ -495,17 +495,25 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
495
495
  return { content: [{ type: "text", text: `Polaris is already up to date (v${currentVersion}).` }] };
496
496
  }
497
497
 
498
- // Update global binary
499
- const npmResult = Bun.spawnSync(["npm", "install", "-g", "@lightupai/polaris@latest"], { stdout: "pipe", stderr: "pipe" });
498
+ // Update the copy that's actually in use: ~/.polaris/mcp (no sudo required).
499
+ // This is the same location the version was read from, and where the daemon
500
+ // and MCP server binaries are resolved by run-daemon.sh and claude mcp add.
501
+ const mcpDir = `${homedir()}/.polaris/mcp`;
502
+ const npmResult = Bun.spawnSync(
503
+ ["npm", "install", "--prefix", mcpDir, `@lightupai/polaris@${latestVersion}`],
504
+ { stdout: "pipe", stderr: "pipe" }
505
+ );
500
506
  if (npmResult.exitCode !== 0) {
501
507
  return { content: [{ type: "text", text: `Update failed:\n${npmResult.stderr.toString().trim()}` }] };
502
508
  }
503
509
 
504
- // Refresh hooks, MCP package, and daemon registration
505
- const installResult = Bun.spawnSync(["polaris", "install"], { stdout: "pipe", stderr: "pipe" });
510
+ // Refresh hooks, migrate session state, and restart the daemon using the
511
+ // local polaris binary (not relying on PATH, which may not include it).
512
+ const polarisBin = `${mcpDir}/node_modules/.bin/polaris`;
513
+ const installResult = Bun.spawnSync([polarisBin, "install"], { stdout: "pipe", stderr: "pipe" });
506
514
  const msg = installResult.exitCode === 0
507
515
  ? `Updated Polaris from v${currentVersion} to v${latestVersion}.\n\nRestart Claude Code for the new MCP server to take effect.`
508
- : `Updated global binary to v${latestVersion}, but \`polaris install\` failed — run it manually.\n${installResult.stderr.toString().trim()}`;
516
+ : `Updated package to v${latestVersion}, but \`polaris install\` failed — run it manually.\n${installResult.stderr.toString().trim()}`;
509
517
  return { content: [{ type: "text", text: msg }] };
510
518
  }
511
519
 
@@ -1042,6 +1042,13 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
1042
1042
  }
1043
1043
  }
1044
1044
  }
1045
+ // Include seconds since last delivered event so the status line can show
1046
+ // a liveness indicator (e.g. "· 2m ago") without a second request.
1047
+ let lastEventAge: number | null = null;
1048
+ const cp = await readCheckpoint(ccSessionId);
1049
+ if (cp && cp.project === mapping.project && cp.session === mapping.session) {
1050
+ lastEventAge = Math.floor((Date.now() - new Date(cp.lastEventTimestamp).getTime()) / 1000);
1051
+ }
1045
1052
  return json({
1046
1053
  connected: true,
1047
1054
  project: mapping.project,
@@ -1049,6 +1056,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
1049
1056
  user: mapping.user,
1050
1057
  slackChannel,
1051
1058
  account: emailFromToken(await getTokenForProfile(mapping.profile)),
1059
+ lastEventAge,
1052
1060
  });
1053
1061
  }
1054
1062