@lightupai/polaris 0.0.92 → 0.0.93
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/README.md +24 -6
- package/hooks/statusline.sh +9 -13
- package/package.json +1 -1
- package/skills/polaris/SKILL.md +6 -2
- package/src/cli/cli.ts +75 -48
- package/src/client/client.ts +40 -12
- package/src/daemon/daemon.ts +110 -53
- package/tests/client.test.ts +5 -0
- package/tests/daemon.test.ts +8 -0
- package/tests/e2e.test.ts +12 -0
- package/tests/reply-attribution.test.ts +2 -0
package/README.md
CHANGED
|
@@ -113,18 +113,36 @@ polaris install # Install MCP server, hooks, skill, status line + resta
|
|
|
113
113
|
polaris login # Authenticate against production
|
|
114
114
|
polaris login --local # Authenticate against localhost (for local dev)
|
|
115
115
|
|
|
116
|
-
#
|
|
117
|
-
polaris profiles
|
|
118
|
-
polaris use
|
|
119
|
-
polaris use
|
|
116
|
+
# Multi-account support
|
|
117
|
+
polaris profiles # List all logged-in accounts
|
|
118
|
+
polaris use you@work.com # Switch to a work account
|
|
119
|
+
polaris use you@personal.com # Switch to a personal account (triggers login if not yet authenticated)
|
|
120
120
|
|
|
121
121
|
# Other commands
|
|
122
122
|
polaris daemon # Start the local daemon (kills any existing daemon first)
|
|
123
|
-
polaris status # Show active
|
|
124
|
-
polaris logout # Remove active
|
|
123
|
+
polaris status # Show active account, daemon state, sessions
|
|
124
|
+
polaris logout # Remove active account credentials
|
|
125
125
|
polaris logout --all # Remove all credentials
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### Multiple accounts
|
|
129
|
+
|
|
130
|
+
Polaris supports multiple accounts (work and personal) side by side. Each Claude Code session can connect to a different account independently — switching one session does not affect others.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
# Log in to both accounts (one-time)
|
|
134
|
+
polaris login # first account (stored as you@work.com)
|
|
135
|
+
polaris login # second account (stored as you@personal.com)
|
|
136
|
+
|
|
137
|
+
# Switch the active account for the daemon
|
|
138
|
+
polaris use you@personal.com
|
|
139
|
+
|
|
140
|
+
# Start a new session — it will use whichever account is active at connect time
|
|
141
|
+
/polaris join #my-project
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If you pass an email that isn't logged in yet, `polaris use` opens the browser to authenticate that account, then sets it as active. Google's account picker is pre-filled with the email hint.
|
|
145
|
+
|
|
128
146
|
### Upgrading
|
|
129
147
|
|
|
130
148
|
Because `polaris` is a **global install**, use `npm install -g` to upgrade — not `npx`:
|
package/hooks/statusline.sh
CHANGED
|
@@ -10,16 +10,7 @@ if [ -n "$POLARIS_DAEMON_SECRET" ]; then
|
|
|
10
10
|
SECRET_HEADER="x-polaris-daemon-secret: $POLARIS_DAEMON_SECRET"
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
PROFILE=""
|
|
15
|
-
CONFIG_FILE="${HOME}/.polaris/config.json"
|
|
16
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
17
|
-
PROFILE=$(jq -r '.active // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
18
|
-
fi
|
|
19
|
-
PROFILE_TAG=""
|
|
20
|
-
if [ -n "$PROFILE" ]; then
|
|
21
|
-
PROFILE_TAG="[${PROFILE}] "
|
|
22
|
-
fi
|
|
13
|
+
# Account is read from the per-session daemon status (set after connection)
|
|
23
14
|
|
|
24
15
|
# Read stdin (session JSON from the coding agent)
|
|
25
16
|
INPUT=$(cat)
|
|
@@ -38,11 +29,16 @@ if [ "$CONNECTED" = "true" ]; then
|
|
|
38
29
|
SESSION=$(echo "$STATUS" | jq -r '.session' 2>/dev/null)
|
|
39
30
|
USER=$(echo "$STATUS" | jq -r '.user' 2>/dev/null)
|
|
40
31
|
SLACK=$(echo "$STATUS" | jq -r '.slackChannel // empty' 2>/dev/null)
|
|
32
|
+
ACCOUNT=$(echo "$STATUS" | jq -r '.account // empty' 2>/dev/null)
|
|
33
|
+
ACCOUNT_TAG=""
|
|
34
|
+
if [ -n "$ACCOUNT" ]; then
|
|
35
|
+
ACCOUNT_TAG="[${ACCOUNT}] "
|
|
36
|
+
fi
|
|
41
37
|
if [ -n "$SLACK" ]; then
|
|
42
|
-
echo "polaris: ${
|
|
38
|
+
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}"
|
|
43
39
|
else
|
|
44
|
-
echo "polaris: ${
|
|
40
|
+
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER})"
|
|
45
41
|
fi
|
|
46
42
|
else
|
|
47
|
-
echo "polaris:
|
|
43
|
+
echo "polaris: not connected"
|
|
48
44
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.93",
|
|
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/skills/polaris/SKILL.md
CHANGED
|
@@ -13,9 +13,13 @@ Manage your connection to a Polaris collaboration session.
|
|
|
13
13
|
|
|
14
14
|
Based on the arguments provided, do ONE of the following:
|
|
15
15
|
|
|
16
|
-
**`/polaris join #channel-name`** — Connect to a
|
|
16
|
+
**`/polaris join #channel-name`** — Connect to an existing project, or create a new one:
|
|
17
17
|
1. Call `polaris_connect` with the given channel and user identity `user:manu.bansal`
|
|
18
|
-
2.
|
|
18
|
+
2. If the response is `project_not_found`:
|
|
19
|
+
- Show the list of existing projects from the response
|
|
20
|
+
- Ask the user: "Project '#channel-name' doesn't exist. Create it? [y/N]"
|
|
21
|
+
- If yes: call `polaris_connect` again with `create: true`
|
|
22
|
+
- If no: stop and show the existing project list so they can pick one
|
|
19
23
|
3. Report the connection status including the session name
|
|
20
24
|
|
|
21
25
|
**`/polaris rename <new-name>`** — Rename the current channel:
|
package/src/cli/cli.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// polaris login — authenticate via Google SSO (production)
|
|
7
7
|
// polaris login --local — authenticate against localhost
|
|
8
8
|
// polaris login --url <url> — authenticate against a custom URL
|
|
9
|
-
// polaris login --profile <name> — explicit profile
|
|
10
|
-
// polaris use <
|
|
11
|
-
// polaris profiles — list
|
|
9
|
+
// polaris login --profile <name> — explicit profile key (default: email)
|
|
10
|
+
// polaris use <email> — switch active account (triggers login if not found)
|
|
11
|
+
// polaris profiles — list accounts
|
|
12
12
|
// polaris daemon — start the local daemon
|
|
13
13
|
// polaris status — show connection status
|
|
14
14
|
// polaris recover — re-POST locally logged events missing upstream
|
|
@@ -279,8 +279,9 @@ Based on the arguments provided, do ONE of the following:
|
|
|
279
279
|
|
|
280
280
|
// --- Login ---
|
|
281
281
|
|
|
282
|
-
async function login(appUrl: string, profileName?: string) {
|
|
283
|
-
|
|
282
|
+
async function login(appUrl: string, profileName?: string, loginHint?: string) {
|
|
283
|
+
// Profile key is determined after auth (we use the email). profileName is an
|
|
284
|
+
// explicit override (e.g. --profile flag); otherwise we'll use userInfo.email.
|
|
284
285
|
|
|
285
286
|
// Browser OAuth
|
|
286
287
|
let resolveToken: (token: string) => void;
|
|
@@ -306,9 +307,10 @@ async function login(appUrl: string, profileName?: string) {
|
|
|
306
307
|
});
|
|
307
308
|
|
|
308
309
|
const callbackPort = callbackServer.port;
|
|
309
|
-
|
|
310
|
+
let authUrl = `${appUrl}/auth/cli?port=${callbackPort}`;
|
|
311
|
+
if (loginHint) authUrl += `&login_hint=${encodeURIComponent(loginHint)}`;
|
|
310
312
|
|
|
311
|
-
console.log(` Opening browser for Google sign-in
|
|
313
|
+
console.log(` Opening browser for Google sign-in...`);
|
|
312
314
|
console.log(` If the browser doesn't open, visit: ${authUrl}\n`);
|
|
313
315
|
|
|
314
316
|
const proc = Bun.spawn(
|
|
@@ -344,9 +346,10 @@ async function login(appUrl: string, profileName?: string) {
|
|
|
344
346
|
console.log(` Organization: ${orgName}`);
|
|
345
347
|
console.log(` Participant ID: ${userInfo.participant_id}`);
|
|
346
348
|
|
|
347
|
-
// Save to profile
|
|
349
|
+
// Save to profile — keyed by email unless an explicit --profile name was given
|
|
348
350
|
const config = await loadConfig();
|
|
349
|
-
|
|
351
|
+
const profileKey = profileName ?? userInfo.email;
|
|
352
|
+
config.profiles[profileKey] = {
|
|
350
353
|
api: appToApi(appUrl),
|
|
351
354
|
app: appUrl,
|
|
352
355
|
token,
|
|
@@ -355,7 +358,7 @@ async function login(appUrl: string, profileName?: string) {
|
|
|
355
358
|
org_id: userInfo.org_id,
|
|
356
359
|
participant_id: userInfo.participant_id,
|
|
357
360
|
};
|
|
358
|
-
config.active =
|
|
361
|
+
config.active = profileKey;
|
|
359
362
|
await saveConfig(config);
|
|
360
363
|
|
|
361
364
|
// Also write legacy credentials.json for backward compat (daemon reads it)
|
|
@@ -365,7 +368,7 @@ async function login(appUrl: string, profileName?: string) {
|
|
|
365
368
|
service_url: appUrl,
|
|
366
369
|
}, null, 2));
|
|
367
370
|
|
|
368
|
-
console.log(` ✓
|
|
371
|
+
console.log(` ✓ Logged in as ${userInfo.email}`);
|
|
369
372
|
|
|
370
373
|
// Re-install skill with personalized participant ID
|
|
371
374
|
await mkdir(join(CLAUDE_DIR, "skills", "polaris"), { recursive: true });
|
|
@@ -434,54 +437,78 @@ Based on the arguments provided, do ONE of the following:
|
|
|
434
437
|
|
|
435
438
|
// --- Use ---
|
|
436
439
|
|
|
437
|
-
|
|
440
|
+
// Accepts an email address or a legacy profile key. If the account isn't
|
|
441
|
+
// found locally, starts the login flow (with login_hint pre-filled).
|
|
442
|
+
async function use(arg: string) {
|
|
438
443
|
const config = await loadConfig();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
444
|
+
|
|
445
|
+
// 1. Direct key match (backward compat with old "prod" / "local" style names)
|
|
446
|
+
let matchedKey: string | undefined;
|
|
447
|
+
if (config.profiles[arg]) {
|
|
448
|
+
matchedKey = arg;
|
|
449
|
+
} else {
|
|
450
|
+
// 2. Search by email field
|
|
451
|
+
const entry = Object.entries(config.profiles).find(([, p]) => p.email === arg);
|
|
452
|
+
if (entry) matchedKey = entry[0];
|
|
448
453
|
}
|
|
449
|
-
config.active = profileName;
|
|
450
|
-
await saveConfig(config);
|
|
451
454
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
455
|
+
if (matchedKey) {
|
|
456
|
+
config.active = matchedKey;
|
|
457
|
+
await saveConfig(config);
|
|
458
|
+
const profile = config.profiles[matchedKey];
|
|
459
|
+
// Update legacy credentials.json for daemon
|
|
460
|
+
await writeFile(LEGACY_CREDENTIALS_FILE, JSON.stringify({
|
|
461
|
+
token: profile.token,
|
|
462
|
+
email: profile.email,
|
|
463
|
+
name: profile.name,
|
|
464
|
+
org_id: profile.org_id,
|
|
465
|
+
participant_id: profile.participant_id,
|
|
466
|
+
service_url: profile.app,
|
|
467
|
+
}, null, 2));
|
|
468
|
+
console.log(`Active account: ${profile.email}`);
|
|
469
|
+
console.log("Restart the daemon for this to take effect.");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
462
472
|
|
|
463
|
-
|
|
464
|
-
|
|
473
|
+
// 3. Not found locally — start auth (pre-fill email hint when it looks like
|
|
474
|
+
// an email address so Google can skip account selection)
|
|
475
|
+
console.log(`Account "${arg}" not found locally. Starting authentication...\n`);
|
|
476
|
+
const isEmail = arg.includes("@");
|
|
477
|
+
await login(DEFAULT_APP_URL, undefined, isEmail ? arg : undefined);
|
|
478
|
+
console.log("\n✓ Login complete!");
|
|
479
|
+
// Replace daemon so new credentials are picked up immediately
|
|
480
|
+
await killExistingDaemon();
|
|
481
|
+
const daemonPath = join(import.meta.dir, "..", "daemon", "daemon.ts");
|
|
482
|
+
Bun.spawn(["bun", "run", daemonPath], {
|
|
483
|
+
stdout: "ignore",
|
|
484
|
+
stderr: "ignore",
|
|
485
|
+
env: { ...process.env },
|
|
486
|
+
}).unref?.();
|
|
487
|
+
console.log(" ✓ Daemon started in background");
|
|
465
488
|
}
|
|
466
489
|
|
|
467
490
|
// --- Profiles ---
|
|
468
491
|
|
|
469
492
|
async function profiles() {
|
|
470
493
|
const config = await loadConfig();
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
473
|
-
console.log("No
|
|
494
|
+
const keys = Object.keys(config.profiles);
|
|
495
|
+
if (keys.length === 0) {
|
|
496
|
+
console.log("No accounts configured. Run: polaris login");
|
|
474
497
|
return;
|
|
475
498
|
}
|
|
476
|
-
console.log("
|
|
477
|
-
for (const
|
|
478
|
-
const p = config.profiles[
|
|
479
|
-
const active =
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
console.log("Accounts:\n");
|
|
500
|
+
for (const key of keys) {
|
|
501
|
+
const p = config.profiles[key];
|
|
502
|
+
const active = key === config.active ? " (active)" : "";
|
|
503
|
+
// Use email as the primary label; show key only when it differs (legacy style)
|
|
504
|
+
const label = p.email || key;
|
|
505
|
+
const keyHint = p.email && key !== p.email ? ` [key: ${key}]` : "";
|
|
506
|
+
console.log(` ${label}${active}${keyHint}`);
|
|
507
|
+
if (p.name) console.log(` Name: ${p.name}`);
|
|
508
|
+
console.log(` API: ${p.api}`);
|
|
483
509
|
console.log("");
|
|
484
510
|
}
|
|
511
|
+
console.log(`Use \`polaris use <email>\` to switch accounts.`);
|
|
485
512
|
}
|
|
486
513
|
|
|
487
514
|
// --- Daemon ---
|
|
@@ -852,7 +879,7 @@ switch (command) {
|
|
|
852
879
|
|
|
853
880
|
case "use":
|
|
854
881
|
if (!args[1]) {
|
|
855
|
-
console.error("Usage: polaris use <
|
|
882
|
+
console.error("Usage: polaris use <email>");
|
|
856
883
|
process.exit(1);
|
|
857
884
|
}
|
|
858
885
|
await use(args[1]);
|
|
@@ -908,8 +935,8 @@ switch (command) {
|
|
|
908
935
|
console.log(" polaris install — install local components (no auth)");
|
|
909
936
|
console.log(" polaris login — authenticate (production)");
|
|
910
937
|
console.log(" polaris login --local — authenticate (local dev)");
|
|
911
|
-
console.log(" polaris use <
|
|
912
|
-
console.log(" polaris profiles — list all
|
|
938
|
+
console.log(" polaris use <email> — switch active account (triggers login if needed)");
|
|
939
|
+
console.log(" polaris profiles — list all accounts");
|
|
913
940
|
console.log(" polaris daemon — start the local daemon");
|
|
914
941
|
console.log(" polaris status — show connection status");
|
|
915
942
|
console.log(" polaris recover — re-POST locally logged events missing upstream");
|
package/src/client/client.ts
CHANGED
|
@@ -51,10 +51,10 @@ function sessionStatePath(): string {
|
|
|
51
51
|
return `${homedir()}/.polaris/sessions/${hash}.json`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
async function saveSessionState(project: string, session: string, user: string): Promise<void> {
|
|
54
|
+
async function saveSessionState(project: string, session: string, user: string, profile: string): Promise<void> {
|
|
55
55
|
try {
|
|
56
56
|
await mkdir(`${homedir()}/.polaris/sessions`, { recursive: true });
|
|
57
|
-
await writeFile(sessionStatePath(), JSON.stringify({ project, session, user, cwd: process.cwd() }));
|
|
57
|
+
await writeFile(sessionStatePath(), JSON.stringify({ project, session, user, profile, cwd: process.cwd() }));
|
|
58
58
|
} catch { /* best-effort */ }
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -64,19 +64,29 @@ async function clearSessionState(): Promise<void> {
|
|
|
64
64
|
} catch { /* best-effort */ }
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
async function loadSessionState(): Promise<{ project: string; session: string; user: string } | null> {
|
|
67
|
+
async function loadSessionState(): Promise<{ project: string; session: string; user: string; profile: string } | null> {
|
|
68
68
|
try {
|
|
69
69
|
const data = JSON.parse(await readFile(sessionStatePath(), "utf-8"));
|
|
70
|
-
if (data.project && data.session && data.user) return data;
|
|
70
|
+
if (data.project && data.session && data.user) return { ...data, profile: data.profile ?? "" };
|
|
71
71
|
} catch { /* no saved state */ }
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
async function loadActiveProfile(): Promise<string> {
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(await readFile(`${homedir()}/.polaris/config.json`, "utf-8"));
|
|
78
|
+
return data.active ?? "";
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
// --- Current connection state ---
|
|
76
85
|
|
|
77
86
|
let currentProject = "";
|
|
78
87
|
let currentSession = "";
|
|
79
88
|
let currentUser = "";
|
|
89
|
+
let currentProfile = "";
|
|
80
90
|
|
|
81
91
|
// --- MCP Server ---
|
|
82
92
|
|
|
@@ -98,14 +108,15 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
98
108
|
tools: [
|
|
99
109
|
{
|
|
100
110
|
name: "polaris_connect",
|
|
101
|
-
description: "Connect this session to
|
|
111
|
+
description: "Connect this session to an existing Polaris project. Use create:true to create a new project.",
|
|
102
112
|
inputSchema: {
|
|
103
113
|
type: "object" as const,
|
|
104
114
|
properties: {
|
|
105
|
-
channel: { type: "string", description: "Project workspace to join (e.g., #my-project).
|
|
115
|
+
channel: { type: "string", description: "Project workspace to join (e.g., #my-project). Omit to list existing projects." },
|
|
106
116
|
user: { type: "string", description: "Your participant ID (e.g., user:manu)" },
|
|
107
117
|
session: { type: "string", description: "Session name (optional — auto-generated if omitted)" },
|
|
108
118
|
agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" },
|
|
119
|
+
create: { type: "boolean", description: "Create the project if it doesn't exist (default: false). Only set true when explicitly starting a new project." },
|
|
109
120
|
},
|
|
110
121
|
required: ["user"],
|
|
111
122
|
},
|
|
@@ -206,7 +217,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
206
217
|
const { name, arguments: args } = req.params;
|
|
207
218
|
|
|
208
219
|
if (name === "polaris_connect") {
|
|
209
|
-
const { channel, user, session, agent } = args as { channel?: string; user: string; session?: string; agent?: string };
|
|
220
|
+
const { channel, user, session, agent, create } = args as { channel?: string; user: string; session?: string; agent?: string; create?: boolean };
|
|
210
221
|
|
|
211
222
|
// If no channel specified, list available channels
|
|
212
223
|
if (!channel) {
|
|
@@ -215,7 +226,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
215
226
|
if (res.ok) {
|
|
216
227
|
const body = await res.json() as { channels: string[] };
|
|
217
228
|
if (body.channels.length === 0) {
|
|
218
|
-
return { content: [{ type: "text", text: "No projects found.
|
|
229
|
+
return { content: [{ type: "text", text: "No projects found. Create one with: `/polaris join #my-project` with create:true" }] };
|
|
219
230
|
}
|
|
220
231
|
return { content: [{ type: "text", text: `Available projects:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #project-name` }] };
|
|
221
232
|
}
|
|
@@ -224,20 +235,31 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
const project = channel.replace(/^#/, ""); // strip leading # if present
|
|
238
|
+
const activeProfile = await loadActiveProfile();
|
|
227
239
|
try {
|
|
228
240
|
const res = await daemonPost("/connect", {
|
|
229
241
|
ccSessionId: CC_SESSION_ID,
|
|
230
242
|
project,
|
|
231
243
|
user,
|
|
244
|
+
profile: activeProfile,
|
|
232
245
|
...(session ? { session } : {}),
|
|
233
246
|
...(agent ? { agent } : {}),
|
|
247
|
+
...(create ? { create: true } : {}),
|
|
234
248
|
});
|
|
235
|
-
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string };
|
|
249
|
+
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string; existing?: string[]; account?: string };
|
|
250
|
+
if (res.status === 404 && body.error === "project_not_found") {
|
|
251
|
+
const accountLine = body.account ? ` in ${body.account}` : "";
|
|
252
|
+
const list = body.existing && body.existing.length > 0
|
|
253
|
+
? `\n\nAvailable projects:\n${body.existing.map(p => ` #${p}`).join("\n")}`
|
|
254
|
+
: "\n\nNo projects exist yet in this account.";
|
|
255
|
+
return { content: [{ type: "text", text: `Project "#${project}" not found${accountLine}.${list}\n\nTo create it: polaris_connect with create:true` }] };
|
|
256
|
+
}
|
|
236
257
|
if (res.ok) {
|
|
237
258
|
currentProject = body.project ?? project;
|
|
238
259
|
currentSession = body.session ?? session ?? "";
|
|
239
260
|
currentUser = user;
|
|
240
|
-
|
|
261
|
+
currentProfile = activeProfile;
|
|
262
|
+
await saveSessionState(currentProject, currentSession, currentUser, currentProfile);
|
|
241
263
|
return { content: [{ type: "text", text: `Connected to #${currentProject}/${currentSession} as ${user}.` }] };
|
|
242
264
|
}
|
|
243
265
|
return { content: [{ type: "text", text: `Failed to connect: ${body.error ?? "unknown error"}` }] };
|
|
@@ -252,6 +274,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
252
274
|
currentProject = "";
|
|
253
275
|
currentSession = "";
|
|
254
276
|
currentUser = "";
|
|
277
|
+
currentProfile = "";
|
|
255
278
|
await clearSessionState();
|
|
256
279
|
return { content: [{ type: "text", text: "Disconnected from Polaris." }] };
|
|
257
280
|
} catch {
|
|
@@ -390,9 +413,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
390
413
|
// --- Register with daemon and connect stdio ---
|
|
391
414
|
|
|
392
415
|
async function main() {
|
|
416
|
+
const activeProfile = await loadActiveProfile();
|
|
417
|
+
|
|
393
418
|
// Register with daemon (best-effort — daemon might not be running yet)
|
|
394
419
|
try {
|
|
395
|
-
await daemonPost("/register", { ccSessionId: CC_SESSION_ID });
|
|
420
|
+
await daemonPost("/register", { ccSessionId: CC_SESSION_ID, profile: activeProfile });
|
|
396
421
|
} catch {
|
|
397
422
|
console.error("Warning: Polaris daemon not reachable. Start it with `bun run src/daemon/daemon.ts`.");
|
|
398
423
|
}
|
|
@@ -400,19 +425,22 @@ async function main() {
|
|
|
400
425
|
// Auto-reconnect if a previous session was active in this workspace
|
|
401
426
|
const saved = await loadSessionState();
|
|
402
427
|
if (saved) {
|
|
428
|
+
const reconnectProfile = saved.profile || activeProfile;
|
|
403
429
|
try {
|
|
404
430
|
const res = await daemonPost("/connect", {
|
|
405
431
|
ccSessionId: CC_SESSION_ID,
|
|
406
432
|
project: saved.project,
|
|
407
433
|
session: saved.session,
|
|
408
434
|
user: saved.user,
|
|
435
|
+
profile: reconnectProfile,
|
|
409
436
|
});
|
|
410
437
|
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; error?: string };
|
|
411
438
|
if (res.ok) {
|
|
412
439
|
currentProject = body.project ?? saved.project;
|
|
413
440
|
currentSession = body.session ?? saved.session;
|
|
414
441
|
currentUser = saved.user;
|
|
415
|
-
|
|
442
|
+
currentProfile = reconnectProfile;
|
|
443
|
+
console.error(`Polaris auto-reconnected to #${currentProject}/${currentSession} (${reconnectProfile || "default"})`);
|
|
416
444
|
} else {
|
|
417
445
|
// Session or project no longer exists — clear stale state
|
|
418
446
|
await clearSessionState();
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface SessionMapping {
|
|
|
11
11
|
session: string;
|
|
12
12
|
user: string;
|
|
13
13
|
agent: string;
|
|
14
|
+
profile: string; // which account profile this session is using
|
|
14
15
|
slackChannel?: string;
|
|
15
16
|
ws: WebSocket | null;
|
|
16
17
|
pendingMapping?: boolean; // true until the first hook event from this shell claims the mapping
|
|
@@ -46,49 +47,75 @@ async function loadConfig(): Promise<PolarisConfig | null> {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function
|
|
50
|
-
// 1. Env var override (Makefile
|
|
50
|
+
function getServiceUrlForProfile(profile: string): string {
|
|
51
|
+
// 1. Env var override (Makefile / tests)
|
|
51
52
|
if (process.env.POLARIS_SERVICE_URL) return process.env.POLARIS_SERVICE_URL;
|
|
52
|
-
// 2.
|
|
53
|
-
if (cachedConfig?.
|
|
53
|
+
// 2. Named profile
|
|
54
|
+
if (profile && cachedConfig?.profiles[profile]?.api) return cachedConfig.profiles[profile].api;
|
|
55
|
+
// 3. Active profile
|
|
56
|
+
if (cachedConfig?.active && cachedConfig.profiles[cachedConfig.active]?.api) {
|
|
54
57
|
return cachedConfig.profiles[cachedConfig.active].api;
|
|
55
58
|
}
|
|
56
|
-
// 3. Fallback
|
|
57
59
|
return "https://api.withpolaris.ai";
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
function getServiceUrl(): string {
|
|
63
|
+
return getServiceUrlForProfile(cachedConfig?.active ?? "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Per-profile token cache. Key "" is used for env-var / test mode.
|
|
67
|
+
const tokenCache = new Map<string, string | null>();
|
|
68
|
+
|
|
69
|
+
async function getTokenForProfile(profile: string): Promise<string | null> {
|
|
70
|
+
// Env var always wins (test mode) — ignore profile entirely
|
|
64
71
|
if (process.env.POLARIS_AUTH_TOKEN !== undefined) {
|
|
65
|
-
|
|
66
|
-
return cachedToken;
|
|
72
|
+
return process.env.POLARIS_AUTH_TOKEN || null;
|
|
67
73
|
}
|
|
68
|
-
|
|
74
|
+
if (tokenCache.has(profile)) return tokenCache.get(profile)!;
|
|
69
75
|
const config = await loadConfig();
|
|
76
|
+
// Named profile
|
|
77
|
+
if (profile && config?.profiles[profile]?.token) {
|
|
78
|
+
tokenCache.set(profile, config.profiles[profile].token);
|
|
79
|
+
return config.profiles[profile].token;
|
|
80
|
+
}
|
|
81
|
+
// Active profile fallback
|
|
70
82
|
if (config?.active && config.profiles[config.active]?.token) {
|
|
71
|
-
|
|
72
|
-
return
|
|
83
|
+
tokenCache.set(profile, config.profiles[config.active].token);
|
|
84
|
+
return config.profiles[config.active].token;
|
|
73
85
|
}
|
|
74
|
-
//
|
|
86
|
+
// Legacy credentials.json
|
|
75
87
|
try {
|
|
76
88
|
const credsPath = join(homedir(), ".polaris", "credentials.json");
|
|
77
89
|
const creds = JSON.parse(await readFile(credsPath, "utf-8"));
|
|
78
|
-
|
|
79
|
-
return
|
|
90
|
+
tokenCache.set(profile, creds.token ?? null);
|
|
91
|
+
return creds.token ?? null;
|
|
80
92
|
} catch {
|
|
81
|
-
|
|
93
|
+
tokenCache.set(profile, null);
|
|
82
94
|
return null;
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
function emailFromToken(token: string | null): string | null {
|
|
99
|
+
if (!token) return null;
|
|
100
|
+
try {
|
|
101
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
102
|
+
return payload.email ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function authHeadersForProfile(profile: string): Promise<Record<string, string>> {
|
|
109
|
+
const token = await getTokenForProfile(profile);
|
|
88
110
|
if (token) return { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
|
|
89
111
|
return { "Content-Type": "application/json" };
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
// Non-session calls (channel listing, team, etc.) use the active profile
|
|
115
|
+
async function authHeaders(): Promise<Record<string, string>> {
|
|
116
|
+
return authHeadersForProfile(cachedConfig?.active ?? "");
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
// --- Daemon shared-secret auth ---
|
|
93
120
|
//
|
|
94
121
|
// When a secret is resolved (env POLARIS_DAEMON_SECRET, else config.json
|
|
@@ -111,7 +138,7 @@ function getDaemonSecret(): string | null {
|
|
|
111
138
|
// --- Cloud WebSocket management ---
|
|
112
139
|
|
|
113
140
|
function connectCloudWs(mapping: SessionMapping) {
|
|
114
|
-
const serviceUrl =
|
|
141
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
115
142
|
const wsUrl = serviceUrl.replace(/^http/, "ws");
|
|
116
143
|
const ws = new WebSocket(`${wsUrl}/projects/${mapping.project}/sessions/${mapping.session}/ws`);
|
|
117
144
|
|
|
@@ -198,6 +225,7 @@ interface OutboxEntry {
|
|
|
198
225
|
t: string;
|
|
199
226
|
project: string;
|
|
200
227
|
session: string;
|
|
228
|
+
profile: string; // which account to use when flushing
|
|
201
229
|
body: unknown; // the exact { sender, payload } body for the events endpoint
|
|
202
230
|
}
|
|
203
231
|
|
|
@@ -212,10 +240,10 @@ function ensureOutboxDir(): Promise<void> {
|
|
|
212
240
|
return outboxReady;
|
|
213
241
|
}
|
|
214
242
|
|
|
215
|
-
async function enqueueOutbox(project: string, session: string, body: unknown): Promise<void> {
|
|
243
|
+
async function enqueueOutbox(project: string, session: string, profile: string, body: unknown): Promise<void> {
|
|
216
244
|
try {
|
|
217
245
|
await ensureOutboxDir();
|
|
218
|
-
const entry: OutboxEntry = { t: new Date().toISOString(), project, session, body };
|
|
246
|
+
const entry: OutboxEntry = { t: new Date().toISOString(), project, session, profile, body };
|
|
219
247
|
const file = join(OUTBOX_DIR, `${Date.now()}-${crypto.randomUUID().slice(0, 8)}.json`);
|
|
220
248
|
await writeFile(file, JSON.stringify(entry) + "\n");
|
|
221
249
|
pendingOutbox.add(file);
|
|
@@ -234,14 +262,15 @@ function scheduleOutboxFlush(): void {
|
|
|
234
262
|
}
|
|
235
263
|
|
|
236
264
|
async function flushOutbox(): Promise<void> {
|
|
237
|
-
const serviceUrl = getServiceUrl();
|
|
238
265
|
let hadFailure = false;
|
|
239
266
|
for (const file of Array.from(pendingOutbox)) {
|
|
240
267
|
try {
|
|
241
268
|
const entry = JSON.parse(await readFile(file, "utf-8")) as OutboxEntry;
|
|
269
|
+
const profile = entry.profile ?? ""; // legacy entries without profile use active
|
|
270
|
+
const serviceUrl = getServiceUrlForProfile(profile);
|
|
242
271
|
const res = await fetch(
|
|
243
272
|
`${serviceUrl}/projects/${entry.project}/sessions/${entry.session}/events`,
|
|
244
|
-
{ method: "POST", headers: await
|
|
273
|
+
{ method: "POST", headers: await authHeadersForProfile(profile), body: JSON.stringify(entry.body) }
|
|
245
274
|
);
|
|
246
275
|
if (res.ok || (res.status >= 400 && res.status < 500)) {
|
|
247
276
|
if (!res.ok) {
|
|
@@ -306,10 +335,10 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
306
335
|
} else {
|
|
307
336
|
// Auto-detect: query API for most recent event, backfill from there
|
|
308
337
|
try {
|
|
309
|
-
const serviceUrl =
|
|
338
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
310
339
|
const res = await fetch(
|
|
311
340
|
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/messages`,
|
|
312
|
-
{ headers: await
|
|
341
|
+
{ headers: await authHeadersForProfile(mapping.profile) }
|
|
313
342
|
);
|
|
314
343
|
if (res.ok) {
|
|
315
344
|
const events = (await res.json()) as Array<{ timestamp: string }>;
|
|
@@ -362,8 +391,8 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
362
391
|
|
|
363
392
|
// Replay events to the API
|
|
364
393
|
let recovered = 0;
|
|
365
|
-
const serviceUrl =
|
|
366
|
-
const headers = await
|
|
394
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
395
|
+
const headers = await authHeadersForProfile(mapping.profile);
|
|
367
396
|
|
|
368
397
|
for (const entry of logEntries) {
|
|
369
398
|
const hookEvent = entry.payload.hook_event_name as string | undefined;
|
|
@@ -449,16 +478,50 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
449
478
|
session?: string;
|
|
450
479
|
user: string;
|
|
451
480
|
agent?: string;
|
|
481
|
+
profile?: string;
|
|
482
|
+
create?: boolean;
|
|
452
483
|
};
|
|
453
484
|
await logEvent("/connect", body);
|
|
454
485
|
if (!body.ccSessionId || !body.project || !body.user) {
|
|
455
486
|
return error("ccSessionId, project, and user are required", 400);
|
|
456
487
|
}
|
|
457
488
|
|
|
489
|
+
// Resolve profile: explicit > active > ""
|
|
490
|
+
const profile = body.profile || cachedConfig?.active || "";
|
|
491
|
+
const serviceUrl = getServiceUrlForProfile(profile);
|
|
492
|
+
const headers = await authHeadersForProfile(profile);
|
|
493
|
+
|
|
458
494
|
// Generate session name if not provided
|
|
459
495
|
const sessionName = body.session || generateSessionName();
|
|
460
496
|
const agentId = body.agent || "agent:claude";
|
|
461
497
|
|
|
498
|
+
// Check whether the project exists. If not, refuse unless create: true.
|
|
499
|
+
const checkRes = await fetch(`${serviceUrl}/projects/${body.project}`, { headers });
|
|
500
|
+
if (!checkRes.ok) {
|
|
501
|
+
if (checkRes.status === 404) {
|
|
502
|
+
// Return existing project list so the caller can show a helpful error
|
|
503
|
+
const listRes = await fetch(`${serviceUrl}/projects`, { headers });
|
|
504
|
+
const projects: string[] = listRes.ok
|
|
505
|
+
? ((await listRes.json() as { name: string }[]).map((p) => p.name))
|
|
506
|
+
: [];
|
|
507
|
+
if (!body.create) {
|
|
508
|
+
const account = emailFromToken(await getTokenForProfile(profile));
|
|
509
|
+
return json({ error: "project_not_found", project: body.project, existing: projects, account }, 404);
|
|
510
|
+
}
|
|
511
|
+
// create: true — create the project now
|
|
512
|
+
const createRes = await fetch(`${serviceUrl}/projects`, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers,
|
|
515
|
+
body: JSON.stringify({ name: body.project }),
|
|
516
|
+
});
|
|
517
|
+
if (!createRes.ok && createRes.status !== 409) {
|
|
518
|
+
return error(`Failed to create project: ${await createRes.text()}`, 500);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
return error(`Failed to verify project: ${await checkRes.text()}`, 500);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
462
525
|
// Disconnect existing cloud WS if switching sessions
|
|
463
526
|
disconnectCloudWs(body.ccSessionId);
|
|
464
527
|
|
|
@@ -468,19 +531,12 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
468
531
|
session: sessionName,
|
|
469
532
|
user: body.user,
|
|
470
533
|
agent: agentId,
|
|
534
|
+
profile,
|
|
471
535
|
ws: null,
|
|
472
536
|
pendingMapping: true, // waiting for hook event to map the real CC session ID
|
|
473
537
|
};
|
|
474
538
|
sessions.set(body.ccSessionId, mapping);
|
|
475
539
|
|
|
476
|
-
// Ensure the project exists on the cloud service (create if not)
|
|
477
|
-
const serviceUrl = getServiceUrl();
|
|
478
|
-
await fetch(`${serviceUrl}/projects`, {
|
|
479
|
-
method: "POST",
|
|
480
|
-
headers: await authHeaders(),
|
|
481
|
-
body: JSON.stringify({ name: body.project }),
|
|
482
|
-
}); // Ignore 409 (already exists)
|
|
483
|
-
|
|
484
540
|
// Ensure the session exists (create if not, claim driver)
|
|
485
541
|
// Retry with new name on 409 (collision with generated name)
|
|
486
542
|
let attempts = 0;
|
|
@@ -488,7 +544,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
488
544
|
while (!created && attempts < 3) {
|
|
489
545
|
const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
|
|
490
546
|
method: "POST",
|
|
491
|
-
headers
|
|
547
|
+
headers,
|
|
492
548
|
body: JSON.stringify({ name: mapping.session, driver: body.user }),
|
|
493
549
|
});
|
|
494
550
|
if (sessionRes.ok) {
|
|
@@ -498,7 +554,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
498
554
|
// Explicit session name — claim driver instead of retrying
|
|
499
555
|
await fetch(`${serviceUrl}/projects/${body.project}/sessions/${mapping.session}/driver`, {
|
|
500
556
|
method: "POST",
|
|
501
|
-
headers
|
|
557
|
+
headers,
|
|
502
558
|
body: JSON.stringify({ driver: body.user }),
|
|
503
559
|
});
|
|
504
560
|
created = true;
|
|
@@ -519,9 +575,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
519
575
|
|
|
520
576
|
// Fetch Slack channel name for status display
|
|
521
577
|
try {
|
|
522
|
-
const projRes = await fetch(`${serviceUrl}/projects/${body.project}`, {
|
|
523
|
-
headers: await authHeaders(),
|
|
524
|
-
});
|
|
578
|
+
const projRes = await fetch(`${serviceUrl}/projects/${body.project}`, { headers });
|
|
525
579
|
if (projRes.ok) {
|
|
526
580
|
const projData = await projRes.json() as { slack_channel_name?: string };
|
|
527
581
|
mapping.slackChannel = projData.slack_channel_name ?? undefined;
|
|
@@ -630,7 +684,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
630
684
|
|
|
631
685
|
// Relay to cloud service; on network failure or upstream 5xx,
|
|
632
686
|
// persist to the write-ahead outbox instead of dropping
|
|
633
|
-
const serviceUrl =
|
|
687
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
634
688
|
const relayBody = { sender, payload: body };
|
|
635
689
|
let res: Response | null = null;
|
|
636
690
|
try {
|
|
@@ -638,7 +692,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
638
692
|
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
639
693
|
{
|
|
640
694
|
method: "POST",
|
|
641
|
-
headers: await
|
|
695
|
+
headers: await authHeadersForProfile(mapping.profile),
|
|
642
696
|
body: JSON.stringify(relayBody),
|
|
643
697
|
}
|
|
644
698
|
);
|
|
@@ -649,7 +703,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
649
703
|
if (!res || res.status >= 500) {
|
|
650
704
|
console.error(`polaris daemon: upstream relay failed (${res ? res.status : "network error"}) — queued to outbox`);
|
|
651
705
|
await logEvent("/events", body, { status: res?.status ?? 0, body: "queued to outbox" });
|
|
652
|
-
await enqueueOutbox(mapping.project, mapping.session, relayBody);
|
|
706
|
+
await enqueueOutbox(mapping.project, mapping.session, mapping.profile, relayBody);
|
|
653
707
|
// The event is durably accepted (outbox), so still drain injects
|
|
654
708
|
if (hookEvent === "UserPromptSubmit") {
|
|
655
709
|
const queue = injectQueues.get(mapping.ccSessionId);
|
|
@@ -684,11 +738,13 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
684
738
|
const body = (await req.json()) as { oldName: string; newName: string };
|
|
685
739
|
if (!body.oldName || !body.newName) return error("oldName and newName required", 400);
|
|
686
740
|
|
|
687
|
-
//
|
|
688
|
-
const
|
|
741
|
+
// Find the profile for the session connected to this project
|
|
742
|
+
const renamingSession = Array.from(sessions.values()).find((m) => m.project === body.oldName);
|
|
743
|
+
const renameProfile = renamingSession?.profile ?? cachedConfig?.active ?? "";
|
|
744
|
+
const serviceUrl = getServiceUrlForProfile(renameProfile);
|
|
689
745
|
const res = await fetch(`${serviceUrl}/projects/${body.oldName}/rename`, {
|
|
690
746
|
method: "POST",
|
|
691
|
-
headers: await
|
|
747
|
+
headers: await authHeadersForProfile(renameProfile),
|
|
692
748
|
body: JSON.stringify({ name: body.newName }),
|
|
693
749
|
});
|
|
694
750
|
if (!res.ok) {
|
|
@@ -750,6 +806,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
750
806
|
session: mapping.session,
|
|
751
807
|
user: mapping.user,
|
|
752
808
|
slackChannel,
|
|
809
|
+
account: emailFromToken(await getTokenForProfile(mapping.profile)),
|
|
753
810
|
});
|
|
754
811
|
}
|
|
755
812
|
|
|
@@ -775,7 +832,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
775
832
|
const mapping = sessions.get(body.ccSessionId);
|
|
776
833
|
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
777
834
|
|
|
778
|
-
const serviceUrl =
|
|
835
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
779
836
|
const relayBody = {
|
|
780
837
|
// Replies come from the agent, not the human driver
|
|
781
838
|
sender: mapping.agent,
|
|
@@ -791,7 +848,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
791
848
|
`${serviceUrl}/projects/${mapping.project}/sessions/${mapping.session}/events`,
|
|
792
849
|
{
|
|
793
850
|
method: "POST",
|
|
794
|
-
headers: await
|
|
851
|
+
headers: await authHeadersForProfile(mapping.profile),
|
|
795
852
|
body: JSON.stringify(relayBody),
|
|
796
853
|
}
|
|
797
854
|
);
|
|
@@ -801,7 +858,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
801
858
|
if (!res || res.status >= 500) {
|
|
802
859
|
console.error(`polaris daemon: upstream reply relay failed (${res ? res.status : "network error"}) — queued to outbox`);
|
|
803
860
|
await logEvent("/reply", body, { status: res?.status ?? 0, body: "queued to outbox" });
|
|
804
|
-
await enqueueOutbox(mapping.project, mapping.session, relayBody);
|
|
861
|
+
await enqueueOutbox(mapping.project, mapping.session, mapping.profile, relayBody);
|
|
805
862
|
return json({ status: "queued" });
|
|
806
863
|
}
|
|
807
864
|
if (!res.ok) {
|
|
@@ -823,10 +880,10 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
823
880
|
const mapping = sessions.get(ccSessionId);
|
|
824
881
|
if (!mapping || !mapping.project) return error("Not connected", 400);
|
|
825
882
|
|
|
826
|
-
const serviceUrl =
|
|
883
|
+
const serviceUrl = getServiceUrlForProfile(mapping.profile);
|
|
827
884
|
const res = await fetch(
|
|
828
885
|
`${serviceUrl}/projects/${mapping.project}/sessions/${targetSession}/messages`,
|
|
829
|
-
{ headers: await
|
|
886
|
+
{ headers: await authHeadersForProfile(mapping.profile) }
|
|
830
887
|
);
|
|
831
888
|
if (!res.ok) {
|
|
832
889
|
const err = await res.text();
|
package/tests/client.test.ts
CHANGED
|
@@ -53,6 +53,7 @@ describe("end-to-end: daemon + cloud service", () => {
|
|
|
53
53
|
project: "pj",
|
|
54
54
|
session: "fxm",
|
|
55
55
|
user: "user:manu",
|
|
56
|
+
create: true,
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
await post(daemonUrl, "/events", {
|
|
@@ -74,6 +75,7 @@ describe("end-to-end: daemon + cloud service", () => {
|
|
|
74
75
|
project: "pj",
|
|
75
76
|
session: "fxm",
|
|
76
77
|
user: "user:manu",
|
|
78
|
+
create: true,
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
const wsUrl = serviceUrl.replace("http", "ws");
|
|
@@ -99,12 +101,14 @@ describe("end-to-end: daemon + cloud service", () => {
|
|
|
99
101
|
project: "pj",
|
|
100
102
|
session: "fxm",
|
|
101
103
|
user: "user:manu",
|
|
104
|
+
create: true,
|
|
102
105
|
});
|
|
103
106
|
await post(daemonUrl, "/connect", {
|
|
104
107
|
ccSessionId: "multi-2",
|
|
105
108
|
project: "pj",
|
|
106
109
|
session: "fxk",
|
|
107
110
|
user: "user:krishna",
|
|
111
|
+
create: true,
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
await post(daemonUrl, "/events", {
|
|
@@ -139,6 +143,7 @@ describe("capture.sh with daemon", () => {
|
|
|
139
143
|
project: "pj",
|
|
140
144
|
session: "fxm",
|
|
141
145
|
user: "user:manu",
|
|
146
|
+
create: true,
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
const hookPayload = JSON.stringify({
|
package/tests/daemon.test.ts
CHANGED
|
@@ -69,6 +69,7 @@ describe("daemon /connect", () => {
|
|
|
69
69
|
project: "pj",
|
|
70
70
|
session: "fxm",
|
|
71
71
|
user: "user:manu",
|
|
72
|
+
create: true,
|
|
72
73
|
});
|
|
73
74
|
expect(res.status).toBe(200);
|
|
74
75
|
const body = await res.json();
|
|
@@ -96,6 +97,7 @@ describe("daemon /connect", () => {
|
|
|
96
97
|
project: "pj",
|
|
97
98
|
session: "fxm",
|
|
98
99
|
user: "user:manu",
|
|
100
|
+
create: true,
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
const res = await post(daemonUrl, "/connect", {
|
|
@@ -103,6 +105,7 @@ describe("daemon /connect", () => {
|
|
|
103
105
|
project: "pj",
|
|
104
106
|
session: "fxk",
|
|
105
107
|
user: "user:manu",
|
|
108
|
+
create: true,
|
|
106
109
|
});
|
|
107
110
|
expect(res.status).toBe(200);
|
|
108
111
|
|
|
@@ -119,6 +122,7 @@ describe("daemon /disconnect", () => {
|
|
|
119
122
|
project: "pj",
|
|
120
123
|
session: "fxm",
|
|
121
124
|
user: "user:manu",
|
|
125
|
+
create: true,
|
|
122
126
|
});
|
|
123
127
|
|
|
124
128
|
const res = await post(daemonUrl, "/disconnect", { ccSessionId: "cc-5" });
|
|
@@ -137,6 +141,7 @@ describe("daemon /events (hook relay)", () => {
|
|
|
137
141
|
project: "pj",
|
|
138
142
|
session: "fxm",
|
|
139
143
|
user: "user:manu",
|
|
144
|
+
create: true,
|
|
140
145
|
});
|
|
141
146
|
|
|
142
147
|
const res = await post(daemonUrl, "/events", {
|
|
@@ -173,6 +178,7 @@ describe("daemon /status", () => {
|
|
|
173
178
|
project: "pj",
|
|
174
179
|
session: "fxm",
|
|
175
180
|
user: "user:manu",
|
|
181
|
+
create: true,
|
|
176
182
|
});
|
|
177
183
|
|
|
178
184
|
const res = await get(daemonUrl, "/status/cc-7");
|
|
@@ -195,12 +201,14 @@ describe("daemon /status", () => {
|
|
|
195
201
|
project: "pj",
|
|
196
202
|
session: "fxm",
|
|
197
203
|
user: "user:manu",
|
|
204
|
+
create: true,
|
|
198
205
|
});
|
|
199
206
|
await post(daemonUrl, "/connect", {
|
|
200
207
|
ccSessionId: "cc-8b",
|
|
201
208
|
project: "pj",
|
|
202
209
|
session: "fxk",
|
|
203
210
|
user: "user:krishna",
|
|
211
|
+
create: true,
|
|
204
212
|
});
|
|
205
213
|
|
|
206
214
|
const res = await get(daemonUrl, "/status");
|
package/tests/e2e.test.ts
CHANGED
|
@@ -56,6 +56,7 @@ describe("e2e: two drivers on same project", () => {
|
|
|
56
56
|
project: "pj",
|
|
57
57
|
session: "fxm",
|
|
58
58
|
user: "user:manu",
|
|
59
|
+
create: true,
|
|
59
60
|
});
|
|
60
61
|
expect(manuRes.status).toBe(200);
|
|
61
62
|
|
|
@@ -65,6 +66,7 @@ describe("e2e: two drivers on same project", () => {
|
|
|
65
66
|
project: "pj",
|
|
66
67
|
session: "fxk",
|
|
67
68
|
user: "user:krishna",
|
|
69
|
+
create: true,
|
|
68
70
|
});
|
|
69
71
|
expect(krishnaRes.status).toBe(200);
|
|
70
72
|
|
|
@@ -108,12 +110,14 @@ describe("e2e: advisor injection", () => {
|
|
|
108
110
|
project: "pj",
|
|
109
111
|
session: "fxm",
|
|
110
112
|
user: "user:manu",
|
|
113
|
+
create: true,
|
|
111
114
|
});
|
|
112
115
|
await post(daemonUrl, "/connect", {
|
|
113
116
|
ccSessionId: "cc-krishna",
|
|
114
117
|
project: "pj",
|
|
115
118
|
session: "fxk",
|
|
116
119
|
user: "user:krishna",
|
|
120
|
+
create: true,
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
// Priya advises fxk only
|
|
@@ -141,6 +145,7 @@ describe("e2e: advisor injection", () => {
|
|
|
141
145
|
project: "pj",
|
|
142
146
|
session: "fxm",
|
|
143
147
|
user: "user:manu",
|
|
148
|
+
create: true,
|
|
144
149
|
});
|
|
145
150
|
|
|
146
151
|
// Subscribe to session-level WS
|
|
@@ -171,12 +176,14 @@ describe("e2e: project-level WebSocket", () => {
|
|
|
171
176
|
project: "pj",
|
|
172
177
|
session: "fxm",
|
|
173
178
|
user: "user:manu",
|
|
179
|
+
create: true,
|
|
174
180
|
});
|
|
175
181
|
await post(daemonUrl, "/connect", {
|
|
176
182
|
ccSessionId: "cc-proj-b",
|
|
177
183
|
project: "pj",
|
|
178
184
|
session: "fxk",
|
|
179
185
|
user: "user:krishna",
|
|
186
|
+
create: true,
|
|
180
187
|
});
|
|
181
188
|
|
|
182
189
|
// Subscribe to project-level WS
|
|
@@ -213,6 +220,7 @@ describe("e2e: handoff", () => {
|
|
|
213
220
|
project: "pj",
|
|
214
221
|
session: "fxm",
|
|
215
222
|
user: "user:manu",
|
|
223
|
+
create: true,
|
|
216
224
|
});
|
|
217
225
|
|
|
218
226
|
// Manu works
|
|
@@ -270,6 +278,7 @@ describe("e2e: status line", () => {
|
|
|
270
278
|
project: "pj",
|
|
271
279
|
session: "fxm",
|
|
272
280
|
user: "user:manu",
|
|
281
|
+
create: true,
|
|
273
282
|
});
|
|
274
283
|
|
|
275
284
|
const connRes = await get(daemonUrl, "/status/cc-status-1");
|
|
@@ -290,6 +299,7 @@ describe("e2e: status line", () => {
|
|
|
290
299
|
project: "pj",
|
|
291
300
|
session: "fxm",
|
|
292
301
|
user: "user:manu",
|
|
302
|
+
create: true,
|
|
293
303
|
});
|
|
294
304
|
|
|
295
305
|
await post(daemonUrl, "/disconnect", { ccSessionId: "cc-status-2" });
|
|
@@ -307,6 +317,7 @@ describe("e2e: capture.sh through daemon", () => {
|
|
|
307
317
|
project: "pj",
|
|
308
318
|
session: "fxm",
|
|
309
319
|
user: "user:manu",
|
|
320
|
+
create: true,
|
|
310
321
|
});
|
|
311
322
|
|
|
312
323
|
const hookPayload = JSON.stringify({
|
|
@@ -365,6 +376,7 @@ describe("e2e: session switching", () => {
|
|
|
365
376
|
project: "pj",
|
|
366
377
|
session: "fxm",
|
|
367
378
|
user: "user:manu",
|
|
379
|
+
create: true,
|
|
368
380
|
});
|
|
369
381
|
|
|
370
382
|
await post(daemonUrl, "/events", {
|
|
@@ -63,6 +63,7 @@ describe("daemon /reply attribution", () => {
|
|
|
63
63
|
session: "fx-reply",
|
|
64
64
|
user: "user:manu",
|
|
65
65
|
agent: "agent:replybot",
|
|
66
|
+
create: true,
|
|
66
67
|
});
|
|
67
68
|
expect(connectRes.status).toBe(200);
|
|
68
69
|
|
|
@@ -86,6 +87,7 @@ describe("daemon /reply attribution", () => {
|
|
|
86
87
|
project: "pj-reply",
|
|
87
88
|
session: "fx-reply-default",
|
|
88
89
|
user: "user:krishna",
|
|
90
|
+
create: true,
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
const replyRes = await post(daemonUrl, "/reply", {
|