@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 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
- # Manage environments
117
- polaris profiles # List all profiles
118
- polaris use local # Switch to local dev
119
- polaris use prod # Switch to production
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 profile, daemon state, sessions
124
- polaris logout # Remove active profile credentials
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`:
@@ -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
- # Read active profile name from ~/.polaris/config.json (best-effort)
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: ${PROFILE_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}"
38
+ echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}"
43
39
  else
44
- echo "polaris: ${PROFILE_TAG}${PROJECT}/${SESSION} (${USER})"
40
+ echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER})"
45
41
  fi
46
42
  else
47
- echo "polaris: ${PROFILE_TAG}not connected"
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.92",
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",
@@ -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 channel:
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. A session name is auto-generated
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 name
10
- // polaris use <profile> — switch active profile
11
- // polaris profiles — list profiles
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
- const derivedName = profileName ?? deriveProfileName(appUrl);
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
- const authUrl = `${appUrl}/auth/cli?port=${callbackPort}`;
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 (${derivedName})...`);
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
- config.profiles[derivedName] = {
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 = derivedName;
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(` ✓ Profile "${derivedName}" saved and set as active`);
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
- async function use(profileName: string) {
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
- if (!config.profiles[profileName]) {
440
- console.error(`Profile "${profileName}" not found.`);
441
- const names = Object.keys(config.profiles);
442
- if (names.length > 0) {
443
- console.error(`Available profiles: ${names.join(", ")}`);
444
- } else {
445
- console.error("No profiles configured. Run: polaris login");
446
- }
447
- process.exit(1);
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
- // Update legacy credentials.json for daemon
453
- const profile = config.profiles[profileName];
454
- await writeFile(LEGACY_CREDENTIALS_FILE, JSON.stringify({
455
- token: profile.token,
456
- email: profile.email,
457
- name: profile.name,
458
- org_id: profile.org_id,
459
- participant_id: profile.participant_id,
460
- service_url: profile.app,
461
- }, null, 2));
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
- console.log(`Active profile: ${profileName} (${profile.api})`);
464
- console.log("Restart the daemon for this to take effect.");
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 names = Object.keys(config.profiles);
472
- if (names.length === 0) {
473
- console.log("No profiles configured. Run: polaris login");
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("Profiles:\n");
477
- for (const name of names) {
478
- const p = config.profiles[name];
479
- const active = name === config.active ? " (active)" : "";
480
- console.log(` ${name}${active}`);
481
- console.log(` API: ${p.api}`);
482
- console.log(` User: ${p.name} (${p.email})`);
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 <profile>");
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 <profile> — switch active profile");
912
- console.log(" polaris profiles — list all profiles");
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");
@@ -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 a Polaris project and session. Creates the session if it doesn't exist.",
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). Becomes the Slack channel name if a floor is connected. Omit to list existing projects." },
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. Start one with: `/polaris join #my-project`" }] };
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
- await saveSessionState(currentProject, currentSession, currentUser);
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
- console.error(`Polaris auto-reconnected to #${currentProject}/${currentSession}`);
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();
@@ -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 getServiceUrl(): string {
50
- // 1. Env var override (Makefile uses this for local dev)
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. Active profile (read synchronously from cache — loaded at startup)
53
- if (cachedConfig?.active && cachedConfig.profiles[cachedConfig.active]) {
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
- let cachedToken: string | null | undefined = undefined;
61
- async function getAuthToken(): Promise<string | null> {
62
- if (cachedToken !== undefined) return cachedToken;
63
- // 1. Env var (for testing). Empty string means "no auth".
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
- cachedToken = process.env.POLARIS_AUTH_TOKEN || null;
66
- return cachedToken;
72
+ return process.env.POLARIS_AUTH_TOKEN || null;
67
73
  }
68
- // 2. Active profile in config.json
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
- cachedToken = config.profiles[config.active].token;
72
- return cachedToken;
83
+ tokenCache.set(profile, config.profiles[config.active].token);
84
+ return config.profiles[config.active].token;
73
85
  }
74
- // 3. Legacy credentials.json
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
- cachedToken = creds.token ?? null;
79
- return cachedToken;
90
+ tokenCache.set(profile, creds.token ?? null);
91
+ return creds.token ?? null;
80
92
  } catch {
81
- cachedToken = null;
93
+ tokenCache.set(profile, null);
82
94
  return null;
83
95
  }
84
96
  }
85
97
 
86
- async function authHeaders(): Promise<Record<string, string>> {
87
- const token = await getAuthToken();
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 = getServiceUrl();
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 authHeaders(), body: JSON.stringify(entry.body) }
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 = getServiceUrl();
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 authHeaders() }
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 = getServiceUrl();
366
- const headers = await authHeaders();
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: await authHeaders(),
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: await authHeaders(),
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 = getServiceUrl();
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 authHeaders(),
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
- // Call cloud API to rename in DB
688
- const serviceUrl = getServiceUrl();
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 authHeaders(),
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 = getServiceUrl();
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 authHeaders(),
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 = getServiceUrl();
883
+ const serviceUrl = getServiceUrlForProfile(mapping.profile);
827
884
  const res = await fetch(
828
885
  `${serviceUrl}/projects/${mapping.project}/sessions/${targetSession}/messages`,
829
- { headers: await authHeaders() }
886
+ { headers: await authHeadersForProfile(mapping.profile) }
830
887
  );
831
888
  if (!res.ok) {
832
889
  const err = await res.text();
@@ -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({
@@ -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", {