@lasso-ai/cli 1.5.0 → 2.0.1

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,202 @@
1
+ "use strict";
2
+ /**
3
+ * Codex CLI Adapter
4
+ *
5
+ * Adapter for OpenAI's Codex CLI (`codex exec`), a local coding agent with its
6
+ * own agentic loop.
7
+ *
8
+ * Invocation: codex exec --json --skip-git-repo-check -s workspace-write [-m model] <prompt>
9
+ * (resume: codex exec resume <thread_id> --json --skip-git-repo-check <prompt>)
10
+ * with stdin ignored and cwd set to the project path. Output is JSONL
11
+ * (thread.started / item.* / turn.*) normalized into BackendEvents.
12
+ *
13
+ * File changes are not trusted from the agent's report — the orchestrator's
14
+ * WorkspaceGuard diffs the project after the run and restores it before review.
15
+ *
16
+ * Reference: https://github.com/openai/codex
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.CodexCLIAdapter = void 0;
20
+ const node_util_1 = require("node:util");
21
+ const local_agent_adapter_1 = require("./local-agent-adapter");
22
+ const execFileAsync = (0, node_util_1.promisify)(require("node:child_process").execFile);
23
+ class CodexCLIAdapter extends local_agent_adapter_1.LocalAgentAdapter {
24
+ provider = "codex_cli";
25
+ constructor(config) {
26
+ super({ ...config, type: "local_agent", provider: "codex_cli" });
27
+ }
28
+ getCapabilities() {
29
+ return {
30
+ canStreamEvents: true,
31
+ canIsolateWorkspace: false,
32
+ providesStructuredDiffs: false,
33
+ supportsSessionContinuation: true,
34
+ supportsCancellation: true,
35
+ hasInternalLoop: true,
36
+ supportedFileOperations: "both",
37
+ canRunCommands: true,
38
+ authMethod: "cli_session",
39
+ forbiddenCommands: ["rm -rf /", "mkfs"],
40
+ };
41
+ }
42
+ get binary() {
43
+ return "codex";
44
+ }
45
+ get installInstructions() {
46
+ return "Install Codex CLI: npm install -g @openai/codex (or https://github.com/openai/codex). Then run 'codex login'.";
47
+ }
48
+ get versionArgs() {
49
+ return ["--version"];
50
+ }
51
+ get supportsResume() {
52
+ return true;
53
+ }
54
+ async checkAuthentication() {
55
+ try {
56
+ await execFileAsync("codex", ["login", "status"], { timeout: 10000, windowsHide: true });
57
+ return { authenticated: true };
58
+ }
59
+ catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ return {
62
+ authenticated: false,
63
+ error: `Codex CLI is not logged in (${message.split("\n")[0]}).`,
64
+ fixInstructions: "Run 'codex login' and sign in with ChatGPT or an API key.",
65
+ };
66
+ }
67
+ }
68
+ buildCommand(context) {
69
+ const cfg = this.config;
70
+ const resumeId = this.resumeSessionId(context);
71
+ const prompt = this.buildTaskPrompt(context);
72
+ const args = ["exec"];
73
+ if (resumeId) {
74
+ // `resume` does not accept -s; sandbox policy goes through a config override.
75
+ args.push("resume", resumeId, "--json", "--skip-git-repo-check", "-c", 'sandbox_mode="workspace-write"');
76
+ if (this.modelId)
77
+ args.push("-m", this.modelId);
78
+ }
79
+ else {
80
+ args.push("--json", "--skip-git-repo-check", "-s", "workspace-write");
81
+ if (this.modelId)
82
+ args.push("-m", this.modelId);
83
+ }
84
+ args.push(prompt);
85
+ return { bin: "codex", args, cwd: cfg.projectPath };
86
+ }
87
+ handleLine(line, context, sink) {
88
+ let event;
89
+ try {
90
+ event = JSON.parse(line);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ switch (event.type) {
96
+ case "thread.started": {
97
+ if (event.thread_id)
98
+ sink.state.sessionId = event.thread_id;
99
+ sink.progress(`Codex thread started${event.thread_id ? ` (${event.thread_id})` : ""}`);
100
+ return true;
101
+ }
102
+ case "turn.started": {
103
+ sink.progress("Codex turn started");
104
+ return true;
105
+ }
106
+ case "item.started":
107
+ case "item.updated": {
108
+ const item = event.item || {};
109
+ if (item.item_type === "command_execution" && item.command) {
110
+ sink.toolStarted("shell", item.command.slice(0, 200));
111
+ }
112
+ else if (item.item_type === "file_change") {
113
+ sink.toolStarted("file_change", undefined);
114
+ }
115
+ return true;
116
+ }
117
+ case "item.completed": {
118
+ this.handleItemCompleted(event.item || {}, sink);
119
+ return true;
120
+ }
121
+ case "error": {
122
+ const message = event.message || event.error?.message || "Codex reported an error";
123
+ this.recordFailure(sink, message);
124
+ return true;
125
+ }
126
+ case "turn.failed": {
127
+ const message = event.error?.message || "Codex turn failed";
128
+ this.recordFailure(sink, message);
129
+ return true;
130
+ }
131
+ case "turn.completed": {
132
+ sink.state.receivedTerminalSuccess = true;
133
+ return true;
134
+ }
135
+ default:
136
+ return true;
137
+ }
138
+ }
139
+ handleItemCompleted(item, sink) {
140
+ switch (item.item_type) {
141
+ case "command_execution": {
142
+ const success = typeof item.exit_code === "number" ? item.exit_code === 0 : item.status === "completed";
143
+ sink.toolCompleted("shell", success, (item.command || "").slice(0, 200));
144
+ if (!success && item.aggregated_output) {
145
+ sink.error(`Command failed (exit ${item.exit_code ?? "?"}): ${item.aggregated_output.slice(-300)}`, { recoverable: true });
146
+ }
147
+ break;
148
+ }
149
+ case "file_change": {
150
+ sink.toolCompleted("file_change", true);
151
+ for (const change of item.changes || []) {
152
+ if (!change.path)
153
+ continue;
154
+ const rel = this.relPath(change.path);
155
+ const kind = (change.kind || "").toLowerCase();
156
+ if (kind.includes("delete"))
157
+ sink.fileChange("deleted", rel);
158
+ else
159
+ sink.fileChange("modified", rel);
160
+ }
161
+ break;
162
+ }
163
+ case "assistant_message": {
164
+ if (item.text) {
165
+ sink.progress(item.text);
166
+ sink.state.summary = item.text;
167
+ }
168
+ break;
169
+ }
170
+ case "error": {
171
+ const message = item.error?.message || item.message || "Codex item error";
172
+ this.recordFailure(sink, message);
173
+ break;
174
+ }
175
+ default:
176
+ break;
177
+ }
178
+ }
179
+ recordFailure(sink, message) {
180
+ if (!sink.state.failureMessage)
181
+ sink.state.failureMessage = message;
182
+ if (/usage limit|rate limit|quota|billing|upgrade to plus/i.test(message)) {
183
+ sink.state.authFailure = true;
184
+ }
185
+ // "error" and "turn.failed" often carry the same message — record once.
186
+ if (!sink.state.errors.some((e) => e.message === message)) {
187
+ sink.error(message, { recoverable: false, authFailure: sink.state.authFailure });
188
+ }
189
+ }
190
+ inspectFailureText(text) {
191
+ if (!text)
192
+ return null;
193
+ if (/not logged in|please run ['"]?codex login|authentication/i.test(text)) {
194
+ return { message: "Codex CLI is not authenticated.", authFailure: true };
195
+ }
196
+ if (/usage limit|upgrade to plus|quota exceeded/i.test(text)) {
197
+ return { message: "Codex usage limit reached — try again later or upgrade the account.", authFailure: true };
198
+ }
199
+ return null;
200
+ }
201
+ }
202
+ exports.CodexCLIAdapter = CodexCLIAdapter;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Cursor Adapter
3
+ *
4
+ * Adapter for the Cursor Agent CLI (binary: `cursor-agent`), a local coding
5
+ * agent with its own agentic loop.
6
+ *
7
+ * Invocation: cursor-agent -p --output-format stream-json --trust [--model m]
8
+ * [--resume <chatId>] <prompt>, cwd set to the project path.
9
+ *
10
+ * Stream-JSON events (system/init, assistant, tool_call, result) are normalized
11
+ * into BackendEvents. Success requires Cursor's terminal result event with
12
+ * is_error=false AND a zero exit code; on failure Cursor exits non-zero, often
13
+ * without a terminal event (stderr carries the error).
14
+ *
15
+ * File changes are not trusted from the agent's report — the orchestrator's
16
+ * WorkspaceGuard diffs the project after the run and restores it before review.
17
+ *
18
+ * Reference: https://cursor.com/docs/cli/reference/output-format
19
+ */
20
+ import { LocalAgentAdapter, LocalAgentConfig, CommandSpec, EventSink } from "./local-agent-adapter";
21
+ import { BackendCapabilities, TaskContext } from "../backend-interface";
22
+ interface CursorConfig extends LocalAgentConfig {
23
+ provider: "cursor";
24
+ }
25
+ export declare class CursorAdapter extends LocalAgentAdapter {
26
+ readonly provider: "cursor";
27
+ constructor(config: CursorConfig);
28
+ getCapabilities(): BackendCapabilities;
29
+ protected get binary(): string;
30
+ protected get installInstructions(): string;
31
+ protected get versionArgs(): string[];
32
+ protected get supportsResume(): boolean;
33
+ protected checkAuthentication(): Promise<{
34
+ authenticated: boolean;
35
+ error?: string;
36
+ fixInstructions?: string;
37
+ }>;
38
+ protected buildCommand(context: TaskContext): CommandSpec;
39
+ protected handleLine(line: string, context: TaskContext, sink: EventSink): boolean;
40
+ private handleToolCall;
41
+ private describeArgs;
42
+ protected inspectFailureText(text: string): {
43
+ message: string;
44
+ authFailure?: boolean;
45
+ } | null;
46
+ }
47
+ export {};
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * Cursor Adapter
4
+ *
5
+ * Adapter for the Cursor Agent CLI (binary: `cursor-agent`), a local coding
6
+ * agent with its own agentic loop.
7
+ *
8
+ * Invocation: cursor-agent -p --output-format stream-json --trust [--model m]
9
+ * [--resume <chatId>] <prompt>, cwd set to the project path.
10
+ *
11
+ * Stream-JSON events (system/init, assistant, tool_call, result) are normalized
12
+ * into BackendEvents. Success requires Cursor's terminal result event with
13
+ * is_error=false AND a zero exit code; on failure Cursor exits non-zero, often
14
+ * without a terminal event (stderr carries the error).
15
+ *
16
+ * File changes are not trusted from the agent's report — the orchestrator's
17
+ * WorkspaceGuard diffs the project after the run and restores it before review.
18
+ *
19
+ * Reference: https://cursor.com/docs/cli/reference/output-format
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.CursorAdapter = void 0;
23
+ const node_util_1 = require("node:util");
24
+ const local_agent_adapter_1 = require("./local-agent-adapter");
25
+ const execFileAsync = (0, node_util_1.promisify)(require("node:child_process").execFile);
26
+ const TOOL_KIND_MAP = {
27
+ readToolCall: "read",
28
+ editToolCall: "edit",
29
+ writeToolCall: "write",
30
+ deleteToolCall: "delete",
31
+ shellToolCall: "shell",
32
+ grepToolCall: "grep",
33
+ globToolCall: "glob",
34
+ lsToolCall: "ls",
35
+ todoToolCall: "todo",
36
+ };
37
+ const FILE_TOOLS = new Set(["edit", "write", "delete"]);
38
+ class CursorAdapter extends local_agent_adapter_1.LocalAgentAdapter {
39
+ provider = "cursor";
40
+ constructor(config) {
41
+ super({ ...config, type: "local_agent", provider: "cursor" });
42
+ }
43
+ getCapabilities() {
44
+ return {
45
+ canStreamEvents: true,
46
+ canIsolateWorkspace: false,
47
+ providesStructuredDiffs: false,
48
+ supportsSessionContinuation: true,
49
+ supportsCancellation: true,
50
+ hasInternalLoop: true,
51
+ supportedFileOperations: "both",
52
+ canRunCommands: true,
53
+ authMethod: "cli_session",
54
+ forbiddenCommands: ["rm -rf /", "mkfs"],
55
+ };
56
+ }
57
+ get binary() {
58
+ return "cursor-agent";
59
+ }
60
+ get installInstructions() {
61
+ return "Install the Cursor Agent CLI: curl https://cursor.com/install -fsS | bash, then run 'cursor-agent login'.";
62
+ }
63
+ get versionArgs() {
64
+ return ["-v"];
65
+ }
66
+ get supportsResume() {
67
+ return true;
68
+ }
69
+ async checkAuthentication() {
70
+ try {
71
+ const { stdout } = (await execFileAsync("cursor-agent", ["status", "--format", "json"], {
72
+ timeout: 10000,
73
+ windowsHide: true,
74
+ }));
75
+ const jsonStart = stdout.indexOf("{");
76
+ if (jsonStart >= 0) {
77
+ try {
78
+ const parsed = JSON.parse(stdout.slice(jsonStart));
79
+ if (parsed && typeof parsed.isAuthenticated === "boolean") {
80
+ return parsed.isAuthenticated
81
+ ? { authenticated: true }
82
+ : {
83
+ authenticated: false,
84
+ error: "Cursor Agent is not authenticated.",
85
+ fixInstructions: "Run 'cursor-agent login' to authenticate with Cursor.",
86
+ };
87
+ }
88
+ }
89
+ catch {
90
+ // Fall through to exit-code based result.
91
+ }
92
+ }
93
+ return { authenticated: true };
94
+ }
95
+ catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ return {
98
+ authenticated: false,
99
+ error: `Could not verify Cursor Agent authentication: ${message.split("\n")[0]}`,
100
+ fixInstructions: "Run 'cursor-agent login' to authenticate with Cursor.",
101
+ };
102
+ }
103
+ }
104
+ buildCommand(context) {
105
+ const resumeId = this.resumeSessionId(context);
106
+ const args = ["-p", "--output-format", "stream-json", "--trust"];
107
+ if (this.modelId)
108
+ args.push("--model", this.modelId);
109
+ if (resumeId)
110
+ args.push("--resume", resumeId);
111
+ args.push(this.buildTaskPrompt(context));
112
+ return { bin: "cursor-agent", args, cwd: this.config.projectPath };
113
+ }
114
+ handleLine(line, context, sink) {
115
+ let event;
116
+ try {
117
+ event = JSON.parse(line);
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ if (event.session_id)
123
+ sink.state.sessionId = event.session_id;
124
+ switch (event.type) {
125
+ case "system": {
126
+ if (event.subtype === "init") {
127
+ sink.state.sessionId = event.session_id || sink.state.sessionId;
128
+ sink.progress(`Cursor session started${event.model ? ` (model: ${event.model})` : ""}`);
129
+ }
130
+ return true;
131
+ }
132
+ case "assistant": {
133
+ const text = (event.message?.content || [])
134
+ .filter((c) => c.type === "text" && c.text)
135
+ .map((c) => c.text)
136
+ .join("\n");
137
+ if (text)
138
+ sink.progress(text);
139
+ return true;
140
+ }
141
+ case "tool_call": {
142
+ this.handleToolCall(event, sink);
143
+ return true;
144
+ }
145
+ case "result": {
146
+ if (event.is_error || event.subtype !== "success") {
147
+ sink.state.failureMessage = event.result || `Cursor result subtype: ${event.subtype || "unknown"}`;
148
+ sink.error(sink.state.failureMessage, { recoverable: false });
149
+ }
150
+ else {
151
+ sink.state.receivedTerminalSuccess = true;
152
+ if (event.result)
153
+ sink.state.summary = event.result;
154
+ }
155
+ return true;
156
+ }
157
+ default:
158
+ return true;
159
+ }
160
+ }
161
+ handleToolCall(event, sink) {
162
+ const toolCall = event.tool_call || {};
163
+ const kindKey = Object.keys(toolCall)[0];
164
+ if (!kindKey)
165
+ return;
166
+ const toolName = TOOL_KIND_MAP[kindKey] || kindKey;
167
+ const call = toolCall[kindKey] || {};
168
+ const args = call.args || {};
169
+ if (event.subtype === "started") {
170
+ sink.toolStarted(toolName, this.describeArgs(toolName, args));
171
+ return;
172
+ }
173
+ if (event.subtype === "completed") {
174
+ const hasError = Boolean(call.result && call.result.error);
175
+ sink.toolCompleted(toolName, !hasError, this.describeArgs(toolName, args));
176
+ if (hasError && call.result?.error) {
177
+ const err = call.result.error;
178
+ sink.error(`${toolName} failed: ${err.errorMessage || JSON.stringify(call.result.error).slice(0, 300)}`, { recoverable: true });
179
+ }
180
+ if (FILE_TOOLS.has(toolName) && typeof args.path === "string") {
181
+ const rel = this.relPath(args.path);
182
+ // Guard diff decides create vs modify; only delete is unambiguous here.
183
+ if (toolName === "delete")
184
+ sink.fileChange("deleted", rel);
185
+ else
186
+ sink.fileChange("modified", rel);
187
+ }
188
+ }
189
+ }
190
+ describeArgs(toolName, args) {
191
+ if (typeof args.command === "string")
192
+ return args.command.slice(0, 200);
193
+ if (typeof args.path === "string")
194
+ return String(args.path);
195
+ if (typeof args.pattern === "string")
196
+ return String(args.pattern);
197
+ if (typeof args.query === "string")
198
+ return String(args.query);
199
+ void toolName;
200
+ return undefined;
201
+ }
202
+ inspectFailureText(text) {
203
+ if (!text)
204
+ return null;
205
+ if (/authentication required|please run ['"]?(cursor-agent |agent )?login|not logged in|unauthorized/i.test(text)) {
206
+ return { message: "Cursor Agent authentication failed.", authFailure: true };
207
+ }
208
+ if (/rate limit|usage limit|quota exceeded/i.test(text)) {
209
+ return { message: "Cursor usage limit or quota exceeded.", authFailure: true };
210
+ }
211
+ return null;
212
+ }
213
+ }
214
+ exports.CursorAdapter = CursorAdapter;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Local coding agent factory.
3
+ *
4
+ * Creates the adapter for a local_agent backend config. Adapters are loaded
5
+ * lazily so consumers that never run local agents (e.g. hosted-only server
6
+ * paths) don't pay for parsing the adapter modules.
7
+ */
8
+ import { BackendConfig, IBackend, BackendCapabilities } from "../backend-interface";
9
+ export declare const LOCAL_AGENT_PROVIDERS: readonly ["claude_code", "opencode", "codex_cli", "cursor"];
10
+ export type LocalAgentProvider = (typeof LOCAL_AGENT_PROVIDERS)[number];
11
+ export declare function isLocalAgentProvider(provider: string): provider is LocalAgentProvider;
12
+ export declare function isLocalAgentConfig(config: BackendConfig | undefined | null): config is BackendConfig & {
13
+ type: "local_agent";
14
+ };
15
+ export declare function createLocalAgentBackend(config: BackendConfig & {
16
+ type: "local_agent";
17
+ }): Promise<IBackend>;
18
+ /** Capabilities for a local agent provider (all share the local-agent profile). */
19
+ export declare function localAgentCapabilities(_provider: string): BackendCapabilities;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Local coding agent factory.
4
+ *
5
+ * Creates the adapter for a local_agent backend config. Adapters are loaded
6
+ * lazily so consumers that never run local agents (e.g. hosted-only server
7
+ * paths) don't pay for parsing the adapter modules.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LOCAL_AGENT_PROVIDERS = void 0;
11
+ exports.isLocalAgentProvider = isLocalAgentProvider;
12
+ exports.isLocalAgentConfig = isLocalAgentConfig;
13
+ exports.createLocalAgentBackend = createLocalAgentBackend;
14
+ exports.localAgentCapabilities = localAgentCapabilities;
15
+ const backend_interface_1 = require("../backend-interface");
16
+ exports.LOCAL_AGENT_PROVIDERS = ["claude_code", "opencode", "codex_cli", "cursor"];
17
+ function isLocalAgentProvider(provider) {
18
+ return exports.LOCAL_AGENT_PROVIDERS.includes(provider);
19
+ }
20
+ function isLocalAgentConfig(config) {
21
+ return Boolean(config && config.type === "local_agent" && isLocalAgentProvider(config.provider));
22
+ }
23
+ async function createLocalAgentBackend(config) {
24
+ switch (config.provider) {
25
+ case "claude_code": {
26
+ const { ClaudeCodeAdapter } = await import("./claude-code-adapter.js");
27
+ return new ClaudeCodeAdapter(config);
28
+ }
29
+ case "opencode": {
30
+ const { OpenCodeAdapter } = await import("./opencode-adapter.js");
31
+ return new OpenCodeAdapter(config);
32
+ }
33
+ case "codex_cli": {
34
+ const { CodexCLIAdapter } = await import("./codex-cli-adapter.js");
35
+ return new CodexCLIAdapter(config);
36
+ }
37
+ case "cursor": {
38
+ const { CursorAdapter } = await import("./cursor-adapter.js");
39
+ return new CursorAdapter(config);
40
+ }
41
+ default:
42
+ throw new Error(`Unsupported local agent: ${config.provider}`);
43
+ }
44
+ }
45
+ /** Capabilities for a local agent provider (all share the local-agent profile). */
46
+ function localAgentCapabilities(_provider) {
47
+ return { ...backend_interface_1.DEFAULT_LOCAL_CAPABILITIES };
48
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * LocalAgentAdapter - Shared base for external coding agent backends.
3
+ *
4
+ * Claude Code, OpenCode, Codex CLI, and Cursor are full coding agents with
5
+ * their own internal loops. Lasso does NOT wrap them in a competing model
6
+ * loop: it delegates a task, streams their native structured output,
7
+ * normalizes it into BackendEvents, and lets the orchestrator own
8
+ * diff review, verification, and session state.
9
+ *
10
+ * Every adapter in this package:
11
+ * - invokes the agent's official non-interactive CLI with structured output
12
+ * (JSON/JSONL), never human-readable terminal scraping,
13
+ * - checks installation and authentication separately from execution,
14
+ * - relies on WorkspaceGuard (owned by the orchestrator) for rollback, so
15
+ * no agent's changes ever persist before user approval.
16
+ */
17
+ import { ChildProcess } from "node:child_process";
18
+ import { IBackend, BackendType, BackendProvider, BackendCapabilities, TaskContext, BackendEvent, BackendEvent as Event, BackendResult } from "../backend-interface";
19
+ import { AgentError } from "../agent-runtime";
20
+ export interface LocalAgentConfig {
21
+ type: "local_agent";
22
+ provider: BackendProvider;
23
+ modelId?: string;
24
+ projectPath: string;
25
+ /** Permission/approval mode passed to the agent CLI, if supported */
26
+ permissionMode?: string;
27
+ /** Provider-specific knobs */
28
+ providerConfig?: Record<string, unknown>;
29
+ }
30
+ export interface CommandSpec {
31
+ bin: string;
32
+ args: string[];
33
+ cwd: string;
34
+ env?: NodeJS.ProcessEnv;
35
+ }
36
+ export type AgentRunState = {
37
+ /** External agent session/thread id (for continuation) */
38
+ sessionId?: string;
39
+ /** Final assistant summary, when the agent provides one */
40
+ summary?: string;
41
+ /** Errors reported by the agent while running */
42
+ errors: AgentError[];
43
+ /** Normalized conversation (user prompt + assistant summary) */
44
+ conversation: Array<{
45
+ role: "user" | "assistant" | "system";
46
+ content: string;
47
+ timestamp: number;
48
+ }>;
49
+ /** Tools executed during the run (for progress + observations) */
50
+ tools: Array<{
51
+ name: string;
52
+ success?: boolean;
53
+ detail?: string;
54
+ }>;
55
+ /** True only when the agent's OWN terminal success signal was observed */
56
+ receivedTerminalSuccess: boolean;
57
+ /** Failure message when the run failed */
58
+ failureMessage?: string;
59
+ /** Set when the failure is an authentication/authorization problem */
60
+ authFailure?: boolean;
61
+ /** Set when the run was cancelled by Lasso */
62
+ cancelled: boolean;
63
+ };
64
+ export declare function newRunState(): AgentRunState;
65
+ /** Sink used by adapters while parsing native events → normalized BackendEvents */
66
+ export declare class EventSink {
67
+ private readonly taskId;
68
+ private readonly emitRaw;
69
+ readonly state: AgentRunState;
70
+ constructor(taskId: string, emitRaw: (event: Event) => void, state: AgentRunState);
71
+ emit(type: Event["type"], data?: Record<string, unknown>): void;
72
+ progress(message: string): void;
73
+ toolStarted(name: string, detail?: string): void;
74
+ toolCompleted(name: string, success?: boolean, detail?: string): void;
75
+ fileChange(operation: "created" | "modified" | "deleted", filePath: string): void;
76
+ error(message: string, options?: {
77
+ recoverable?: boolean;
78
+ authFailure?: boolean;
79
+ asEvent?: boolean;
80
+ }): void;
81
+ }
82
+ export declare abstract class LocalAgentAdapter implements IBackend {
83
+ readonly id: string;
84
+ readonly type: BackendType;
85
+ abstract readonly provider: BackendProvider;
86
+ readonly modelId: string;
87
+ protected readonly config: LocalAgentConfig;
88
+ protected process?: ChildProcess;
89
+ private currentTaskId?;
90
+ private killTimer?;
91
+ constructor(config: LocalAgentConfig);
92
+ abstract getCapabilities(): BackendCapabilities;
93
+ /** Executable name resolved via PATH (e.g. "claude", "codex", "cursor-agent") */
94
+ protected abstract get binary(): string;
95
+ /** Human-readable install documentation URL or command */
96
+ protected abstract get installInstructions(): string;
97
+ /** Command used to probe installation + version */
98
+ protected abstract get versionArgs(): string[];
99
+ /** Check authentication using the agent's own session/credentials */
100
+ protected abstract checkAuthentication(): Promise<{
101
+ authenticated: boolean;
102
+ error?: string;
103
+ fixInstructions?: string;
104
+ }>;
105
+ /** Build the CLI invocation for this task */
106
+ protected abstract buildCommand(context: TaskContext): CommandSpec;
107
+ /**
108
+ * Parse one line of the agent's structured output.
109
+ * Return false when the line could not be parsed (used for diagnostics).
110
+ */
111
+ protected abstract handleLine(line: string, context: TaskContext, sink: EventSink): boolean;
112
+ /**
113
+ * Optional: extra stderr/stdout text inspection for failures
114
+ * (e.g. "Authentication required" messages).
115
+ */
116
+ protected inspectFailureText(text: string): {
117
+ message: string;
118
+ authFailure?: boolean;
119
+ } | null;
120
+ /** Whether this adapter can resume its own external session */
121
+ protected get supportsResume(): boolean;
122
+ checkAvailability(): Promise<{
123
+ available: boolean;
124
+ installed: boolean;
125
+ authenticated: boolean;
126
+ version?: string;
127
+ error?: string;
128
+ fixInstructions?: string;
129
+ }>;
130
+ executeTask(context: TaskContext, onEvent: (event: BackendEvent) => void, signal?: AbortSignal): Promise<BackendResult>;
131
+ cancelTask(taskId: string): Promise<void>;
132
+ switchBackend(newBackend: IBackend, context: TaskContext): Promise<{
133
+ transferableContext: Record<string, unknown>;
134
+ lostContext: string[];
135
+ }>;
136
+ /** Build the shared task prompt (project context + continuation context). */
137
+ protected buildTaskPrompt(context: TaskContext): string;
138
+ /** Resume reference from Lasso's canonical session state (if any). */
139
+ protected resumeSessionId(context: TaskContext): string | undefined;
140
+ /** Convert an absolute path reported by the agent to a project-relative one. */
141
+ protected relPath(absoluteOrRelative: string): string;
142
+ private buildResult;
143
+ }