@lightupai/polaris 0.0.31 → 0.0.32
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/skills/polaris/SKILL.md +6 -2
- package/src/client/client.ts +33 -0
- package/src/daemon/daemon.ts +126 -1
package/package.json
CHANGED
package/skills/polaris/SKILL.md
CHANGED
|
@@ -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
|
|
5
|
-
argument-hint: [join #channel | rename <new-name> | disconnect | (no args for status)]
|
|
4
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_backfill
|
|
5
|
+
argument-hint: [join #channel | backfill [duration] | rename <new-name> | disconnect | (no args for status)]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Polaris — Multiplayer Collaboration
|
|
@@ -22,6 +22,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
22
22
|
1. Call `polaris_rename` with the new name
|
|
23
23
|
2. Report the result
|
|
24
24
|
|
|
25
|
+
**`/polaris backfill [duration]`** — Recover lost events:
|
|
26
|
+
1. Call `polaris_backfill` with the optional duration (e.g., `2h`, `30m`)
|
|
27
|
+
2. Report how many events were recovered
|
|
28
|
+
|
|
25
29
|
**`/polaris disconnect`** — Disconnect:
|
|
26
30
|
1. Call `polaris_disconnect`
|
|
27
31
|
2. Confirm disconnection
|
package/src/client/client.ts
CHANGED
|
@@ -110,6 +110,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
110
110
|
required: ["session"],
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: "polaris_backfill",
|
|
115
|
+
description: "Recover lost events from local daemon logs. Use when events were lost due to disconnection or API downtime.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object" as const,
|
|
118
|
+
properties: {
|
|
119
|
+
duration: { type: "string", description: "Time range to backfill (e.g., '2h', '30m', '1d'). Auto-detects if omitted." },
|
|
120
|
+
from: { type: "string", description: "ISO timestamp to backfill from. Overrides duration." },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
113
124
|
],
|
|
114
125
|
}));
|
|
115
126
|
|
|
@@ -228,6 +239,28 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
if (name === "polaris_backfill") {
|
|
243
|
+
if (!currentProject) {
|
|
244
|
+
return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
|
|
245
|
+
}
|
|
246
|
+
const { duration, from } = (args ?? {}) as { duration?: string; from?: string };
|
|
247
|
+
try {
|
|
248
|
+
const res = await daemonPost("/backfill", {
|
|
249
|
+
ccSessionId: CC_SESSION_ID,
|
|
250
|
+
...(duration ? { duration } : {}),
|
|
251
|
+
...(from ? { from } : {}),
|
|
252
|
+
});
|
|
253
|
+
const body = await res.json() as { recovered: number; source: string; gaps: string[] };
|
|
254
|
+
if (res.ok) {
|
|
255
|
+
const gapInfo = body.gaps.length > 0 ? `\nGaps: ${body.gaps.join(", ")}` : "";
|
|
256
|
+
return { content: [{ type: "text", text: `Backfill complete: ${body.recovered} events recovered from ${body.source}.${gapInfo}` }] };
|
|
257
|
+
}
|
|
258
|
+
return { content: [{ type: "text", text: `Backfill failed: ${(body as unknown as { error?: string }).error ?? "unknown error"}` }] };
|
|
259
|
+
} catch {
|
|
260
|
+
return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
231
264
|
throw new Error(`Unknown tool: ${name}`);
|
|
232
265
|
});
|
|
233
266
|
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -154,6 +154,114 @@ async function logEvent(endpoint: string, payload: unknown, response?: { status:
|
|
|
154
154
|
} catch { /* best-effort — don't break the request */ }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// --- Session history (persistent connect/disconnect log for backfill) ---
|
|
158
|
+
|
|
159
|
+
const SESSION_HISTORY_FILE = join(homedir(), ".polaris", "session-history.jsonl");
|
|
160
|
+
|
|
161
|
+
async function logSessionChange(action: "connect" | "disconnect", ccSessionId: string, project: string, session: string, user: string, agent: string): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
await ensureLogDir();
|
|
164
|
+
const entry = JSON.stringify({ t: new Date().toISOString(), action, ccSessionId, project, session, user, agent });
|
|
165
|
+
await appendFile(SESSION_HISTORY_FILE, entry + "\n");
|
|
166
|
+
} catch { /* best-effort */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Backfill: recover lost events from daemon log ---
|
|
170
|
+
|
|
171
|
+
async function backfill(mapping: SessionMapping, duration?: string, from?: string): Promise<{ recovered: number; source: string; gaps: string[] }> {
|
|
172
|
+
const now = new Date();
|
|
173
|
+
let since: Date;
|
|
174
|
+
|
|
175
|
+
if (from) {
|
|
176
|
+
since = new Date(from);
|
|
177
|
+
} else if (duration) {
|
|
178
|
+
const match = duration.match(/^(\d+)(h|m|d)$/);
|
|
179
|
+
if (!match) return { recovered: 0, source: "none", gaps: ["invalid duration format"] };
|
|
180
|
+
const val = parseInt(match[1]);
|
|
181
|
+
const unit = match[2];
|
|
182
|
+
since = new Date(now.getTime() - val * (unit === "h" ? 3600000 : unit === "m" ? 60000 : 86400000));
|
|
183
|
+
} else {
|
|
184
|
+
// Auto-detect: query API for most recent event, backfill from there
|
|
185
|
+
try {
|
|
186
|
+
const serviceUrl = getServiceUrl();
|
|
187
|
+
const res = await fetch(
|
|
188
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/messages`,
|
|
189
|
+
{ headers: await authHeaders() }
|
|
190
|
+
);
|
|
191
|
+
if (res.ok) {
|
|
192
|
+
const events = (await res.json()) as Array<{ timestamp: string }>;
|
|
193
|
+
if (events.length > 0) {
|
|
194
|
+
since = new Date(events[events.length - 1].timestamp);
|
|
195
|
+
} else {
|
|
196
|
+
since = new Date(now.getTime() - 3600000); // default 1h
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
since = new Date(now.getTime() - 3600000);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
since = new Date(now.getTime() - 3600000);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Read daemon log files covering the time range
|
|
207
|
+
const logEntries: Array<{ t: string; endpoint: string; payload: Record<string, unknown> }> = [];
|
|
208
|
+
const startDate = since.toISOString().slice(0, 10);
|
|
209
|
+
const endDate = now.toISOString().slice(0, 10);
|
|
210
|
+
|
|
211
|
+
// Collect all dates in range
|
|
212
|
+
const dates: string[] = [];
|
|
213
|
+
const d = new Date(startDate);
|
|
214
|
+
while (d.toISOString().slice(0, 10) <= endDate) {
|
|
215
|
+
dates.push(d.toISOString().slice(0, 10));
|
|
216
|
+
d.setDate(d.getDate() + 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const date of dates) {
|
|
220
|
+
const logFile = join(LOG_DIR, `daemon-${date}.jsonl`);
|
|
221
|
+
try {
|
|
222
|
+
const content = await readFile(logFile, "utf-8");
|
|
223
|
+
for (const line of content.split("\n")) {
|
|
224
|
+
if (!line.trim()) continue;
|
|
225
|
+
try {
|
|
226
|
+
const entry = JSON.parse(line);
|
|
227
|
+
if (entry.endpoint !== "/events") continue;
|
|
228
|
+
if (new Date(entry.t) <= since) continue;
|
|
229
|
+
if (new Date(entry.t) > now) continue;
|
|
230
|
+
logEntries.push(entry);
|
|
231
|
+
} catch { /* skip malformed lines */ }
|
|
232
|
+
}
|
|
233
|
+
} catch { /* log file doesn't exist for this date — not an error */ }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (logEntries.length === 0) {
|
|
237
|
+
return { recovered: 0, source: "daemon_log", gaps: [`no log entries found since ${since.toISOString()}`] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Replay events to the API
|
|
241
|
+
let recovered = 0;
|
|
242
|
+
const serviceUrl = getServiceUrl();
|
|
243
|
+
const headers = await authHeaders();
|
|
244
|
+
|
|
245
|
+
for (const entry of logEntries) {
|
|
246
|
+
const hookEvent = entry.payload.hook_event_name as string | undefined;
|
|
247
|
+
const sender = hookEvent === "UserPromptSubmit" ? mapping.user : mapping.agent;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch(
|
|
251
|
+
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers,
|
|
255
|
+
body: JSON.stringify({ sender, payload: entry.payload }),
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
if (res.ok) recovered++;
|
|
259
|
+
} catch { /* skip failed replays */ }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { recovered, source: "daemon_log", gaps: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
157
265
|
// --- HTTP Server ---
|
|
158
266
|
|
|
159
267
|
function json(data: unknown, status = 200): Response {
|
|
@@ -294,6 +402,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
294
402
|
|
|
295
403
|
// Connect to cloud WebSocket
|
|
296
404
|
connectCloudWs(mapping);
|
|
405
|
+
await logSessionChange("connect", mapping.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
|
|
297
406
|
|
|
298
407
|
return json({
|
|
299
408
|
status: "connected",
|
|
@@ -314,7 +423,8 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
314
423
|
if (!body.ccSessionId) return error("ccSessionId required", 400);
|
|
315
424
|
disconnectCloudWs(body.ccSessionId);
|
|
316
425
|
const mapping = sessions.get(body.ccSessionId);
|
|
317
|
-
if (mapping) {
|
|
426
|
+
if (mapping && mapping.project) {
|
|
427
|
+
await logSessionChange("disconnect", body.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
|
|
318
428
|
mapping.project = "";
|
|
319
429
|
mapping.session = "";
|
|
320
430
|
mapping.user = "";
|
|
@@ -541,6 +651,21 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
541
651
|
return json(data);
|
|
542
652
|
}
|
|
543
653
|
|
|
654
|
+
// POST /backfill — recover lost events from daemon log
|
|
655
|
+
if (method === "POST" && pathname === "/backfill") {
|
|
656
|
+
try {
|
|
657
|
+
const body = (await req.json()) as { ccSessionId: string; duration?: string; from?: string };
|
|
658
|
+
if (!body.ccSessionId) return error("ccSessionId required", 400);
|
|
659
|
+
const mapping = sessions.get(body.ccSessionId);
|
|
660
|
+
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
661
|
+
|
|
662
|
+
const result = await backfill(mapping, body.duration, body.from);
|
|
663
|
+
return json(result);
|
|
664
|
+
} catch {
|
|
665
|
+
return error("Invalid JSON", 400);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
544
669
|
return error("Not found", 404);
|
|
545
670
|
},
|
|
546
671
|
});
|