import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { mkdir, writeFile, readFile, rm } from "node:fs/promises"; import { homedir } from "node:os"; import { createHash } from "node:crypto"; // --- Configuration --- const DAEMON_URL = process.env.POLARIS_DAEMON_URL ?? "http://127.0.0.1:4322"; // Shared local daemon-auth secret (installed by `polaris install`); sent as // x-polaris-daemon-secret when present, else the daemon runs unauthenticated. const DAEMON_SECRET = process.env.POLARIS_DAEMON_SECRET || null; // Stable session ID for this MCP instance: explicit override, else Claude Code's // session id if it exposes one, else a generated UUID (daemon learns the hook id as an alias). const CC_SESSION_ID = process.env.POLARIS_CC_SESSION_ID ?? process.env.CLAUDE_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? crypto.randomUUID(); // --- Daemon communication --- function daemonHeaders(): Record { const headers: Record = { "Content-Type": "application/json" }; if (DAEMON_SECRET) headers["x-polaris-daemon-secret"] = DAEMON_SECRET; return headers; } async function daemonPost(path: string, body: unknown): Promise { return fetch(`${DAEMON_URL}${path}`, { method: "POST", headers: daemonHeaders(), body: JSON.stringify(body), }); } async function daemonGet(path: string): Promise { return fetch(`${DAEMON_URL}${path}`, { headers: daemonHeaders() }); } // --- Session state persistence --- function sessionStatePath(): string { // Keyed by CC session ID so two Claude Code sessions in the same directory // get independent state files and never auto-reconnect into each other's // project. Previously keyed by cwd hash, which caused unrelated sessions to // silently inherit another session's project connection (#154, #153). const hash = createHash("sha256").update(CC_SESSION_ID).digest("hex").slice(0, 16); return `${homedir()}/.polaris/sessions/${hash}.json`; } async function saveSessionState(project: string, session: string, user: string, profile: string): Promise { try { await mkdir(`${homedir()}/.polaris/sessions`, { recursive: true }); await writeFile(sessionStatePath(), JSON.stringify({ project, session, user, profile, cwd: process.cwd(), ccSessionId: CC_SESSION_ID })); } catch { /* best-effort */ } } async function clearSessionState(): Promise { try { await rm(sessionStatePath(), { force: true }); } catch { /* best-effort */ } } async function loadSessionState(): Promise<{ project: string; session: string; user: string; profile: string } | null> { try { const data = JSON.parse(await readFile(sessionStatePath(), "utf-8")); if (data.project && data.session && data.user) return { ...data, profile: data.profile ?? "" }; } catch { /* no saved state */ } return null; } async function loadActiveProfile(): Promise { try { const data = JSON.parse(await readFile(`${homedir()}/.polaris/config.json`, "utf-8")); return data.active ?? ""; } catch { return ""; } } // --- Current connection state --- let currentProject = ""; let currentSession = ""; let currentUser = ""; let currentProfile = ""; // --- MCP Server --- const mcp = new Server( { name: "polaris", version: "0.0.1" }, { capabilities: { // claude/channel push delivery is off by default — injects reach the // agent via the UserPromptSubmit hook (see daemon injectQueues). See // deliverInjectViaChannel below for the opt-in push scaffold. experimental: { "claude/channel": {} }, tools: {}, }, instructions: `You are connected to Polaris — a multiplayer collaboration system. Messages from advisors and teammates may arrive as tags. Use /polaris commands to manage your session, or call the polaris tools directly. When you finish a task and would otherwise go idle — especially if you are holding a shared resource (port, file lock, etc.) that another session may need — call polaris_standby() instead of stopping. It blocks until a sibling session sends you a message, then returns it immediately so you can act without requiring a human prompt. If the user reports the Polaris daemon is not running or keeps crashing, tell them to run \`polaris install\` — it registers the daemon with launchd on macOS or a systemd user service on Linux for auto-restart on crash and at login. Do NOT suggest manually configuring launchd or systemd — \`polaris install\` handles it.`, } ); mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "polaris_connect", description: "Connect this session to an existing Polaris project. Use create:true to create a new project.", inputSchema: { type: "object" as const, properties: { channel: { type: "string", description: "Project workspace to join (e.g., #my-project). Omit to list existing projects." }, user: { type: "string", description: "Your participant ID (e.g., user:manu)" }, session: { type: "string", description: "Session name (optional — auto-generated if omitted)" }, agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" }, create: { type: "boolean", description: "Create the project if it doesn't exist (default: false). Only set true when explicitly starting a new project." }, }, required: ["user"], }, }, { name: "polaris_disconnect", description: "Disconnect from the current Polaris session.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "polaris_status", description: "Show current Polaris connection status.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "polaris_standby", description: "Wait for an incoming message from a sibling session. Blocks until a message arrives or the timeout elapses. Use this when you are idle but must remain responsive to coordination requests (e.g. holding a resource another session needs). Returns the message immediately when it arrives — no human prompt required.", inputSchema: { type: "object" as const, properties: { timeout: { type: "number", description: "Seconds to wait (default 60)" }, }, required: [], }, }, { name: "polaris_send", description: "Send a message directly to a sibling session in the same project. The message is injected into that session's next prompt and logged in both sessions' Polaris timelines.", inputSchema: { type: "object" as const, properties: { session: { type: "string", description: "Target session name (e.g. 'feedback')" }, message: { type: "string", description: "Message to send" }, }, required: ["session", "message"], }, }, { name: "polaris_reply", description: "Send a message to the project floor (visible to all advisors, and to the linked Slack channel if a floor is connected).", inputSchema: { type: "object" as const, properties: { message: { type: "string", description: "Message to send" }, }, required: ["message"], }, }, { name: "polaris_rename", description: "Rename the current PROJECT (and its linked Slack channel). To rename just this session thread, use polaris_rename_session instead.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "New project name" }, }, required: ["name"], }, }, { name: "polaris_rename_session", description: "Rename this session thread (e.g. from 's-3f95' to 'auth-feature'). Does not affect the project or other sessions.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "New session name (e.g. 'auth-feature', 'bugfix-154')" }, }, required: ["name"], }, }, { name: "polaris_context", description: "Fetch activity from a sibling session in this project. Use this to see what other drivers have been doing.", inputSchema: { type: "object" as const, properties: { session: { type: "string", description: "Name of the sibling session to fetch context from" }, }, required: ["session"], }, }, { name: "polaris_team", description: "List team members with their Slack identities. Use this to resolve @mentions before posting to Slack.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "polaris_update", description: "Update Polaris to the latest version. Runs npm install -g then polaris install to refresh hooks and the MCP server.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "polaris_backfill", description: "Recover lost events from local daemon logs. Use when events were lost due to disconnection or API downtime.", inputSchema: { type: "object" as const, properties: { duration: { type: "string", description: "Time range to backfill (e.g., '2h', '30m', '1d'). Auto-detects if omitted." }, from: { type: "string", description: "ISO timestamp to backfill from. Overrides duration." }, dry_run: { type: "boolean", description: "If true, report how many events would be sent without actually sending them." }, }, }, }, ], })); // --- claude/channel push adapter --- // Push content into the Claude Code agent window in real time via the // experimental claude/channel notification. Best-effort; falls back to the // UserPromptSubmit hook path if the push fails or the host doesn't support it. async function deliverViaChannel( content: string, meta: Record = {} ): Promise { try { await mcp.notification({ method: "notifications/claude/channel", params: { content, meta }, }); return true; } catch { return false; } } mcp.setRequestHandler(CallToolRequestSchema, async (req) => { const { name, arguments: args } = req.params; if (name === "polaris_connect") { const { channel, user, session, agent, create } = args as { channel?: string; user: string; session?: string; agent?: string; create?: boolean }; // If no channel specified, list available channels if (!channel) { try { const res = await daemonGet("/channels"); if (res.ok) { const body = await res.json() as { channels: string[] }; if (body.channels.length === 0) { return { content: [{ type: "text", text: "No projects found. Create one with: `/polaris join #my-project` with create:true" }] }; } return { content: [{ type: "text", text: `Available projects:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #project-name` }] }; } } catch { /* fall through */ } return { content: [{ type: "text", text: "Specify a project: `/polaris join #my-project`" }] }; } const project = channel.replace(/^#/, ""); // strip leading # if present const activeProfile = await loadActiveProfile(); try { const res = await daemonPost("/connect", { ccSessionId: CC_SESSION_ID, project, user, profile: activeProfile, ...(session ? { session } : {}), ...(agent ? { agent } : {}), ...(create ? { create: true } : {}), }); const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string; existing?: string[]; account?: string; gap?: { events: number; since: string } | null }; if (res.status === 404 && body.error === "project_not_found") { const accountLine = body.account ? ` in ${body.account}` : ""; const list = body.existing && body.existing.length > 0 ? `\n\nAvailable projects:\n${body.existing.map(p => ` #${p}`).join("\n")}` : "\n\nNo projects exist yet in this account."; return { content: [{ type: "text", text: `Project "#${project}" not found${accountLine}.${list}\n\nTo create it: polaris_connect with create:true` }] }; } if (res.ok) { currentProject = body.project ?? project; currentSession = body.session ?? session ?? ""; currentUser = user; currentProfile = activeProfile; await saveSessionState(currentProject, currentSession, currentUser, currentProfile); let msg = `Connected to #${currentProject}/${currentSession} as ${user}.`; if (body.gap && body.gap.events > 0) { msg += `\n${body.gap.events} events since ${body.gap.since} not delivered — re-sending now.`; } return { content: [{ type: "text", text: msg }] }; } return { content: [{ type: "text", text: `Failed to connect: ${body.error ?? "unknown error"}` }] }; } catch { return { content: [{ type: "text", text: "Failed to connect — is the Polaris daemon running? Start it with `polaris daemon` or `bun run src/daemon/daemon.ts`." }] }; } } if (name === "polaris_disconnect") { try { await daemonPost("/disconnect", { ccSessionId: CC_SESSION_ID }); currentProject = ""; currentSession = ""; currentUser = ""; currentProfile = ""; await clearSessionState(); return { content: [{ type: "text", text: "Disconnected from Polaris." }] }; } catch { return { content: [{ type: "text", text: "Failed to disconnect — daemon may not be running." }] }; } } if (name === "polaris_status") { try { const [statusRes, checkpointRes] = await Promise.all([ daemonGet(`/status/${CC_SESSION_ID}`), daemonGet(`/checkpoint/${CC_SESSION_ID}`), ]); const body = (await statusRes.json()) as { connected: boolean; project?: string; session?: string; user?: string; account?: string; slackChannel?: string }; if (body.connected) { const accountTag = body.account ? ` [${body.account}]` : ""; const slackTag = body.slackChannel ? ` #${body.slackChannel}` : ""; let text = `Connected: ${body.project}/${body.session} as ${body.user}${accountTag}${slackTag}`; try { const cp = await checkpointRes.json() as { hasCheckpoint: boolean; lastEventTimestamp?: string; pending?: number }; if (cp.hasCheckpoint && cp.lastEventTimestamp) { const ts = new Date(cp.lastEventTimestamp).toISOString().slice(11, 19) + "Z"; text += `\nLast delivered: ${ts}, ${cp.pending ?? 0} pending`; } } catch { /* best-effort */ } return { content: [{ type: "text", text }] }; } return { content: [{ type: "text", text: "Not connected to any Polaris session." }] }; } catch { return { content: [{ type: "text", text: "Polaris daemon not reachable." }] }; } } if (name === "polaris_standby") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const { timeout = 60 } = (args ?? {}) as { timeout?: number }; const totalSec = Math.max(timeout, 1); const deadline = Date.now() + totalSec * 1000; const POLL_SEC = 60; // max per sub-poll; keeps each request short-lived const RETRY_DELAY_MS = 3_000; // pause before retrying after a transient failure while (true) { const remainingSec = Math.ceil((deadline - Date.now()) / 1000); if (remainingSec <= 0) break; const pollSec = Math.min(remainingSec, POLL_SEC); try { const res = await fetch( `${DAEMON_URL}/standby/${encodeURIComponent(CC_SESSION_ID)}?timeout=${pollSec}`, { headers: daemonHeaders(), signal: AbortSignal.timeout((pollSec + 30) * 1000), } ); if (res.status === 400) { // Session no longer connected — no point retrying return { content: [{ type: "text", text: "Not connected to a Polaris session." }] }; } const body = await res.json() as { status: string; pushes: Array<{ from: string; content: string; timestamp: string }> }; if (body.pushes.length > 0) { const SEP = "━".repeat(48); const formatted = body.pushes .map((p) => `${SEP}\n POLARIS — from: ${p.from}\n${SEP}\n${p.content}\n${SEP}`) .join("\n\n"); return { content: [{ type: "text", text: formatted }] }; } // status === "timeout" — loop back if budget remains } catch { // Transient failure (daemon restart, network blip) — retry after a short pause const retryAt = Date.now() + RETRY_DELAY_MS; if (retryAt >= deadline) break; await Bun.sleep(RETRY_DELAY_MS); } } return { content: [{ type: "text", text: `No messages received (waited ${totalSec}s).` }] }; } if (name === "polaris_send") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const { session: targetSession, message } = args as { session: string; message: string }; try { const res = await daemonPost("/send", { ccSessionId: CC_SESSION_ID, targetSession, message }); if (res.ok) { const SEP = "━".repeat(48); const banner = `${SEP}\n POLARIS — sent to: ${currentProject}/${targetSession}\n${SEP}\n${message}\n${SEP}`; return { content: [{ type: "text", text: banner }] }; } const body = await res.json(); return { content: [{ type: "text", text: `Failed to send: ${(body as { error?: string }).error ?? res.status}` }] }; } catch { return { content: [{ type: "text", text: "Failed to reach the daemon." }] }; } } if (name === "polaris_reply") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const message = (args as { message: string }).message; try { const res = await daemonPost("/reply", { ccSessionId: CC_SESSION_ID, message }); if (res.ok) { return { content: [{ type: "text", text: "Reply sent to the floor." }] }; } const body = await res.json(); return { content: [{ type: "text", text: `Failed to send reply: ${(body as { error?: string }).error ?? res.status}` }] }; } catch { return { content: [{ type: "text", text: "Failed to reach the daemon." }] }; } } if (name === "polaris_rename") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const newName = (args as { name: string }).name; try { const res = await daemonPost("/rename", { oldName: currentProject, newName }); const body = await res.json(); if (res.ok) { const oldName = currentProject; currentProject = newName; return { content: [{ type: "text", text: `Renamed project "${oldName}" to "${newName}".` }] }; } return { content: [{ type: "text", text: `Failed to rename: ${(body as { error?: string }).error ?? "unknown error"}` }] }; } catch { return { content: [{ type: "text", text: "Failed to rename — is the Polaris daemon running?" }] }; } } if (name === "polaris_rename_session") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const newName = (args as { name: string }).name; try { const res = await daemonPost("/rename-session", { ccSessionId: CC_SESSION_ID, newName }); const body = await res.json() as { status?: string; oldName?: string; newName?: string; error?: string }; if (res.ok) { const oldName = currentSession; currentSession = newName; await saveSessionState(currentProject, currentSession, currentUser, currentProfile); return { content: [{ type: "text", text: `Renamed session "${oldName}" to "${newName}".` }] }; } return { content: [{ type: "text", text: `Failed to rename session: ${body.error ?? "unknown error"}` }] }; } catch { return { content: [{ type: "text", text: "Failed to rename session — is the Polaris daemon running?" }] }; } } if (name === "polaris_context") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const targetSession = (args as { session: string }).session; try { const res = await daemonGet(`/context/${CC_SESSION_ID}/${targetSession}`); if (!res.ok) { return { content: [{ type: "text", text: `Could not fetch session "${targetSession}": ${res.status}` }] }; } const events = (await res.json()) as Array<{ sender: string; payload: { prompt?: string; stop_response?: string; content?: string }; }>; const summary = events .map((e) => { const p = e.payload; const text = p.prompt ?? p.stop_response ?? p.content ?? JSON.stringify(p); return `[${e.sender}] ${text}`; }) .join("\n"); return { content: [{ type: "text", text: summary || "(no activity yet)" }] }; } catch { return { content: [{ type: "text", text: "Failed to reach the daemon." }] }; } } if (name === "polaris_team") { try { const res = await daemonGet("/team"); if (res.ok) { const body = await res.json() as { members: Array<{ name: string; participant_id: string | null; slack_id: string | null; slack_handle: string | null; slack_display: string | null; polaris_user: boolean; alias: string | null }> }; if (body.members.length === 0) { return { content: [{ type: "text", text: "No team members found." }] }; } const taggable = body.members.filter((m) => m.slack_id && m.slack_handle); const list = taggable .map((m) => { const shortAlias = m.alias && m.alias !== m.slack_handle ? `@${m.alias}` : ""; const handle = `@${m.slack_handle}`; const display = shortAlias ? `${shortAlias} (${handle})` : handle; return ` ${display} — ${m.name}${m.polaris_user ? " ✓" : ""} [${m.slack_id}]`; }) .join("\n"); const notTaggable = body.members.filter((m) => !m.slack_id); const note = notTaggable.length > 0 ? `\n\nNot on Slack: ${notTaggable.map(m => m.name).join(", ")}` : ""; return { content: [{ type: "text", text: `Team (use @alias or @handle to tag, ✓ = Polaris user):\n${list}${note}` }] }; } return { content: [{ type: "text", text: "Failed to fetch team list." }] }; } catch { return { content: [{ type: "text", text: "Failed to reach the daemon." }] }; } } if (name === "polaris_backfill") { if (!currentProject) { return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] }; } const { duration, from, dry_run } = (args ?? {}) as { duration?: string; from?: string; dry_run?: boolean }; try { const res = await daemonPost("/backfill", { ccSessionId: CC_SESSION_ID, ...(duration ? { duration } : {}), ...(from ? { from } : {}), ...(dry_run ? { dry_run: true } : {}), }); const body = await res.json() as { recovered: number; would_recover?: number; source: string; gaps: string[] }; if (res.ok) { const gapInfo = body.gaps.length > 0 ? `\nGaps: ${body.gaps.join(", ")}` : ""; if (dry_run) { return { content: [{ type: "text", text: `Dry run: ${body.would_recover ?? 0} events in daemon log would be sent.${gapInfo}` }] }; } return { content: [{ type: "text", text: `Backfill complete: ${body.recovered} events recovered from ${body.source}.${gapInfo}` }] }; } return { content: [{ type: "text", text: `Backfill failed: ${(body as unknown as { error?: string }).error ?? "unknown error"}` }] }; } catch { return { content: [{ type: "text", text: "Failed to reach the daemon." }] }; } } if (name === "polaris_update") { const installedPkgPath = `${homedir()}/.polaris/mcp/node_modules/@lightupai/polaris/package.json`; let currentVersion = ""; try { const pkg = JSON.parse(await readFile(installedPkgPath, "utf-8")) as { version: string }; currentVersion = pkg.version ?? ""; } catch { /* package not installed yet */ } let latestVersion = ""; try { const res = await fetch("https://registry.npmjs.org/@lightupai/polaris/latest", { signal: AbortSignal.timeout(5000) }); if (res.ok) ({ version: latestVersion } = await res.json() as { version: string }); } catch { /* network unavailable */ } if (!latestVersion) { return { content: [{ type: "text", text: "Could not reach npm to check for updates. Check your network connection." }] }; } if (latestVersion === currentVersion) { return { content: [{ type: "text", text: `Polaris is already up to date (v${currentVersion}).` }] }; } // Update the copy that's actually in use: ~/.polaris/mcp (no sudo required). // This is the same location the version was read from, and where the daemon // and MCP server binaries are resolved by run-daemon.sh and claude mcp add. const mcpDir = `${homedir()}/.polaris/mcp`; const npmResult = Bun.spawnSync( ["npm", "install", "--prefix", mcpDir, `@lightupai/polaris@${latestVersion}`], { stdout: "pipe", stderr: "pipe" } ); if (npmResult.exitCode !== 0) { return { content: [{ type: "text", text: `Update failed:\n${npmResult.stderr.toString().trim()}` }] }; } // Refresh hooks, migrate session state, and restart the daemon using the // local polaris binary (not relying on PATH, which may not include it). const polarisBin = `${mcpDir}/node_modules/.bin/polaris`; const installResult = Bun.spawnSync([polarisBin, "install"], { stdout: "pipe", stderr: "pipe" }); if (installResult.exitCode !== 0) { return { content: [{ type: "text", text: `Updated package to v${latestVersion}, but \`polaris install\` failed — run it manually.\n${installResult.stderr.toString().trim()}` }] }; } // Exit with code 42 after the response flushes. The bin/polaris-mcp wrapper // catches this code and immediately restarts the inner process with the new // binary — Claude Code's stdio pipes stay open, so no manual reconnect needed. setTimeout(() => process.exit(42), 500); return { content: [{ type: "text", text: `Updated Polaris from v${currentVersion} to v${latestVersion}. MCP server restarting…` }] }; } throw new Error(`Unknown tool: ${name}`); }); // --- Register with daemon and connect stdio --- async function main() { const activeProfile = await loadActiveProfile(); // Register with daemon (best-effort — daemon might not be running yet) try { await daemonPost("/register", { ccSessionId: CC_SESSION_ID, profile: activeProfile }); } catch { console.error("Warning: Polaris daemon not reachable. Start it with `bun run src/daemon/daemon.ts`."); } // Auto-reconnect if a previous session was active in this workspace const saved = await loadSessionState(); if (saved) { const reconnectProfile = saved.profile || activeProfile; try { const res = await daemonPost("/connect", { ccSessionId: CC_SESSION_ID, project: saved.project, session: saved.session, user: saved.user, profile: reconnectProfile, }); const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; error?: string; gap?: { events: number; since: string } | null }; if (res.ok) { currentProject = body.project ?? saved.project; currentSession = body.session ?? saved.session; currentUser = saved.user; currentProfile = reconnectProfile; // Show email when it looks like one; fall back to profile key or "default" const accountLabel = reconnectProfile.includes("@") ? reconnectProfile : (reconnectProfile || "default"); let reconnMsg = `Polaris auto-reconnected to #${currentProject}/${currentSession} [${accountLabel}]`; if (body.gap && body.gap.events > 0) { reconnMsg += `; re-sending ${body.gap.events} events since ${body.gap.since}`; } console.error(reconnMsg); } else { // Session or project no longer exists — clear stale state await clearSessionState(); } } catch { // Daemon not running — keep state file for next startup when daemon is available } } // Heartbeat: check every 60s whether the daemon still has our session mapped. // If not (dropped WS, daemon restart, compaction), silently re-connect so // activity reaches the channel without a manual /polaris join. setInterval(async () => { if (!currentProject) return; // not connected — nothing to heartbeat try { const res = await daemonGet(`/status/${CC_SESSION_ID}`); const body = await res.json() as { connected?: boolean; project?: string; session?: string }; if (body.connected) return; // all good // Daemon lost the mapping — attempt to reconnect to the same session const reconnectRes = await daemonPost("/connect", { ccSessionId: CC_SESSION_ID, project: currentProject, session: currentSession, user: currentUser, profile: currentProfile, }); if (reconnectRes.ok) { const reconnBody = await reconnectRes.json() as { gap?: { events: number; since: string } | null }; let reconnMsg = `Polaris: session dropped; auto-reconnected to #${currentProject}/${currentSession}`; if (reconnBody.gap && reconnBody.gap.events > 0) { reconnMsg += `; re-sending ${reconnBody.gap.events} events`; } console.error(reconnMsg); } else { console.error(`Polaris: session dropped and reconnect failed. Run /polaris to check status.`); currentProject = ""; currentSession = ""; currentUser = ""; } } catch { /* daemon unreachable — will retry next heartbeat */ } }, 60_000); // Poll the daemon every 3 seconds for pending direct-message channel pushes. // When session A calls polaris_send → session B, the daemon queues a push for B. // This loop drains it and fires a notifications/claude/channel so the message // appears visibly in the agent window without waiting for a UserPromptSubmit. setInterval(async () => { if (!currentProject || !currentSession) return; try { const res = await daemonGet(`/channel-push/${encodeURIComponent(currentProject)}/${encodeURIComponent(currentSession)}`); if (!res.ok) return; const body = await res.json() as { pushes?: Array<{ from: string; content: string; timestamp: string }> }; const pushes = body.pushes ?? []; const SEP = "━".repeat(48); for (const p of pushes) { const banner = `${SEP}\n POLARIS — from: ${p.from}\n${SEP}\n${p.content}\n${SEP}`; await deliverViaChannel(banner, { from: p.from, type: "direct", timestamp: p.timestamp }); } } catch { /* daemon unreachable — will retry next tick */ } }, 3_000); const transport = new StdioServerTransport(); await mcp.connect(transport); console.error(`Polaris MCP client started (session: ${CC_SESSION_ID})`); // Notify the host that the tool list is ready. On a normal first start this // is redundant (Claude Code fetches tools during initialize); on a hot // restart via the bin/polaris-mcp wrapper (after polaris_update) this is // what prompts Claude Code to re-fetch the updated tool list without a // manual reconnect. try { await mcp.notification({ method: "notifications/tools/list_changed" }); } catch { /* host may not support the notification — ignore */ } } if (import.meta.main) { await main(); } export { mcp, CC_SESSION_ID, main as startClient };