@lasso-ai/cli 1.2.1 → 1.2.3

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/CHANGELOG.md CHANGED
@@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - **One-shot mode as sidebar panel**: Changed one-shot mode from a centered modal to a right-side sidebar panel that only appears during one-shot mode. When users exit one-shot mode, they return to the normal edit mode with just the toolbar. This provides a cleaner, more integrated experience similar to modern AI editors.
13
+ - **Mini toolbar positioning**: Adjusted the mini toolbar position to `left: calc(50% + 140px)` (reduced from 280px) to reduce overlap with the main toolbar. Added smooth transition and centered positioning when the main toolbar is dismissed. Mini toolbar is now hidden when one-shot sidebar is open to avoid UI clutter.
14
+
10
15
  ### Added
11
16
 
17
+ - **Multiple access points for one-shot mode**: Added a Command button to the toolbar (⌘ icon) for opening the one-shot sidebar, in addition to the existing ⌘K keyboard shortcut. The sidebar includes a model selector dropdown allowing users to choose their AI model before submitting a one-shot request.
18
+ - **Model selector in command sidebar**: The one-shot sidebar now displays the currently selected model with a dropdown to switch between available models. The selector is organized by provider (Anthropic, OpenAI, Google, Local Agents) for easy navigation.
19
+ - **Cursor CLI support**: Added Cursor as a local agent option alongside Claude Code, Codex, and OpenCode. Users can now select Cursor models in the model dropdown if Cursor is installed on their system. Cursor uses similar output parsing to Claude Code with progress tracking and error handling.
20
+ - **Model locking based on API key configuration**: Models are now locked with a lock icon in the dropdown when the user hasn't configured the provider's API key in their dashboard. Free plan users can only use models from providers with configured keys. Paid plan users have all models unlocked. CLI agents (Claude Code, Codex, OpenCode, Cursor) are always unlocked since users manage their own credentials.
12
21
  - **One-shot mode with global command bar**: Press ⌘K to open a command interface
