@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 +1 -1
- package/src/client/client.ts +35 -2
- package/src/daemon/daemon.ts +19 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
package/src/client/client.ts
CHANGED
|
@@ -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();
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -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
|
-
|
|
168
|
-
if (
|
|
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
|
-
|
|
171
|
-
|
|
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);
|