@lasso-ai/cli 1.1.0 → 1.1.2
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 +55 -37
- package/CHANGELOG.md +14 -0
- package/README.md +42 -41
- package/dist/cli/agent.js +77 -3
- package/dist/cli/bridge.d.ts +18 -1
- package/dist/cli/bridge.js +65 -13
- package/dist/overlay.js +706 -159
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -68,10 +68,10 @@ Three parties, all coordinating around one shared filesystem:
|
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Everything except the coding agent call runs locally. Edit proposals are never written to
|
|
71
|
-
disk without an explicit accept from the user. The
|
|
72
|
-
|
|
73
|
-
the only party that ever leaves the machine, and only with the
|
|
74
|
-
assembled for it.
|
|
71
|
+
disk without an explicit accept from the user. The New Page workflow is a separate explicit
|
|
72
|
+
write path: it creates only the file at the folder the user selected in the project
|
|
73
|
+
explorer. The agent is the only party that ever leaves the machine, and only with the
|
|
74
|
+
exact context the CLI assembled for it.
|
|
75
75
|
|
|
76
76
|
## 4. Browser layer (capture only)
|
|
77
77
|
|
|
@@ -91,10 +91,13 @@ Responsibilities, and _only_ these:
|
|
|
91
91
|
actions; acceptance is the transition that completes an edit task.
|
|
92
92
|
- Keep a stable component conversation identity so repeated prompts reuse conversation
|
|
93
93
|
and change history across DOM replacement during HMR.
|
|
94
|
-
- Render the secondary toolbar, custom tooltips, and the
|
|
95
|
-
project-folder
|
|
94
|
+
- Render the secondary toolbar, custom tooltips, and the New Page dialog with a lazy
|
|
95
|
+
project-folder tree. The tree is populated per-directory on expand; the CLI never
|
|
96
|
+
sends the whole project at once. Selecting a folder sets the write target, and a
|
|
97
|
+
new nested folder is created inline through its own bridge message.
|
|
96
98
|
- Render task-scoped permission/input prompts and send explicit responses back over
|
|
97
|
-
the bridge.
|
|
99
|
+
the bridge. The prompt must re-enable pointer events on its panel, because the
|
|
100
|
+
overlay host disables them to keep the toolbar click-through.
|
|
98
101
|
- **Never mutate the DOM.** All visual changes the user sees post-edit come from the
|
|
99
102
|
framework's own HMR reload, not from this layer.
|
|
100
103
|
|
|
@@ -121,10 +124,17 @@ A Node process that wraps the user's existing dev server. Entry: `src/cli/index.
|
|
|
121
124
|
on receipt.
|
|
122
125
|
6. On accept: snapshot the file's prior content (for undo), then apply the diff.
|
|
123
126
|
7. On undo: restore the snapshot, full stop — no re-generation needed, so retries are free.
|
|
124
|
-
8. For New Page:
|
|
125
|
-
|
|
127
|
+
8. For New Page: answer `list_page_folders` one directory at a time so the overlay can
|
|
128
|
+
build its tree lazily, skipping dot-entries and build/vendor directories. Every
|
|
129
|
+
request resolves against the project root, so `..` and absolute paths are refused.
|
|
130
|
+
Create a file only after the user confirms, and refuse to overwrite. Nested folder
|
|
131
|
+
creation arrives separately as `create_page_folder` and never accepts a slash or a
|
|
132
|
+
dotted name.
|
|
126
133
|
9. For local agents: keep child-process stdin attached to its task and forward explicit
|
|
127
|
-
overlay responses when the agent requests permission or input.
|
|
134
|
+
overlay responses when the agent requests permission or input. Classify stderr
|
|
135
|
+
before prompting: a filesystem or transport failure is reported as an error with a
|
|
136
|
+
suggested fix, because Allow/Deny cannot resolve it. Only a genuine approval or
|
|
137
|
+
sign-in request becomes a prompt, and the text is cleaned of log prefixes first.
|
|
128
138
|
|
|
129
139
|
### 5.2 Source resolution strategies
|
|
130
140
|
|
|
@@ -160,38 +170,46 @@ context (source + screenshot + instruction)
|
|
|
160
170
|
│ Agent adapter │ interface: given context, return old/new string pairs
|
|
161
171
|
└───────┬───────┘
|
|
162
172
|
│
|
|
163
|
-
|
|
164
|
-
▼ ▼
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
(default) `claude -p`) Aider, etc.)
|
|
173
|
+
┌──────┼──────────────┬─────────────────┐
|
|
174
|
+
▼ ▼ ▼ ▼
|
|
175
|
+
Hosted Claude Code Codex OpenCode
|
|
176
|
+
model `claude -p` `codex exec` `opencode run`
|
|
168
177
|
```
|
|
169
178
|
|
|
170
|
-
- **
|
|
171
|
-
directly (`@anthropic-ai/sdk`)
|
|
172
|
-
from §5.3.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
- **Hosted model** (`anthropic` | `openai` | `google` | `ollama`): the default path when
|
|
180
|
+
no local CLI is selected. Lasso calls the provider SDK directly (`@anthropic-ai/sdk`)
|
|
181
|
+
with the assembled context and the old/new-string contract from §5.3. The provider and
|
|
182
|
+
key come from `LASSO_AGENT_PROVIDER` plus the matching `*_API_KEY`, read from the
|
|
183
|
+
process environment or the project `.env`.
|
|
184
|
+
- **Claude Code passthrough** (`claude-code`): the CLI shells out to Claude Code in
|
|
185
|
+
headless/print mode (`claude -p … --permission-mode plan --output-format
|
|
186
|
+
stream-json --max-turns 3`), passing the assembled context as the prompt and parsing
|
|
187
|
+
its output back into the same diff-preview pipeline. This lets a user keep using their
|
|
188
|
+
existing Claude Code setup (subscription, project memory, MCP tools) while still
|
|
189
|
+
getting the point-and-select UX.
|
|
190
|
+
- **Codex passthrough** (`codex`): `codex exec --json --sandbox read-only
|
|
191
|
+
--skip-git-repo-check`. Lasso passes the repository trust check itself, so Codex's own
|
|
192
|
+
check is skipped, and the read-only sandbox keeps it from writing.
|
|
193
|
+
- **OpenCode passthrough** (`opencode`): `opencode run --format json --print-logs
|
|
194
|
+
--log-level INFO --agent plan`. The `plan` agent is read-only; `--print-logs` is why
|
|
195
|
+
diagnostics reach the bridge, which classifies them rather than surfacing raw output.
|
|
196
|
+
|
|
197
|
+
Every local agent is invoked read-only, so it can only propose a patch. Lasso writes
|
|
198
|
+
nothing until the user accepts the diff. If an agent asks for approval or input, the
|
|
199
|
+
child's stdin stays attached and the request is surfaced as a task-scoped overlay prompt.
|
|
200
|
+
|
|
201
|
+
Availability is detected rather than configured: at bridge start Lasso runs `which` for
|
|
202
|
+
`claude`, `codex`, and `opencode`, and only detected agents appear in the model picker.
|
|
203
|
+
OpenCode's model list is discovered with `opencode models`.
|
|
204
|
+
|
|
205
|
+
Selection happens in the overlay model picker, not in `lasso.config.json`. That file
|
|
206
|
+
carries only the project `id` and local `domain`:
|
|
184
207
|
|
|
185
208
|
```json
|
|
186
|
-
// lasso.config.json
|
|
209
|
+
// lasso.config.json — the only project fields
|
|
187
210
|
{
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// lasso.config.json (after `lasso init` — id is the only project field)
|
|
192
|
-
{
|
|
193
|
-
"agent": "builtin",
|
|
194
|
-
"id": "proj_3f2a9c…" // workspace-scoped; commit it so teammates share the session
|
|
211
|
+
"id": "proj_3f2a9c…", // workspace-scoped; commit it so teammates share the session
|
|
212
|
+
"domain": "my-project.lasso"
|
|
195
213
|
}
|
|
196
214
|
```
|
|
197
215
|
|
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
20
|
overlay prompt with Allow, Deny, or custom text.
|
|
21
21
|
- **Secondary overlay toolbar**: added a New Page workflow with a project-folder
|
|
22
22
|
explorer, filename/content form, path validation, and explicit file creation.
|
|
23
|
+
- **Redesigned New Page dialog**: replaced the flat folder dropdown with a
|
|
24
|
+
VS Code-style lazy tree of the project. You type only a page name and pick a
|
|
25
|
+
folder; nested folders are created inline with a `+` button, folders load on
|
|
26
|
+
demand, a filter box narrows deep trees, and clicking a file reuses its name.
|
|
27
|
+
Selecting a folder no longer collapses it, and the starter file is generated
|
|
28
|
+
from the extension (`.tsx`, `.ts`, `.css`, `.json`, `.md`).
|
|
29
|
+
- **Readable agent permission prompts**: the prompt no longer dumps raw local-agent
|
|
30
|
+
log lines. Filesystem failures such as `EACCES: permission denied` and
|
|
31
|
+
`Cause([Die([…])])` now surface as errors with a suggested fix, because
|
|
32
|
+
Allow/Deny can never resolve them. Only genuine approval and sign-in requests
|
|
33
|
+
open the Allow/Deny prompt.
|
|
34
|
+
- **Clickable agent prompt**: the prompt panel opts back into pointer events, so
|
|
35
|
+
Allow, Deny, and custom options respond to clicks again; Enter submits typed
|
|
36
|
+
input and Escape sends Deny, matching the close button.
|
|
23
37
|
- **Custom toolbar tooltips**: toolbar labels now use an accessible Lasso-styled
|
|
24
38
|
tooltip on hover and keyboard focus instead of browser-native title popovers.
|
|
25
39
|
- **Filtered agent progress**: useful tool/progress events remain visible while
|
package/README.md
CHANGED
|
@@ -90,10 +90,13 @@ Turn on Lasso Mode and interact with your application normally.
|
|
|
90
90
|
### Overlay toolbars
|
|
91
91
|
|
|
92
92
|
The overlay includes a primary editing toolbar and a compact secondary toolbar.
|
|
93
|
-
The secondary toolbar's **New Page** action opens a
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
file
|
|
93
|
+
The secondary toolbar's **New Page** action opens a project explorer. You type a page
|
|
94
|
+
name and pick the folder it should live in — the tree loads lazily as you expand
|
|
95
|
+
it, a filter box narrows deep trees, and the `+` button next to **Folder** creates
|
|
96
|
+
a new nested folder inline. Clicking a file in the tree reuses its name so you only
|
|
97
|
+
type the part that is missing. Starter content is generated from the extension, and
|
|
98
|
+
Lasso never chooses a source folder for you or overwrites an existing file without
|
|
99
|
+
confirmation.
|
|
97
100
|
|
|
98
101
|
### Source-aware editing
|
|
99
102
|
|
|
@@ -360,60 +363,58 @@ That's it.
|
|
|
360
363
|
|
|
361
364
|
### Create a page manually
|
|
362
365
|
|
|
363
|
-
Use **New Page** in the secondary toolbar to open the
|
|
364
|
-
|
|
365
|
-
|
|
366
|
+
Use **New Page** in the secondary toolbar to open the project explorer. Type a page
|
|
367
|
+
name, expand the tree to pick the target folder, and select **Create page**. Use the
|
|
368
|
+
`+` button beside **Folder** to create a nested folder without leaving the dialog,
|
|
369
|
+
or the filter box to narrow a large tree. The bridge validates that the folder stays
|
|
366
370
|
inside the project and refuses to overwrite an existing file.
|
|
367
371
|
|
|
368
372
|
---
|
|
369
373
|
|
|
370
374
|
## Agent configuration
|
|
371
375
|
|
|
372
|
-
|
|
376
|
+
You choose the coding agent in the overlay's model picker, not in a config file.
|
|
377
|
+
`lasso.config.json` only records your project `id` and local `domain` — it has no
|
|
378
|
+
agent field.
|
|
373
379
|
|
|
374
|
-
|
|
375
|
-
{
|
|
376
|
-
"agent": "builtin"
|
|
377
|
-
}
|
|
378
|
-
```
|
|
380
|
+
### Local CLI agents
|
|
379
381
|
|
|
380
|
-
|
|
382
|
+
Lasso shells out to a coding CLI installed on your machine. Each one appears as its own
|
|
383
|
+
group in the model picker **only when its binary is on the `PATH` that `lasso dev`
|
|
384
|
+
sees**, so install it before starting the dev server (or restart afterwards):
|
|
381
385
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
386
|
+
| Agent | Binary | Lasso runs |
|
|
387
|
+
| ------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------------- |
|
|
388
|
+
| [Claude Code](https://lasso.byorello.space/docs/claude-code) | `claude` | `claude -p … --permission-mode plan --output-format stream-json --max-turns 3` |
|
|
389
|
+
| [Codex](https://lasso.byorello.space/docs/codex) | `codex` | `codex exec --json --sandbox read-only --skip-git-repo-check` |
|
|
390
|
+
| [OpenCode](https://lasso.byorello.space/docs/opencode) | `opencode` | `opencode run --format json --print-logs --agent plan` |
|
|
387
391
|
|
|
388
|
-
|
|
392
|
+
All three run read-only, so the agent proposes a patch and never writes to your project.
|
|
393
|
+
Sign each CLI in once in a real terminal; Lasso inherits the credentials it stores for
|
|
394
|
+
your user.
|
|
389
395
|
|
|
390
|
-
|
|
396
|
+
### Hosted agents
|
|
397
|
+
|
|
398
|
+
To use a hosted model instead, set the provider and key in your shell or in the project
|
|
399
|
+
`.env` before running `lasso dev`:
|
|
391
400
|
|
|
392
401
|
```bash
|
|
402
|
+
export LASSO_AGENT_PROVIDER=anthropic # or openai, google, ollama
|
|
393
403
|
export ANTHROPIC_API_KEY=your_key
|
|
404
|
+
export LASSO_AGENT_MODEL=... # optional
|
|
394
405
|
```
|
|
395
406
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
"agent": "claude-code"
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
Lasso can delegate the coding task to Claude Code in headless mode while keeping the same selection → context → diff → accept workflow.
|
|
407
|
+
Supported keys are `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
408
|
+
`GOOGLE_GENERATIVE_AI_API_KEY` (or `GEMINI_API_KEY`), and `OLLAMA_BASE_URL` with
|
|
409
|
+
`OLLAMA_MODEL`. If `LASSO_AGENT_PROVIDER` is unset, Lasso picks a provider from whichever
|
|
410
|
+
key it finds, defaulting to `anthropic`.
|
|
405
411
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
```json
|
|
409
|
-
{
|
|
410
|
-
"agent": "custom"
|
|
411
|
-
}
|
|
412
|
-
```
|
|
412
|
+
Hosted agents additionally require your Lasso key from `lasso auth login`, and the
|
|
413
|
+
credentials are never written to `lasso.config.json`.
|
|
413
414
|
|
|
414
|
-
|
|
415
|
+
### The adapter contract
|
|
415
416
|
|
|
416
|
-
|
|
417
|
+
Whichever agent runs, the contract is the same:
|
|
417
418
|
|
|
418
419
|
```text
|
|
419
420
|
Lasso context
|
|
@@ -491,8 +492,8 @@ The CLI:
|
|
|
491
492
|
8. Shows you the diff
|
|
492
493
|
9. Writes only after you accept
|
|
493
494
|
|
|
494
|
-
The separate New Page workflow is also explicit: it writes only after you
|
|
495
|
-
a folder
|
|
495
|
+
The separate New Page workflow is also explicit: it writes only after you pick
|
|
496
|
+
a folder in the project explorer, type a name, and choose **Create page**.
|
|
496
497
|
|
|
497
498
|
The goal is simple:
|
|
498
499
|
|
package/dist/cli/agent.js
CHANGED
|
@@ -303,6 +303,70 @@ function snippet(value, max = 80) {
|
|
|
303
303
|
const s = String(value ?? "").trim().replace(/\s+/g, " ");
|
|
304
304
|
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
305
305
|
}
|
|
306
|
+
const ANSI_PATTERN = /\u001B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
307
|
+
const LOG_FIELD_PATTERN = /([A-Za-z_][\w.-]*)=("(?:[^"\\]|\\.)*"|[^\s]*)/g;
|
|
308
|
+
function cleanAgentText(value) {
|
|
309
|
+
return value.replace(ANSI_PATTERN, "").replace(/\s+/g, " ").trim();
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Local agents (notably OpenCode) print structured logs such as
|
|
313
|
+
* `timestamp=… level=ERROR run=… message="Failed to fetch models.dev" cause="…"`.
|
|
314
|
+
* Those lines are machine diagnostics, never user-facing copy, so they are parsed
|
|
315
|
+
* into their fields instead of being shown verbatim.
|
|
316
|
+
*/
|
|
317
|
+
function parseAgentLogFields(line) {
|
|
318
|
+
if (!/^\s*(?:timestamp|time|ts)=\S/.test(line))
|
|
319
|
+
return null;
|
|
320
|
+
const fields = {};
|
|
321
|
+
for (const match of line.matchAll(LOG_FIELD_PATTERN)) {
|
|
322
|
+
const key = match[1];
|
|
323
|
+
const raw = match[2] ?? "";
|
|
324
|
+
fields[key] = raw.startsWith('"')
|
|
325
|
+
? raw.slice(1, -1).replace(/\\(["\\])/g, "$1")
|
|
326
|
+
: raw;
|
|
327
|
+
}
|
|
328
|
+
return fields;
|
|
329
|
+
}
|
|
330
|
+
// Filesystem failures that a permission request can never resolve.
|
|
331
|
+
const AGENT_FAILURE_MARKERS = /\b(?:EACCES|EPERM|ENOENT|EEXIST|EISDIR|ENOTDIR|ENOSPC|EMFILE|ENOTEMPTY)\b|\bCause\(\[Die\(/i;
|
|
332
|
+
const AGENT_FAILURE_HINTS = [
|
|
333
|
+
{
|
|
334
|
+
pattern: /EACCES: permission denied[\s\S]*(?:locks?|state|storage|\.config|config\/)/i,
|
|
335
|
+
message: "The local agent cannot write to its own state folder. Fix that folder's ownership (for example `sudo chown -R $(whoami) ~/.local/state/opencode`) and retry.",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
pattern: /EACCES: permission denied/i,
|
|
339
|
+
message: "The local agent was denied access to a file or folder it needs. Check the permissions on its state directory, then retry.",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
pattern: /failed to fetch models\.dev/i,
|
|
343
|
+
message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
function agentFailureHint(line) {
|
|
347
|
+
return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
|
|
348
|
+
}
|
|
349
|
+
const PERMISSION_PROMPT_PATTERN = /permission\s+(?:is\s+)?(?:required|requested|needed|denied)|needs?\s+(?:your\s+)?permission|permission\s+to\s+(?:run|edit|write|read|execute|create|delete|modify|access|apply)|approve\s+(?:this|the|tool|action|command)|authori[sz]e\s+(?:this|the|tool|action|command)|authentication\s+(?:is\s+)?required|(?:please\s+)?log\s?in|sign\s?in\s+(?:required|to\s+continue)|press\s+.{1,24}?to\s+(?:continue|approve|confirm)|waiting for (?:your )?(?:permission|approval|input|response)/i;
|
|
350
|
+
/**
|
|
351
|
+
* Returns human-readable copy for a real approval/authentication request, or
|
|
352
|
+
* `null` when the line is anything else. A structured error log never becomes a
|
|
353
|
+
* permission prompt: Allow/Deny cannot fix a filesystem failure, and hiding the
|
|
354
|
+
* error behind those buttons is what made the prompt unreadable.
|
|
355
|
+
*/
|
|
356
|
+
function permissionPromptMessage(raw) {
|
|
357
|
+
const line = cleanAgentText(raw);
|
|
358
|
+
if (!line)
|
|
359
|
+
return null;
|
|
360
|
+
if (parseAgentLogFields(line) || AGENT_FAILURE_MARKERS.test(line))
|
|
361
|
+
return null;
|
|
362
|
+
if (!PERMISSION_PROMPT_PATTERN.test(line))
|
|
363
|
+
return null;
|
|
364
|
+
const detail = line
|
|
365
|
+
.replace(/^[A-Za-z0-9_.-]+:\s*/, "")
|
|
366
|
+
.replace(/^["'\s]+|["'\s]+$/g, "")
|
|
367
|
+
.trim();
|
|
368
|
+
return snippet(detail || line, 240);
|
|
369
|
+
}
|
|
306
370
|
function progressEvent(message, detail) {
|
|
307
371
|
const value = detail == null ? "" : String(detail).trim();
|
|
308
372
|
return value ? { message, detail: value } : { message };
|
|
@@ -410,6 +474,9 @@ function progressEventsFromRaw(raw, provider) {
|
|
|
410
474
|
}
|
|
411
475
|
function localAgentError(command, stderr, exitCode) {
|
|
412
476
|
const output = stderr.trim();
|
|
477
|
+
const hint = agentFailureHint(output);
|
|
478
|
+
if (hint)
|
|
479
|
+
return `${command === "opencode" ? "OpenCode" : command} could not finish: ${hint}`;
|
|
413
480
|
if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
|
|
414
481
|
return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
|
|
415
482
|
}
|
|
@@ -486,10 +553,14 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
|
|
|
486
553
|
for (const progress of progressEventsFromRaw(line, provider)) {
|
|
487
554
|
onProgress?.(progress.message, progress.detail);
|
|
488
555
|
}
|
|
489
|
-
|
|
490
|
-
|
|
556
|
+
const promptMessage = permissionPromptMessage(line);
|
|
557
|
+
if (promptMessage) {
|
|
558
|
+
sendPrompt(promptMessage);
|
|
491
559
|
}
|
|
492
|
-
if (stream === "stderr" && line.trim()) {
|
|
560
|
+
else if (stream === "stderr" && line.trim()) {
|
|
561
|
+
const hint = agentFailureHint(line);
|
|
562
|
+
if (hint)
|
|
563
|
+
onProgress?.(`${label} · ${hint}`);
|
|
493
564
|
if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
|
|
494
565
|
onProgress?.("OpenCode · provider rejected the request; stopping this task…");
|
|
495
566
|
child.kill("SIGTERM");
|
|
@@ -538,6 +609,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
|
|
|
538
609
|
for (const progress of progressEventsFromRaw(stderrTail, provider)) {
|
|
539
610
|
onProgress?.(progress.message, progress.detail);
|
|
540
611
|
}
|
|
612
|
+
const hint = agentFailureHint(stderrTail);
|
|
613
|
+
if (hint)
|
|
614
|
+
onProgress?.(`${label} · ${hint}`);
|
|
541
615
|
if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
|
|
542
616
|
onProgress?.("OpenCode · provider rejected the request; stopping this task…");
|
|
543
617
|
}
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -97,11 +97,16 @@ export type BridgeMessage = {
|
|
|
97
97
|
response: string;
|
|
98
98
|
} | {
|
|
99
99
|
type: "list_page_folders";
|
|
100
|
+
path?: string;
|
|
100
101
|
} | {
|
|
101
102
|
type: "create_page";
|
|
102
103
|
folder: string;
|
|
103
104
|
fileName: string;
|
|
104
105
|
content: string;
|
|
106
|
+
} | {
|
|
107
|
+
type: "create_page_folder";
|
|
108
|
+
parent: string;
|
|
109
|
+
name: string;
|
|
105
110
|
} | {
|
|
106
111
|
type: "git_status";
|
|
107
112
|
} | {
|
|
@@ -132,6 +137,11 @@ type ModelOption = {
|
|
|
132
137
|
label: string;
|
|
133
138
|
provider: "anthropic" | "openai" | "google" | "ollama" | "cli";
|
|
134
139
|
};
|
|
140
|
+
type PageEntry = {
|
|
141
|
+
name: string;
|
|
142
|
+
path: string;
|
|
143
|
+
type: "directory" | "file";
|
|
144
|
+
};
|
|
135
145
|
export type ServerBridgeMessage = {
|
|
136
146
|
type: "config";
|
|
137
147
|
apiKeyConfigured: boolean;
|
|
@@ -165,11 +175,18 @@ export type ServerBridgeMessage = {
|
|
|
165
175
|
prompt: AgentPrompt;
|
|
166
176
|
} | {
|
|
167
177
|
type: "page_folders";
|
|
168
|
-
|
|
178
|
+
path: string;
|
|
179
|
+
entries: PageEntry[];
|
|
180
|
+
project?: string;
|
|
181
|
+
error?: string;
|
|
169
182
|
} | {
|
|
170
183
|
type: "page_created";
|
|
171
184
|
path: string;
|
|
172
185
|
error?: string;
|
|
186
|
+
} | {
|
|
187
|
+
type: "page_folder_created";
|
|
188
|
+
path: string;
|
|
189
|
+
error?: string;
|
|
173
190
|
} | {
|
|
174
191
|
type: "assistant_message";
|
|
175
192
|
taskId?: string;
|
package/dist/cli/bridge.js
CHANGED
|
@@ -181,22 +181,33 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
181
181
|
let activeAgentController = null;
|
|
182
182
|
const taskControllers = new Map();
|
|
183
183
|
let localAgents = new Set();
|
|
184
|
-
const
|
|
185
|
-
|
|
184
|
+
const pageIgnoredDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "out", "coverage", ".vercel", ".cache"]);
|
|
185
|
+
const resolveInsideProject = (root, relative) => {
|
|
186
|
+
const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
|
|
187
|
+
return target === root || target.startsWith(`${root}${node_path_1.default.sep}`) ? target : null;
|
|
188
|
+
};
|
|
189
|
+
// Directories are returned first so the overlay can render an explorer-style
|
|
190
|
+
// tree that mirrors how VS Code orders folders above files.
|
|
191
|
+
const pageEntries = (root, relative) => {
|
|
192
|
+
const directory = resolveInsideProject(root, relative);
|
|
193
|
+
if (!directory)
|
|
186
194
|
return [];
|
|
187
|
-
|
|
195
|
+
let entries = [];
|
|
188
196
|
try {
|
|
189
197
|
for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
|
|
190
|
-
if (
|
|
198
|
+
if (entry.name.startsWith(".") || (entry.isDirectory() && pageIgnoredDirs.has(entry.name)))
|
|
191
199
|
continue;
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
entries.push({
|
|
201
|
+
name: entry.name,
|
|
202
|
+
path: relative && relative !== "." ? node_path_1.default.join(relative, entry.name) : entry.name,
|
|
203
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
204
|
+
});
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
catch {
|
|
197
|
-
//
|
|
208
|
+
// Folders that cannot be read are treated as empty.
|
|
198
209
|
}
|
|
199
|
-
return
|
|
210
|
+
return entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
200
211
|
};
|
|
201
212
|
bridgeServer.on("request", (req, res) => {
|
|
202
213
|
if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
|
|
@@ -341,19 +352,60 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
341
352
|
console.log("[lasso] received from overlay:", msg);
|
|
342
353
|
}
|
|
343
354
|
if (msg.type === "list_page_folders") {
|
|
344
|
-
|
|
355
|
+
const requested = msg.path || ".";
|
|
356
|
+
const root = node_path_1.default.resolve(cwd);
|
|
357
|
+
const directory = resolveInsideProject(root, requested);
|
|
358
|
+
if (!directory) {
|
|
359
|
+
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: [], error: "That folder is outside the current project." }));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: pageEntries(root, requested), project: node_path_1.default.basename(root) }));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (msg.type === "create_page_folder") {
|
|
366
|
+
const root = node_path_1.default.resolve(cwd);
|
|
367
|
+
const name = node_path_1.default.basename(msg.name || "");
|
|
368
|
+
const parent = resolveInsideProject(root, msg.parent || ".");
|
|
369
|
+
if (!parent || !name || name !== msg.name || name === "." || name === "..") {
|
|
370
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: msg.name || "", error: "Enter a folder name without slashes or dots." }));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const target = node_path_1.default.resolve(parent, name);
|
|
374
|
+
if (!target.startsWith(`${root}${node_path_1.default.sep}`)) {
|
|
375
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: name, error: "Choose a folder inside the current project." }));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
node_fs_1.default.mkdirSync(target);
|
|
380
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: node_path_1.default.relative(root, target) }));
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
socket.send(JSON.stringify({
|
|
384
|
+
type: "page_folder_created",
|
|
385
|
+
path: node_path_1.default.relative(root, target),
|
|
386
|
+
error: error instanceof Error && error.message.includes("EEXIST") ? "A folder with that name already exists." : "The folder could not be created.",
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
345
389
|
return;
|
|
346
390
|
}
|
|
347
391
|
if (msg.type === "create_page") {
|
|
348
392
|
const root = node_path_1.default.resolve(cwd);
|
|
349
|
-
const folder = msg.folder
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
393
|
+
const folder = resolveInsideProject(root, msg.folder || ".");
|
|
394
|
+
// A page name may contain "/" so the overlay can create nested routes in
|
|
395
|
+
// one step, but every segment is validated and resolved inside the project.
|
|
396
|
+
const segments = (msg.fileName || "").split("/").map((segment) => segment.trim()).filter(Boolean);
|
|
397
|
+
const fileName = segments.pop() || "";
|
|
398
|
+
if (!folder || !fileName || segments.some((segment) => segment === "." || segment === "..") || fileName === "." || fileName === "..") {
|
|
399
|
+
socket.send(JSON.stringify({ type: "page_created", path: msg.fileName || "", error: "Choose a valid project folder and file name." }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const target = node_path_1.default.resolve(folder, ...segments, fileName);
|
|
403
|
+
if (!target.startsWith(`${root}${node_path_1.default.sep}`)) {
|
|
353
404
|
socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "Choose a valid project folder and file name." }));
|
|
354
405
|
return;
|
|
355
406
|
}
|
|
356
407
|
try {
|
|
408
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
|
|
357
409
|
node_fs_1.default.writeFileSync(target, msg.content.slice(0, 200000), { encoding: "utf8", flag: "wx" });
|
|
358
410
|
socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root, target) }));
|
|
359
411
|
}
|