@lightupai/polaris 0.0.114 → 0.0.117

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.
@@ -57,8 +57,28 @@ if [ -f "$INSTALLED_PKG" ]; then
57
57
  ) &
58
58
  fi
59
59
 
60
- if [ -n "$CACHED_LATEST" ] && [ -n "$INSTALLED_VERSION" ] && [ "$CACHED_LATEST" != "$INSTALLED_VERSION" ]; then
61
- UPDATE_MSG=" [update available: v${CACHED_LATEST}]"
60
+ # Only notify when the registry version is strictly newer (prevents false
61
+ # positives when a freshly-updated client's cache still shows an older latest).
62
+ if [ -n "$CACHED_LATEST" ] && [ -n "$INSTALLED_VERSION" ]; then
63
+ _ver_gt() {
64
+ # Returns true (0) when $1 is semver-greater than $2
65
+ # Works for the "0.0.NNN" format used by polaris.
66
+ IFS=. read -r _a1 _a2 _a3 <<_EOF_
67
+ $1
68
+ _EOF_
69
+ IFS=. read -r _b1 _b2 _b3 <<_EOF_
70
+ $2
71
+ _EOF_
72
+ [ "${_a1:-0}" -gt "${_b1:-0}" ] && return 0
73
+ [ "${_a1:-0}" -lt "${_b1:-0}" ] && return 1
74
+ [ "${_a2:-0}" -gt "${_b2:-0}" ] && return 0
75
+ [ "${_a2:-0}" -lt "${_b2:-0}" ] && return 1
76
+ [ "${_a3:-0}" -gt "${_b3:-0}" ] && return 0
77
+ return 1
78
+ }
79
+ if _ver_gt "$CACHED_LATEST" "$INSTALLED_VERSION"; then
80
+ UPDATE_MSG=" [v${INSTALLED_VERSION}, update available]"
81
+ fi
62
82
  fi
63
83
  fi
64
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.114",
3
+ "version": "0.0.117",
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",
@@ -143,6 +143,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
143
143
  properties: {},
144
144
  },
145
145
  },
146
+ {
147
+ name: "polaris_send",
148
+ 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.",
149
+ inputSchema: {
150
+ type: "object" as const,
151
+ properties: {
152
+ session: { type: "string", description: "Target session name (e.g. 'feedback')" },
153
+ message: { type: "string", description: "Message to send" },
154
+ },
155
+ required: ["session", "message"],
156
+ },
157
+ },
146
158
  {
147
159
  name: "polaris_reply",
148
160
  description: "Send a message to the project floor (visible to all advisors, and to the linked Slack channel if a floor is connected).",
@@ -338,6 +350,23 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
338
350
  }
339
351
  }
340
352
 
353
+ if (name === "polaris_send") {
354
+ if (!currentProject) {
355
+ return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
356
+ }
357
+ const { session: targetSession, message } = args as { session: string; message: string };
358
+ try {
359
+ const res = await daemonPost("/send", { ccSessionId: CC_SESSION_ID, targetSession, message });
360
+ if (res.ok) {
361
+ return { content: [{ type: "text", text: `Message sent to session '${targetSession}'.` }] };
362
+ }
363
+ const body = await res.json();
364
+ return { content: [{ type: "text", text: `Failed to send: ${(body as { error?: string }).error ?? res.status}` }] };
365
+ } catch {
366
+ return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
367
+ }
368
+ }
369
+
341
370
  if (name === "polaris_reply") {
342
371
  if (!currentProject) {
343
372
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
@@ -1139,6 +1139,79 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
1139
1139
  }
1140
1140
  }
1141
1141
 
1142
+ // POST /send — deliver a message to a sibling session and log it on both timelines
1143
+ if (method === "POST" && pathname === "/send") {
1144
+ try {
1145
+ const body = (await req.json()) as { ccSessionId: string; targetSession: string; message: string };
1146
+ if (!body.ccSessionId || !body.targetSession || !body.message) {
1147
+ return error("ccSessionId, targetSession, and message required", 400);
1148
+ }
1149
+ const sender = sessions.get(body.ccSessionId);
1150
+ if (!sender || !sender.project) return error("Not connected", 400);
1151
+
1152
+ // Find the target session by name within the same project
1153
+ const target = Array.from(sessions.values()).find(
1154
+ (m) => m.project === sender.project && m.session === body.targetSession && m.ccSessionId !== body.ccSessionId
1155
+ );
1156
+ if (!target) {
1157
+ return error(`Session '${body.targetSession}' is not connected in project '${sender.project}'`, 404);
1158
+ }
1159
+
1160
+ const serviceUrl = getServiceUrlForProfile(sender.profile);
1161
+ const headers = await authHeadersForProfile(sender.profile);
1162
+ const timestamp = new Date().toISOString();
1163
+
1164
+ // 1. Log outbound event on sender's timeline
1165
+ await fetch(`${serviceUrl}/projects/${sender.project}/sessions/${sender.session}/events`, {
1166
+ method: "POST",
1167
+ headers,
1168
+ body: JSON.stringify({
1169
+ sender: sender.agent,
1170
+ payload: {
1171
+ hook_event_name: "SendMessage",
1172
+ session_id: body.ccSessionId,
1173
+ to: body.targetSession,
1174
+ message: body.message,
1175
+ },
1176
+ }),
1177
+ });
1178
+
1179
+ // 2. Log inbound event on target's timeline (attributed to the sender's session name)
1180
+ await fetch(`${serviceUrl}/projects/${target.project}/sessions/${target.session}/events`, {
1181
+ method: "POST",
1182
+ headers,
1183
+ body: JSON.stringify({
1184
+ sender: sender.agent,
1185
+ payload: {
1186
+ hook_event_name: "ReceiveMessage",
1187
+ session_id: target.ccSessionId,
1188
+ from: sender.session,
1189
+ message: body.message,
1190
+ },
1191
+ }),
1192
+ });
1193
+
1194
+ // 3. Inject into target's local queue for delivery on next UserPromptSubmit
1195
+ let queue = injectQueues.get(target.ccSessionId);
1196
+ if (!queue) {
1197
+ queue = [];
1198
+ injectQueues.set(target.ccSessionId, queue);
1199
+ }
1200
+ // Also register under any alias ccSessionId the target may be using
1201
+ for (const [id, m] of sessions) {
1202
+ if (m.project === target.project && m.session === target.session && !injectQueues.has(id)) {
1203
+ injectQueues.set(id, queue);
1204
+ }
1205
+ }
1206
+ queue.push({ from: sender.session, content: body.message, timestamp });
1207
+
1208
+ await logEvent("/send", body, undefined, { project: sender.project, session: sender.session, ccSessionId: body.ccSessionId });
1209
+ return json({ status: "delivered", from: sender.session, to: body.targetSession });
1210
+ } catch {
1211
+ return error("Invalid JSON", 400);
1212
+ }
1213
+ }
1214
+
1142
1215
  // GET /context/:ccSessionId/:session — proxy context fetch from cloud API
1143
1216
  if (method === "GET" && pathname.match(/^\/context\/[^/]+\/[^/]+$/)) {
1144
1217
  const parts = pathname.split("/");