@lasso-ai/cli 1.5.0 → 2.0.0
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/dist/cli/agent.d.ts +1 -0
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge.d.ts +8 -5
- package/dist/cli/bridge.js +357 -151
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +28 -10
- package/dist/cli/runtime-client.d.ts +91 -10
- package/dist/cli/runtime-client.js +38 -3
- package/dist/overlay.js +44 -28
- package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
- package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
- package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
- package/dist/runtime/__tests__/state-machine.test.js +107 -0
- package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
- package/dist/runtime/__tests__/verification-engine.test.js +91 -0
- package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
- package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
- package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
- package/dist/runtime/adapters/claude-code-adapter.js +230 -0
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
- package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
- package/dist/runtime/adapters/cursor-adapter.js +214 -0
- package/dist/runtime/adapters/factory.d.ts +19 -0
- package/dist/runtime/adapters/factory.js +48 -0
- package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
- package/dist/runtime/adapters/local-agent-adapter.js +408 -0
- package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
- package/dist/runtime/adapters/opencode-adapter.js +210 -0
- package/dist/runtime/agent-runtime.d.ts +286 -0
- package/dist/runtime/agent-runtime.js +8 -0
- package/dist/runtime/agent-state-machine.d.ts +67 -0
- package/dist/runtime/agent-state-machine.js +234 -0
- package/dist/runtime/backend-interface.d.ts +272 -0
- package/dist/runtime/backend-interface.js +38 -0
- package/dist/runtime/event-mapper.d.ts +11 -0
- package/dist/runtime/event-mapper.js +40 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +43 -0
- package/dist/runtime/local-session-runner.d.ts +60 -0
- package/dist/runtime/local-session-runner.js +185 -0
- package/dist/runtime/review.d.ts +24 -0
- package/dist/runtime/review.js +30 -0
- package/dist/runtime/verification-engine.d.ts +71 -0
- package/dist/runtime/verification-engine.js +374 -0
- package/dist/runtime/workspace-guard.d.ts +70 -0
- package/dist/runtime/workspace-guard.js +321 -0
- package/docs/local-coding-agents.md +183 -0
- package/package.json +22 -3
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace Guard - Capture, diff, and roll back project state.
|
|
4
|
+
*
|
|
5
|
+
* Lasso's non-negotiable review workflow requires:
|
|
6
|
+
* 1. Capture the original project state before an agent runs.
|
|
7
|
+
* 2. Collect resulting file changes after the agent runs.
|
|
8
|
+
* 3. Roll back to the original state so nothing is applied before review.
|
|
9
|
+
* 4. On approval, re-apply the changes; on rejection, leave the project untouched.
|
|
10
|
+
*
|
|
11
|
+
* The guard backs up every non-ignored file (in memory, or on disk for
|
|
12
|
+
* oversized files), computes a normalized diff against the captured state,
|
|
13
|
+
* and can restore the workspace exactly (content + permission bits + deletions).
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.WorkspaceGuard = void 0;
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
23
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
24
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
25
|
+
"node_modules",
|
|
26
|
+
".git",
|
|
27
|
+
".hg",
|
|
28
|
+
".svn",
|
|
29
|
+
".next",
|
|
30
|
+
".nuxt",
|
|
31
|
+
".output",
|
|
32
|
+
".svelte-kit",
|
|
33
|
+
".turbo",
|
|
34
|
+
".parcel-cache",
|
|
35
|
+
".cache",
|
|
36
|
+
".vercel",
|
|
37
|
+
".netlify",
|
|
38
|
+
".lasso",
|
|
39
|
+
".yarn",
|
|
40
|
+
".pnpm-store",
|
|
41
|
+
"dist",
|
|
42
|
+
"build",
|
|
43
|
+
"out",
|
|
44
|
+
"coverage",
|
|
45
|
+
"__pycache__",
|
|
46
|
+
".venv",
|
|
47
|
+
"venv",
|
|
48
|
+
"target",
|
|
49
|
+
".gradle",
|
|
50
|
+
".terraform",
|
|
51
|
+
]);
|
|
52
|
+
function hashBuffer(buf) {
|
|
53
|
+
return node_crypto_1.default.createHash("sha256").update(buf).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
function looksBinary(buf) {
|
|
56
|
+
const sample = buf.subarray(0, Math.min(buf.length, 8192));
|
|
57
|
+
return sample.includes(0);
|
|
58
|
+
}
|
|
59
|
+
class WorkspaceGuard {
|
|
60
|
+
root;
|
|
61
|
+
files = new Map();
|
|
62
|
+
skipped = [];
|
|
63
|
+
tempDir;
|
|
64
|
+
ignoreDirs;
|
|
65
|
+
ignoreFn;
|
|
66
|
+
tempDirCreated = false;
|
|
67
|
+
disposed = false;
|
|
68
|
+
constructor(root, ignoreDirs, ignoreFn) {
|
|
69
|
+
this.root = root;
|
|
70
|
+
this.ignoreDirs = ignoreDirs;
|
|
71
|
+
this.ignoreFn = ignoreFn;
|
|
72
|
+
this.tempDir = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-guard-${node_crypto_1.default.randomBytes(6).toString("hex")}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Capture the full current state of the workspace.
|
|
76
|
+
*/
|
|
77
|
+
static async capture(projectPath, options = {}) {
|
|
78
|
+
const root = node_path_1.default.resolve(projectPath);
|
|
79
|
+
if (!node_fs_1.default.existsSync(root) || !node_fs_1.default.statSync(root).isDirectory()) {
|
|
80
|
+
throw new Error(`Project path does not exist or is not a directory: ${root}`);
|
|
81
|
+
}
|
|
82
|
+
const ignoreDirs = new Set([...DEFAULT_IGNORE_DIRS, ...(options.ignoreDirs || [])]);
|
|
83
|
+
const maxMemory = options.maxMemoryFileSize ?? 4 * 1024 * 1024;
|
|
84
|
+
const maxDepth = options.maxDepth ?? 32;
|
|
85
|
+
const ignoreFn = options.ignore;
|
|
86
|
+
const guard = new WorkspaceGuard(root, ignoreDirs, ignoreFn);
|
|
87
|
+
const walk = (dir, relBase, depth) => {
|
|
88
|
+
if (depth > maxDepth)
|
|
89
|
+
return;
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const relPath = relBase ? node_path_1.default.join(relBase, entry.name) : entry.name;
|
|
99
|
+
const absPath = node_path_1.default.join(dir, entry.name);
|
|
100
|
+
if (entry.isSymbolicLink()) {
|
|
101
|
+
guard.skipped.push(relPath);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const isDir = entry.isDirectory();
|
|
105
|
+
if (isDir && ignoreDirs.has(entry.name))
|
|
106
|
+
continue;
|
|
107
|
+
if (ignoreFn && ignoreFn(relPath, isDir))
|
|
108
|
+
continue;
|
|
109
|
+
if (isDir) {
|
|
110
|
+
walk(absPath, relPath, depth + 1);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!entry.isFile()) {
|
|
114
|
+
guard.skipped.push(relPath);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const stat = node_fs_1.default.statSync(absPath);
|
|
119
|
+
let content = node_fs_1.default.readFileSync(absPath);
|
|
120
|
+
let tempPath;
|
|
121
|
+
if (content.length > maxMemory) {
|
|
122
|
+
// Oversized: keep a disk copy so rollback stays possible without holding it in RAM.
|
|
123
|
+
guard.ensureTempDir();
|
|
124
|
+
tempPath = node_path_1.default.join(guard.tempDir, relPath);
|
|
125
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(tempPath), { recursive: true });
|
|
126
|
+
node_fs_1.default.copyFileSync(absPath, tempPath);
|
|
127
|
+
content = null;
|
|
128
|
+
}
|
|
129
|
+
guard.files.set(relPath, {
|
|
130
|
+
absPath,
|
|
131
|
+
content,
|
|
132
|
+
tempPath,
|
|
133
|
+
hash: hashBuffer(node_fs_1.default.readFileSync(absPath)),
|
|
134
|
+
size: stat.size,
|
|
135
|
+
mode: stat.mode,
|
|
136
|
+
binary: content ? looksBinary(content) : false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
guard.skipped.push(relPath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
walk(root, "", 0);
|
|
145
|
+
return guard;
|
|
146
|
+
}
|
|
147
|
+
/** Number of files captured (excludes ignored/skipped entries). */
|
|
148
|
+
get fileCount() {
|
|
149
|
+
return this.files.size;
|
|
150
|
+
}
|
|
151
|
+
/** Paths that could not be captured (symlinks, unreadable entries). */
|
|
152
|
+
get skippedPaths() {
|
|
153
|
+
return [...this.skipped];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Compare the current on-disk state against the captured state.
|
|
157
|
+
* Must be called BEFORE restore() when collecting an agent's changes.
|
|
158
|
+
*/
|
|
159
|
+
diff() {
|
|
160
|
+
const changes = [];
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const [relPath, entry] of this.files) {
|
|
163
|
+
seen.add(relPath);
|
|
164
|
+
let stat = null;
|
|
165
|
+
try {
|
|
166
|
+
stat = node_fs_1.default.statSync(entry.absPath);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
stat = null;
|
|
170
|
+
}
|
|
171
|
+
if (!stat || !stat.isFile()) {
|
|
172
|
+
changes.push({ filePath: relPath, operation: "deleted", oldContent: entry.binary ? undefined : entry.content?.toString("utf8") });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let current;
|
|
176
|
+
try {
|
|
177
|
+
current = node_fs_1.default.readFileSync(entry.absPath);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (hashBuffer(current) === entry.hash)
|
|
183
|
+
continue;
|
|
184
|
+
if (entry.binary || looksBinary(current)) {
|
|
185
|
+
changes.push({ filePath: relPath, operation: "modified", binary: true });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
changes.push({
|
|
189
|
+
filePath: relPath,
|
|
190
|
+
operation: "modified",
|
|
191
|
+
oldContent: entry.content ? entry.content.toString("utf8") : undefined,
|
|
192
|
+
newContent: current.toString("utf8"),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Files that did not exist at capture time.
|
|
197
|
+
for (const change of this.listNewFiles(seen)) {
|
|
198
|
+
changes.push(change);
|
|
199
|
+
}
|
|
200
|
+
return changes;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Restore the workspace to the captured state:
|
|
204
|
+
* modified files are reverted, created files are deleted, deleted files are recreated.
|
|
205
|
+
*/
|
|
206
|
+
async restore() {
|
|
207
|
+
if (this.disposed)
|
|
208
|
+
return;
|
|
209
|
+
this.disposed = true;
|
|
210
|
+
for (const [relPath, entry] of this.files) {
|
|
211
|
+
let currentExists = false;
|
|
212
|
+
try {
|
|
213
|
+
currentExists = node_fs_1.default.statSync(entry.absPath).isFile();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
currentExists = false;
|
|
217
|
+
}
|
|
218
|
+
const desired = entry.content ?? (entry.tempPath ? node_fs_1.default.readFileSync(entry.tempPath) : null);
|
|
219
|
+
if (!desired)
|
|
220
|
+
continue;
|
|
221
|
+
try {
|
|
222
|
+
if (currentExists) {
|
|
223
|
+
const current = node_fs_1.default.readFileSync(entry.absPath);
|
|
224
|
+
if (hashBuffer(current) === hashBuffer(desired)) {
|
|
225
|
+
// Unchanged — still normalize permissions below.
|
|
226
|
+
node_fs_1.default.chmodSync(entry.absPath, entry.mode);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(entry.absPath), { recursive: true });
|
|
231
|
+
node_fs_1.default.writeFileSync(entry.absPath, desired);
|
|
232
|
+
node_fs_1.default.chmodSync(entry.absPath, entry.mode);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Best effort: continue restoring remaining files.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Delete files created during execution.
|
|
239
|
+
for (const change of this.listNewFiles(new Set(this.files.keys()))) {
|
|
240
|
+
try {
|
|
241
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(this.root, change.filePath));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Ignore — file may already be gone.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.cleanupTemp();
|
|
248
|
+
}
|
|
249
|
+
/** Restore without comparing (used by dispose paths). */
|
|
250
|
+
async dispose() {
|
|
251
|
+
if (!this.disposed) {
|
|
252
|
+
await this.restore();
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
this.cleanupTemp();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
listNewFiles(capturedPaths) {
|
|
259
|
+
const created = [];
|
|
260
|
+
const walk = (dir, relBase, depth) => {
|
|
261
|
+
if (depth > 40)
|
|
262
|
+
return;
|
|
263
|
+
let entries;
|
|
264
|
+
try {
|
|
265
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
const relPath = relBase ? node_path_1.default.join(relBase, entry.name) : entry.name;
|
|
272
|
+
const absPath = node_path_1.default.join(dir, entry.name);
|
|
273
|
+
if (entry.isSymbolicLink())
|
|
274
|
+
continue;
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
if (this.ignoreDirs.has(entry.name))
|
|
277
|
+
continue;
|
|
278
|
+
walk(absPath, relPath, depth + 1);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!entry.isFile())
|
|
282
|
+
continue;
|
|
283
|
+
if (this.ignoreFn && this.ignoreFn(relPath, false))
|
|
284
|
+
continue;
|
|
285
|
+
if (capturedPaths.has(relPath))
|
|
286
|
+
continue;
|
|
287
|
+
let binary = false;
|
|
288
|
+
let content;
|
|
289
|
+
try {
|
|
290
|
+
const buf = node_fs_1.default.readFileSync(absPath);
|
|
291
|
+
binary = looksBinary(buf);
|
|
292
|
+
content = binary ? undefined : buf.toString("utf8");
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
created.push(binary ? { filePath: relPath, operation: "created", binary: true } : { filePath: relPath, operation: "created", newContent: content });
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
walk(this.root, "", 0);
|
|
301
|
+
return created;
|
|
302
|
+
}
|
|
303
|
+
ensureTempDir() {
|
|
304
|
+
if (this.tempDirCreated)
|
|
305
|
+
return;
|
|
306
|
+
node_fs_1.default.mkdirSync(this.tempDir, { recursive: true });
|
|
307
|
+
this.tempDirCreated = true;
|
|
308
|
+
}
|
|
309
|
+
cleanupTemp() {
|
|
310
|
+
if (!this.tempDirCreated)
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
node_fs_1.default.rmSync(this.tempDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Temp cleanup is best effort.
|
|
317
|
+
}
|
|
318
|
+
this.tempDirCreated = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.WorkspaceGuard = WorkspaceGuard;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Local Coding Agents
|
|
2
|
+
|
|
3
|
+
Lasso can drive local coding agents — Claude Code, OpenCode, Codex CLI, and
|
|
4
|
+
Cursor Agent — with the same session management, progress events, review flow,
|
|
5
|
+
verification, and corrective runs as hosted models. Local agents run
|
|
6
|
+
headlessly against your project, but **nothing is applied until you review and
|
|
7
|
+
approve the diff**.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Local agents execute **on your machine, inside the `lasso` CLI process**
|
|
12
|
+
(`LocalRuntime`). Prompts, project context, and source code never travel
|
|
13
|
+
through Lasso's server — you can point the CLI at any server URL (or none)
|
|
14
|
+
and local agents keep working. Hosted models are unchanged: they still run
|
|
15
|
+
server-side through the Lasso runtime API and the Model Gateway.
|
|
16
|
+
|
|
17
|
+
Local agent lifecycle:
|
|
18
|
+
|
|
19
|
+
1. Before an agent runs, the CLI captures your workspace (a `WorkspaceGuard`
|
|
20
|
+
snapshot of every non-ignored file).
|
|
21
|
+
2. The agent's CLI is spawned in your project directory and its structured
|
|
22
|
+
output is normalized into runtime events (progress, tool calls, file
|
|
23
|
+
operations, completion).
|
|
24
|
+
3. After the run — success **or** failure — the workspace is rolled back to
|
|
25
|
+
the captured state. The guard's diff becomes the reviewable change set.
|
|
26
|
+
4. Approving a change re-applies it; discarding leaves the project untouched.
|
|
27
|
+
5. After apply, Lasso runs verification (typecheck/build, when configured).
|
|
28
|
+
Failures are fed back for an automatic corrective run (bounded attempts).
|
|
29
|
+
|
|
30
|
+
Because the agent writes into the real project directory, the guard's diff is
|
|
31
|
+
the single source of truth for what changed — agent-reported file lists are
|
|
32
|
+
never trusted on their own.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Local agents are detected per-machine by the CLI. Install only the ones you want:
|
|
37
|
+
|
|
38
|
+
| Agent | Provider id | Install |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Claude Code | `claude_code` | `npm install -g @anthropic-ai/claude-code` |
|
|
41
|
+
| OpenCode | `opencode` | `npm install -g opencode-ai` |
|
|
42
|
+
| Codex CLI | `codex_cli` | `npm install -g @openai/codex` |
|
|
43
|
+
| Cursor Agent | `cursor` | `curl https://cursor.com/install -fsS \| bash` |
|
|
44
|
+
|
|
45
|
+
The binary names the CLI probes: `claude`, `opencode`, `codex`, `cursor-agent`
|
|
46
|
+
(Cursor's CLI is `cursor-agent`, not `cursor`).
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
Each agent is authenticated with its own login flow. Lasso only *reads*
|
|
51
|
+
authentication state via each agent's structured status command:
|
|
52
|
+
|
|
53
|
+
- **Claude Code** — `claude auth status` (JSON `loggedIn`). Login: run `claude`
|
|
54
|
+
once and complete the flow, or `claude auth login`.
|
|
55
|
+
- **OpenCode** — `opencode providers list` (stored credentials marker). Login:
|
|
56
|
+
`opencode auth login` or configure a provider API key.
|
|
57
|
+
- **Codex CLI** — `codex login status` (exit code). Login: `codex login`
|
|
58
|
+
(ChatGPT account or API key).
|
|
59
|
+
- **Cursor Agent** — `cursor-agent status --format json` (JSON
|
|
60
|
+
`isAuthenticated`). Login: `cursor-agent login`.
|
|
61
|
+
|
|
62
|
+
An agent that is installed but not authenticated stays visible in the model
|
|
63
|
+
menu as **locked**, with the fix instructions shown inline.
|
|
64
|
+
|
|
65
|
+
Account limits (for example Claude "credit balance is too low" or Codex "usage
|
|
66
|
+
limit") are surfaced as authentication-class failures with a clear message —
|
|
67
|
+
they are not treated as code errors.
|
|
68
|
+
|
|
69
|
+
## Model selection
|
|
70
|
+
|
|
71
|
+
Model option ids are prefixed with the provider (`claude_code:sonnet`,
|
|
72
|
+
`opencode:big-pickle`, …). The CLI normalizes them for each agent:
|
|
73
|
+
|
|
74
|
+
- `claude_code:sonnet` → `sonnet` (also `opus`, `haiku`)
|
|
75
|
+
- `opencode:<name>` → `opencode/<name>` (lists real models via `opencode models`
|
|
76
|
+
when available)
|
|
77
|
+
- `codex_cli:default` → empty (the agent's own configured default model)
|
|
78
|
+
- `cursor:<id>` → passed through (e.g. `sonnet-4`)
|
|
79
|
+
|
|
80
|
+
An empty `modelId` means "use the agent's default".
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Local agents are selected via the backend config attached to a runtime session.
|
|
85
|
+
The CLI bridge builds this automatically from the overlay's model menu:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
{
|
|
89
|
+
type: "local_agent",
|
|
90
|
+
provider: "claude_code" | "opencode" | "codex_cli" | "cursor",
|
|
91
|
+
modelId: string, // normalized id, "" = agent default
|
|
92
|
+
projectPath: string, // must be the project root
|
|
93
|
+
permissionMode?: string // claude only, default "acceptEdits"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For programmatic use, `LocalRuntime` (from `@lasso-ai/cli`) implements the
|
|
98
|
+
same session surface as `RuntimeClient` — `createSession`, `runSession`,
|
|
99
|
+
`getSession`, `verifySession`, `switchModel`, `cancelSession` — but executes
|
|
100
|
+
in-process:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { getLocalRuntime } from "@lasso-ai/cli/local-runtime";
|
|
104
|
+
|
|
105
|
+
const runtime = getLocalRuntime();
|
|
106
|
+
const session = await runtime.createSession({
|
|
107
|
+
userPrompt: "Add a loading state to the submit button",
|
|
108
|
+
project: { projectId: "app", hasTypeScript: true },
|
|
109
|
+
model: { provider: "claude_code", id: "sonnet" },
|
|
110
|
+
backendConfig: {
|
|
111
|
+
type: "local_agent",
|
|
112
|
+
provider: "claude_code",
|
|
113
|
+
modelId: "sonnet",
|
|
114
|
+
projectPath: "/path/to/project",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
await runtime.runSession(session.id, "/path/to/project", (event) => {
|
|
118
|
+
console.log(event.type);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The shared runtime core (`WorkspaceGuard`, `VerificationEngine`,
|
|
123
|
+
`LocalSessionRunner`, adapters) is published as `@lasso-ai/cli/runtime` and is
|
|
124
|
+
the same code the Lasso server uses for its colocated Mode B deployments.
|
|
125
|
+
|
|
126
|
+
## Sessions
|
|
127
|
+
|
|
128
|
+
Local runtime sessions are **in-memory, per CLI process**: they live as long
|
|
129
|
+
as the `lasso` bridge process does and are gone when it exits. Review payloads
|
|
130
|
+
(`pendingChanges`) are persisted in session metadata and applied from the
|
|
131
|
+
review panel regardless. Hosted sessions continue to live server-side (and in
|
|
132
|
+
the server's MongoDB when configured).
|
|
133
|
+
|
|
134
|
+
## Limitations
|
|
135
|
+
|
|
136
|
+
- **Review-first is mandatory.** There is no "apply silently" mode; every run
|
|
137
|
+
ends in a review (or a no-change completion).
|
|
138
|
+
- **Ephemeral local sessions.** Local agent sessions do not survive a CLI
|
|
139
|
+
restart; hosted sessions do.
|
|
140
|
+
- **Verification is best-effort.** Typecheck runs only when a `tsconfig.json`
|
|
141
|
+
exists; build only when `package.json` has a `build` script. Missing
|
|
142
|
+
tooling is reported as *skipped* (inconclusive), never as a pass or a
|
|
143
|
+
failure.
|
|
144
|
+
- **Session continuation varies.** Claude Code (`--resume`), Codex
|
|
145
|
+
(`exec resume`), and Cursor (`--resume`) can continue their own external
|
|
146
|
+
sessions; OpenCode is re-driven with conversation context each run. Agent
|
|
147
|
+
internal tool-call state is not portable across backends — switching agents
|
|
148
|
+
mid-task transfers conversation/observations/errors only.
|
|
149
|
+
- **Cancellation** sends SIGTERM (then SIGKILL after 3s) to the agent process.
|
|
150
|
+
In-flight tool calls are not undone by the agent itself; the workspace guard
|
|
151
|
+
rollback is what keeps the project clean.
|
|
152
|
+
- **Permissions.** Claude runs with `--permission-mode acceptEdits`; Codex runs
|
|
153
|
+
with `sandbox_mode="workspace-write"`; agents are instructed not to commit,
|
|
154
|
+
push, or act outside the project directory.
|
|
155
|
+
|
|
156
|
+
## Troubleshooting
|
|
157
|
+
|
|
158
|
+
| Symptom | Likely cause | Fix |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| Backend locked: "not installed" | Binary not on your `PATH` | Install the CLI globally; restart the `lasso` CLI so it picks up `PATH` changes |
|
|
161
|
+
| Backend locked: "not logged in" | Missing agent credentials | Run the agent's login command listed in the fix instructions |
|
|
162
|
+
| "Credit balance is too low" / "usage limit" | Provider account limits | Top up or wait for the limit window; the run is failed cleanly with no partial apply |
|
|
163
|
+
| "finished without reporting a successful result" | Agent exited 0 without a terminal success event | Usually a CLI version mismatch — update the agent |
|
|
164
|
+
| Changes appear, then vanish before review | Expected: workspace is rolled back until you approve | Approve in the review panel to apply |
|
|
165
|
+
| Local agent run works, hosted models don't | Hosted models need the Lasso server API | Check the CLI's server URL / API key; local agents are unaffected |
|
|
166
|
+
|
|
167
|
+
## API
|
|
168
|
+
|
|
169
|
+
Local agents do not require any server endpoints — the CLI runs them
|
|
170
|
+
directly. The server API remains for hosted models and backend discovery:
|
|
171
|
+
|
|
172
|
+
- `GET /api/v1/agent-runtime/backends` — probe hosted providers and (for
|
|
173
|
+
colocated deployments) local agents.
|
|
174
|
+
- `POST /api/v1/agent-runtime/sessions` — create a hosted-model session (or a
|
|
175
|
+
colocated local-agent session on self-hosted deployments).
|
|
176
|
+
- `POST /api/v1/agent-runtime/sessions/:id/run` — run (also used for
|
|
177
|
+
corrective re-runs).
|
|
178
|
+
- `POST /api/v1/agent-runtime/sessions/:id/verify` — post-apply verification;
|
|
179
|
+
returns `{ passed, errors, observations, command, requiresFix }`.
|
|
180
|
+
- `POST /api/v1/agent-runtime/sessions/:id/model` — switch hosted model or
|
|
181
|
+
local agent mid-session; reports `transferableContext` / `lostContext`.
|
|
182
|
+
- `POST /api/v1/agent-runtime/sessions/:id/cancel` — abort the running agent
|
|
183
|
+
and restore the workspace.
|
package/package.json
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lasso-ai/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/cli/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/cli/index.d.ts",
|
|
10
|
+
"default": "./dist/cli/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./runtime": {
|
|
13
|
+
"types": "./dist/runtime/index.d.ts",
|
|
14
|
+
"default": "./dist/runtime/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./local-runtime": {
|
|
17
|
+
"types": "./dist/cli/local-runtime.d.ts",
|
|
18
|
+
"default": "./dist/cli/local-runtime.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
7
22
|
"bin": {
|
|
8
23
|
"lasso": "dist/cli/index.js"
|
|
9
24
|
},
|
|
@@ -15,7 +30,8 @@
|
|
|
15
30
|
"CONTRIBUTING.md",
|
|
16
31
|
"SECURITY.md",
|
|
17
32
|
"SUPPORT.md",
|
|
18
|
-
"docs/PUBLISHING.md"
|
|
33
|
+
"docs/PUBLISHING.md",
|
|
34
|
+
"docs/local-coding-agents.md"
|
|
19
35
|
],
|
|
20
36
|
"engines": {
|
|
21
37
|
"node": ">=18.0.0"
|
|
@@ -33,7 +49,7 @@
|
|
|
33
49
|
"format": "prettier --write .",
|
|
34
50
|
"deploy": "pnpm build && npm publish --access public",
|
|
35
51
|
"format:check": "prettier --check .",
|
|
36
|
-
"test": "
|
|
52
|
+
"test": "node --import tsx --test src/runtime/__tests__/*.test.ts"
|
|
37
53
|
},
|
|
38
54
|
"keywords": [
|
|
39
55
|
"ai",
|
|
@@ -105,5 +121,8 @@
|
|
|
105
121
|
"tsx": "^4.23.13",
|
|
106
122
|
"typescript": "^7.0.2",
|
|
107
123
|
"vite": "^8.3.0"
|
|
124
|
+
},
|
|
125
|
+
"overrides": {
|
|
126
|
+
"@lasso-ai/cli": "link:"
|
|
108
127
|
}
|
|
109
128
|
}
|