@lightupai/polaris 0.0.116 → 0.0.120

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.
@@ -32,22 +32,36 @@ try {
32
32
  } | null;
33
33
  const items = body?.pendingInjects;
34
34
  if (Array.isArray(items) && items.length > 0) {
35
- // Present injects with honest provenance and let the model apply its own
36
- // judgment. NOTE (verified live): the model treats content arriving via
37
- // this channel as not-from-the-developer and will surface/consider it but
38
- // will NOT obey imperative instructions through it — no hook wording
39
- // reliably changes that, nor should it. Use injects to SHARE CONTEXT with
40
- // the agent, not to remote-control its actions. (For trusted imperative
41
- // steering, the claude/channel path is the right mechanism — deferred.)
42
- const formatted =
43
- "Context shared by teammates collaborating in this session via Polaris " +
44
- "(for your awareness; it did not come directly from the developer):\n" +
45
- items.map((i) => `- ${i.from}: ${i.content}`).join("\n");
35
+ const direct = items.filter((i) => i.type === "direct");
36
+ const context = items.filter((i) => i.type !== "direct");
37
+
38
+ const parts: string[] = [];
39
+
40
+ // Direct session-to-session messages: printed verbatim with a loud banner
41
+ // so the receiver sees exactly what was sent, unambiguously attributed.
42
+ const SEP = "━".repeat(48);
43
+ for (const i of direct) {
44
+ parts.push(`${SEP}\n POLARIS — from: ${i.from}\n${SEP}\n${i.content}\n${SEP}`);
45
+ }
46
+
47
+ // Advisor context: presented as background awareness, not direct commands.
48
+ // NOTE (verified live): the model treats content arriving via this channel
49
+ // as not-from-the-developer and will surface/consider it but will NOT obey
50
+ // imperative instructions — no hook wording reliably changes that, nor
51
+ // should it. Use injects to SHARE CONTEXT, not to remote-control actions.
52
+ if (context.length > 0) {
53
+ parts.push(
54
+ "Context shared by teammates collaborating in this session via Polaris " +
55
+ "(for your awareness; it did not come directly from the developer):\n" +
56
+ context.map((i) => `- ${i.from}: ${i.content}`).join("\n")
57
+ );
58
+ }
59
+
46
60
  process.stdout.write(
47
61
  JSON.stringify({
48
62
  hookSpecificOutput: {
49
63
  hookEventName: "UserPromptSubmit",
50
- additionalContext: formatted,
64
+ additionalContext: parts.join("\n\n"),
51
65
  },
52
66
  })
53
67
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.116",
3
+ "version": "0.0.120",
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,25 @@ 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
+ const SEP = "━".repeat(48);
362
+ const banner = `${SEP}\n POLARIS — sent to: ${currentProject}/${targetSession}\n${SEP}\n${message}\n${SEP}`;
363
+ return { content: [{ type: "text", text: banner }] };
364
+ }
365
+ const body = await res.json();
366
+ return { content: [{ type: "text", text: `Failed to send: ${(body as { error?: string }).error ?? res.status}` }] };
367
+ } catch {
368
+ return { content: [{ type: "text", text: "Failed to reach the daemon." }] };
369
+ }
370
+ }
371
+
341
372
  if (name === "polaris_reply") {
342
373
  if (!currentProject) {
343
374
  return { content: [{ type: "text", text: "Not connected to a Polaris session. Use polaris_connect first." }] };
@@ -26,7 +26,7 @@ function generateSessionName(): string {
26
26
  const sessions = new Map<string, SessionMapping>(); // keyed by ccSessionId
27
27
 
28
28
  // Advisor injects queued for delivery via the UserPromptSubmit hook
29
- const injectQueues = new Map<string, Array<{ from: string; content: string; timestamp: string }>>(); // keyed by ccSessionId
29
+ const injectQueues = new Map<string, Array<{ from: string; content: string; timestamp: string; type?: "direct" }>>(); // keyed by ccSessionId
30
30
 
31
31
  // --- Config resolution (env var > config.json > legacy credentials.json > defaults) ---
32
32
 
@@ -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.project}/${sender.session}`, content: body.message, timestamp, type: "direct" });
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("/");