@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
package/dist/overlay.js CHANGED
@@ -7837,7 +7837,7 @@
7837
7837
  if (!name) return "?";
7838
7838
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || "").join("") || "?";
7839
7839
  }
7840
- function timeAgo2(value2) {
7840
+ function timeAgo(value2) {
7841
7841
  if (!value2) return "";
7842
7842
  const ms = new Date(value2).getTime();
7843
7843
  if (!Number.isFinite(ms)) return "";
@@ -18489,7 +18489,7 @@
18489
18489
  author.textContent = msg.author?.name || "Unknown";
18490
18490
  const time = document.createElement("span");
18491
18491
  time.className = "lasso-pin-thread-time";
18492
- time.textContent = timeAgo2(msg.createdAt);
18492
+ time.textContent = timeAgo(msg.createdAt);
18493
18493
  row.append(av, author, time);
18494
18494
  const body = document.createElement("div");
18495
18495
  body.className = "lasso-pin-thread-body";
@@ -20910,8 +20910,9 @@
20910
20910
  (c) => `
20911
20911
  <div class="lasso-command-diff-file">
20912
20912
  <div class="lasso-command-diff-file-header">${escapeHtml4(c.filePath)}</div>
20913
- <pre class="lasso-command-diff-content">${c.oldString ? `<span class="lasso-diff-del">- ${escapeHtml4(c.oldString.slice(0, 300))}</span>
20914
- ` : ""}<span class="lasso-diff-add">+ ${escapeHtml4(c.newString.slice(0, 300))}</span></pre>
20913
+ <pre class="lasso-command-diff-content">${c.operation === "deleted" || c.newString === "" && c.oldString !== "" ? `<span class="lasso-diff-del">- ${escapeHtml4(c.oldString.slice(0, 300))}</span>
20914
+ <span class="lasso-diff-del">(file deleted)</span>` : `${c.oldString ? `<span class="lasso-diff-del">- ${escapeHtml4(c.oldString.slice(0, 300))}</span>
20915
+ ` : ""}${c.newString ? `<span class="lasso-diff-add">+ ${escapeHtml4(c.newString.slice(0, 300))}</span>` : ""}`}</pre>
20915
20916
  </div>
20916
20917
  `
20917
20918
  ).join("")}
@@ -21835,7 +21836,6 @@ The agent is automatically attempting to fix the build errors...`;
21835
21836
  }
21836
21837
  if (isPromptTask) {
21837
21838
  closeReview();
21838
- closeConflict();
21839
21839
  resetDrag(false);
21840
21840
  state.pendingChanges = [];
21841
21841
  }
@@ -22106,7 +22106,11 @@ The agent is automatically attempting to fix the build errors...`;
22106
22106
  anthropic: "Anthropic",
22107
22107
  nvidia: "NVIDIA",
22108
22108
  ollama: "Local",
22109
- cli: "CLI"
22109
+ cli: "CLI",
22110
+ claude_code: "Claude Code",
22111
+ opencode: "OpenCode",
22112
+ codex_cli: "Codex CLI",
22113
+ cursor: "Cursor"
22110
22114
  };
