@lasso-ai/cli 1.2.4 → 1.2.6
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/ARCHITECTURE.md +10 -3
- package/CHANGELOG.md +55 -0
- package/README.md +23 -3
- package/dist/cli/agent.d.ts +2 -2
- package/dist/cli/agent.js +18 -5
- package/dist/cli/bridge.js +7 -5
- package/dist/cli/oneshot.js +140 -15
- package/dist/cli/project.js +8 -5
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -85,6 +85,13 @@ Responsibilities, and _only_ these:
|
|
|
85
85
|
`_debugSource` fiber data.
|
|
86
86
|
- Take a screenshot of the selection (`html2canvas`) — visual context measurably improves
|
|
87
87
|
edit quality even when the AI has the source.
|
|
88
|
+
- Capture the page's runtime health: uncaught `window` errors, unhandled promise
|
|
89
|
+
rejections, and the host app's `console.error` output. Listeners are installed when
|
|
90
|
+
the overlay script evaluates (before the app's own modules), so failures during
|
|
91
|
+
initial load — a broken import, a missing named export — are recorded as well. The
|
|
92
|
+
latest five unique entries travel with every request (`context.runtimeErrors` for
|
|
93
|
+
select/drag, `pageContext.runtimeErrors` for one-shot) and are cleared on apply so
|
|
94
|
+
they always describe the code that is running now.
|
|
88
95
|
- Show the inline prompt box anchored to the selection (never a blocking `prompt()`).
|
|
89
96
|
- Render the diff/accept/undo UI once the CLI streams a proposed change back.
|
|
90
97
|
- Render concurrent Agent Tasks with In progress/Completed tabs and task-level review
|
|
@@ -118,8 +125,8 @@ A Node process that wraps the user's existing dev server. Entry: `src/cli/index.
|
|
|
118
125
|
never touched on disk.
|
|
119
126
|
3. Host the WebSocket bridge (`src/cli/bridge.ts`) the browser overlay connects to.
|
|
120
127
|
4. On a selection + instruction: read the referenced source file(s), assemble context
|
|
121
|
-
(source snippet, imports, relevant CSS/Tailwind config, screenshot,
|
|
122
|
-
hand it to the coding agent (§6).
|
|
128
|
+
(source snippet, imports, relevant CSS/Tailwind config, screenshot, captured
|
|
129
|
+
console/runtime errors, instruction), and hand it to the coding agent (§6).
|
|
123
130
|
5. Hold the returned diff in memory and stream it to the browser for preview — never write
|
|
124
131
|
on receipt.
|
|
125
132
|
6. On accept: snapshot the file's prior content (for undo), then apply the diff.
|
|
@@ -163,7 +170,7 @@ Lasso ships its own default agent, but the CLI's job is to assemble _context_, n
|
|
|
163
170
|
married to one model.
|
|
164
171
|
|
|
165
172
|
```
|
|
166
|
-
context (source + screenshot + instruction)
|
|
173
|
+
context (source + screenshot + console errors + instruction)
|
|
167
174
|
│
|
|
168
175
|
▼
|
|
169
176
|
┌───────────────┐
|
package/CHANGELOG.md
CHANGED
|
@@ -7,13 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **One-shot generation no longer demands a provider API key when a local agent is
|
|
13
|
+
selected.** Picking Claude Code, Codex, OpenCode, or Cursor for a one-shot request
|
|
14
|
+
used to bail out with *"Please configure an AI provider API key or model in settings
|
|
15
|
+
to enable code generation."* Those agents authenticate themselves, so the request now
|
|
16
|
+
runs through the local CLI first. If the local CLI cannot run (not installed, not
|
|
17
|
+
signed in, out of credits) and a Lasso key is available, the request falls back to the
|
|
18
|
+
Lasso Agent Gateway with a hosted provider/model it understands (`claude-code`/`cursor`
|
|
19
|
+
→ Anthropic, `codex` → OpenAI, `opencode` by model prefix). Without a key, the error
|
|
20
|
+
now names the CLI command and `lasso auth login` instead of pointing at settings.
|
|
21
|
+
- **Local agents no longer hang on stdin.** `codex exec` and `opencode run` read stdin
|
|
22
|
+
until EOF, so every local-agent request stalled until the 5-minute timeout. Stdin is
|
|
23
|
+
closed as soon as the process spawns (none of these command lines are interactive).
|
|
24
|
+
- **Ollama requests stay local.** One-shot requests for Ollama are no longer routed to
|
|
25
|
+
the gateway, which cannot reach the user's `localhost`.
|
|
26
|
+
|
|
10
27
|
### Changed
|
|
11
28
|
|
|
29
|
+
- **`lasso init` registers against the Lasso API, not the realtime server.**
|
|
30
|
+
Project registration (`init`, `register`, `create`) now posts to
|
|
31
|
+
`serverUrlFrom()` — `https://api.lasso.byorello.space` by default — because the
|
|
32
|
+
API owns the workspace's project records and stores the local `.lasso` domain.
|
|
33
|
+
The realtime/collab server (`…/api/v1/collab/projects/register`) is only tried
|
|
34
|
+
when the API cannot be reached. Override with `LASSO_API_URL` /
|
|
35
|
+
`LASSO_SERVER_URL`.
|
|
12
36
|
- **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
37
|
- **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
38
|
|
|
15
39
|
### Added
|
|
16
40
|
|
|
41
|
+
- **Console-aware agent context**: the overlay now captures the page's runtime
|
|
42
|
+
health — uncaught `window` errors, unhandled promise rejections, and anything
|
|
43
|
+
the host app logs through `console.error` — starting from the moment the overlay
|
|
44
|
+
script loads, so failures during initial page load (for example
|
|
45
|
+
`SyntaxError: The requested module … does not provide an export named …` from a
|
|
46
|
+
broken import) are recorded too. The latest five unique entries (1200 chars each)
|
|
47
|
+
are attached to every request:
|
|
48
|
+
- **Select / component flow**: `edit`, `ask`, and drag requests carry
|
|
49
|
+
`context.runtimeErrors`, which the agent prompt renders as an explicit
|
|
50
|
+
`LIVE CONSOLE/RUNTIME ERRORS` block alongside the element and source context.
|
|
51
|
+
- **One-shot flow**: `pageContext.runtimeErrors` travels with the ⌘K request and
|
|
52
|
+
renders as a `Console/Runtime Errors` line in the live page context — local
|
|
53
|
+
plan/conversation prompts and the hosted agent gateway alike.
|
|
54
|
+
- **Runtime errors are part of the task**: agent instructions treat listed console
|
|
55
|
+
errors as work to fix, and require verifying named package exports (via
|
|
56
|
+
`run_command` over `node_modules` or the package docs) before importing them, so a
|
|
57
|
+
guessed export name cannot kill the page with a SyntaxError.
|
|
58
|
+
- **Error buffer resets on apply**: applying a change clears the captured runtime
|
|
59
|
+
errors, so the next prompt only reflects errors produced by the new code.
|
|
17
60
|
- **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
61
|
- **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
62
|
- **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.
|
|
@@ -130,6 +173,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
130
173
|
name, version, workspaceId }` so the overlay can join the right (workspace-
|
|
131
174
|
gated) session. The bridge only forwards a config it successfully authenticated.
|
|
132
175
|
|
|
176
|
+
### Fixed
|
|
177
|
+
|
|
178
|
+
- **Useless `Project registration failed (400)` from `lasso init`**: the CLI only
|
|
179
|
+
read `body.message`, but the registration endpoint answers with `{ error }`, so
|
|
180
|
+
every server-side validation message was dropped and collapsed into the generic
|
|
181
|
+
status line. The server text is surfaced now, e.g.
|
|
182
|
+
`"domain" is not allowed (HTTP 400)`.
|
|
183
|
+
- **Selected element missing from one-shot page context**: `capturePageContext()`
|
|
184
|
+
read `state.selectedElement`, which does not exist on the overlay state (the
|
|
185
|
+
property is `state.selected`), so the inspected element never reached the page
|
|
186
|
+
context. The same mistake failed `typecheck:overlay`.
|
|
187
|
+
|
|
133
188
|
## [0.1.0] - 2026-09-21
|
|
134
189
|
|
|
135
190
|
### Added
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Lasso works differently.
|
|
|
66
66
|
| | Lasso |
|
|
67
67
|
|---|---|
|
|
68
68
|
| 🎯 **Point at the UI** | Select exactly what you want to change |
|
|
69
|
-
| 🧠 **AI understands context** | The selected component is resolved back to your source |
|
|
69
|
+
| 🧠 **AI understands context** | The selected component is resolved back to your source, plus the live page state (DOM summary, screenshot, console/runtime errors) |
|
|
70
70
|
| 📝 **Real code changes** | Lasso modifies your actual source files |
|
|
71
71
|
| 👀 **Preview first** | Every change comes back as a diff |
|
|
72
72
|
| 🔒 **You stay in control** | Nothing is written until you explicitly accept |
|
|
@@ -97,6 +97,24 @@ Lasso now supports two equally powerful ways to build:
|
|
|
97
97
|
These modes work together naturally: use one-shot to generate large features, then
|
|
98
98
|
visual mode to refine specific parts.
|
|
99
99
|
|
|
100
|
+
### Console-aware context
|
|
101
|
+
|
|
102
|
+
Both modes receive the page's runtime health alongside the DOM and screenshot
|
|
103
|
+
context. The overlay captures:
|
|
104
|
+
|
|
105
|
+
- uncaught `window` errors and unhandled promise rejections
|
|
106
|
+
- anything your app logs through `console.error`
|
|
107
|
+
|
|
108
|
+
Capture starts with the overlay script itself, so failures during the initial page
|
|
109
|
+
load (a broken import, a missing named export) are recorded too. The latest five
|
|
110
|
+
unique entries ride along with every request — as `context.runtimeErrors` for
|
|
111
|
+
select/drag prompts and as `pageContext.runtimeErrors` for ⌘K one-shot prompts —
|
|
112
|
+
and the agent is instructed to treat listed errors as part of the task instead of
|
|
113
|
+
working around them.
|
|
114
|
+
|
|
115
|
+
The buffer is cleared when you apply a change, so the next prompt only reflects
|
|
116
|
+
errors produced by the new code.
|
|
117
|
+
|
|
100
118
|
### Visual selection
|
|
101
119
|
|
|
102
120
|
Turn on Lasso Mode and interact with your application normally.
|
|
@@ -156,7 +174,8 @@ or:
|
|
|
156
174
|
|
|
157
175
|
> "Add a loading state below this button."
|
|
158
176
|
|
|
159
|
-
Lasso assembles the relevant source context
|
|
177
|
+
Lasso assembles the relevant source context — source files, screenshot, and the
|
|
178
|
+
page's console/runtime errors — and sends it to your configured coding agent.
|
|
160
179
|
|
|
161
180
|
### Diff-first workflow
|
|
162
181
|
|
|
@@ -496,6 +515,7 @@ Lasso has three main pieces.
|
|
|
496
515
|
│ │
|
|
497
516
|
│ Select UI element │
|
|
498
517
|
│ Capture screenshot │
|
|
518
|
+
│ Console errors │
|
|
499
519
|
│ Presence, locks, │
|
|
500
520
|
│ comments, voice │
|
|
501
521
|
└──────────┬───────────┘
|
|
@@ -544,7 +564,7 @@ The CLI:
|
|
|
544
564
|
2. Resolves the selected component
|
|
545
565
|
3. Reads the relevant source
|
|
546
566
|
4. Collects imports and surrounding context
|
|
547
|
-
5. Captures the selected UI
|
|
567
|
+
5. Captures the selected UI and the page's console/runtime errors
|
|
548
568
|
6. Sends the assembled context to your configured agent
|
|
549
569
|
7. Receives a proposed change
|
|
550
570
|
8. Shows you the diff
|
package/dist/cli/agent.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type SourceChange = {
|
|
|
3
3
|
oldString: string;
|
|
4
4
|
newString: string;
|
|
5
5
|
};
|
|
6
|
-
type AgentInput = {
|
|
6
|
+
export type AgentInput = {
|
|
7
7
|
taskId?: string;
|
|
8
8
|
instruction: string;
|
|
9
9
|
model: string;
|
|
@@ -97,6 +97,7 @@ export type AgentStepAction = {
|
|
|
97
97
|
changes: SourceChange[];
|
|
98
98
|
};
|
|
99
99
|
export declare function parseAgentAction(text: string): AgentStepAction;
|
|
100
|
+
export declare function runLocalAgentPrompt(cwd: string, config: AgentConfig, prompt: string, signal?: AbortSignal, onProgress?: AgentProgress, taskId?: string, onPrompt?: AgentPromptHandler): Promise<string>;
|
|
100
101
|
export declare function proposeChanges(cwd: string, input: AgentInput, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<{
|
|
101
102
|
summary: string;
|
|
102
103
|
packages?: string[];
|
|
@@ -104,4 +105,3 @@ export declare function proposeChanges(cwd: string, input: AgentInput, config: A
|
|
|
104
105
|
}>;
|
|
105
106
|
export declare function answerQuestion(cwd: string, input: AgentAnswer, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<string>;
|
|
106
107
|
export declare function generateCommitMessage(cwd: string, status: string[], config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress): Promise<string>;
|
|
107
|
-
export {};
|
package/dist/cli/agent.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.executeWebSearch = executeWebSearch;
|
|
|
10
10
|
exports.executeAgentCommand = executeAgentCommand;
|
|
11
11
|
exports.detectLocalAgents = detectLocalAgents;
|
|
12
12
|
exports.parseAgentAction = parseAgentAction;
|
|
13
|
+
exports.runLocalAgentPrompt = runLocalAgentPrompt;
|
|
13
14
|
exports.proposeChanges = proposeChanges;
|
|
14
15
|
exports.answerQuestion = answerQuestion;
|
|
15
16
|
exports.generateCommitMessage = generateCommitMessage;
|
|
@@ -755,6 +756,12 @@ function localCommand(provider, model, prompt) {
|
|
|
755
756
|
function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
|
|
756
757
|
return new Promise((resolve, reject) => {
|
|
757
758
|
const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
759
|
+
// Headless invocations: codex/opencode/cursor read stdin until EOF and otherwise hang
|
|
760
|
+
// until our timeout, so close stdin right away. Prompt answers can no longer be piped
|
|
761
|
+
// back in, which is fine — none of these command lines are interactive. Swallow the
|
|
762
|
+
// EPIPE that a late write would raise so it cannot crash the CLI.
|
|
763
|
+
child.stdin?.on("error", () => { });
|
|
764
|
+
child.stdin?.end();
|
|
758
765
|
const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : provider === "cursor" ? "Cursor" : "Claude Code";
|
|
759
766
|
let stdout = "";
|
|
760
767
|
let stderr = "";
|
|
@@ -882,6 +889,16 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
|
|
|
882
889
|
throw new Error(localAgentError(command, result.stderr, result.exitCode));
|
|
883
890
|
return extractLocalAgentProposal(result.stdout, config.provider);
|
|
884
891
|
}
|
|
892
|
+
// Runs a local coding-agent CLI (claude/codex/opencode/cursor) and returns its final text.
|
|
893
|
+
// The CLI authenticates itself (its own login), so no provider API key is required.
|
|
894
|
+
async function runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, taskId, onPrompt) {
|
|
895
|
+
const provider = config.provider;
|
|
896
|
+
const local = localCommand(provider, config.model, prompt);
|
|
897
|
+
const result = await runLocalCommand(cwd, local.command, local.args, provider, signal, onProgress, taskId, onPrompt);
|
|
898
|
+
if (result.exitCode !== 0)
|
|
899
|
+
throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
|
|
900
|
+
return extractLocalAgentText(result.stdout, provider);
|
|
901
|
+
}
|
|
885
902
|
async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
|
|
886
903
|
const context = await contextFor(cwd, input.element);
|
|
887
904
|
const runtimeErrors = input.context?.runtimeErrors || [];
|
|
@@ -1176,11 +1193,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
|
|
|
1176
1193
|
const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
|
|
1177
1194
|
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."}`;
|
|
1178
1195
|
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
|
|
1179
|
-
|
|
1180
|
-
const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
|
|
1181
|
-
if (result.exitCode !== 0)
|
|
1182
|
-
throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
|
|
1183
|
-
return extractLocalAgentText(result.stdout, config.provider).trim();
|
|
1196
|
+
return (await runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, input.taskId, onPrompt)).trim();
|
|
1184
1197
|
}
|
|
1185
1198
|
const callServerGatewayAnswer = async (gatewayKey) => {
|
|
1186
1199
|
let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
|
package/dist/cli/bridge.js
CHANGED
|
@@ -556,7 +556,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
556
556
|
taskControllers.set(msg.taskId, controller);
|
|
557
557
|
let selectedConfig;
|
|
558
558
|
if (localProvider) {
|
|
559
|
-
selectedConfig = { provider: cliProvider, model: msg.model };
|
|
559
|
+
selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
|
|
560
560
|
}
|
|
561
561
|
else {
|
|
562
562
|
const requestedProvider = (msg.provider || agentConfig.provider);
|
|
@@ -608,7 +608,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
608
608
|
}
|
|
609
609
|
let selectedConfig;
|
|
610
610
|
if (localProvider) {
|
|
611
|
-
selectedConfig = { provider: cliProvider, model: msg.model };
|
|
611
|
+
selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
|
|
612
612
|
}
|
|
613
613
|
else {
|
|
614
614
|
const requestedProvider = (msg.provider || agentConfig.provider);
|
|
@@ -765,21 +765,23 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
765
765
|
const provider = (msg.provider || agentConfig?.provider || "google");
|
|
766
766
|
const model = msg.model || agentConfig?.model || (provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : provider === "google" ? "gemini-2.5-flash" : "claude-3-7-sonnet");
|
|
767
767
|
const localKey = localProviderKeys[provider];
|
|
768
|
+
const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
|
|
769
|
+
const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
|
|
768
770
|
const selectedConfig = localProvider
|
|
769
|
-
? { provider: cliProvider, model: msg.model || "claude-code:sonnet" }
|
|
771
|
+
? { provider: cliProvider, model: msg.model || "claude-code:sonnet", lassoKey: resolvedLassoKey, serverUrl: targetApiUrl }
|
|
770
772
|
: {
|
|
771
773
|
...(agentConfig || {}),
|
|
772
774
|
provider,
|
|
773
775
|
model,
|
|
774
776
|
apiKey: localKey || apiKey,
|
|
777
|
+
lassoKey: resolvedLassoKey,
|
|
778
|
+
serverUrl: targetApiUrl,
|
|
775
779
|
baseUrl: provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : provider === "ollama" ? `${(process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api").replace(/\/api\/?$/, "")}/v1` : undefined,
|
|
776
780
|
};
|
|
777
781
|
const controller = new AbortController();
|
|
778
782
|
taskControllers.set(taskId, controller);
|
|
779
783
|
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: "Planning your request..." }));
|
|
780
784
|
const { runOneShotAgent } = await import("./oneshot.js");
|
|
781
|
-
const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
|
|
782
|
-
const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
|
|
783
785
|
runOneShotAgent(cwd, msg.prompt, msg.scope || "project", selectedConfig, controller.signal, (status, message, detail) => {
|
|
784
786
|
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
785
787
|
socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
|
package/dist/cli/oneshot.js
CHANGED
|
@@ -9,7 +9,37 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const node_child_process_1 = require("node:child_process");
|
|
11
11
|
const node_util_1 = require("node:util");
|
|
12
|
+
const agent_1 = require("./agent");
|
|
12
13
|
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
14
|
+
const CLI_PROVIDERS = new Set(["claude-code", "codex", "opencode", "cursor"]);
|
|
15
|
+
function isCliProvider(provider) {
|
|
16
|
+
return Boolean(provider && CLI_PROVIDERS.has(provider));
|
|
17
|
+
}
|
|
18
|
+
function cliLabel(provider) {
|
|
19
|
+
return provider === "claude-code" ? "Claude Code" : provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : provider === "codex" ? "Codex" : "CLI agent";
|
|
20
|
+
}
|
|
21
|
+
function cliCommand(provider) {
|
|
22
|
+
return provider === "claude-code" ? "claude" : provider || "the agent CLI";
|
|
23
|
+
}
|
|
24
|
+
// The Lasso Agent Gateway only understands hosted providers, so a CLI selection is
|
|
25
|
+
// normalized to the closest hosted provider/model when we have to fall back to it.
|
|
26
|
+
const GATEWAY_MODELS = {
|
|
27
|
+
anthropic: "claude-sonnet-4-5-20250929",
|
|
28
|
+
openai: "gpt-4.1",
|
|
29
|
+
google: "gemini-2.5-flash",
|
|
30
|
+
};
|
|
31
|
+
function gatewayTargetForCli(provider, model) {
|
|
32
|
+
const bare = model && model.includes(":") ? model.slice(model.indexOf(":") + 1) : model || "";
|
|
33
|
+
if (provider === "codex")
|
|
34
|
+
return { provider: "openai", model: GATEWAY_MODELS.openai };
|
|
35
|
+
if (provider === "cursor")
|
|
36
|
+
return bare.includes("gpt") ? { provider: "openai", model: GATEWAY_MODELS.openai } : { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
|
|
37
|
+
if (bare.startsWith("openai/") || /^gpt/i.test(bare))
|
|
38
|
+
return { provider: "openai", model: GATEWAY_MODELS.openai };
|
|
39
|
+
if (bare.startsWith("google/") || /^gemini/i.test(bare))
|
|
40
|
+
return { provider: "google", model: GATEWAY_MODELS.google };
|
|
41
|
+
return { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
|
|
42
|
+
}
|
|
13
43
|
function isConversationalPrompt(prompt) {
|
|
14
44
|
const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
|
|
15
45
|
const greetings = [
|
|
@@ -39,7 +69,7 @@ function runtimeErrorObservation(pageContext) {
|
|
|
39
69
|
return "- Console/Runtime Errors: None";
|
|
40
70
|
return `- Console/Runtime Errors (the page is currently throwing these):\n${errors.map((e) => ` ! ${e}`).join("\n")}`;
|
|
41
71
|
}
|
|
42
|
-
async function generateConversationalReply(prompt, framework, config, signal, messages, pageContext) {
|
|
72
|
+
async function generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext, onProgress, taskId, onPrompt) {
|
|
43
73
|
const history = messages?.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n") || "";
|
|
44
74
|
let pageObservation = "";
|
|
45
75
|
if (pageContext) {
|
|
@@ -68,13 +98,19 @@ Respond conversationally, concisely, and helpfully.
|
|
|
68
98
|
${pageContext ? "You have live access to the browser page context. When the user asks about the page, design, layout, or components, reference the actual headings, sections, buttons, and text rendered on screen. Provide concrete, expert design and UX feedback. Never claim the page has no content or components when rendered headings and sections are present." : ""}
|
|
69
99
|
Do not output code changes, file patches, or JSON schemas — just talk to the developer naturally and offer assistance.`;
|
|
70
100
|
const userPrompt = `${history ? `Conversation History:\n${history}\n\n` : ""}${pageObservation}\nUser message: ${prompt}`;
|
|
101
|
+
// CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so no
|
|
102
|
+
// provider API key is needed. Failures are thrown so the caller can fall back.
|
|
103
|
+
if (isCliProvider(config.provider)) {
|
|
104
|
+
const text = await (0, agent_1.runLocalAgentPrompt)(cwd, config, `${systemPrompt}\n\n${userPrompt}`, signal, onProgress, taskId, onPrompt);
|
|
105
|
+
return text.trim() || "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
|
|
106
|
+
}
|
|
71
107
|
if (!config.apiKey && config.provider !== "ollama") {
|
|
72
108
|
if (pageContext && (prompt.toLowerCase().includes("landing page") || prompt.toLowerCase().includes("page"))) {
|
|
73
109
|
const headings = (pageContext.domSummary?.headings || []).map((h) => `"${h.text}"`).join(", ");
|
|
74
110
|
const sections = (pageContext.domSummary?.sections || []).map((s) => s.name).join(", ");
|
|
75
|
-
return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components,
|
|
111
|
+
return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components, run \`lasso auth login\` or add an AI provider API key in settings, then ask me what to modify!`;
|
|
76
112
|
}
|
|
77
|
-
return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
|
|
113
|
+
return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix. Run `lasso auth login` (or add a provider API key in settings) to unlock AI replies.";
|
|
78
114
|
}
|
|
79
115
|
try {
|
|
80
116
|
const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
|
|
@@ -179,21 +215,20 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
179
215
|
rawServerUrl = rawServerUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
|
|
180
216
|
}
|
|
181
217
|
const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
config.provider === "opencode" ||
|
|
185
|
-
config.provider === "cursor";
|
|
218
|
+
const useCliAgent = isCliProvider(config.provider);
|
|
219
|
+
const gatewayKey = apiKey || config.lassoKey || (config.apiKey && (config.apiKey.startsWith("lss_live_") || config.apiKey.startsWith("lss_")) ? config.apiKey : "") || process.env.LASSO_API_KEY || "";
|
|
186
220
|
const hasLocalProviderKey = Boolean(config.apiKey &&
|
|
187
221
|
config.apiKey !== "ollama" &&
|
|
188
222
|
!config.apiKey.startsWith("lss_live_") &&
|
|
189
223
|
!config.apiKey.startsWith("lss_"));
|
|
190
224
|
// If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
|
|
191
225
|
// When they DO have a local key in .env, it runs locally to save server resources!
|
|
192
|
-
|
|
193
|
-
|
|
226
|
+
// Ollama always runs locally (the server cannot reach the user's localhost).
|
|
227
|
+
const shouldRouteToServer = !useCliAgent && config.provider !== "ollama" && !hasLocalProviderKey && Boolean(targetServerUrl);
|
|
228
|
+
const callGateway = async (target) => {
|
|
194
229
|
try {
|
|
195
230
|
if (!isConversational && !pageContext) {
|
|
196
|
-
onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${
|
|
231
|
+
onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${target.model || "claude-3-7-sonnet"}`);
|
|
197
232
|
}
|
|
198
233
|
const endpoint = `${targetServerUrl.replace(/\/$/, "")}/api/v1/agent/session`;
|
|
199
234
|
const headers = {
|
|
@@ -235,8 +270,8 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
235
270
|
prompt,
|
|
236
271
|
messages,
|
|
237
272
|
model: {
|
|
238
|
-
id:
|
|
239
|
-
provider:
|
|
273
|
+
id: target.model,
|
|
274
|
+
provider: target.provider,
|
|
240
275
|
},
|
|
241
276
|
context: {
|
|
242
277
|
framework,
|
|
@@ -315,10 +350,100 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
315
350
|
error: serverErr instanceof Error ? serverErr.message : "Failed to connect to Lasso Agent Gateway.",
|
|
316
351
|
};
|
|
317
352
|
}
|
|
353
|
+
return { ok: false, error: "The agent session returned no result." };
|
|
354
|
+
};
|
|
355
|
+
const gatewayFallback = async (reason) => {
|
|
356
|
+
if (!gatewayKey || !targetServerUrl) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
error: `${reason} — run \`${cliCommand(config.provider)}\` once to authenticate it, or run \`lasso auth login\` to use the Lasso gateway.`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
onProgress("thinking", `${cliLabel(config.provider)} is unavailable; using the Lasso Agent Gateway...`, reason);
|
|
363
|
+
const target = gatewayTargetForCli(config.provider, config.model);
|
|
364
|
+
const result = await callGateway({ provider: target.provider, model: target.model });
|
|
365
|
+
if (!result.ok && result.error) {
|
|
366
|
+
return { ok: false, error: `${reason} — gateway fallback failed: ${result.error}` };
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
};
|
|
370
|
+
// CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so they run
|
|
371
|
+
// locally first instead of demanding a provider API key from settings.
|
|
372
|
+
if (useCliAgent && !isConversational) {
|
|
373
|
+
onProgress("thinking", `Running ${cliLabel(config.provider)}...`, `Local CLI agent · ${config.model || ""}`);
|
|
374
|
+
try {
|
|
375
|
+
const selectedElement = pageContext?.selectedElement;
|
|
376
|
+
const input = {
|
|
377
|
+
taskId,
|
|
378
|
+
instruction: prompt,
|
|
379
|
+
model: config.model || "",
|
|
380
|
+
messages,
|
|
381
|
+
context: {
|
|
382
|
+
runtimeErrors: pageContext?.runtimeErrors,
|
|
383
|
+
screenshots: pageContext?.screenshot ? { full: pageContext.screenshot } : undefined,
|
|
384
|
+
viewport: pageContext?.viewport,
|
|
385
|
+
},
|
|
386
|
+
element: {
|
|
387
|
+
tag: selectedElement?.tag || "project",
|
|
388
|
+
group: scope === "component" ? "component" : "workspace",
|
|
389
|
+
label: selectedElement?.text || "project",
|
|
390
|
+
html: selectedElement ? `<${selectedElement.tag}> ${selectedElement.text || ""}` : undefined,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
const proposal = await (0, agent_1.proposeChanges)(cwd, input, config, signal, (message, detail, level) => {
|
|
394
|
+
onProgress(level === "error" ? "error" : "working", message, detail);
|
|
395
|
+
}, (agentPrompt) => {
|
|
396
|
+
onPrompt({ question: agentPrompt.message, options: agentPrompt.options });
|
|
397
|
+
});
|
|
398
|
+
if (signal.aborted) {
|
|
399
|
+
return { ok: false, error: "Operation was cancelled" };
|
|
400
|
+
}
|
|
401
|
+
if (!proposal.changes.length) {
|
|
402
|
+
return { ok: true, summary: proposal.summary, changes: [], thinking: [], totalThinkingTimeMs: Date.now() - startTime };
|
|
403
|
+
}
|
|
404
|
+
onProgress("review", "Implementation complete", `${proposal.changes.length} files modified`);
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
summary: proposal.summary,
|
|
408
|
+
changes: proposal.changes,
|
|
409
|
+
thinking: [
|
|
410
|
+
{ title: "Analyzed project structure", detail: `Detected ${framework} framework` },
|
|
411
|
+
{ title: `${cliLabel(config.provider)} generated a proposal`, detail: `${proposal.changes.length} file(s)` },
|
|
412
|
+
],
|
|
413
|
+
totalThinkingTimeMs: Date.now() - startTime,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
if (signal.aborted) {
|
|
418
|
+
return { ok: false, error: "Operation was cancelled" };
|
|
419
|
+
}
|
|
420
|
+
return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (useCliAgent && isConversational) {
|
|
424
|
+
try {
|
|
425
|
+
const reply = await generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext, (message, detail) => onProgress("thinking", message, detail), taskId, (agentPrompt) => onPrompt({ question: agentPrompt.message, options: agentPrompt.options }));
|
|
426
|
+
return {
|
|
427
|
+
ok: true,
|
|
428
|
+
summary: reply,
|
|
429
|
+
changes: [],
|
|
430
|
+
thinking: [],
|
|
431
|
+
totalThinkingTimeMs: Date.now() - startTime,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (signal.aborted) {
|
|
436
|
+
return { ok: false, error: "Operation was cancelled" };
|
|
437
|
+
}
|
|
438
|
+
return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (shouldRouteToServer) {
|
|
442
|
+
return await callGateway({ provider: config.provider, model: config.model });
|
|
318
443
|
}
|
|
319
444
|
// If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
|
|
320
445
|
if (isConversational) {
|
|
321
|
-
const reply = await generateConversationalReply(prompt, framework, config, signal, messages, pageContext);
|
|
446
|
+
const reply = await generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext);
|
|
322
447
|
return {
|
|
323
448
|
ok: true,
|
|
324
449
|
summary: reply,
|
|
@@ -578,11 +703,11 @@ Existing code context (study these carefully before writing — especially the C
|
|
|
578
703
|
${fileSnippets || "No existing components found. Create appropriate files in src/."}
|
|
579
704
|
|
|
580
705
|
Analyze the existing code structure, then respond in JSON:`;
|
|
581
|
-
if (!config.apiKey && config.provider !== "ollama") {
|
|
706
|
+
if (!config.apiKey && config.provider !== "ollama" && !isCliProvider(config.provider)) {
|
|
582
707
|
return {
|
|
583
708
|
steps: [],
|
|
584
709
|
isConversational: true,
|
|
585
|
-
reply: "
|
|
710
|
+
reply: "I couldn't generate code because no AI provider is available. Run `lasso auth login` to use your Lasso account, or add an AI provider API key in settings, then try again.",
|
|
586
711
|
};
|
|
587
712
|
}
|
|
588
713
|
try {
|
package/dist/cli/project.js
CHANGED
|
@@ -211,7 +211,9 @@ async function initProject(cwd, fileEnv, forcedDomain) {
|
|
|
211
211
|
try {
|
|
212
212
|
let response;
|
|
213
213
|
try {
|
|
214
|
-
|
|
214
|
+
// Registration lives on the Lasso API (api.lasso.byorello.space): it owns
|
|
215
|
+
// the workspace's project records and stores the local `.lasso` domain.
|
|
216
|
+
response = await (0, auth_1.fetchWithTimeout)(`${serverUrl}/api/v1/collab/projects/register`, {
|
|
215
217
|
method: "POST",
|
|
216
218
|
headers: {
|
|
217
219
|
"content-type": "application/json",
|
|
@@ -221,8 +223,8 @@ async function initProject(cwd, fileEnv, forcedDomain) {
|
|
|
221
223
|
});
|
|
222
224
|
}
|
|
223
225
|
catch {
|
|
224
|
-
// Fall back to
|
|
225
|
-
response = await (0, auth_1.fetchWithTimeout)(`${
|
|
226
|
+
// Fall back to the realtime/collab server if the API is unreachable
|
|
227
|
+
response = await (0, auth_1.fetchWithTimeout)(`${collabApi}/collab/projects/register`, {
|
|
226
228
|
method: "POST",
|
|
227
229
|
headers: {
|
|
228
230
|
"content-type": "application/json",
|
|
@@ -239,7 +241,8 @@ async function initProject(cwd, fileEnv, forcedDomain) {
|
|
|
239
241
|
if (response.status === 403) {
|
|
240
242
|
return { ok: false, error: "Your API key does not have access to a Lasso workspace." };
|
|
241
243
|
}
|
|
242
|
-
|
|
244
|
+
const detail = body.message || body.error;
|
|
245
|
+
return { ok: false, error: detail ? `${detail} (HTTP ${response.status})` : `Project registration failed (${response.status}).` };
|
|
243
246
|
}
|
|
244
247
|
const projectId = body.project?.id || existingConfig?.id;
|
|
245
248
|
if (!projectId) {
|
|
@@ -264,7 +267,7 @@ async function initProject(cwd, fileEnv, forcedDomain) {
|
|
|
264
267
|
}
|
|
265
268
|
catch (error) {
|
|
266
269
|
const reason = error instanceof Error ? error.message : "request failed";
|
|
267
|
-
return { ok: false, error: `Could not reach the Lasso
|
|
270
|
+
return { ok: false, error: `Could not reach the Lasso API at ${serverUrl} (${reason}). Is it running?` };
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
/**
|
package/package.json
CHANGED