13
22
  anywhere in your app. Describe what you want to build (e.g., "Build a complete billing
14
23
  system with invoices and payment tracking") and Lasso will plan, execute, and
package/README.md CHANGED
@@ -200,7 +200,7 @@ Use:
200
200
 
201
201
  Your selection and project context stay inside the Lasso pipeline while the actual coding agent can be swapped independently.
202
202
 
203
- When installed, Lasso automatically detects the `claude`, `codex`, and
203
+ When installed, Lasso automatically detects the `claude`, `codex`, `cursor`, and
204
204
  `opencode` commands on your PATH and adds the available local agents to the
205
205
  prompt model menu. Local agents run in read-only/planning mode and return a
206
206
  reviewable proposal; Lasso remains the only process that writes accepted
@@ -444,9 +444,10 @@ sees**, so install it before starting the dev server (or restart afterwards):
444
444
  | ------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------------- |
445
445
  | [Claude Code](https://lasso.byorello.space/docs/claude-code) | `claude` | `claude -p … --permission-mode plan --output-format stream-json --max-turns 3` |
446
446
  | [Codex](https://lasso.byorello.space/docs/codex) | `codex` | `codex exec --json --sandbox read-only --skip-git-repo-check` |
447
+ | [Cursor](https://lasso.byorello.space/docs/cursor) | `cursor` | `cursor agent run --non-interactive --model "<model>"` |
447
448
  | [OpenCode](https://lasso.byorello.space/docs/opencode) | `opencode` | `opencode run --format json --print-logs --agent plan` |
448
449
 
449
- All three run read-only, so the agent proposes a patch and never writes to your project.
450
+ All four run read-only, so the agent proposes a patch and never writes to your project.
450
451
  Sign each CLI in once in a real terminal; Lasso inherits the credentials it stores for
451
452
  your user.
452
453
 
@@ -39,12 +39,12 @@ type AgentInput = {
39
39
  };
40
40
  };
41
41
  export type AgentConfig = {
42
- provider: "anthropic" | "openai" | "google" | "ollama" | "claude-code" | "codex" | "opencode";
42
+ provider: "anthropic" | "openai" | "google" | "ollama" | "claude-code" | "codex" | "opencode" | "cursor";
43
43
  apiKey?: string;
44
44
  model?: string;
45
45
  baseUrl?: string;
46
46
  };
47
- export type LocalAgent = "claude-code" | "codex" | "opencode";
47
+ export type LocalAgent = "claude-code" | "codex" | "opencode" | "cursor";
48
48
  export type AgentProgressLevel = "working" | "error";
49
49
  export type AgentProgress = (message: string, detail?: string, level?: AgentProgressLevel) => void;
50
50
  export type AgentPrompt = {
package/dist/cli/agent.js CHANGED
@@ -23,7 +23,7 @@ function respondToAgentPrompt(taskId, value) {
23
23
  }
24
24
  async function detectLocalAgents() {
25
25
  const found = new Set();
26
- for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"]]) {
26
+ for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"], ["cursor", "cursor"]]) {
27
27
  try {
28
28
  await execFileAsync(process.platform === "win32" ? "where.exe" : "which", [command]);
29
29
  found.add(name);
@@ -229,6 +229,33 @@ function extractLocalAgentText(raw, provider) {
229
229
  }
230
230
  return results.at(-1) || raw;
231
231
  }
232
+ if (provider === "cursor") {
233
+ // Cursor uses similar output format to claude-code
234
+ try {
235
+ const payload = JSON.parse(raw);
236
+ if (payload.result)
237
+ return payload.result;
238
+ }
239
+ catch {
240
+ // Stream-json output is handled below.
241
+ }
242
+ const results = [];
243
+ for (const line of raw.split(/\r?\n/)) {
244
+ try {
245
+ const event = JSON.parse(line);
246
+ if (typeof event.result === "string")
247
+ results.push(event.result);
248
+ const message = event.message;
249
+ for (const part of message?.content || [])
250
+ if (part.type === "text" && part.text)
251
+ results.push(part.text);
252
+ }
253
+ catch {
254
+ // Ignore progress lines that are not JSON.
255
+ }
256
+ }
257
+ return results.at(-1) || raw;
258
+ }
232
259
  const texts = [];
233
260
  for (const line of raw.split(/\r?\n/)) {
234
261
  try {
@@ -294,7 +321,7 @@ function extractLocalAgentProposal(raw, provider) {
294
321
  const event = JSON.parse(line);
295
322
  const item = event.item;
296
323
  const part = event.part;
297
- const values = provider === "claude-code"
324
+ const values = provider === "claude-code" || provider === "cursor"
298
325
  ? [event.result, ...(event.message?.content || []).map((entry) => entry.text)]
299
326
  : provider === "opencode"
300
327
  ? [...collectTextFields(event), part?.type === "text" ? part.text : undefined]
@@ -335,7 +362,7 @@ function extractLocalAgentProposal(raw, provider) {
335
362
  // Keep looking; this output may only be a progress or tool event.
336
363
  }
337
364
  }
338
- throw new Error(`${provider === "opencode" ? "OpenCode" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
365
+ throw new Error(`${provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
339
366
  }
340
367
  function snippet(value, max = 80) {
341
368
  const s = String(value ?? "").trim().replace(/\s+/g, " ");
@@ -413,9 +440,10 @@ function progressFromLine(raw, provider) {
413
440
  try {
414
441
  const event = JSON.parse(raw);
415
442
  const item = event.item;
416
- if (provider === "claude-code") {
443
+ if (provider === "claude-code" || provider === "cursor") {
444
+ const agentName = provider === "cursor" ? "Cursor" : "Claude Code";
417
445
  if (event.type === "system")
418
- return progressEvent("Claude Code connected");
446
+ return progressEvent(`${agentName} connected`);
419
447
  // tool_use blocks inside assistant messages
420
448
  const toolBlock = event.message?.content?.find?.((p) => p.type === "tool_use");
421
449
  if (toolBlock) {
@@ -423,13 +451,13 @@ function progressFromLine(raw, provider) {
423
451
  const inp = toolBlock.input;
424
452
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
425
453
  return progressEvent(detail
426
- ? `Claude Code · ${toolName} ${snippet(detail)}`
427
- : `Claude Code · ${toolName}`, detail);
454
+ ? `${agentName} · ${toolName} ${snippet(detail)}`
455
+ : `${agentName} · ${toolName}`, detail);
428
456
  }
429
457
  // thinking blocks inside assistant messages
430
458
  const thinkBlock = event.message?.content?.find?.((p) => p.type === "thinking");
431
459
  if (thinkBlock?.thinking) {
432
- return progressEvent(`Claude Code · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
460
+ return progressEvent(`${agentName} · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
433
461
  }
434
462
  // top-level tool event fields (stream-json verbose format)
435
463
  const tool = event.tool_name || event.name || event.tool?.name;
@@ -437,14 +465,14 @@ function progressFromLine(raw, provider) {
437
465
  const inp = event.tool_input;
438
466
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
439
467
  return progressEvent(detail
440
- ? `Claude Code · ${tool} ${snippet(detail)}`
441
- : `Claude Code · ${tool}`, detail);
468
+ ? `${agentName} · ${tool} ${snippet(detail)}`
469
+ : `${agentName} · ${tool}`, detail);
442
470
  }
443
471
  if (event.type === "result" || event.result) {
444
- return progressEvent("Claude Code · preparing the proposal");
472
+ return progressEvent(`${agentName} · preparing the proposal`);
445
473
  }
446
474
  if (event.type === "assistant")
447
- return progressEvent("Claude Code · reasoning about the change");
475
+ return progressEvent(`${agentName} · reasoning about the change`);
448
476
  }
449
477
  else if (provider === "codex") {
450
478
  const type = item?.type || event.type || "";
@@ -514,10 +542,13 @@ function localAgentError(command, stderr, exitCode) {
514
542
  const output = stderr.trim();
515
543
  const hint = agentFailureHint(output);
516
544
  if (hint)
517
- return `${command === "opencode" ? "OpenCode" : command} could not finish: ${hint}`;
545
+ return `${command === "opencode" ? "OpenCode" : command === "cursor" ? "Cursor" : command} could not finish: ${hint}`;
518
546
  if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
519
547
  return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
520
548
  }
549
+ if (command === "cursor" && /waiting for permission or authentication/i.test(output)) {
550
+ return "Cursor needs permission or authentication. Run Cursor once in a terminal, approve the requested access or sign in, then retry in Lasso.";
551
+ }
521
552
  if (command === "codex" && output.includes("missing field `base_instructions`")) {
522
553
  return "Codex could not read its models cache because it uses an older cache format. Update Codex, then retry. Lasso already bypasses the repository trust check.";
523
554
  }
@@ -537,6 +568,9 @@ function localCommand(provider, model, prompt) {
537
568
  if (provider === "claude-code") {
538
569
  return { command: "claude", args: ["-p", prompt || "", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan", "--max-turns", "3", ...(selectedModel ? ["--model", selectedModel] : [])] };
539
570
  }
571
+ if (provider === "cursor") {
572
+ return { command: "cursor", args: ["agent", "run", "--non-interactive", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
573
+ }
540
574
  if (provider === "opencode") {
541
575
  return { command: "opencode", args: ["run", "--format", "json", "--print-logs", "--log-level", "INFO", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
542
576
  }
@@ -545,7 +579,7 @@ function localCommand(provider, model, prompt) {
545
579
  function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
546
580
  return new Promise((resolve, reject) => {
547
581
  const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
548
- const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
582
+ const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : provider === "cursor" ? "Cursor" : "Claude Code";
549
583
  let stdout = "";
550
584
  let stderr = "";
551
585
  let pendingStdout = "";
@@ -653,6 +687,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
653
687
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
654
688
  onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
655
689
  }
690
+ if (provider === "cursor" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
691
+ onProgress?.("Cursor · provider rejected the request; stopping this task…", undefined, "error");
692
+ }
656
693
  }
657
694
  resolve({ stdout, stderr, exitCode: code ?? 1 });
658
695
  });
@@ -678,7 +715,7 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
678
715
  ? `\n\nDRAG REPOSITIONING TASK:\nThe user dragged this element by dx: ${input.context.drag.delta?.dx ?? 0}px, dy: ${input.context.drag.delta?.dy ?? 0}px to target coordinates (left: ${input.context.drag.targetRect?.left ?? 0}px, top: ${input.context.drag.targetRect?.top ?? 0}px). Modify the source code (CSS classes, Tailwind classes, flex/grid alignment, margin offsets, or positioning properties) so the element is accurately rendered at this target position.`
679
716
  : "";
680
717
  const instruction = `Selection context:\n${JSON.stringify(input.element, null, 2)}\n${JSON.stringify(visualContext, null, 2)}\n\nConversation history:\n${history}\n\nPrevious change history for this selection:\n${priorChanges}\n\nRelevant source context:\n${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}\n\nCRITICAL: You MUST return ONLY JSON with actual file changes. Do NOT explain what you see or describe the code. Always propose concrete edits when given an instruction. Return JSON format: {"summary":"brief action taken","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}`;
681
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
718
+ if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
682
719
  return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
683
720
  }
684
721
  const system = "You are Lasso, a source-code editing agent. Your ONLY job is to propose concrete file changes. NEVER explain, describe, or analyze code without proposing edits. ALWAYS return valid JSON with at least one change when the user requests a modification. Each oldString must occur exactly once in its file. Never rewrite whole files. Keep changes focused and minimal. If you cannot find the exact text to change, look harder at the provided context - do not give up and explain instead.";
@@ -734,7 +771,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
734
771
  const context = await contextFor(cwd, input.element);
735
772
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
736
773
  const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
737
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
774
+ if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
738
775
  const local = localCommand(config.provider, config.model, prompt);
739
776
  const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
740
777
  if (result.exitCode !== 0)
@@ -263,15 +263,39 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
263
263
  return [];
264
264
  }
265
265
  }
266
- const availableModels = async () => {
266
+ async function cursorModels() {
267
+ if (!localAgents.has("cursor"))
268
+ return [];
269
+ try {
270
+ const result = await execFileAsync("cursor", ["model", "list"], { maxBuffer: 1024 * 1024 });
271
+ return result.stdout
272
+ .split(/\r?\n/)
273
+ .map((line) => line.trim())
274
+ .filter((line) => line && !line.startsWith("Available models"))
275
+ .map((line) => {
276
+ const parts = line.split(/\s+/);
277
+ const id = parts[0]?.trim();
278
+ const name = parts.slice(1).join(" ").trim();
279
+ return id ? { id: `cursor:${id}`, label: `Cursor · ${name}`, provider: "cli" } : null;
280
+ })
281
+ .filter((model) => model !== null);
282
+ }
283
+ catch {
284
+ return [];
285
+ }
286
+ }
287
+ const availableModels = async (collabConfig = null) => {
267
288
  localAgents = await (0, agent_1.detectLocalAgents)();
268
289
  const locals = await localModels();
269
290
  const discoveredOpenCodeModels = await openCodeModels();
291
+ const discoveredCursorModels = await cursorModels();
270
292
  if (locals.length && !agentConfig) {
271
293
  const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
272
294
  agentConfig = { provider: "ollama", apiKey: "ollama", model: process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL, baseUrl: `${base.replace(/\/api\/?$/, "")}/v1` };
273
295
  }
274
- return [
296
+ const isPaidPlan = collabConfig?.plan && collabConfig.plan !== "free";
297
+ const configuredProviders = collabConfig?.configuredProviders || [];
298
+ const allModels = [
275
299
  { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
276
300
  { id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
277
301
  { id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
@@ -298,17 +322,57 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
298
322
  { id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet", provider: "cli" },
299
323
  { id: "opencode:openai/gpt-5", label: "OpenCode · GPT-5", provider: "cli" },
300
324
  ] : []),
325
+ ...(localAgents.has("cursor") ? [
326
+ { id: "cursor:claude-sonnet-4.5", label: "Cursor · Claude Sonnet 4.5", provider: "cli" },
327
+ { id: "cursor:claude-opus-4", label: "Cursor · Claude Opus 4", provider: "cli" },
328
+ { id: "cursor:claude-3.5-sonnet", label: "Cursor · Claude 3.5 Sonnet", provider: "cli" },
329
+ { id: "cursor:gpt-4o", label: "Cursor · GPT-4o", provider: "cli" },
330
+ { id: "cursor:gpt-4o-mini", label: "Cursor · GPT-4o mini", provider: "cli" },
331
+ ] : []),
301
332
  ...discoveredOpenCodeModels,
333
+ ...discoveredCursorModels,
302
334
  ...locals,
303
335
  ];
336
+ // Lock models based on provider API key configuration
337
+ // Paid plans have all models unlocked
338
+ // Free plans only have models unlocked for providers with configured API keys
339
+ const providerKeyMapping = {
340
+ anthropic: "ANTHROPIC_API_KEY",
341
+ openai: "OPENAI_API_KEY",
342
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
343
+ ollama: "OLLAMA_BASE_URL",
344
+ };
345
+ return allModels.map((model) => {
346
+ if (model.provider === "cli") {
347
+ // CLI agents are always unlocked (user manages their own credentials)
348
+ return model;
349
+ }
350
+ if (isPaidPlan) {
351
+ // Paid plans have all hosted models unlocked
352
+ return model;
353
+ }
354
+ // Free plans: check if provider is configured
355
+ const envKey = providerKeyMapping[model.provider];
356
+ const isConfigured = configuredProviders.includes(model.provider) ||
357
+ Boolean(process.env[envKey]) ||
358
+ Boolean(fileEnv[envKey]);
359
+ if (!isConfigured) {
360
+ return {
361
+ ...model,
362
+ locked: true,
363
+ lockedReason: "Configure your API key in the dashboard to use this model",
364
+ };
365
+ }
366
+ return model;
367
+ });
304
368
  };
305
369
  wss.on("connection", (socket) => {
306
370
  overlaySocket = socket;
307
371
  console.log(chalk_1.default.green("✓") + " Overlay connected");
308
- socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? collabConfig : null }));
309
- void availableModels().then((models) => {
372
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null }));
373
+ void availableModels(collabConfig).then((models) => {
310
374
  if (socket.readyState === socket.OPEN)
311
- socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? collabConfig : null }));
375
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null }));
312
376
  });
313
377
  void getGitState(cwd).then((git) => {
314
378
  if (socket.readyState === socket.OPEN)
@@ -438,7 +502,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
438
502
  }
439
503
  if (msg.type === "ask") {
440
504
  const localProvider = msg.provider === "cli";
441
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
505
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
442
506
  if (!lassoKeyConfigured && !localProvider) {
443
507
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
444
508
  return;
@@ -473,7 +537,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
473
537
  }
474
538
  else if (msg.type === "edit") {
475
539
  const localProvider = msg.provider === "cli";
476
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
540
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
477
541
  if (!lassoKeyConfigured && !localProvider) {
478
542
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
479
543
  return;
@@ -509,7 +573,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
509
573
  }
510
574
  else if (msg.type === "git_generate_message") {
511
575
  const localProvider = msg.provider === "cli";
512
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
576
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
513
577
  if (!agentConfig && !localProvider) {
514
578
  socket.send(JSON.stringify({ type: "git_commit_message", error: "No AI provider is configured." }));
515
579
  return;
@@ -605,7 +669,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
605
669
  return;
606
670
  }
607
671
  const localProvider = msg.provider === "cli";
608
- const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : "codex";
672
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
609
673
  const selectedConfig = localProvider
610
674
  ? { provider: cliProvider, model: msg.model || "claude-code:sonnet" }
611
675
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model || agentConfig.model };
@@ -11,6 +11,8 @@ export interface CollabConfig {
11
11
  workspaceId?: string;
12
12
  token?: string;
13
13
  apiKey?: string;
14
+ plan?: string;
15
+ configuredProviders?: string[];
14
16
  }
15
17
  export interface InitResult {
16
18
  ok: boolean;
@@ -292,6 +292,8 @@ async function resolveCollabSession(cwd, fileEnv, projectId) {
292
292
  config.registered = true;
293
293
  config.claimed = Boolean(body.session?.id);
294
294
  config.registeredAt = new Date().toISOString();
295
+ config.plan = body.project?.plan || "free";
296
+ config.configuredProviders = body.project?.configuredProviders || [];
295
297
  console.log(chalk_1.default.green("✓") +
296
298
  " Project session opened " +
297
299
  chalk_1.default.cyan(config.name || projectId) +