@lasso-ai/cli 1.4.5 → 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 (59) hide show
  1. package/dist/cli/agent.d.ts +10 -89
  2. package/dist/cli/agent.js +66 -1412
  3. package/dist/cli/backends.d.ts +53 -0
  4. package/dist/cli/backends.js +267 -0
  5. package/dist/cli/bridge-old.d.ts +259 -0
  6. package/dist/cli/bridge-old.js +1350 -0
  7. package/dist/cli/bridge.d.ts +22 -12
  8. package/dist/cli/bridge.js +824 -1174
  9. package/dist/cli/host/install.js +12 -1
  10. package/dist/cli/host/next-host-entry.js +0 -2
  11. package/dist/cli/host/vite-host-entry.js +1 -3
  12. package/dist/cli/local-runtime.d.ts +28 -0
  13. package/dist/cli/local-runtime.js +294 -0
  14. package/dist/cli/oneshot.js +177 -961
  15. package/dist/cli/runtime-client.d.ts +310 -0
  16. package/dist/cli/runtime-client.js +178 -0
  17. package/dist/cli/snapshot.d.ts +3 -1
  18. package/dist/cli/snapshot.js +8 -1
  19. package/dist/overlay.js +44 -28
  20. package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
  21. package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
  22. package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
  23. package/dist/runtime/__tests__/state-machine.test.js +107 -0
  24. package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
  25. package/dist/runtime/__tests__/verification-engine.test.js +91 -0
  26. package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
  27. package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
  28. package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
  29. package/dist/runtime/adapters/claude-code-adapter.js +230 -0
  30. package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
  31. package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
  32. package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
  33. package/dist/runtime/adapters/cursor-adapter.js +214 -0
  34. package/dist/runtime/adapters/factory.d.ts +19 -0
  35. package/dist/runtime/adapters/factory.js +48 -0
  36. package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
  37. package/dist/runtime/adapters/local-agent-adapter.js +408 -0
  38. package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
  39. package/dist/runtime/adapters/opencode-adapter.js +210 -0
  40. package/dist/runtime/agent-runtime.d.ts +286 -0
  41. package/dist/runtime/agent-runtime.js +8 -0
  42. package/dist/runtime/agent-state-machine.d.ts +67 -0
  43. package/dist/runtime/agent-state-machine.js +234 -0
  44. package/dist/runtime/backend-interface.d.ts +272 -0
  45. package/dist/runtime/backend-interface.js +38 -0
  46. package/dist/runtime/event-mapper.d.ts +11 -0
  47. package/dist/runtime/event-mapper.js +40 -0
  48. package/dist/runtime/index.d.ts +20 -0
  49. package/dist/runtime/index.js +43 -0
  50. package/dist/runtime/local-session-runner.d.ts +60 -0
  51. package/dist/runtime/local-session-runner.js +185 -0
  52. package/dist/runtime/review.d.ts +24 -0
  53. package/dist/runtime/review.js +30 -0
  54. package/dist/runtime/verification-engine.d.ts +71 -0
  55. package/dist/runtime/verification-engine.js +374 -0
  56. package/dist/runtime/workspace-guard.d.ts +70 -0
  57. package/dist/runtime/workspace-guard.js +321 -0
  58. package/docs/local-coding-agents.md +183 -0
  59. package/package.json +22 -3
@@ -27,7 +27,18 @@ const AGENT_LABEL = "com.lasso.host";
27
27
  const LAUNCH_AGENT_FILE = node_path_1.default.join(node_os_1.default.homedir(), "Library", "LaunchAgents", `${AGENT_LABEL}.plist`);
28
28
  function cliEntry() {
29
29
  const entry = process.argv[1];
30
- return entry && entry.endsWith(".js") ? entry : null;
30
+ if (!entry)
31
+ return null;
32
+ // Resolve symlinks (e.g. pnpm's global bin is extensionless) so the
33
+ // spawned child gets a real .js path.
34
+ let resolved = entry;
35
+ try {
36
+ resolved = node_fs_1.default.realpathSync(entry);
37
+ }
38
+ catch {
39
+ // keep the original if realpath fails
40
+ }
41
+ return resolved.endsWith(".js") ? resolved : null;
31
42
  }