22111
22115
  var providerIcons = {
22112
22116
  google: google_icon_default,
@@ -22114,13 +22118,20 @@ The agent is automatically attempting to fix the build errors...`;
22114
22118
  anthropic: anthropic_icon_default,
22115
22119
  nvidia: nvidia_default,
22116
22120
  ollama: terminal_default,
22117
- cli: terminal_default
22121
+ cli: terminal_default,
22122
+ claude_code: terminal_default,
22123
+ opencode: terminal_default,
22124
+ codex_cli: terminal_default,
22125
+ cursor: terminal_default
22118
22126
  };
22119
22127
  function providerIcon(provider, active = false) {
22120
- if (provider === "cli") {
22121
- return `<span class="${active ? "lasso-model-active-icon" : "lasso-model-item-icon"} provider-cli" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false"><circle cx="12" cy="12" r="9" fill="currentColor" opacity=".16"/><circle cx="12" cy="9" r="3" fill="currentColor"/><path d="M6.5 18c.9-2.4 2.7-3.6 5.5-3.6s4.6 1.2 5.5 3.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>`;
22128
+ if (provider === "cli" || provider === "claude_code" || provider === "opencode" || provider === "codex_cli" || provider === "cursor") {
22129
+ return `<span class="${active ? "lasso-model-active-icon" : "lasso-model-item-icon"} provider-${provider}" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false"><circle cx="12" cy="12" r="9" fill="currentColor" opacity=".16"/><circle cx="12" cy="9" r="3" fill="currentColor"/><path d="M6.5 18c.9-2.4 2.7-3.6 5.5-3.6s4.6 1.2 5.5 3.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>`;
22122
22130
  }
22123
22131
  const icon = providerIcons[provider];
22132
+ if (!icon) {
22133
+ return `<span class="${active ? "lasso-model-active-icon" : "lasso-model-item-icon"} provider-${provider}" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false"><circle cx="12" cy="12" r="9" fill="currentColor" opacity=".16"/><circle cx="12" cy="9" r="3" fill="currentColor"/><path d="M6.5 18c.9-2.4 2.7-3.6 5.5-3.6s4.6 1.2 5.5 3.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>`;
22134
+ }
22124
22135
  return `<span class="${active ? "lasso-model-active-icon" : "lasso-model-item-icon"} provider-${provider}" aria-hidden="true"><svg viewBox="0 0 ${icon.width} ${icon.height}" xmlns="http://www.w3.org/2000/svg" focusable="false">${icon.body}</svg></span>`;
22125
22136
  }
22126
22137
  function componentConversationId(element) {
@@ -22623,37 +22634,42 @@ The agent is automatically attempting to fix the build errors...`;
22623
22634
  state.bridgeSocket.send(JSON.stringify({ type: "agent_prompt_response", taskId: taskId2, response }));
22624
22635
  appendChat("assistant", `Responded to the agent: ${response}`, taskId2);
22625
22636
  }
