@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.
Files changed (52) hide show
  1. package/dist/cli/agent.d.ts +1 -0
  2. package/dist/cli/backends.d.ts +53 -0
  3. package/dist/cli/backends.js +267 -0
  4. package/dist/cli/bridge.d.ts +8 -5
  5. package/dist/cli/bridge.js +357 -151
  6. package/dist/cli/host/install.js +12 -1
  7. package/dist/cli/local-runtime.d.ts +28 -0
  8. package/dist/cli/local-runtime.js +294 -0
  9. package/dist/cli/oneshot.js +28 -10
  10. package/dist/cli/runtime-client.d.ts +91 -10
  11. package/dist/cli/runtime-client.js +38 -3
  12. package/dist/overlay.js +44 -28
  13. package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
  14. package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
  15. package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
  16. package/dist/runtime/__tests__/state-machine.test.js +107 -0
  17. package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
  18. package/dist/runtime/__tests__/verification-engine.test.js +91 -0
  19. package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
  20. package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
  21. package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
  22. package/dist/runtime/adapters/claude-code-adapter.js +230 -0
  23. package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
  24. package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
  25. package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
  26. package/dist/runtime/adapters/cursor-adapter.js +214 -0
  27. package/dist/runtime/adapters/factory.d.ts +19 -0
  28. package/dist/runtime/adapters/factory.js +48 -0
  29. package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
  30. package/dist/runtime/adapters/local-agent-adapter.js +408 -0
  31. package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
  32. package/dist/runtime/adapters/opencode-adapter.js +210 -0
  33. package/dist/runtime/agent-runtime.d.ts +286 -0
  34. package/dist/runtime/agent-runtime.js +8 -0
  35. package/dist/runtime/agent-state-machine.d.ts +67 -0
  36. package/dist/runtime/agent-state-machine.js +234 -0
  37. package/dist/runtime/backend-interface.d.ts +272 -0
  38. package/dist/runtime/backend-interface.js +38 -0
  39. package/dist/runtime/event-mapper.d.ts +11 -0
  40. package/dist/runtime/event-mapper.js +40 -0
  41. package/dist/runtime/index.d.ts +20 -0
  42. package/dist/runtime/index.js +43 -0
  43. package/dist/runtime/local-session-runner.d.ts +60 -0
  44. package/dist/runtime/local-session-runner.js +185 -0
  45. package/dist/runtime/review.d.ts +24 -0
  46. package/dist/runtime/review.js +30 -0
  47. package/dist/runtime/verification-engine.d.ts +71 -0
  48. package/dist/runtime/verification-engine.js +374 -0
  49. package/dist/runtime/workspace-guard.d.ts +70 -0
  50. package/dist/runtime/workspace-guard.js +321 -0
  51. package/docs/local-coding-agents.md +183 -0
  52. package/package.json +22 -3
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WorkspaceGuard diff → review payload normalization.
3
+ *
4
+ * Shared by the server orchestrator and the CLI LocalRuntime: the guard diff
5
+ * is the authoritative record of what an agent changed, normalized into the
6
+ * pendingChanges contract consumed by the review UI and the apply flow.
7
+ */
8
+ import { FileChange } from "./workspace-guard";
9
+ import { AgentAction } from "./agent-runtime";
10
+ export interface PendingChange {
11
+ filePath: string;
12
+ oldString: string;
13
+ newString: string;
14
+ operation: "created" | "modified" | "deleted";
15
+ }
16
+ export interface WorkspaceReview {
17
+ /** Reviewable (text) changes in the wire format used by the review UI. */
18
+ changes: PendingChange[];
19
+ /** Paths of binary changes — preserved for rollback, not shown as text. */
20
+ binaryPaths: string[];
21
+ /** Normalized session actions matching the reviewable changes. */
22
+ actions: AgentAction[];
23
+ }
24
+ export declare function diffToReview(rawChanges: FileChange[]): WorkspaceReview;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * WorkspaceGuard diff → review payload normalization.
4
+ *
5
+ * Shared by the server orchestrator and the CLI LocalRuntime: the guard diff
6
+ * is the authoritative record of what an agent changed, normalized into the
7
+ * pendingChanges contract consumed by the review UI and the apply flow.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.diffToReview = diffToReview;
11
+ function diffToReview(rawChanges) {
12
+ const binaryPaths = rawChanges.filter((c) => c.binary).map((c) => c.filePath);
13
+ const applyable = rawChanges.filter((c) => !c.binary);
14
+ const changes = applyable.map((change) => ({
15
+ filePath: change.filePath,
16
+ oldString: change.operation === "created" ? "" : change.oldContent ?? "",
17
+ newString: change.operation === "deleted" ? "" : change.newContent ?? "",
18
+ operation: change.operation,
19
+ }));
20
+ const actions = applyable.map((change) => ({
21
+ id: crypto.randomUUID(),
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 ?? "" },
28
+ }));
29
+ return { changes, binaryPaths, actions };
30
+ }
@@ -0,0 +1,71 @@
1
+ import { Observation, AgentError, AgentPhase } from "./agent-runtime";
2
+ /**
3
+ * Verification Engine - Verifies changes after execution
4
+ *
5
+ * Mandatory verification step in the agent loop.
6
+ * Timeouts are failures or inconclusive observations, never proof of success.
7
+ */
8
+ export interface VerificationConfig {
9
+ typecheckTimeout: number;
10
+ buildTimeout: number;
11
+ devServerTimeout: number;
12
+ browserTimeout: number;
13
+ }
14
+ export interface VerificationResult {
15
+ type: "typecheck" | "build" | "dev_server" | "browser";
16
+ success: boolean;
17
+ /** Set when the check could not meaningfully run (no tsconfig, no build script). */
18
+ skipped?: boolean;
19
+ /** Command that produced this result, when applicable. */
20
+ command?: string;
21
+ output?: string;
22
+ error?: string;
23
+ observations: Observation[];
24
+ errors: AgentError[];
25
+ duration: number;
26
+ }
27
+ export declare class VerificationEngine {
28
+ private projectPath;
29
+ private config;
30
+ constructor(projectPath: string, config?: Partial<VerificationConfig>);
31
+ /**
32
+ * Run all verifications
33
+ */
34
+ verifyAll(phase: AgentPhase): Promise<VerificationResult[]>;
35
+ /**
36
+ * Verify TypeScript type checking
37
+ */
38
+ verifyTypecheck(phase: AgentPhase): Promise<VerificationResult>;
39
+ /**
40
+ * Verify build
41
+ */
42
+ verifyBuild(phase: AgentPhase): Promise<VerificationResult>;
43
+ /**
44
+ * Verify dev server health
45
+ */
46
+ verifyDevServer(phase: AgentPhase): Promise<VerificationResult>;
47
+ /**
48
+ * Verify browser/runtime state
49
+ */
50
+ verifyBrowser(phase: AgentPhase): Promise<VerificationResult>;
51
+ /**
52
+ * Check if verification passed overall
53
+ */
54
+ allPassed(results: VerificationResult[]): boolean;
55
+ /**
56
+ * Get all verification errors
57
+ */
58
+ getErrors(results: VerificationResult[]): AgentError[];
59
+ /**
60
+ * Get all verification observations
61
+ */
62
+ getObservations(results: VerificationResult[]): Observation[];
63
+ /**
64
+ * Generate a unique ID
65
+ */
66
+ private generateId;
67
+ /** tsconfig.json (or any tsconfig.*.json) present in the project root. */
68
+ private hasTypeScriptConfig;
69
+ /** package.json with a non-empty build script. */
70
+ private hasBuildScript;
71
+ }
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VerificationEngine = void 0;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_util_1 = require("node:util");
11
+ const exec = (0, node_util_1.promisify)(node_child_process_1.exec);
12
+ class VerificationEngine {
13
+ projectPath;
14
+ config;
15
+ constructor(projectPath, config) {
16
+ this.projectPath = projectPath;
17
+ this.config = {
18
+ typecheckTimeout: config?.typecheckTimeout || 60000,
19
+ buildTimeout: config?.buildTimeout || 120000,
20
+ devServerTimeout: config?.devServerTimeout || 30000,
21
+ browserTimeout: config?.browserTimeout || 60000,
22
+ };
23
+ }
24
+ /**
25
+ * Run all verifications
26
+ */
27
+ async verifyAll(phase) {
28
+ const results = [];
29
+ // Type checking
30
+ const typecheckResult = await this.verifyTypecheck(phase);
31
+ results.push(typecheckResult);
32
+ // If typecheck failed, skip build
33
+ if (!typecheckResult.success) {
34
+ return results;
35
+ }
36
+ // Build
37
+ const buildResult = await this.verifyBuild(phase);
38
+ results.push(buildResult);
39
+ // If build failed, skip dev server check
40
+ if (!buildResult.success) {
41
+ return results;
42
+ }
43
+ // Dev server health
44
+ const devServerResult = await this.verifyDevServer(phase);
45
+ results.push(devServerResult);
46
+ // Browser check (placeholder)
47
+ const browserResult = await this.verifyBrowser(phase);
48
+ results.push(browserResult);
49
+ return results;
50
+ }
51
+ /**
52
+ * Verify TypeScript type checking
53
+ */
54
+ async verifyTypecheck(phase) {
55
+ const startTime = Date.now();
56
+ const observations = [];
57
+ const errors = [];
58
+ // Inconclusive without a tsconfig: never a pass and never a failure.
59
+ if (!this.hasTypeScriptConfig()) {
60
+ observations.push({
61
+ id: this.generateId(),
62
+ type: "build",
63
+ summary: "TypeScript check skipped (no tsconfig.json)",
64
+ data: { type: "typecheck", success: true, skipped: true },
65
+ timestamp: Date.now(),
66
+ phase,
67
+ });
68
+ return {
69
+ type: "typecheck",
70
+ success: true,
71
+ skipped: true,
72
+ observations,
73
+ errors,
74
+ duration: Date.now() - startTime,
75
+ };
76
+ }
77
+ try {
78
+ const command = "npx tsc --noEmit";
79
+ const { stdout, stderr } = await exec(command, {
80
+ cwd: this.projectPath,
81
+ timeout: this.config.typecheckTimeout,
82
+ });
83
+ const duration = Date.now() - startTime;
84
+ // Non-zero exit throws (caught below), so reaching here means tsc
85
+ // reported no type errors — stderr may still hold npm notices.
86
+ observations.push({
87
+ id: this.generateId(),
88
+ type: "build",
89
+ summary: "TypeScript check passed",
90
+ data: {
91
+ type: "typecheck",
92
+ success: true,
93
+ output: stdout + stderr,
94
+ },
95
+ timestamp: Date.now(),
96
+ phase,
97
+ });
98
+ return {
99
+ type: "typecheck",
100
+ success: true,
101
+ command: "npx tsc --noEmit",
102
+ output: stdout + stderr,
103
+ observations,
104
+ errors,
105
+ duration,
106
+ };
107
+ }
108
+ catch (error) {
109
+ const duration = Date.now() - startTime;
110
+ observations.push({
111
+ id: this.generateId(),
112
+ type: "build",
113
+ summary: "TypeScript check failed",
114
+ data: {
115
+ type: "typecheck",
116
+ success: false,
117
+ error: error.message,
118
+ },
119
+ timestamp: Date.now(),
120
+ phase,
121
+ });
122
+ errors.push({
123
+ id: this.generateId(),
124
+ type: "verification",
125
+ message: error.message,
126
+ phase,
127
+ timestamp: Date.now(),
128
+ details: { stderr: error.stderr, stdout: error.stdout },
129
+ recoverable: true,
130
+ });
131
+ return {
132
+ type: "typecheck",
133
+ success: false,
134
+ command: "npx tsc --noEmit",
135
+ error: error.message,
136
+ observations,
137
+ errors,
138
+ duration,
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Verify build
144
+ */
145
+ async verifyBuild(phase) {
146
+ const startTime = Date.now();
147
+ const observations = [];
148
+ const errors = [];
149
+ // Inconclusive when the project has no build script.
150
+ if (!this.hasBuildScript()) {
151
+ observations.push({
152
+ id: this.generateId(),
153
+ type: "build",
154
+ summary: "Build skipped (no build script)",
155
+ data: { type: "build", success: true, skipped: true },
156
+ timestamp: Date.now(),
157
+ phase,
158
+ });
159
+ return {
160
+ type: "build",
161
+ success: true,
162
+ skipped: true,
163
+ observations,
164
+ errors,
165
+ duration: Date.now() - startTime,
166
+ };
167
+ }
168
+ try {
169
+ // Try common build commands
170
+ const commands = ["npm run build", "pnpm build", "yarn build"];
171
+ let result;
172
+ let usedCommand = "";
173
+ let lastError = "";
174
+ for (const cmd of commands) {
175
+ try {
176
+ result = await exec(cmd, {
177
+ cwd: this.projectPath,
178
+ timeout: this.config.buildTimeout,
179
+ });
180
+ usedCommand = cmd;
181
+ break;
182
+ }
183
+ catch (err) {
184
+ lastError = err?.stderr || err?.message || `${cmd} failed`;
185
+ const managerMissing = err?.code === 127 || /command not found|ENOENT/i.test(String(err?.message || ""));
186
+ if (!managerMissing) {
187
+ // The build script ran and failed — don't mask it by trying
188
+ // other package managers.
189
+ break;
190
+ }
191
+ continue;
192
+ }
193
+ }
194
+ const duration = Date.now() - startTime;
195
+ if (!result) {
196
+ observations.push({
197
+ id: this.generateId(),
198
+ type: "build",
199
+ summary: "Build failed",
200
+ data: { type: "build", success: false, error: lastError },
201
+ timestamp: Date.now(),
202
+ phase,
203
+ });
204
+ errors.push({
205
+ id: this.generateId(),
206
+ type: "verification",
207
+ message: "Build failed",
208
+ phase,
209
+ timestamp: Date.now(),
210
+ details: { output: lastError },
211
+ recoverable: true,
212
+ });
213
+ return {
214
+ type: "build",
215
+ success: false,
216
+ command: "npm run build",
217
+ error: "Build failed",
218
+ output: lastError,
219
+ observations,
220
+ errors,
221
+ duration,
222
+ };
223
+ }
224
+ observations.push({
225
+ id: this.generateId(),
226
+ type: "build",
227
+ summary: `Build succeeded: ${usedCommand}`,
228
+ data: {
229
+ type: "build",
230
+ success: true,
231
+ command: usedCommand,
232
+ },
233
+ timestamp: Date.now(),
234
+ phase,
235
+ });
236
+ return {
237
+ type: "build",
238
+ success: true,
239
+ command: usedCommand,
240
+ output: result.stdout,
241
+ observations,
242
+ errors,
243
+ duration,
244
+ };
245
+ }
246
+ catch (error) {
247
+ const duration = Date.now() - startTime;
248
+ observations.push({
249
+ id: this.generateId(),
250
+ type: "build",
251
+ summary: "Build failed",
252
+ data: {
253
+ type: "build",
254
+ success: false,
255
+ error: error.message,
256
+ },
257
+ timestamp: Date.now(),
258
+ phase,
259
+ });
260
+ errors.push({
261
+ id: this.generateId(),
262
+ type: "verification",
263
+ message: "Build failed",
264
+ phase,
265
+ timestamp: Date.now(),
266
+ details: { stderr: error.stderr, stdout: error.stdout },
267
+ recoverable: true,
268
+ });
269
+ return {
270
+ type: "build",
271
+ success: false,
272
+ error: error.message,
273
+ observations,
274
+ errors,
275
+ duration,
276
+ };
277
+ }
278
+ }
279
+ /**
280
+ * Verify dev server health
281
+ */
282
+ async verifyDevServer(phase) {
283
+ const startTime = Date.now();
284
+ const observations = [];
285
+ 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
+ };
303
+ }
304
+ /**
305
+ * Verify browser/runtime state
306
+ */
307
+ async verifyBrowser(phase) {
308
+ const startTime = Date.now();
309
+ const observations = [];
310
+ 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
+ };
328
+ }
329
+ /**
330
+ * Check if verification passed overall
331
+ */
332
+ allPassed(results) {
333
+ return results.every((r) => r.success);
334
+ }
335
+ /**
336
+ * Get all verification errors
337
+ */
338
+ getErrors(results) {
339
+ return results.flatMap((r) => r.errors);
340
+ }
341
+ /**
342
+ * Get all verification observations
343
+ */
344
+ getObservations(results) {
345
+ return results.flatMap((r) => r.observations);
346
+ }
347
+ /**
348
+ * Generate a unique ID
349
+ */
350
+ generateId() {
351
+ return `ver_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
352
+ }
353
+ /** tsconfig.json (or any tsconfig.*.json) present in the project root. */
354
+ hasTypeScriptConfig() {
355
+ try {
356
+ return node_fs_1.default.readdirSync(this.projectPath).some((name) => /^tsconfig(\..+)?\.json$/.test(name));
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ }
362
+ /** package.json with a non-empty build script. */
363
+ hasBuildScript() {
364
+ try {
365
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(this.projectPath, "package.json"), "utf8");
366
+ const pkg = JSON.parse(raw);
367
+ return typeof pkg?.scripts?.build === "string" && pkg.scripts.build.trim().length > 0;
368
+ }
369
+ catch {
370
+ return false;
371
+ }
372
+ }
373
+ }
374
+ exports.VerificationEngine = VerificationEngine;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Workspace Guard - Capture, diff, and roll back project state.
3
+ *
4
+ * Lasso's non-negotiable review workflow requires:
5
+ * 1. Capture the original project state before an agent runs.
6
+ * 2. Collect resulting file changes after the agent runs.
7
+ * 3. Roll back to the original state so nothing is applied before review.
8
+ * 4. On approval, re-apply the changes; on rejection, leave the project untouched.
9
+ *
10
+ * The guard backs up every non-ignored file (in memory, or on disk for
11
+ * oversized files), computes a normalized diff against the captured state,
12
+ * and can restore the workspace exactly (content + permission bits + deletions).
13
+ */
14
+ export interface FileChange {
15
+ /** Project-relative path using the platform separator */
16
+ filePath: string;
17
+ operation: "created" | "modified" | "deleted";
18
+ /** Full original text content (text files only) */
19
+ oldContent?: string;
20
+ /** Full new text content (text files only) */
21
+ newContent?: string;
22
+ /** True when the file is binary — contents are preserved for rollback but not reviewable as text */
23
+ binary?: boolean;
24
+ }
25
+ export interface WorkspaceGuardOptions {
26
+ /** Directory names that are never captured (defaults cover Node/tooling outputs) */
27
+ ignoreDirs?: string[];
28
+ /** Extra ignore predicates (relative path, isDirectory) */
29
+ ignore?: (relPath: string, isDir: boolean) => boolean;
30
+ /** Max bytes held in memory per file; larger files are copied to a temp dir (default 4MB) */
31
+ maxMemoryFileSize?: number;
32
+ /** Max total bytes held in memory before oversized handling kicks in per file (default 64MB) */
33
+ maxTotalBytes?: number;
34
+ /** Max directory recursion depth (default 32) */
35
+ maxDepth?: number;
36
+ }
37
+ export declare class WorkspaceGuard {
38
+ readonly root: string;
39
+ private readonly files;
40
+ private readonly skipped;
41
+ private readonly tempDir;
42
+ private readonly ignoreDirs;
43
+ private readonly ignoreFn?;
44
+ private tempDirCreated;
45
+ private disposed;
46
+ private constructor();
47
+ /**
48
+ * Capture the full current state of the workspace.
49
+ */
50
+ static capture(projectPath: string, options?: WorkspaceGuardOptions): Promise<WorkspaceGuard>;
51
+ /** Number of files captured (excludes ignored/skipped entries). */
52
+ get fileCount(): number;
53
+ /** Paths that could not be captured (symlinks, unreadable entries). */
54
+ get skippedPaths(): string[];
55
+ /**
56
+ * Compare the current on-disk state against the captured state.
57
+ * Must be called BEFORE restore() when collecting an agent's changes.
58
+ */
59
+ diff(): FileChange[];
60
+ /**
61
+ * Restore the workspace to the captured state:
62
+ * modified files are reverted, created files are deleted, deleted files are recreated.
63
+ */
64
+ restore(): Promise<void>;
65
+ /** Restore without comparing (used by dispose paths). */
66
+ dispose(): Promise<void>;
67
+ private listNewFiles;
68
+ private ensureTempDir;
69
+ private cleanupTemp;
70
+ }