32
43
  function spawnDaemon(env) {
33
44
  const port = (0, paths_1.hostProxyPort)(env);
@@ -130,13 +130,11 @@ void (async () => {
130
130
  await waitForNext(nextPort, nextProcess);
131
131
  }
132
132
  catch (error) {
133
- bridge.close();
134
133
  nextProcess.kill("SIGTERM");
135
134
  throw error;
136
135
  }
137
136
  server.listen(publicPort, "127.0.0.1");
138
137
  const shutdown = () => {
139
- bridge.close();
140
138
  nextProcess.kill("SIGTERM");
141
139
  proxy.close();
142
140
  server.close(() => process.exit(0));
@@ -27,7 +27,7 @@ function lassoOverlayPlugin() {
27
27
  };
28
28
  }
29
29
  void (async () => {
30
- const bridge = await (0, bridge_1.startBridge)(cwd, null, bridgePort);
30
+ await (0, bridge_1.startBridge)(cwd, null, bridgePort);
31
31
  const require = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, "package.json"));
32
32
  const viteEntry = require.resolve("vite");
33
33
  const { createServer } = await import((0, node_url_1.pathToFileURL)(viteEntry).href);
@@ -49,11 +49,9 @@ void (async () => {
49
49
  });
50
50
  await server.listen();
51
51
  process.on("SIGTERM", () => {
52
- bridge.close();
53
52
  void server.close().then(() => process.exit(0));
54
53
  });
55
54
  process.on("SIGINT", () => {
56
- bridge.close();
57
55
  void server.close().then(() => process.exit(0));
58
56
  });
59
57
  })();