22637
+ var LOCAL_AGENT_PROVIDERS = /* @__PURE__ */ new Set(["cli", "claude_code", "opencode", "codex_cli", "cursor"]);
22626
22638
  function buildModelMenuContent(menuEl, onRefresh) {
22627
22639
  const filters = document.createElement("div");
22628
22640
  filters.className = "lasso-model-filters";
22629
- const providers = ["all", ...Array.from(new Set(state.MODELS.map((m) => m.provider)))];
22641
+ const allProviders = Array.from(new Set(state.MODELS.map((m) => m.provider)));
22642
+ const hasLocal = allProviders.some((p) => LOCAL_AGENT_PROVIDERS.has(p));
22643
+ const hostedProviders = allProviders.filter((p) => !LOCAL_AGENT_PROVIDERS.has(p));
22644
+ const providers = ["all", ...hostedProviders, ...hasLocal ? ["cli"] : []];
22630
22645
  for (const provider of providers) {
22631
22646
  const filter2 = document.createElement("button");
22632
22647
  filter2.type = "button";
22633
22648
  filter2.className = `lasso-model-filter${state.modelFilter === provider ? " active" : ""}`;
22634
22649
  filter2.dataset.providerFilter = provider;
22635
- filter2.textContent = provider === "all" ? "All" : providerLabels[provider];
22650
+ filter2.textContent = provider === "all" ? "All" : providerLabels[provider] || provider;
22636
22651
  filters.appendChild(filter2);
22637
22652
  }
22638
22653
  const children = [filters];
22639
- const visibleProviders = state.modelFilter === "all" ? providers.slice(1) : [state.modelFilter];
22654
+ const visibleProviders = state.modelFilter === "all" ? providers.slice(1) : state.modelFilter === "cli" ? ["cli"] : [state.modelFilter];
22640
22655
  for (const provider of visibleProviders) {
22641
- const models = state.MODELS.filter((m) => m.provider === provider);
22656
+ const isCliGroup = provider === "cli";
22657
+ const models = isCliGroup ? state.MODELS.filter((m) => LOCAL_AGENT_PROVIDERS.has(m.provider)) : state.MODELS.filter((m) => m.provider === provider);
22642
22658
  if (!models.length) continue;
22643
22659
  const group = document.createElement("div");
22644
22660
  group.className = "lasso-model-group";
22645
- group.textContent = providerLabels[provider];
22661
+ group.textContent = isCliGroup ? "CLI" : providerLabels[provider] || provider;
22646
22662
  children.push(group);
22647
- if (provider === "cli") {
22663
+ if (isCliGroup) {
22648
22664
  const cliGroups = /* @__PURE__ */ new Map();
22649
22665
  for (const model of models) {
22650
- const key = model.id.split(":", 1)[0] || "cli";
22666
+ const key = model.provider;
22651
22667
  const groupModels = cliGroups.get(key) || [];
22652
22668
  groupModels.push(model);
22653
22669
  cliGroups.set(key, groupModels);
22654
22670
  }
22655
- const selectedCliGroup = state.selectedModel.id.split(":", 1)[0];
22656
- if (selectedCliGroup && !openCliGroups.size) openCliGroups.add(selectedCliGroup);
22671
+ const selectedCliGroup = state.selectedModel.provider;
22672
+ if (selectedCliGroup && LOCAL_AGENT_PROVIDERS.has(selectedCliGroup) && !openCliGroups.size) openCliGroups.add(selectedCliGroup);
22657
22673
  for (const [cliKey, cliModels] of cliGroups) {
22658
22674
  const subGroup = document.createElement("div");
22659
22675
  subGroup.className = "lasso-cli-subgroup";
@@ -22661,7 +22677,7 @@ The agent is automatically attempting to fix the build errors...`;
22661
22677
  subHeader.type = "button";
22662
22678
  subHeader.className = "lasso-cli-subgroup-header";
22663
22679
  subHeader.setAttribute("aria-expanded", String(openCliGroups.has(cliKey)));
22664
- subHeader.innerHTML = `<span>${cliLabel(cliKey)}</span><span class="lasso-cli-subgroup-chevron">${openCliGroups.has(cliKey) ? "\u2303" : "\u2304"}</span>`;
22680
+ subHeader.innerHTML = `<span>${providerLabels[cliKey] || cliKey}</span><span class="lasso-cli-subgroup-chevron">${openCliGroups.has(cliKey) ? "\u2303" : "\u2304"}</span>`;
22665
22681
  const subItems = document.createElement("div");
22666
22682
  subItems.className = "lasso-cli-subgroup-items";
22667
22683
  subItems.hidden = !openCliGroups.has(cliKey);
@@ -22692,13 +22708,6 @@ The agent is automatically attempting to fix the build errors...`;
22692
22708
  buildModelMenuContent(modelMenu, refreshModelMenu);
22693
22709
  syncModelMenu2();
22694
22710
  }
22695
- function cliLabel(key) {
22696
- if (key === "claude-code") return "Claude Code";
22697
- if (key === "codex") return "Codex";
22698
- if (key === "opencode") return "OpenCode";
22699
- if (key === "cursor") return "Cursor";
22700
- return key;
22701
- }
22702
22711
  function modelItem(model) {
22703
22712
  const item = document.createElement("button");
22704
22713
  item.type = "button";
@@ -23019,10 +23028,17 @@ The agent is automatically attempting to fix the build errors...`;
23019
23028
  code.className = "lasso-review-code";
23020
23029
  const oldCode = document.createElement("pre");
23021
23030
  oldCode.className = "lasso-review-old";
23022
- oldCode.textContent = `- ${change.oldString}`;
23023
23031
  const newCode = document.createElement("pre");
23024
23032
  newCode.className = "lasso-review-new";
23025
- newCode.textContent = `+ ${change.newString}`;
23033
+ const isDelete = change.operation === "deleted" || change.newString === "" && change.oldString !== "";
23034
+ if (isDelete) {
23035
+ oldCode.textContent = `- ${change.oldString}`;
23036
+ newCode.textContent = "(file deleted)";
23037
+ newCode.classList.add("lasso-review-deleted");
23038
+ } else {
23039
+ oldCode.textContent = change.oldString ? `- ${change.oldString}` : "(new file)";
23040
+ newCode.textContent = change.newString ? `+ ${change.newString}` : "";
23041
+ }
23026
23042
  code.append(oldCode, newCode);
23027
23043
  file.append(name, code);
23028
23044
  return file;
@@ -0,0 +1,392 @@
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
+ const node_test_1 = require("node:test");
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const local_agent_adapter_1 = require("../adapters/local-agent-adapter");
12
+ const claude_code_adapter_1 = require("../adapters/claude-code-adapter");
13
+ const opencode_adapter_1 = require("../adapters/opencode-adapter");
14
+ const codex_cli_adapter_1 = require("../adapters/codex-cli-adapter");
15
+ const cursor_adapter_1 = require("../adapters/cursor-adapter");
16
+ // ── test harness ───────────────────────────────────────────────────────
17
+ function makeContext(_projectPath, overrides = {}) {
18
+ return {
19
+ taskId: "task-1",
20
+ userPrompt: "Add a greeting function",
21
+ project: {
22
+ projectId: "proj-1",
23
+ hasTypeScript: true,
24
+ hasTailwind: false,
25
+ hasReact: false,
26
+ hasNext: false,
27
+ hasVite: true,
28
+ files: ["src/index.ts"],
29
+ },
30
+ conversation: [{ role: "user", content: "Add a greeting function", timestamp: Date.now() }],
31
+ previousObservations: [],
32
+ previousErrors: [],
33
+ currentPhase: "execute",
34
+ constraints: { safetyLevel: "standard" },
35
+ ...overrides,
36
+ };
37
+ }
38
+ function makeSink(taskId = "task-1") {
39
+ const events = [];
40
+ const state = (0, local_agent_adapter_1.newRunState)();
41
+ const sink = new local_agent_adapter_1.EventSink(taskId, (event) => events.push(event), state);
42
+ return { events, state, sink };
43
+ }
44
+ function eventsOfType(events, type) {
45
+ return events.filter((e) => e.type === type);
46
+ }
47
+ // Expose protected parsing/command construction for assertions.
48
+ class ClaudeProbe extends claude_code_adapter_1.ClaudeCodeAdapter {
49
+ parse(line, ctx, sink) {
50
+ return this.handleLine(line, ctx, sink);
51
+ }
52
+ command(ctx) {
53
+ return this.buildCommand(ctx);
54
+ }
55
+ }
56
+ class OpenCodeProbe extends opencode_adapter_1.OpenCodeAdapter {
57
+ parse(line, ctx, sink) {
58
+ return this.handleLine(line, ctx, sink);
59
+ }
60
+ }
61
+ class CodexProbe extends codex_cli_adapter_1.CodexCLIAdapter {
62
+ parse(line, ctx, sink) {
63
+ return this.handleLine(line, ctx, sink);
64
+ }
65
+ }
66
+ class CursorProbe extends cursor_adapter_1.CursorAdapter {
67
+ parse(line, ctx, sink) {
68
+ return this.handleLine(line, ctx, sink);
69
+ }
70
+ }
71
+ function loadFixtureLines(name) {
72
+ const file = node_path_1.default.join("/tmp", "lasso-probe", name, "out.jsonl");
73
+ if (!node_fs_1.default.existsSync(file))
74
+ return [];
75
+ return node_fs_1.default
76
+ .readFileSync(file, "utf8")
77
+ .split("\n")
78
+ .map((l) => l.trim())
79
+ .filter(Boolean);
80
+ }
81
+ // ── event normalization ────────────────────────────────────────────────
82
+ (0, node_test_1.describe)("local agent event normalization", () => {
83
+ (0, node_test_1.it)("claude: normalizes init, tool_use, and successful result", () => {
84
+ const projectPath = "/proj";
85
+ const adapter = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath });
86
+ const ctx = makeContext(projectPath);
87
+ const { events, state, sink } = makeSink();
88
+ strict_1.default.equal(adapter.parse(JSON.stringify({ type: "system", subtype: "init", session_id: "sess-1", model: "claude-sonnet" }), ctx, sink), true);
89
+ strict_1.default.equal(adapter.parse(JSON.stringify({
90
+ type: "assistant",
91
+ message: {
92
+ role: "assistant",
93
+ content: [
94
+ { type: "text", text: "Editing the file now" },
95
+ { type: "tool_use", name: "Write", input: { file_path: "/proj/src/app.ts", content: "x" } },
96
+ ],
97
+ },
98
+ }), ctx, sink), true);
99
+ strict_1.default.equal(adapter.parse(JSON.stringify({ type: "result", subtype: "success", is_error: false, result: "All done." }), ctx, sink), true);
100
+ strict_1.default.equal(state.sessionId, "sess-1");
101
+ strict_1.default.equal(state.receivedTerminalSuccess, true);
102
+ strict_1.default.equal(state.summary, "All done.");
103
+ strict_1.default.equal(state.failureMessage, undefined);
104
+ const toolStarts = eventsOfType(events, "tool_started");
105
+ strict_1.default.equal(toolStarts.length, 1);
106
+ strict_1.default.equal(toolStarts[0].data?.tool, "Write");
107
+ const fileMods = eventsOfType(events, "file_modified");
108
+ strict_1.default.equal(fileMods.length, 1);
109
+ strict_1.default.equal(fileMods[0].data?.filePath, "src/app.ts");
110
+ const progresses = eventsOfType(events, "task_progress");
111
+ strict_1.default.ok(progresses.some((e) => e.data?.message === "Editing the file now"));
112
+ });
113
+ (0, node_test_1.it)("claude: billing error is an auth failure with terminal error result", () => {
114
+ const projectPath = "/proj";
115
+ const adapter = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath });
116
+ const ctx = makeContext(projectPath);
117
+ const { events, state, sink } = makeSink();
118
+ adapter.parse(JSON.stringify({
119
+ type: "assistant",
120
+ error: "billing_error",
121
+ message: { role: "assistant", content: [{ type: "text", text: "Credit balance is too low" }] },
122
+ }), ctx, sink);
123
+ adapter.parse(JSON.stringify({ type: "result", is_error: true, result: "Credit balance is too low" }), ctx, sink);
124
+ strict_1.default.equal(state.authFailure, true);
125
+ strict_1.default.match(state.failureMessage || "", /credit balance/i);
126
+ strict_1.default.notEqual(state.receivedTerminalSuccess, true);
127
+ const errors = eventsOfType(events, "error");
128
+ strict_1.default.ok(errors.length >= 1);
129
+ strict_1.default.equal(errors[0].data?.error?.type, "permission");
130
+ });
131
+ (0, node_test_1.it)("claude: real fixture (when present) parses without unparseable events", () => {
132
+ const lines = loadFixtureLines("claude");
133
+ if (lines.length === 0)
134
+ return; // gated on local probe availability
135
+ const projectPath = node_path_1.default.join("/tmp", "lasso-probe", "claude");
136
+ const adapter = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath });
137
+ const ctx = makeContext(projectPath);
138
+ const { state, sink } = makeSink();
139
+ for (const line of lines) {
140
+ strict_1.default.equal(adapter.parse(line, ctx, sink), true, `unparseable: ${line.slice(0, 80)}`);
141
+ }
142
+ // The captured run hit a billing error.
143
+ strict_1.default.equal(state.authFailure, true);
144
+ strict_1.default.ok(state.sessionId);
145
+ });
146
+ (0, node_test_1.it)("opencode: normalizes tool_use, text, and step_finish", () => {
147
+ const projectPath = "/proj";
148
+ const adapter = new OpenCodeProbe({ type: "local_agent", provider: "opencode", projectPath });
149
+ const ctx = makeContext(projectPath);
150
+ const { events, state, sink } = makeSink();
151
+ adapter.parse(JSON.stringify({ type: "step_start", sessionID: "ses_1", part: {} }), ctx, sink);
152
+ adapter.parse(JSON.stringify({
153
+ type: "tool_use",
154
+ sessionID: "ses_1",
155
+ part: { type: "tool", tool: "write", state: { status: "completed", input: { content: "ok", filePath: "/proj/hello.txt" } } },
156
+ }), ctx, sink);
157
+ adapter.parse(JSON.stringify({ type: "text", sessionID: "ses_1", part: { type: "text", text: "Created hello.txt." } }), ctx, sink);
158
+ adapter.parse(JSON.stringify({ type: "step_finish", sessionID: "ses_1", part: { type: "step-finish", reason: "stop" } }), ctx, sink);
159
+ strict_1.default.equal(state.sessionId, "ses_1");
160
+ strict_1.default.equal(state.receivedTerminalSuccess, true);
161
+ const toolCompleted = eventsOfType(events, "tool_completed");
162
+ strict_1.default.equal(toolCompleted.length, 1);
163
+ strict_1.default.equal(toolCompleted[0].data?.tool, "write");
164
+ const fileChanges = eventsOfType(events, "file_modified");
165
+ strict_1.default.equal(fileChanges.length, 1);
166
+ strict_1.default.equal(fileChanges[0].data?.filePath, "hello.txt");
167
+ strict_1.default.ok(eventsOfType(events, "task_progress").some((e) => e.data?.message === "Created hello.txt."));
168
+ });
169
+ (0, node_test_1.it)("opencode: error event marks failure", () => {
170
+ const projectPath = "/proj";
171
+ const adapter = new OpenCodeProbe({ type: "local_agent", provider: "opencode", projectPath });
172
+ const ctx = makeContext(projectPath);
173
+ const { events, state, sink } = makeSink();
174
+ adapter.parse(JSON.stringify({ type: "error", sessionID: "ses_1", error: { message: "model unavailable" } }), ctx, sink);
175
+ strict_1.default.equal(state.failureMessage, "model unavailable");
176
+ strict_1.default.equal(eventsOfType(events, "error").length, 1);
177
+ });
178
+ (0, node_test_1.it)("codex: thread, command execution, file change, and turn completion", () => {
179
+ const projectPath = "/proj";
180
+ const adapter = new CodexProbe({ type: "local_agent", provider: "codex_cli", projectPath });
181
+ const ctx = makeContext(projectPath);
182
+ const { events, state, sink } = makeSink();
183
+ adapter.parse(JSON.stringify({ type: "thread.started", thread_id: "thr-1" }), ctx, sink);
184
+ adapter.parse(JSON.stringify({ type: "turn.started" }), ctx, sink);
185
+ adapter.parse(JSON.stringify({ type: "item.started", item: { item_type: "command_execution", command: "ls" } }), ctx, sink);
186
+ adapter.parse(JSON.stringify({ type: "item.completed", item: { item_type: "command_execution", command: "ls", exit_code: 0, status: "completed" } }), ctx, sink);
187
+ adapter.parse(JSON.stringify({
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" }] },
190
+ }), ctx, sink);
191
+ adapter.parse(JSON.stringify({ type: "turn.completed" }), ctx, sink);
192
+ strict_1.default.equal(state.sessionId, "thr-1");
193
+ strict_1.default.equal(state.receivedTerminalSuccess, true);
194
+ strict_1.default.ok(eventsOfType(events, "tool_started").some((e) => e.data?.tool === "shell"));
195
+ strict_1.default.ok(eventsOfType(events, "tool_completed").some((e) => e.data?.tool === "file_change"));
196
+ const deleted = eventsOfType(events, "file_deleted");
197
+ strict_1.default.equal(deleted.length, 1);
198
+ strict_1.default.equal(deleted[0].data?.filePath, node_path_1.default.join("src", "old.ts"));
199
+ strict_1.default.equal(eventsOfType(events, "file_modified").length, 1);
200
+ });
201
+ (0, node_test_1.it)("codex: usage limit is an auth failure (real fixture when present)", () => {
202
+ const lines = loadFixtureLines("codex");
203
+ if (lines.length === 0)
204
+ return;
205
+ const projectPath = node_path_1.default.join("/tmp", "lasso-probe", "codex");
206
+ const adapter = new CodexProbe({ type: "local_agent", provider: "codex_cli", projectPath });
207
+ const ctx = makeContext(projectPath);
208
+ const { events, state, sink } = makeSink();
209
+ for (const line of lines) {
210
+ strict_1.default.equal(adapter.parse(line, ctx, sink), true);
211
+ }
212
+ strict_1.default.equal(state.authFailure, true);
213
+ strict_1.default.match(state.failureMessage || "", /usage limit/i);
214
+ strict_1.default.equal(eventsOfType(events, "error").length, 1);
215
+ strict_1.default.notEqual(state.receivedTerminalSuccess, true);
216
+ });
217
+ (0, node_test_1.it)("cursor: normalizes system init, assistant text, tool calls, and result", () => {
218
+ const projectPath = "/proj";
219
+ const adapter = new CursorProbe({ type: "local_agent", provider: "cursor", projectPath });
220
+ const ctx = makeContext(projectPath);
221
+ const { events, state, sink } = makeSink();
222
+ adapter.parse(JSON.stringify({ type: "system", subtype: "init", session_id: "cur-1", model: "composer-2" }), ctx, sink);
223
+ adapter.parse(JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "Working on it" }] } }), ctx, sink);
224
+ adapter.parse(JSON.stringify({ type: "tool_call", subtype: "started", tool_call: { editToolCall: { args: { path: "/proj/src/a.ts" } } } }), ctx, sink);
225
+ adapter.parse(JSON.stringify({ type: "tool_call", subtype: "completed", tool_call: { editToolCall: { args: { path: "/proj/src/a.ts" } } } }), ctx, sink);
226
+ adapter.parse(JSON.stringify({ type: "result", subtype: "success", is_error: false, result: "Done" }), ctx, sink);
227
+ strict_1.default.equal(state.sessionId, "cur-1");
228
+ strict_1.default.equal(state.receivedTerminalSuccess, true);
229
+ strict_1.default.equal(state.summary, "Done");
230
+ const starts = eventsOfType(events, "tool_started");
231
+ const completes = eventsOfType(events, "tool_completed");
232
+ strict_1.default.equal(starts.length, 1);
233
+ strict_1.default.equal(completes.length, 1);
234
+ strict_1.default.equal(starts[0].data?.tool, "edit");
235
+ strict_1.default.equal(eventsOfType(events, "file_modified").length, 1);
236
+ const progresses = eventsOfType(events, "task_progress");
237
+ strict_1.default.ok(progresses.some((e) => e.data?.message === "Working on it"));
238
+ });
239
+ (0, node_test_1.it)("cursor: error result is a failure", () => {
240
+ const projectPath = "/proj";
241
+ const adapter = new CursorProbe({ type: "local_agent", provider: "cursor", projectPath });
242
+ const ctx = makeContext(projectPath);
243
+ const { events, state, sink } = makeSink();
244
+ adapter.parse(JSON.stringify({ type: "result", subtype: "error", is_error: true, result: "Model overloaded" }), ctx, sink);
245
+ strict_1.default.equal(state.failureMessage, "Model overloaded");
246
+ strict_1.default.notEqual(state.receivedTerminalSuccess, true);
247
+ strict_1.default.equal(eventsOfType(events, "error").length, 1);
248
+ });
249
+ (0, node_test_1.it)("malformed lines are rejected, not thrown", () => {
250
+ const projectPath = "/proj";
251
+ const adapter = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath });
252
+ const ctx = makeContext(projectPath);
253
+ const { sink } = makeSink();
254
+ strict_1.default.equal(adapter.parse("this is not json", ctx, sink), false);
255
+ strict_1.default.equal(adapter.parse(JSON.stringify({ type: "assistant" }), ctx, sink), true);
256
+ });
257
+ });
258
+ // ── capabilities & session continuation ────────────────────────────────
259
+ (0, node_test_1.describe)("backend capabilities", () => {
260
+ (0, node_test_1.it)("local agents advertise an internal loop, streaming, and cancellation", () => {
261
+ const projectPath = "/proj";
262
+ const adapters = [
263
+ new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath }),
264
+ new OpenCodeProbe({ type: "local_agent", provider: "opencode", projectPath }),
265
+ new CodexProbe({ type: "local_agent", provider: "codex_cli", projectPath }),
266
+ new CursorProbe({ type: "local_agent", provider: "cursor", projectPath }),
267
+ ];
268
+ for (const adapter of adapters) {
269
+ const caps = adapter.getCapabilities();
270
+ strict_1.default.equal(caps.hasInternalLoop, true, adapter.provider);
271
+ strict_1.default.equal(caps.canStreamEvents, true, adapter.provider);
272
+ strict_1.default.equal(caps.supportsCancellation, true, adapter.provider);
273
+ strict_1.default.equal(caps.providesStructuredDiffs, false, adapter.provider);
274
+ strict_1.default.equal(caps.canIsolateWorkspace, false, adapter.provider);
275
+ }
276
+ });
277
+ (0, node_test_1.it)("claude command includes resume when an external session exists", () => {
278
+ const projectPath = "/proj";
279
+ const adapter = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath, modelId: "sonnet" });
280
+ const fresh = adapter.command(makeContext(projectPath));
281
+ strict_1.default.ok(fresh.args.includes("--session-id"));
282
+ strict_1.default.ok(!fresh.args.includes("--resume"));
283
+ strict_1.default.ok(fresh.args.includes("--output-format"));
284
+ strict_1.default.ok(fresh.args.includes("stream-json"));
285
+ strict_1.default.ok(fresh.args.includes("--model"));
286
+ const resumed = adapter.command(makeContext(projectPath, { sessionRef: { id: "ext-42" } }));
287
+ const resumeIdx = resumed.args.indexOf("--resume");
288
+ strict_1.default.ok(resumeIdx >= 0);
289
+ strict_1.default.equal(resumed.args[resumeIdx + 1], "ext-42");
290
+ strict_1.default.ok(!resumed.args.includes("--session-id"));
291
+ });
292
+ (0, node_test_1.it)("switching away from a resumable agent reports lost internal state", async () => {
293
+ const projectPath = "/proj";
294
+ const from = new ClaudeProbe({ type: "local_agent", provider: "claude_code", projectPath });
295
+ const to = new CursorProbe({ type: "local_agent", provider: "cursor", projectPath });
296
+ const result = await from.switchBackend(to, makeContext(projectPath, { sessionRef: { id: "ext-1" } }));
297
+ strict_1.default.equal(result.transferableContext.externalSessionId, "ext-1");
298
+ strict_1.default.ok(Array.isArray(result.transferableContext.conversation));
299
+ strict_1.default.ok(result.lostContext.length >= 1);
300
+ strict_1.default.ok(result.lostContext.some((m) => /claude_code/.test(m)));
301
+ });
302
+ });
303
+ // ── execution: success rails, failure rails, cancellation ──────────────
304
+ (0, node_test_1.describe)("local agent execution (fake binary)", () => {
305
+ let dir;
306
+ let scriptPath;
307
+ (0, node_test_1.beforeEach)(() => {
308
+ dir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "lasso-fake-agent-"));
309
+ scriptPath = node_path_1.default.join(dir, "fake-agent.js");
310
+ });
311
+ (0, node_test_1.afterEach)(() => {
312
+ node_fs_1.default.rmSync(dir, { recursive: true, force: true });
313
+ });
314
+ class FakeClaude extends ClaudeProbe {
315
+ script;
316
+ constructor(projectPath, script) {
317
+ super({ type: "local_agent", provider: "claude_code", projectPath });
318
+ this.script = script;
319
+ }
320
+ buildCommand(ctx) {
321
+ void ctx;
322
+ return { bin: process.execPath, args: [this.script], cwd: this.config.projectPath };
323
+ }
324
+ }
325
+ async function runFake(scriptSource, options = {}) {
326
+ node_fs_1.default.writeFileSync(scriptPath, scriptSource);
327
+ const adapter = new FakeClaude(dir, scriptPath);
328
+ const events = [];
329
+ const controller = new AbortController();
330
+ if (options.abortAfterMs !== undefined) {
331
+ setTimeout(() => controller.abort(), options.abortAfterMs);
332
+ }
333
+ const result = await adapter.executeTask(makeContext(dir), (event) => events.push(event), controller.signal);
334
+ return { result, events };
335
+ }
336
+ (0, node_test_1.it)("succeeds only when the agent reports terminal success with exit 0", async () => {
337
+ const { result, events } = await runFake(`
338
+ const lines = [
339
+ JSON.stringify({ type: "system", subtype: "init", session_id: "s1", model: "claude-sonnet" }),
340
+ JSON.stringify({ type: "result", subtype: "success", is_error: false, result: "Finished the task." }),
341
+ ];
342
+ for (const line of lines) console.log(line);
343
+ process.exit(0);
344
+ `);
345
+ strict_1.default.equal(result.success, true);
346
+ strict_1.default.equal(result.status, "completed");
347
+ strict_1.default.equal(result.summary, "Finished the task.");
348
+ strict_1.default.equal(result.errors.length, 0);
349
+ // Guard diff is authoritative — adapters never report file changes.
350
+ strict_1.default.deepEqual(result.fileChanges, []);
351
+ strict_1.default.ok(events.some((e) => e.type === "task_completed"));
352
+ strict_1.default.equal(result.metadata?.externalSessionId, "s1");
353
+ });
354
+ (0, node_test_1.it)("fails when the process exits 0 without a terminal success signal", async () => {
355
+ const { result, events } = await runFake(`process.exit(0);`);
356
+ strict_1.default.equal(result.success, false);
357
+ strict_1.default.equal(result.status, "failed");
358
+ strict_1.default.ok(result.errors.length >= 1);
359
+ strict_1.default.match(result.errors[0].message, /without reporting a successful result/);
360
+ strict_1.default.ok(events.some((e) => e.type === "task_failed"));
361
+ });
362
+ (0, node_test_1.it)("fails with the agent's own failure message", async () => {
363
+ const { result, events } = await runFake(`
364
+ console.log(JSON.stringify({ type: "result", is_error: true, result: "Credit balance is too low" }));
365
+ process.exit(1);
366
+ `);
367
+ strict_1.default.equal(result.success, false);
368
+ strict_1.default.equal(result.status, "failed");
369
+ strict_1.default.ok(result.errors.some((e) => /credit balance/i.test(e.message)));
370
+ strict_1.default.ok(events.some((e) => e.type === "task_failed"));
371
+ });
372
+ (0, node_test_1.it)("cancellation aborts the child and reports a cancelled result", async () => {
373
+ const { result, events } = await runFake(`
374
+ console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "s2", model: "claude-sonnet" }));
375
+ setInterval(() => {}, 1000);
376
+ `, { abortAfterMs: 300 });
377
+ strict_1.default.equal(result.success, false);
378
+ strict_1.default.equal(result.status, "cancelled");
379
+ strict_1.default.ok(events.some((e) => e.type === "task_cancelled"));
380
+ });
381
+ (0, node_test_1.it)("checkAvailability returns structured availability without throwing", async () => {
382
+ const adapter = new FakeClaude(dir, scriptPath);
383
+ const availability = await adapter.checkAvailability();
384
+ strict_1.default.equal(typeof availability.available, "boolean");
385
+ strict_1.default.equal(typeof availability.installed, "boolean");
386
+ strict_1.default.equal(typeof availability.authenticated, "boolean");
387
+ if (!availability.installed) {
388
+ strict_1.default.ok(availability.error);
389
+ strict_1.default.ok(availability.fixInstructions);
390
+ }
391
+ });
392
+ });
@@ -0,0 +1 @@
1
+ export {};