@lasso-ai/cli 2.1.14 → 2.1.16

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.
@@ -186,7 +186,13 @@ function loadFixtureLines(name) {
186
186
  adapter.parse(JSON.stringify({ type: "item.completed", item: { item_type: "command_execution", command: "ls", exit_code: 0, status: "completed" } }), ctx, sink);
187
187
  adapter.parse(JSON.stringify({
188
188
  type: "item.completed",
189
- item: { item_type: "file_change", changes: [{ path: "/proj/src/old.ts", kind: "delete" }, { path: "/proj/src/new.ts", kind: "add" }] },
189
+ item: {
190
+ item_type: "file_change",
191
+ changes: [
192
+ { path: "/proj/src/old.ts", kind: "delete" },
193
+ { path: "/proj/src/new.ts", kind: "add" },
194
+ ],
195
+ },
190
196
  }), ctx, sink);
191
197
  adapter.parse(JSON.stringify({ type: "turn.completed" }), ctx, sink);
192
198
  strict_1.default.equal(state.sessionId, "thr-1");
@@ -212,12 +212,7 @@ class ActionExecutor {
212
212
  };
213
213
  }
214
214
  // Safety check: prevent dangerous commands
215
- const dangerousPatterns = [
216
- /\brm\s+-rf\b/,
217
- /\bdd\s+if=/,
218
- />\s*\/dev\/null/,
219
- />\s*\/dev\/sd/,
220
- ];
215
+ const dangerousPatterns = [/\brm\s+-rf\b/, /\bdd\s+if=/, />\s*\/dev\/null/, />\s*\/dev\/sd/];
221
216
  for (const pattern of dangerousPatterns) {
222
217
  if (pattern.test(command)) {
223
218
  return {
@@ -366,9 +361,7 @@ class ActionExecutor {
366
361
  * boundary: every resolved path must stay inside projectPath.
367
362
  */
368
363
  resolvePath(filePath) {
369
- const resolved = node_path_1.default.isAbsolute(filePath)
370
- ? node_path_1.default.resolve(filePath)
371
- : node_path_1.default.resolve(this.projectPath, filePath);
364
+ const resolved = node_path_1.default.isAbsolute(filePath) ? node_path_1.default.resolve(filePath) : node_path_1.default.resolve(this.projectPath, filePath);
372
365
  const root = node_path_1.default.resolve(this.projectPath);
373
366
  if (resolved !== root && !resolved.startsWith(root + node_path_1.default.sep)) {
374
367
  throw new Error(`Path escapes workspace: ${filePath}`);
@@ -388,12 +381,7 @@ class ActionExecutor {
388
381
  for (const entry of entries) {
389
382
  const fullPath = node_path_1.default.join(currentDir, entry.name);
390
383
  // Skip common ignored directories
391
- if (entry.name === "node_modules" ||
392
- entry.name === ".git" ||
393
- entry.name === "dist" ||
394
- entry.name === "build" ||
395
- entry.name === ".next" ||
396
- entry.name === ".lasso") {
384
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === "build" || entry.name === ".next" || entry.name === ".lasso") {
397
385
  continue;
398
386
  }
399
387
  if (entry.isDirectory()) {
@@ -67,9 +67,7 @@ class ClaudeCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
67
67
  try {
68
68
  const parsed = JSON.parse(stdout.slice(jsonStart));
69
69
  if (parsed && typeof parsed.loggedIn === "boolean") {
70
- return parsed.loggedIn
71
- ? { authenticated: true }
72
- : { authenticated: false, error: "Claude Code is not logged in.", fixInstructions: "Run 'claude' once and log in, or run 'claude auth login'." };
70
+ return parsed.loggedIn ? { authenticated: true } : { authenticated: false, error: "Claude Code is not logged in.", fixInstructions: "Run 'claude' once and log in, or run 'claude auth login'." };
73
71
  }
74
72
  }
75
73
  catch {
@@ -91,15 +89,7 @@ class ClaudeCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
91
89
  const cfg = this.config;
92
90
  const resumeId = this.resumeSessionId(context);
93
91
  const sessionId = node_crypto_1.default.randomUUID();
94
- const args = [
95
- "-p",
96
- this.buildTaskPrompt(context),
97
- "--output-format",
98
- "stream-json",
99
- "--verbose",
100
- "--permission-mode",
101
- cfg.permissionMode || "acceptEdits",
102
- ];
92
+ const args = ["-p", this.buildTaskPrompt(context), "--output-format", "stream-json", "--verbose", "--permission-mode", cfg.permissionMode || "acceptEdits"];
103
93
  if (this.modelId)
104
94
  args.push("--model", this.modelId);
105
95
  if (resumeId)
@@ -160,7 +150,10 @@ class ClaudeCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
160
150
  handleAssistant(event, sink) {
161
151
  const content = event.message?.content || [];
162
152
  if (event.error) {
163
- const text = content.map((c) => c.text || "").filter(Boolean).join(" ");
153
+ const text = content
154
+ .map((c) => c.text || "")
155
+ .filter(Boolean)
156
+ .join(" ");
164
157
  sink.state.failureMessage = text || `Claude Code error: ${event.error}`;
165
158
  if (/billing|auth|unauthorized|credit/i.test(event.error) || /credit balance|not logged in|unauthorized/i.test(sink.state.failureMessage)) {
166
159
  sink.state.authFailure = true;
@@ -199,11 +199,7 @@ class CodexCLIAdapter extends local_agent_adapter_1.LocalAgentAdapter {
199
199
  return error;
200
200
  if (typeof error === "object") {
201
201
  const value = error;
202
- const nested = [
203
- value.message,
204
- value.data?.message,
205
- value.cause?.message,
206
- ].find((item) => typeof item === "string" && item.trim().length > 0);
202
+ const nested = [value.message, value.data?.message, value.cause?.message].find((item) => typeof item === "string" && item.trim().length > 0);
207
203
  if (nested)
208
204
  return nested;
209
205
  try {
@@ -230,11 +230,7 @@ class LocalAgentAdapter {
230
230
  sink.emit("task_completed", { summary: state.summary });
231
231
  }
232
232
  else {
233
- const message = state.failureMessage ||
234
- inspected?.message ||
235
- (exitCode === 0
236
- ? `${this.provider} finished without reporting a successful result.`
237
- : `${this.provider} exited with code ${exitCode}${stderrBuffer ? `: ${stderrBuffer.trim().slice(-500)}` : ""}`);
233
+ const message = state.failureMessage || inspected?.message || (exitCode === 0 ? `${this.provider} finished without reporting a successful result.` : `${this.provider} exited with code ${exitCode}${stderrBuffer ? `: ${stderrBuffer.trim().slice(-500)}` : ""}`);
238
234
  if (!state.errors.some((e) => e.message === message)) {
239
235
  sink.error(message, { recoverable: false, authFailure: state.authFailure });
240
236
  }
@@ -277,14 +273,8 @@ class LocalAgentAdapter {
277
273
  }
278
274
  async switchBackend(newBackend, context) {
279
275
  const lostContext = this.supportsResume
280
- ? [
281
- `${this.provider} maintains its own internal session state that cannot be transferred to ${newBackend.provider}.`,
282
- "Tool call history and per-file context held by the previous agent are not portable.",
283
- ]
284
- : [
285
- `${this.provider} does not expose resumable session state for transfer.`,
286
- "Tool call history and per-file context held by the previous agent are not portable.",
287
- ];
276
+ ? [`${this.provider} maintains its own internal session state that cannot be transferred to ${newBackend.provider}.`, "Tool call history and per-file context held by the previous agent are not portable."]
277
+ : [`${this.provider} does not expose resumable session state for transfer.`, "Tool call history and per-file context held by the previous agent are not portable."];
288
278
  return {
289
279
  transferableContext: {
290
280
  conversation: context.conversation,
@@ -314,7 +304,10 @@ class LocalAgentAdapter {
314
304
  // Retry/continuation replies ("try again", "continue", …) must resume
315
305
  // the previous request instead of being treated as a literal task —
316
306
  // otherwise the agent greps for words like "again".
317
- const normalizedPrompt = context.userPrompt.trim().replace(/[.!]+$/, "").toLowerCase();
307
+ const normalizedPrompt = context.userPrompt
308
+ .trim()
309
+ .replace(/[.!]+$/, "")
310
+ .toLowerCase();
318
311
  const isContinuation = /^(continue|proceed|go on|go ahead|keep going|yes|yep|yup|sure|ok|okay|alright|do it|carry on|resume|next|try again|again|retry|re-try|do it again|run again|run it again|one more time|same again)$/.test(normalizedPrompt) ||
319
312
  /^(try|do|run|go)\s+(it\s+|that\s+)?again$|^one more time$|^retry(\s+it)?$/i.test(normalizedPrompt);
320
313
  if (isContinuation) {
@@ -57,9 +57,7 @@ class OpenCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
57
57
  timeout: 12000,
58
58
  windowsHide: true,
59
59
  }));
60
- const text = `${stdout}\n${stderr}`
61
- .replace(/\u001b\[[0-9;]*m/g, "")
62
- .replace(/\u001b\][^\u0007]*\u0007/g, "");
60
+ const text = `${stdout}\n${stderr}`.replace(/\u001b\[[0-9;]*m/g, "").replace(/\u001b\][^\u0007]*\u0007/g, "");
63
61
  // The credentials section lists a "●" bullet per configured credential.
64
62
  const credentialsStart = text.indexOf("Credentials");
65
63
  const environmentStart = text.indexOf("Environment");
@@ -95,9 +93,7 @@ class OpenCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
95
93
  if (resumeId)
96
94
  args.push("--session", resumeId);
97
95
  args.push(this.buildTaskPrompt(context));
98
- const env = context.mode === "conversation"
99
- ? { OPENCODE_PERMISSION: JSON.stringify({ "*": "deny" }) }
100
- : undefined;
96
+ const env = context.mode === "conversation" ? { OPENCODE_PERMISSION: JSON.stringify({ "*": "deny" }) } : undefined;
101
97
  return { bin: "opencode", args, cwd: this.config.projectPath, env };
102
98
  }
103
99
  handleLine(line, context, sink) {
@@ -172,12 +168,7 @@ class OpenCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
172
168
  return error;
173
169
  if (typeof error === "object") {
174
170
  const e = error;
175
- const nested = [
176
- e.message,
177
- e.data?.message,
178
- e.data?.error,
179
- e.cause?.message,
180
- ].find((item) => typeof item === "string" && item.trim().length > 0);
171
+ const nested = [e.message, e.data?.message, e.data?.error, e.cause?.message].find((item) => typeof item === "string" && item.trim().length > 0);
181
172
  if (nested)
182
173
  return nested;
183
174
  try {
@@ -195,10 +195,12 @@ export interface LassoModelResponse {
195
195
  raw?: string;
196
196
  };
197
197
  decision: {
198
- type: "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
198
+ type: "respond" | "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
199
199
  goal: string;
200
200
  };
201
201
  actions: AgentAction[];
202
+ /** Direct conversational answer when no project work is requested. */
203
+ reply?: string;
202
204
  expectedOutcome?: string;
203
205
  }
204
206
  export type AgentEventType = "agent_started" | "agent_reasoning" | "agent_completed" | "agent_failed" | "agent_cancelled" | "phase_changed" | "tool_started" | "tool_completed" | "tool_call" | "observation" | "plan_created" | "file_changed" | "verification_started" | "verification_completed" | "error" | "model_changed" | "waiting_for_user";
@@ -185,9 +185,7 @@ class AgentStateMachine {
185
185
  * Check if the session can accept new actions
186
186
  */
187
187
  static canAcceptActions(session) {
188
- return (session.status === "running" &&
189
- session.phase !== "complete" &&
190
- session.phase !== "understand");
188
+ return session.status === "running" && session.phase !== "complete" && session.phase !== "understand";
191
189
  }
192
190
  /**
193
191
  * Check if the session should trigger verification
@@ -203,8 +201,7 @@ class AgentStateMachine {
203
201
  * Check if the session should trigger recovery
204
202
  */
205
203
  static shouldRecover(session) {
206
- return (session.phase === "verify" &&
207
- session.errors.filter((e) => e.phase === "verify").length > 0);
204
+ return session.phase === "verify" && session.errors.filter((e) => e.phase === "verify").length > 0;
208
205
  }
209
206
  /**
210
207
  * Check if the session can switch models
@@ -13,18 +13,14 @@ function diffToReview(rawChanges) {
13
13
  const applyable = rawChanges.filter((c) => !c.binary);
14
14
  const changes = applyable.map((change) => ({
15
15
  filePath: change.filePath,
16
- oldString: change.operation === "created" ? "" : change.oldContent ?? "",
17
- newString: change.operation === "deleted" ? "" : change.newContent ?? "",
16
+ oldString: change.operation === "created" ? "" : (change.oldContent ?? ""),
17
+ newString: change.operation === "deleted" ? "" : (change.newContent ?? ""),
18
18
  operation: change.operation,
19
19
  }));
20
20
  const actions = applyable.map((change) => ({
21
21
  id: crypto.randomUUID(),
22
22
  type: change.operation === "created" ? "create_file" : change.operation === "deleted" ? "delete_file" : "edit_file",
23
- data: change.operation === "created"
24
- ? { filePath: change.filePath, content: change.newContent }
25
- : change.operation === "deleted"
26
- ? { filePath: change.filePath }
27
- : { filePath: change.filePath, oldString: change.oldContent ?? "", newString: change.newContent ?? "" },
23
+ data: change.operation === "created" ? { filePath: change.filePath, content: change.newContent } : change.operation === "deleted" ? { filePath: change.filePath } : { filePath: change.filePath, oldString: change.oldContent ?? "", newString: change.newContent ?? "" },
28
24
  }));
29
25
  return { changes, binaryPaths, actions };
30
26
  }
@@ -10,6 +10,14 @@ export interface VerificationConfig {
10
10
  buildTimeout: number;
11
11
  devServerTimeout: number;
12
12
  browserTimeout: number;
13
+ /** URL of the project's already-running dev server, if available. */
14
+ devServerUrl?: string;
15
+ /** Optional browser bridge used for DOM/runtime verification. */
16
+ browserContextProvider?: (action: {
17
+ id: string;
18
+ type: "browser_check";
19
+ data?: Record<string, unknown>;
20
+ }) => Promise<unknown>;
13
21
  }
14
22
  export interface VerificationResult {
15
23
  type: "typecheck" | "build" | "dev_server" | "browser";
@@ -68,4 +76,6 @@ export declare class VerificationEngine {
68
76
  private hasTypeScriptConfig;
69
77
  /** package.json with a non-empty build script. */
70
78
  private hasBuildScript;
79
+ /** Fetch a health URL with a portable timeout for supported Node versions. */
80
+ private fetchWithTimeout;
71
81
  }
@@ -19,6 +19,8 @@ class VerificationEngine {
19
19
  buildTimeout: config?.buildTimeout || 120000,
20
20
  devServerTimeout: config?.devServerTimeout || 30000,
21
21
  browserTimeout: config?.browserTimeout || 60000,
22
+ devServerUrl: config?.devServerUrl,
23
+ browserContextProvider: config?.browserContextProvider,
22
24
  };
23
25
  }
24
26
  /**
@@ -43,7 +45,6 @@ class VerificationEngine {
43
45
  // Dev server health
44
46
  const devServerResult = await this.verifyDevServer(phase);
45
47
  results.push(devServerResult);
46
- // Browser check (placeholder)
47
48
  const browserResult = await this.verifyBrowser(phase);
48
49
  results.push(browserResult);
49
50
  return results;
@@ -283,23 +284,48 @@ class VerificationEngine {
283
284
  const startTime = Date.now();
284
285
  const observations = [];
285
286
  const errors = [];
286
- // TODO: Implement actual dev server health check
287
- // For now, just check if a dev server is likely running
288
- observations.push({
289
- id: this.generateId(),
290
- type: "build",
291
- summary: "Dev server health check skipped (not implemented)",
292
- data: { type: "dev_server", success: true },
293
- timestamp: Date.now(),
294
- phase,
295
- });
296
- return {
297
- type: "dev_server",
298
- success: true,
299
- observations,
300
- errors,
301
- duration: Date.now() - startTime,
302
- };
287
+ const url = this.config.devServerUrl;
288
+ if (!url) {
289
+ observations.push({
290
+ id: this.generateId(),
291
+ type: "build",
292
+ summary: "Dev server health check skipped (no devServerUrl configured)",
293
+ data: { type: "dev_server", success: true, skipped: true },
294
+ timestamp: Date.now(),
295
+ phase,
296
+ });
297
+ return { type: "dev_server", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
298
+ }
299
+ try {
300
+ const response = await this.fetchWithTimeout(url, this.config.devServerTimeout);
301
+ const success = response.status >= 200 && response.status < 400;
302
+ const summary = success ? `Dev server responded with HTTP ${response.status}` : `Dev server returned HTTP ${response.status}`;
303
+ observations.push({
304
+ id: this.generateId(),
305
+ type: "build",
306
+ summary,
307
+ data: { type: "dev_server", success, url, status: response.status },
308
+ timestamp: Date.now(),
309
+ phase,
310
+ });
311
+ if (!success) {
312
+ errors.push({
313
+ id: this.generateId(),
314
+ type: "verification",
315
+ message: summary,
316
+ phase,
317
+ timestamp: Date.now(),
318
+ recoverable: true,
319
+ });
320
+ }
321
+ return { type: "dev_server", success, command: `GET ${url}`, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
322
+ }
323
+ catch (error) {
324
+ const message = `Dev server health check failed: ${error?.message || String(error)}`;
325
+ observations.push({ id: this.generateId(), type: "build", summary: message, data: { type: "dev_server", success: false, url }, timestamp: Date.now(), phase });
326
+ errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
327
+ return { type: "dev_server", success: false, command: `GET ${url}`, error: message, observations, errors, duration: Date.now() - startTime };
328
+ }
303
329
  }
304
330
  /**
305
331
  * Verify browser/runtime state
@@ -308,23 +334,34 @@ class VerificationEngine {
308
334
  const startTime = Date.now();
309
335
  const observations = [];
310
336
  const errors = [];
311
- // TODO: Implement actual browser checking via Puppeteer or similar
312
- // For now, this is a placeholder
313
- observations.push({
314
- id: this.generateId(),
315
- type: "browser",
316
- summary: "Browser check skipped (not implemented)",
317
- data: { type: "browser", success: true },
318
- timestamp: Date.now(),
319
- phase,
320
- });
321
- return {
322
- type: "browser",
323
- success: true,
324
- observations,
325
- errors,
326
- duration: Date.now() - startTime,
327
- };
337
+ const provider = this.config.browserContextProvider;
338
+ if (!provider) {
339
+ observations.push({
340
+ id: this.generateId(),
341
+ type: "browser",
342
+ summary: "Browser verification skipped (no browser bridge configured)",
343
+ data: { type: "browser", success: true, skipped: true },
344
+ timestamp: Date.now(),
345
+ phase,
346
+ });
347
+ return { type: "browser", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
348
+ }
349
+ try {
350
+ const result = await Promise.race([provider({ id: this.generateId(), type: "browser_check", data: { phase } }), new Promise((_, reject) => setTimeout(() => reject(new Error("Browser verification timed out")), this.config.browserTimeout))]);
351
+ const details = result && typeof result === "object" ? result : {};
352
+ const success = details.success !== false;
353
+ const summary = typeof details.summary === "string" ? details.summary : success ? "Browser check passed" : "Browser check failed";
354
+ observations.push({ id: this.generateId(), type: "browser", summary, data: { type: "browser", success, result }, timestamp: Date.now(), phase });
355
+ if (!success)
356
+ errors.push({ id: this.generateId(), type: "verification", message: summary, phase, timestamp: Date.now(), recoverable: true });
357
+ return { type: "browser", success, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
358
+ }
359
+ catch (error) {
360
+ const message = `Browser verification failed: ${error?.message || String(error)}`;
361
+ observations.push({ id: this.generateId(), type: "browser", summary: message, data: { type: "browser", success: false }, timestamp: Date.now(), phase });
362
+ errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
363
+ return { type: "browser", success: false, error: message, observations, errors, duration: Date.now() - startTime };
364
+ }
328
365
  }
329
366
  /**
330
367
  * Check if verification passed overall
@@ -370,5 +407,16 @@ class VerificationEngine {
370
407
  return false;
371
408
  }
372
409
  }
410
+ /** Fetch a health URL with a portable timeout for supported Node versions. */
411
+ async fetchWithTimeout(url, timeoutMs) {
412
+ const controller = new AbortController();
413
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
414
+ try {
415
+ return await fetch(url, { signal: controller.signal, redirect: "manual" });
416
+ }
417
+ finally {
418
+ clearTimeout(timer);
419
+ }
420
+ }
373
421
  }
374
422
  exports.VerificationEngine = VerificationEngine;
@@ -49,12 +49,7 @@ const DEFAULT_IGNORE_DIRS = new Set([
49
49
  ".gradle",
50
50
  ".terraform",
51
51
  ]);
52
- const DEFAULT_IGNORE_FILES = new Set([
53
- "next-env.d.ts",
54
- "tsconfig.tsbuildinfo",
55
- ".DS_Store",
56
- "Thumbs.db",
57
- ]);
52
+ const DEFAULT_IGNORE_FILES = new Set(["next-env.d.ts", "tsconfig.tsbuildinfo", ".DS_Store", "Thumbs.db"]);
58
53
  function shouldIgnoreFile(name) {
59
54
  return DEFAULT_IGNORE_FILES.has(name) || name.endsWith(".tsbuildinfo");
60
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
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",
@@ -46,9 +46,10 @@
46
46
  "typecheck:overlay": "tsc -p config/tsconfig.overlay.json --noEmit",
47
47
  "start": "node dist/cli/index.js",
48
48
  "prepublishOnly": "npm run build",
49
- "format": "prettier --write .",
49
+ "format": "prettier --write src config",
50
50
  "deploy": "pnpm build && npm publish --access public",
51
- "format:check": "prettier --check .",
51
+ "format:check": "prettier --check src config",
52
+ "lint": "npm run typecheck && npm run format:check",
52
53
  "test": "node --import tsx --test src/runtime/__tests__/*.test.ts"
53
54
  },
54
55
  "keywords": [