@@ -0,0 +1,28 @@
1
+ /**
2
+ * LocalRuntime — local coding agent execution on the user's machine.
3
+ *
4
+ * Implements the same session surface the CLI bridge uses for hosted
5
+ * sessions (RuntimeClient), but runs Claude Code / OpenCode / Codex CLI /
6
+ * Cursor Agent in THIS process: prompts, project context, and source code
7
+ * never travel through Lasso's server. Session state is in-memory (per CLI
8
+ * process) and the review-first workflow comes from the shared runtime core
9
+ * (LocalSessionRunner + WorkspaceGuard + VerificationEngine).
10
+ */
11
+ import type { RuntimeSession, RuntimeSessionInput, RuntimeEventHandler, SessionVerificationResult, RuntimeFacade } from "./runtime-client.js";
12
+ export declare class LocalRuntime implements RuntimeFacade {
13
+ private sessions;
14
+ private abortControllers;
15
+ createSession(input: RuntimeSessionInput): Promise<RuntimeSession>;
16
+ getSession(sessionId: string): Promise<RuntimeSession>;
17
+ runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal): Promise<void>;
18
+ verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
19
+ switchModel(sessionId: string, model: {
20
+ provider: string;
21
+ id: string;
22
+ name?: string;
23
+ }, _apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
24
+ cancelSession(sessionId: string): Promise<void>;
25
+ deleteSession(sessionId: string): Promise<void>;
26
+ private toSession;
27
+ }
28
+ export declare function getLocalRuntime(): LocalRuntime;
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ /**
3
+ * LocalRuntime — local coding agent execution on the user's machine.
4
+ *
5
+ * Implements the same session surface the CLI bridge uses for hosted
6
+ * sessions (RuntimeClient), but runs Claude Code / OpenCode / Codex CLI /
7
+ * Cursor Agent in THIS process: prompts, project context, and source code
8
+ * never travel through Lasso's server. Session state is in-memory (per CLI
9
+ * process) and the review-first workflow comes from the shared runtime core
10
+ * (LocalSessionRunner + WorkspaceGuard + VerificationEngine).
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.LocalRuntime = void 0;
17
+ exports.getLocalRuntime = getLocalRuntime;
18
+ const node_crypto_1 = __importDefault(require("node:crypto"));
19
+ const index_js_1 = require("../runtime/index.js");
20
+ class LocalRuntime {
21
+ sessions = new Map();
22
+ abortControllers = new Map();
23
+ async createSession(input) {
24
+ const backendConfig = input.backendConfig;
25
+ if (!(0, index_js_1.isLocalAgentConfig)(backendConfig)) {
26
+ throw new Error("LocalRuntime only accepts local_agent backend configs");
27
+ }
28
+ if (!backendConfig.projectPath) {
29
+ throw new Error("LocalRuntime requires backendConfig.projectPath");
30
+ }
31
+ const id = node_crypto_1.default.randomUUID();
32
+ const now = new Date();
33
+ const record = {
34
+ id,
35
+ phase: "execute",
36
+ status: "running",
37
+ iteration: 0,
38
+ userPrompt: input.userPrompt,
39
+ conversation: [
40
+ { id: node_crypto_1.default.randomUUID(), role: "user", content: input.userPrompt, timestamp: now.getTime() },
41
+ ],
42
+ observations: [],
43
+ actions: [],
44
+ completedActions: [],
45
+ errors: [],
46
+ selection: input.selection,
47
+ project: input.project,
48
+ model: input.model,
49
+ createdAt: now,
50
+ updatedAt: now,
51
+ metadata: { backendConfig },
52
+ backendConfig: { ...backendConfig, type: "local_agent" },
53
+ projectPath: backendConfig.projectPath,
54
+ input,
55
+ running: false,
56
+ };
57
+ this.sessions.set(id, record);
58
+ return this.toSession(record);
59
+ }
60
+ async getSession(sessionId) {
61
+ const record = this.sessions.get(sessionId);
62
+ if (!record)
63
+ throw new Error("Session not found");
64
+ return this.toSession(record);
65
+ }
66
+ async runSession(sessionId, projectPath, onEvent, signal) {
67
+ const record = this.sessions.get(sessionId);
68
+ if (!record)
69
+ throw new Error("Session not found");
70
+ if (record.running)
71
+ throw new Error("Session is already running");
72
+ record.running = true;
73
+ record.status = "running";
74
+ record.phase = "execute";
75
+ record.updatedAt = new Date();
76
+ const controller = new AbortController();
77
+ this.abortControllers.set(sessionId, controller);
78
+ const onOuterAbort = () => controller.abort();
79
+ signal?.addEventListener("abort", onOuterAbort, { once: true });
80
+ const emit = (event) => {
81
+ void onEvent(event);
82
+ };
83
+ try {
84
+ // Fresh capture per run — corrective re-runs capture the post-apply state.
85
+ const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
86
+ record.backend =
87
+ record.backend ||
88
+ (await (0, index_js_1.createLocalAgentBackend)({ ...record.backendConfig, type: "local_agent", projectPath }));
89
+ const previousErrors = (record.errors || []);
90
+ const externalSessionId = typeof record.metadata.backendSessionId === "string" ? record.metadata.backendSessionId : undefined;
91
+ const runner = new index_js_1.LocalSessionRunner({
92
+ sessionId: record.id,
93
+ backend: record.backend,
94
+ guard,
95
+ emit,
96
+ phase: "execute",
97
+ });
98
+ const outcome = await runner.run({
99
+ taskId: record.id,
100
+ userPrompt: record.userPrompt,
101
+ project: {
102
+ projectId: record.project.projectId || "local",
103
+ framework: record.project.framework,
104
+ hasTypeScript: Boolean(record.project.hasTypeScript),
105
+ hasTailwind: false,
106
+ hasReact: Boolean(record.project.hasReact),
107
+ hasNext: Boolean(record.project.hasNext),
108
+ hasVite: Boolean(record.project.hasVite),
109
+ files: record.project.files || [],
110
+ },
111
+ selection: record.selection,
112
+ conversation: record.conversation.map((msg) => ({
113
+ role: msg.role,
114
+ content: msg.content,
115
+ timestamp: msg.timestamp,
116
+ })),
117
+ previousObservations: [],
118
+ previousErrors,
119
+ currentPhase: "execute",
120
+ constraints: { safetyLevel: "standard" },
121
+ sessionRef: externalSessionId ? { id: externalSessionId } : undefined,
122
+ }, controller.signal);
123
+ // Persist review payload + normalized actions for the review flow.
124
+ record.actions = outcome.actions;
125
+ record.observations.push(...outcome.observations.map((obs) => ({
126
+ id: obs.id,
127
+ type: String(obs.type),
128
+ summary: obs.summary,
129
+ data: obs.data,
130
+ timestamp: obs.timestamp,
131
+ phase: String(obs.phase),
132
+ })));
133
+ record.errors = outcome.errors.map((err) => ({
134
+ id: err.id,
135
+ type: String(err.type),
136
+ message: err.message,
137
+ phase: String(err.phase),
138
+ timestamp: err.timestamp,
139
+ recoverable: err.recoverable,
140
+ }));
141
+ if (outcome.conversation.length > 0) {
142
+ record.conversation.push(...outcome.conversation.map((msg) => ({
143
+ id: node_crypto_1.default.randomUUID(),
144
+ role: msg.role,
145
+ content: msg.content,
146
+ timestamp: msg.timestamp,
147
+ })));
148
+ }
149
+ record.metadata = {
150
+ ...record.metadata,
151
+ ...outcome.metadata,
152
+ pendingChanges: outcome.changes,
153
+ binaryChanges: outcome.binaryPaths,
154
+ };
155
+ record.status =
156
+ outcome.status === "waiting" ? "waiting" : outcome.status === "completed" ? "completed" : "failed";
157
+ record.phase = "complete";
158
+ record.updatedAt = new Date();
159
+ }
160
+ catch (error) {
161
+ record.status = "failed";
162
+ record.errors = [
163
+ ...record.errors,
164
+ {
165
+ id: node_crypto_1.default.randomUUID(),
166
+ type: "execution",
167
+ message: error instanceof Error ? error.message : "Local agent run failed",
168
+ phase: "execute",
169
+ timestamp: Date.now(),
170
+ recoverable: false,
171
+ },
172
+ ];
173
+ record.updatedAt = new Date();
174
+ emit({
175
+ type: "error",
176
+ sessionId: record.id,
177
+ timestamp: Date.now(),
178
+ data: { error: error instanceof Error ? error.message : "Local agent run failed" },
179
+ });
180
+ }
181
+ finally {
182
+ signal?.removeEventListener("abort", onOuterAbort);
183
+ this.abortControllers.delete(sessionId);
184
+ record.running = false;
185
+ }
186
+ }
187
+ async verifySession(sessionId, projectPath) {
188
+ const record = this.sessions.get(sessionId);
189
+ if (!record)
190
+ throw new Error("Session not found");
191
+ const engine = new index_js_1.VerificationEngine(projectPath);
192
+ const results = await engine.verifyAll("verify");
193
+ const observations = engine.getObservations(results);
194
+ const errors = engine.getErrors(results);
195
+ const passed = engine.allPassed(results);
196
+ // Keep verification errors so a corrective re-run receives them as context.
197
+ if (!passed) {
198
+ record.errors = [
199
+ ...record.errors,
200
+ ...errors.map((err) => ({
201
+ id: err.id,
202
+ type: String(err.type),
203
+ message: err.message,
204
+ phase: String(err.phase),
205
+ timestamp: err.timestamp,
206
+ recoverable: err.recoverable,
207
+ })),
208
+ ];
209
+ }
210
+ const command = results.map((r) => r.command).find((c) => typeof c === "string");
211
+ return { passed, errors, observations, command, requiresFix: !passed };
212
+ }
213
+ async switchModel(sessionId, model, _apiKey, backendConfig) {
214
+ const record = this.sessions.get(sessionId);
215
+ if (!record)
216
+ throw new Error("Session not found");
217
+ if (!(0, index_js_1.isLocalAgentConfig)(backendConfig)) {
218
+ throw new Error("LocalRuntime.switchModel requires a local_agent backendConfig");
219
+ }
220
+ const nextBackend = await (0, index_js_1.createLocalAgentBackend)({ ...backendConfig, type: "local_agent" });
221
+ if (record.backend) {
222
+ await record.backend.switchBackend(nextBackend, {
223
+ taskId: record.id,
224
+ userPrompt: record.userPrompt,
225
+ project: {
226
+ projectId: record.project.projectId || "local",
227
+ hasTypeScript: Boolean(record.project.hasTypeScript),
228
+ hasTailwind: false,
229
+ hasReact: Boolean(record.project.hasReact),
230
+ hasNext: Boolean(record.project.hasNext),
231
+ hasVite: Boolean(record.project.hasVite),
232
+ files: record.project.files || [],
233
+ },
234
+ conversation: [],
235
+ previousObservations: [],
236
+ previousErrors: [],
237
+ currentPhase: "execute",
238
+ constraints: {},
239
+ sessionRef: typeof record.metadata.backendSessionId === "string"
240
+ ? { id: record.metadata.backendSessionId }
241
+ : undefined,
242
+ });
243
+ }
244
+ record.backend = nextBackend;
245
+ record.backendConfig = { ...backendConfig, type: "local_agent" };
246
+ record.model = { provider: model.provider, id: model.id, name: model.name };
247
+ record.metadata = {
248
+ ...record.metadata,
249
+ backendConfig: record.backendConfig,
250
+ backendModel: model.id,
251
+ };
252
+ // Switching agents drops the previous agent's external session: context
253
+ // cannot be transferred across CLI agents.
254
+ delete record.metadata.backendSessionId;
255
+ record.updatedAt = new Date();
256
+ }
257
+ async cancelSession(sessionId) {
258
+ const controller = this.abortControllers.get(sessionId);
259
+ if (controller)
260
+ controller.abort();
261
+ }
262
+ async deleteSession(sessionId) {
263
+ this.sessions.delete(sessionId);
264
+ this.abortControllers.delete(sessionId);
265
+ }
266
+ toSession(record) {
267
+ return {
268
+ id: record.id,
269
+ phase: record.phase,
270
+ status: record.status,
271
+ iteration: record.iteration,
272
+ userPrompt: record.userPrompt,
273
+ conversation: record.conversation,
274
+ observations: record.observations,
275
+ actions: record.actions,
276
+ completedActions: record.completedActions,
277
+ errors: record.errors,
278
+ selection: record.selection,
279
+ project: record.project,
280
+ model: record.model,
281
+ createdAt: record.createdAt,
282
+ updatedAt: record.updatedAt,
283
+ metadata: record.metadata,
284
+ };
285
+ }
286
+ }
287
+ exports.LocalRuntime = LocalRuntime;
288
+ /** Process-wide local runtime (sessions live for the CLI bridge lifetime). */
289
+ let sharedLocalRuntime;
290
+ function getLocalRuntime() {
291
+ if (!sharedLocalRuntime)
292
+ sharedLocalRuntime = new LocalRuntime();
293
+ return sharedLocalRuntime;
294
+ }