@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.
- package/dist/cli/agent.d.ts +1 -0
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge.d.ts +8 -5
- package/dist/cli/bridge.js +357 -151
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +28 -10
- package/dist/cli/runtime-client.d.ts +91 -10
- package/dist/cli/runtime-client.js +38 -3
- package/dist/overlay.js +44 -28
- package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
- package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
- package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
- package/dist/runtime/__tests__/state-machine.test.js +107 -0
- package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
- package/dist/runtime/__tests__/verification-engine.test.js +91 -0
- package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
- package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
- package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
- package/dist/runtime/adapters/claude-code-adapter.js +230 -0
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
- package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
- package/dist/runtime/adapters/cursor-adapter.js +214 -0
- package/dist/runtime/adapters/factory.d.ts +19 -0
- package/dist/runtime/adapters/factory.js +48 -0
- package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
- package/dist/runtime/adapters/local-agent-adapter.js +408 -0
- package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
- package/dist/runtime/adapters/opencode-adapter.js +210 -0
- package/dist/runtime/agent-runtime.d.ts +286 -0
- package/dist/runtime/agent-runtime.js +8 -0
- package/dist/runtime/agent-state-machine.d.ts +67 -0
- package/dist/runtime/agent-state-machine.js +234 -0
- package/dist/runtime/backend-interface.d.ts +272 -0
- package/dist/runtime/backend-interface.js +38 -0
- package/dist/runtime/event-mapper.d.ts +11 -0
- package/dist/runtime/event-mapper.js +40 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +43 -0
- package/dist/runtime/local-session-runner.d.ts +60 -0
- package/dist/runtime/local-session-runner.js +185 -0
- package/dist/runtime/review.d.ts +24 -0
- package/dist/runtime/review.js +30 -0
- package/dist/runtime/verification-engine.d.ts +71 -0
- package/dist/runtime/verification-engine.js +374 -0
- package/dist/runtime/workspace-guard.d.ts +70 -0
- package/dist/runtime/workspace-guard.js +321 -0
- package/docs/local-coding-agents.md +183 -0
- 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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
22663
|
+
if (isCliGroup) {
|
|
22648
22664
|
const cliGroups = /* @__PURE__ */ new Map();
|
|
22649
22665
|
for (const model of models) {
|
|
22650
|
-
const key = model.
|
|
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.
|
|
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>${
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|