@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.
- 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
|
@@ -0,0 +1,107 @@
|
|
|
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 agent_state_machine_1 = require("../agent-state-machine");
|
|
9
|
+
function makeSession(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
id: "sess-1",
|
|
12
|
+
phase: "verify",
|
|
13
|
+
status: "running",
|
|
14
|
+
iteration: 1,
|
|
15
|
+
userPrompt: "Add a greeting",
|
|
16
|
+
conversation: [],
|
|
17
|
+
observations: [],
|
|
18
|
+
actions: [],
|
|
19
|
+
completedActions: [],
|
|
20
|
+
errors: [],
|
|
21
|
+
project: {
|
|
22
|
+
projectId: "proj-1",
|
|
23
|
+
hasTypeScript: true,
|
|
24
|
+
hasTailwind: false,
|
|
25
|
+
hasReact: false,
|
|
26
|
+
hasNext: false,
|
|
27
|
+
hasVite: true,
|
|
28
|
+
files: [],
|
|
29
|
+
},
|
|
30
|
+
model: { provider: "anthropic", id: "claude-3-5-sonnet-20241022" },
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
updatedAt: new Date(),
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const action = {
|
|
37
|
+
id: "act-1",
|
|
38
|
+
type: "edit_file",
|
|
39
|
+
data: { filePath: "a.ts", oldString: "a", newString: "b" },
|
|
40
|
+
};
|
|
41
|
+
(0, node_test_1.describe)("AgentStateMachine (review / corrective flow)", () => {
|
|
42
|
+
(0, node_test_1.it)("cannot enter recover without verification errors", () => {
|
|
43
|
+
const session = makeSession();
|
|
44
|
+
const result = agent_state_machine_1.AgentStateMachine.transition(session, "recover");
|
|
45
|
+
strict_1.default.equal(result.success, false);
|
|
46
|
+
strict_1.default.match(result.error || "", /without errors/i);
|
|
47
|
+
});
|
|
48
|
+
(0, node_test_1.it)("can enter recover once verification errors exist", () => {
|
|
49
|
+
const session = makeSession({
|
|
50
|
+
errors: [
|
|
51
|
+
{
|
|
52
|
+
id: "err-1",
|
|
53
|
+
type: "verification",
|
|
54
|
+
message: "Build failed",
|
|
55
|
+
phase: "verify",
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
recoverable: true,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
const result = agent_state_machine_1.AgentStateMachine.transition(session, "recover");
|
|
62
|
+
strict_1.default.equal(result.success, true);
|
|
63
|
+
});
|
|
64
|
+
(0, node_test_1.it)("complete is terminal", () => {
|
|
65
|
+
const session = makeSession({ phase: "complete", actions: [action], completedActions: [action] });
|
|
66
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canTransition(session, "execute"), false);
|
|
67
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canTransition(session, "verify"), false);
|
|
68
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canTransition(session, "recover"), false);
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.it)("cannot complete a session with no executed actions", () => {
|
|
71
|
+
const session = makeSession({ actions: [], completedActions: [] });
|
|
72
|
+
const result = agent_state_machine_1.AgentStateMachine.transition(session, "complete");
|
|
73
|
+
strict_1.default.equal(result.success, false);
|
|
74
|
+
strict_1.default.match(result.error || "", /without executing/i);
|
|
75
|
+
});
|
|
76
|
+
(0, node_test_1.it)("cannot complete a failed session", () => {
|
|
77
|
+
const session = makeSession({ status: "failed", actions: [action], completedActions: [action] });
|
|
78
|
+
const result = agent_state_machine_1.AgentStateMachine.transition(session, "complete");
|
|
79
|
+
strict_1.default.equal(result.success, false);
|
|
80
|
+
strict_1.default.match(result.error || "", /failed session/i);
|
|
81
|
+
});
|
|
82
|
+
(0, node_test_1.it)("backend switching allowed only while running and not complete", () => {
|
|
83
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canSwitchModel(makeSession({ status: "running", phase: "execute" })), true);
|
|
84
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canSwitchModel(makeSession({ status: "running", phase: "complete" })), false);
|
|
85
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canSwitchModel(makeSession({ status: "waiting", phase: "execute" })), false);
|
|
86
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.canSwitchModel(makeSession({ status: "cancelled", phase: "execute" })), false);
|
|
87
|
+
});
|
|
88
|
+
(0, node_test_1.it)("next phase after a failed verification is recover, after success is complete", () => {
|
|
89
|
+
const failed = makeSession({
|
|
90
|
+
phase: "verify",
|
|
91
|
+
completedActions: [action],
|
|
92
|
+
errors: [
|
|
93
|
+
{
|
|
94
|
+
id: "err-1",
|
|
95
|
+
type: "verification",
|
|
96
|
+
message: "Build failed",
|
|
97
|
+
phase: "verify",
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
recoverable: true,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.determineNextPhase(failed, { type: "decide", goal: "verify" }), "recover");
|
|
104
|
+
const passed = makeSession({ phase: "verify", completedActions: [action], actions: [action] });
|
|
105
|
+
strict_1.default.equal(agent_state_machine_1.AgentStateMachine.determineNextPhase(passed, { type: "decide", goal: "verify" }), "complete");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
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_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const verification_engine_1 = require("../verification-engine");
|
|
11
|
+
// Temp projects live under the server package so `npx tsc` resolves the
|
|
12
|
+
// locally installed TypeScript instead of hitting the network.
|
|
13
|
+
const TMP_ROOT = node_path_1.default.join(process.cwd(), ".tmp-tests");
|
|
14
|
+
(0, node_test_1.describe)("VerificationEngine", () => {
|
|
15
|
+
let dir;
|
|
16
|
+
(0, node_test_1.beforeEach)(() => {
|
|
17
|
+
node_fs_1.default.mkdirSync(TMP_ROOT, { recursive: true });
|
|
18
|
+
dir = node_fs_1.default.mkdtempSync(node_path_1.default.join(TMP_ROOT, "verify-"));
|
|
19
|
+
});
|
|
20
|
+
(0, node_test_1.afterEach)(() => {
|
|
21
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
(0, node_test_1.it)("skips typecheck when there is no tsconfig (inconclusive, not a failure)", async () => {
|
|
24
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "index.js"), "console.log(1)");
|
|
25
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
26
|
+
const result = await engine.verifyTypecheck("verify");
|
|
27
|
+
strict_1.default.equal(result.type, "typecheck");
|
|
28
|
+
strict_1.default.equal(result.success, true);
|
|
29
|
+
strict_1.default.equal(result.skipped, true);
|
|
30
|
+
strict_1.default.equal(result.errors.length, 0);
|
|
31
|
+
strict_1.default.match(result.observations[0].summary, /skipped/i);
|
|
32
|
+
});
|
|
33
|
+
(0, node_test_1.it)("passes typecheck for a valid project", async () => {
|
|
34
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true, strict: true }, files: ["ok.ts"] }));
|
|
35
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "ok.ts"), "export const x: number = 1;");
|
|
36
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
37
|
+
const result = await engine.verifyTypecheck("verify");
|
|
38
|
+
strict_1.default.equal(result.type, "typecheck");
|
|
39
|
+
strict_1.default.equal(result.command, "npx tsc --noEmit");
|
|
40
|
+
strict_1.default.equal(result.skipped, undefined);
|
|
41
|
+
strict_1.default.equal(result.success, true, result.output || result.error || "");
|
|
42
|
+
strict_1.default.equal(result.errors.length, 0);
|
|
43
|
+
});
|
|
44
|
+
(0, node_test_1.it)("reports a real typecheck failure as a recoverable verification error", async () => {
|
|
45
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true, strict: true }, files: ["bad.ts"] }));
|
|
46
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "bad.ts"), "export const x: number = 'not a number';");
|
|
47
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
48
|
+
const result = await engine.verifyTypecheck("verify");
|
|
49
|
+
strict_1.default.equal(result.success, false);
|
|
50
|
+
strict_1.default.equal(result.errors.length, 1);
|
|
51
|
+
strict_1.default.equal(result.errors[0].type, "verification");
|
|
52
|
+
strict_1.default.equal(result.errors[0].recoverable, true);
|
|
53
|
+
strict_1.default.equal(result.command, "npx tsc --noEmit");
|
|
54
|
+
});
|
|
55
|
+
(0, node_test_1.it)("skips build when package.json has no build script", async () => {
|
|
56
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "package.json"), JSON.stringify({ name: "x", scripts: { test: "echo ok" } }));
|
|
57
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
58
|
+
const result = await engine.verifyBuild("verify");
|
|
59
|
+
strict_1.default.equal(result.success, true);
|
|
60
|
+
strict_1.default.equal(result.skipped, true);
|
|
61
|
+
strict_1.default.equal(result.errors.length, 0);
|
|
62
|
+
strict_1.default.match(result.observations[0].summary, /skipped/i);
|
|
63
|
+
});
|
|
64
|
+
(0, node_test_1.it)("skips build when there is no package.json", async () => {
|
|
65
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
66
|
+
const result = await engine.verifyBuild("verify");
|
|
67
|
+
strict_1.default.equal(result.success, true);
|
|
68
|
+
strict_1.default.equal(result.skipped, true);
|
|
69
|
+
strict_1.default.equal(result.errors.length, 0);
|
|
70
|
+
});
|
|
71
|
+
(0, node_test_1.it)("reports a real build failure as an error (corrective-run input)", async () => {
|
|
72
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "package.json"), JSON.stringify({ name: "x", scripts: { build: "node -e \"console.error('boom'); process.exit(2)\"" } }));
|
|
73
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
74
|
+
const result = await engine.verifyBuild("verify");
|
|
75
|
+
strict_1.default.equal(result.success, false);
|
|
76
|
+
strict_1.default.equal(result.skipped, undefined);
|
|
77
|
+
strict_1.default.equal(result.errors.length, 1);
|
|
78
|
+
strict_1.default.equal(result.errors[0].type, "verification");
|
|
79
|
+
strict_1.default.equal(result.errors[0].recoverable, true);
|
|
80
|
+
strict_1.default.equal(result.command, "npm run build");
|
|
81
|
+
strict_1.default.ok((result.output || "").includes("boom"));
|
|
82
|
+
});
|
|
83
|
+
(0, node_test_1.it)("allPassed/getErrors aggregate across results", async () => {
|
|
84
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "package.json"), JSON.stringify({ name: "x" }));
|
|
85
|
+
const engine = new verification_engine_1.VerificationEngine(dir);
|
|
86
|
+
const results = await Promise.all([engine.verifyTypecheck("verify"), engine.verifyBuild("verify")]);
|
|
87
|
+
strict_1.default.equal(engine.allPassed(results), true);
|
|
88
|
+
strict_1.default.equal(engine.getErrors(results).length, 0);
|
|
89
|
+
strict_1.default.ok(engine.getObservations(results).length >= 2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
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 workspace_guard_1 = require("../workspace-guard");
|
|
12
|
+
(0, node_test_1.describe)("WorkspaceGuard", () => {
|
|
13
|
+
let dir;
|
|
14
|
+
(0, node_test_1.beforeEach)(() => {
|
|
15
|
+
dir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "lasso-guard-"));
|
|
16
|
+
});
|
|
17
|
+
(0, node_test_1.afterEach)(() => {
|
|
18
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
(0, node_test_1.it)("reports no changes right after capture", async () => {
|
|
21
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "hello");
|
|
22
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
23
|
+
strict_1.default.equal(guard.fileCount, 1);
|
|
24
|
+
strict_1.default.deepEqual(guard.diff(), []);
|
|
25
|
+
await guard.dispose();
|
|
26
|
+
});
|
|
27
|
+
(0, node_test_1.it)("rejects a capture of a missing directory", async () => {
|
|
28
|
+
await strict_1.default.rejects(() => workspace_guard_1.WorkspaceGuard.capture(node_path_1.default.join(dir, "does-not-exist")));
|
|
29
|
+
});
|
|
30
|
+
(0, node_test_1.it)("diffs modified, created, and deleted files", async () => {
|
|
31
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "hello");
|
|
32
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "gone.txt"), "bye");
|
|
33
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
34
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "hello world");
|
|
35
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(dir, "sub"));
|
|
36
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "sub", "new.txt"), "fresh");
|
|
37
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(dir, "gone.txt"));
|
|
38
|
+
const changes = guard.diff();
|
|
39
|
+
const byPath = new Map(changes.map((c) => [c.filePath, c]));
|
|
40
|
+
strict_1.default.equal(changes.length, 3);
|
|
41
|
+
const modified = byPath.get("a.txt");
|
|
42
|
+
strict_1.default.equal(modified.operation, "modified");
|
|
43
|
+
strict_1.default.equal(modified.oldContent, "hello");
|
|
44
|
+
strict_1.default.equal(modified.newContent, "hello world");
|
|
45
|
+
const created = byPath.get(node_path_1.default.join("sub", "new.txt"));
|
|
46
|
+
strict_1.default.equal(created.operation, "created");
|
|
47
|
+
strict_1.default.equal(created.newContent, "fresh");
|
|
48
|
+
const deleted = byPath.get("gone.txt");
|
|
49
|
+
strict_1.default.equal(deleted.operation, "deleted");
|
|
50
|
+
strict_1.default.equal(deleted.oldContent, "bye");
|
|
51
|
+
await guard.dispose();
|
|
52
|
+
});
|
|
53
|
+
(0, node_test_1.it)("restore() rolls the workspace back (review rejection)", async () => {
|
|
54
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "original");
|
|
55
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "keep.txt"), "keep me");
|
|
56
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "gone.txt"), "will be deleted");
|
|
57
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
58
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "agent edit");
|
|
59
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "created.txt"), "agent file");
|
|
60
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(dir, "gone.txt"));
|
|
61
|
+
strict_1.default.equal(guard.diff().length, 3);
|
|
62
|
+
await guard.restore();
|
|
63
|
+
strict_1.default.equal(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "a.txt"), "utf8"), "original");
|
|
64
|
+
strict_1.default.equal(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "keep.txt"), "utf8"), "keep me");
|
|
65
|
+
strict_1.default.equal(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "gone.txt"), "utf8"), "will be deleted");
|
|
66
|
+
strict_1.default.ok(!node_fs_1.default.existsSync(node_path_1.default.join(dir, "created.txt")));
|
|
67
|
+
// Post-rollback the workspace matches capture again: nothing pending.
|
|
68
|
+
strict_1.default.deepEqual(guard.diff(), []);
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.it)("ignores default ignore dirs such as node_modules and .git", async () => {
|
|
71
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "app.ts"), "code");
|
|
72
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(dir, "node_modules"));
|
|
73
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "node_modules", "dep.js"), "dep");
|
|
74
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(dir, ".git"));
|
|
75
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, ".git", "HEAD"), "ref");
|
|
76
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
77
|
+
strict_1.default.equal(guard.fileCount, 1);
|
|
78
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "node_modules", "dep.js"), "changed");
|
|
79
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, ".git", "HEAD"), "changed");
|
|
80
|
+
strict_1.default.deepEqual(guard.diff(), []);
|
|
81
|
+
await guard.dispose();
|
|
82
|
+
});
|
|
83
|
+
(0, node_test_1.it)("keeps binary files restorable but excludes them from text diffs", async () => {
|
|
84
|
+
const original = Buffer.from([0x00, 0x01, 0x02, 0x00, 0xff, 0x7f]);
|
|
85
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "logo.bin"), original);
|
|
86
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
87
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "logo.bin"), Buffer.from([0x00, 0x99, 0x00]));
|
|
88
|
+
const changes = guard.diff();
|
|
89
|
+
strict_1.default.equal(changes.length, 1);
|
|
90
|
+
strict_1.default.equal(changes[0].operation, "modified");
|
|
91
|
+
strict_1.default.equal(changes[0].binary, true);
|
|
92
|
+
strict_1.default.equal(changes[0].oldContent, undefined);
|
|
93
|
+
await guard.restore();
|
|
94
|
+
strict_1.default.deepEqual(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "logo.bin")), original);
|
|
95
|
+
});
|
|
96
|
+
(0, node_test_1.it)("restore() is single-use and dispose stays safe", async () => {
|
|
97
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "one");
|
|
98
|
+
const guard = await workspace_guard_1.WorkspaceGuard.capture(dir);
|
|
99
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, "a.txt"), "two");
|
|
100
|
+
await guard.restore();
|
|
101
|
+
await guard.dispose();
|
|
102
|
+
strict_1.default.equal(node_fs_1.default.readFileSync(node_path_1.default.join(dir, "a.txt"), "utf8"), "one");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for the Claude Code CLI (`claude`), the Anthropic coding agent.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code has its own agentic loop, so Lasso delegates the task wholesale:
|
|
7
|
+
* claude -p <prompt> --output-format stream-json --verbose --permission-mode acceptEdits
|
|
8
|
+
*
|
|
9
|
+
* The JSONL stream (system/init, assistant, result) is normalized into
|
|
10
|
+
* BackendEvents. File changes are NOT trusted from the agent's report —
|
|
11
|
+
* the orchestrator's WorkspaceGuard diffs the project after the run and
|
|
12
|
+
* restores it before review, so nothing is applied without approval.
|
|
13
|
+
*
|
|
14
|
+
* Reference: https://docs.claude.com/en/docs/claude-code/headless
|
|
15
|
+
*/
|
|
16
|
+
import { LocalAgentAdapter, LocalAgentConfig, CommandSpec, EventSink } from "./local-agent-adapter";
|
|
17
|
+
import { BackendCapabilities, TaskContext } from "../backend-interface";
|
|
18
|
+
interface ClaudeCodeConfig extends LocalAgentConfig {
|
|
19
|
+
provider: "claude_code";
|
|
20
|
+
maxTurns?: number;
|
|
21
|
+
maxBudgetUsd?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class ClaudeCodeAdapter extends LocalAgentAdapter {
|
|
24
|
+
readonly provider: "claude_code";
|
|
25
|
+
constructor(config: ClaudeCodeConfig);
|
|
26
|
+
getCapabilities(): BackendCapabilities;
|
|
27
|
+
protected get binary(): string;
|
|
28
|
+
protected get installInstructions(): string;
|
|
29
|
+
protected get versionArgs(): string[];
|
|
30
|
+
protected get supportsResume(): boolean;
|
|
31
|
+
protected checkAuthentication(): Promise<{
|
|
32
|
+
authenticated: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
fixInstructions?: string;
|
|
35
|
+
}>;
|
|
36
|
+
protected buildCommand(context: TaskContext): CommandSpec;
|
|
37
|
+
protected handleLine(line: string, context: TaskContext, sink: EventSink): boolean;
|
|
38
|
+
private handleAssistant;
|
|
39
|
+
private describeToolInput;
|
|
40
|
+
private reportFileOperation;
|
|
41
|
+
protected inspectFailureText(text: string): {
|
|
42
|
+
message: string;
|
|
43
|
+
authFailure?: boolean;
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code Adapter
|
|
4
|
+
*
|
|
5
|
+
* Adapter for the Claude Code CLI (`claude`), the Anthropic coding agent.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code has its own agentic loop, so Lasso delegates the task wholesale:
|
|
8
|
+
* claude -p <prompt> --output-format stream-json --verbose --permission-mode acceptEdits
|
|
9
|
+
*
|
|
10
|
+
* The JSONL stream (system/init, assistant, result) is normalized into
|
|
11
|
+
* BackendEvents. File changes are NOT trusted from the agent's report —
|
|
12
|
+
* the orchestrator's WorkspaceGuard diffs the project after the run and
|
|
13
|
+
* restores it before review, so nothing is applied without approval.
|
|
14
|
+
*
|
|
15
|
+
* Reference: https://docs.claude.com/en/docs/claude-code/headless
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.ClaudeCodeAdapter = void 0;
|
|
22
|
+
const node_util_1 = require("node:util");
|
|
23
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
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_FILE_KEYS = ["file_path", "filePath", "path", "notebook_path"];
|
|
27
|
+
class ClaudeCodeAdapter extends local_agent_adapter_1.LocalAgentAdapter {
|
|
28
|
+
provider = "claude_code";
|
|
29
|
+
constructor(config) {
|
|
30
|
+
super({ ...config, type: "local_agent", provider: "claude_code" });
|
|
31
|
+
}
|
|
32
|
+
getCapabilities() {
|
|
33
|
+
return {
|
|
34
|
+
canStreamEvents: true,
|
|
35
|
+
// Operates directly on the project; WorkspaceGuard guarantees rollback before review.
|
|
36
|
+
canIsolateWorkspace: false,
|
|
37
|
+
providesStructuredDiffs: false,
|
|
38
|
+
supportsSessionContinuation: true,
|
|
39
|
+
supportsCancellation: true,
|
|
40
|
+
hasInternalLoop: true,
|
|
41
|
+
supportedFileOperations: "both",
|
|
42
|
+
canRunCommands: true,
|
|
43
|
+
authMethod: "cli_session",
|
|
44
|
+
forbiddenCommands: ["rm -rf /", "mkfs"],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
get binary() {
|
|
48
|
+
return "claude";
|
|
49
|
+
}
|
|
50
|
+
get installInstructions() {
|
|
51
|
+
return "Install Claude Code: npm install -g @anthropic-ai/claude-code (or https://docs.claude.com/en/docs/claude-code/setup)";
|
|
52
|
+
}
|
|
53
|
+
get versionArgs() {
|
|
54
|
+
return ["--version"];
|
|
55
|
+
}
|
|
56
|
+
get supportsResume() {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async checkAuthentication() {
|
|
60
|
+
try {
|
|
61
|
+
const { stdout } = (await execFileAsync("claude", ["auth", "status"], {
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
}));
|
|
65
|
+
const jsonStart = stdout.indexOf("{");
|
|
66
|
+
if (jsonStart >= 0) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(stdout.slice(jsonStart));
|
|
69
|
+
if (parsed && typeof parsed.loggedIn === "boolean") {
|
|
70
|
+
return parsed.loggedIn
|
|
71
|
+
? { authenticated: true }
|
|
72
|
+
: { authenticated: false, error: "Claude Code is not logged in.", fixInstructions: "Run 'claude' once and log in, or run 'claude auth login'." };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Fall through to exit-code based result.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { authenticated: true };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return {
|
|
84
|
+
authenticated: false,
|
|
85
|
+
error: `Could not verify Claude Code authentication: ${message}`,
|
|
86
|
+
fixInstructions: "Run 'claude' once and complete the login flow, then retry.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
buildCommand(context) {
|
|
91
|
+
const cfg = this.config;
|
|
92
|
+
const resumeId = this.resumeSessionId(context);
|
|
93
|
+
const sessionId = node_crypto_1.default.randomUUID();
|
|
94
|
+
const args = [
|
|
95
|
+
"-p",
|
|
96
|
+
this.buildTaskPrompt(context),
|
|
97
|
+
"--output-format",
|
|
98
|
+
"stream-json",
|
|
99
|
+
"--verbose",
|
|
100
|
+
"--permission-mode",
|
|
101
|
+
cfg.permissionMode || "acceptEdits",
|
|
102
|
+
];
|
|
103
|
+
if (this.modelId)
|
|
104
|
+
args.push("--model", this.modelId);
|
|
105
|
+
if (resumeId)
|
|
106
|
+
args.push("--resume", resumeId);
|
|
107
|
+
else
|
|
108
|
+
args.push("--session-id", sessionId);
|
|
109
|
+
if (cfg.maxTurns)
|
|
110
|
+
args.push("--max-turns", String(cfg.maxTurns));
|
|
111
|
+
if (cfg.maxBudgetUsd)
|
|
112
|
+
args.push("--max-budget-usd", String(cfg.maxBudgetUsd));
|
|
113
|
+
return { bin: "claude", args, cwd: cfg.projectPath };
|
|
114
|
+
}
|
|
115
|
+
handleLine(line, context, sink) {
|
|
116
|
+
let event;
|
|
117
|
+
try {
|
|
118
|
+
event = JSON.parse(line);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (typeof event.session_id === "string" && event.session_id) {
|
|
124
|
+
sink.state.sessionId = event.session_id;
|
|
125
|
+
}
|
|
126
|
+
switch (event.type) {
|
|
127
|
+
case "system": {
|
|
128
|
+
if (event.subtype === "init") {
|
|
129
|
+
sink.state.sessionId = event.session_id || sink.state.sessionId;
|
|
130
|
+
sink.progress(`Claude Code session started${event.model ? ` (model: ${event.model})` : ""}`);
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
case "assistant": {
|
|
135
|
+
this.handleAssistant(event, sink);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
case "result": {
|
|
139
|
+
if (event.is_error) {
|
|
140
|
+
sink.state.failureMessage = event.result || "Claude Code reported an error";
|
|
141
|
+
if (/credit balance|billing|quota|rate.?limit/i.test(sink.state.failureMessage)) {
|
|
142
|
+
sink.state.authFailure = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (event.subtype === "success" || !event.is_error) {
|
|
146
|
+
sink.state.receivedTerminalSuccess = true;
|
|
147
|
+
if (event.result)
|
|
148
|
+
sink.state.summary = event.result;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
case "stream_event": {
|
|
153
|
+
// Incremental progress; ignore bulk content, keep assistant-level info only.
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
handleAssistant(event, sink) {
|
|
161
|
+
const content = event.message?.content || [];
|
|
162
|
+
if (event.error) {
|
|
163
|
+
const text = content.map((c) => c.text || "").filter(Boolean).join(" ");
|
|
164
|
+
sink.state.failureMessage = text || `Claude Code error: ${event.error}`;
|
|
165
|
+
if (/billing|auth|unauthorized|credit/i.test(event.error) || /credit balance|not logged in|unauthorized/i.test(sink.state.failureMessage)) {
|
|
166
|
+
sink.state.authFailure = true;
|
|
167
|
+
}
|
|
168
|
+
sink.error(sink.state.failureMessage, { recoverable: false, authFailure: sink.state.authFailure });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const block of content) {
|
|
172
|
+
if (block.type === "text" && block.text) {
|
|
173
|
+
sink.progress(block.text);
|
|
174
|
+
}
|
|
175
|
+
else if (block.type === "tool_use" && block.name) {
|
|
176
|
+
const input = block.input || {};
|
|
177
|
+
sink.toolStarted(block.name, this.describeToolInput(block.name, input));
|
|
178
|
+
this.reportFileOperation(block.name, input, sink);
|
|
179
|
+
}
|
|
180
|
+
else if (block.type === "tool_result" && block.is_error) {
|
|
181
|
+
const msg = typeof block.content === "string" ? block.content : JSON.stringify(block.content).slice(0, 500);
|
|
182
|
+
sink.error(`Tool returned an error: ${msg}`, { recoverable: true, asEvent: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
describeToolInput(name, input) {
|
|
187
|
+
switch (name) {
|
|
188
|
+
case "Bash":
|
|
189
|
+
return typeof input.command === "string" ? input.command.slice(0, 200) : undefined;
|
|
190
|
+
case "Read":
|
|
191
|
+
case "Write":
|
|
192
|
+
case "Edit":
|
|
193
|
+
case "MultiEdit":
|
|
194
|
+
case "NotebookEdit": {
|
|
195
|
+
const key = TOOL_FILE_KEYS.find((k) => typeof input[k] === "string");
|
|
196
|
+
return key ? String(input[key]) : undefined;
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
return typeof input.description === "string" ? input.description : undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
reportFileOperation(toolName, input, sink) {
|
|
203
|
+
const key = TOOL_FILE_KEYS.find((k) => typeof input[k] === "string");
|
|
204
|
+
if (!key)
|
|
205
|
+
return;
|
|
206
|
+
const rel = this.relPath(String(input[key]));
|
|
207
|
+
// Claude's Write creates-or-overwrites; the guard diff decides the real operation.
|
|
208
|
+
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit" || toolName === "NotebookEdit") {
|
|
209
|
+
sink.fileChange("modified", rel);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
inspectFailureText(text) {
|
|
213
|
+
if (!text)
|
|
214
|
+
return null;
|
|
215
|
+
if (/authentication required|not logged in|please run ['"]?claude auth|invalid api key/i.test(text)) {
|
|
216
|
+
return {
|
|
217
|
+
message: "Claude Code authentication failed.",
|
|
218
|
+
authFailure: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (/credit balance is too low|billing_error/i.test(text)) {
|
|
222
|
+
return { message: "Claude Code account has insufficient credit balance.", authFailure: true };
|
|
223
|
+
}
|
|
224
|
+
if (/usage limit reached|rate limit/i.test(text)) {
|
|
225
|
+
return { message: "Claude Code usage limit reached — try again later.", authFailure: true };
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for OpenAI's Codex CLI (`codex exec`), a local coding agent with its
|
|
5
|
+
* own agentic loop.
|
|
6
|
+
*
|
|
7
|
+
* Invocation: codex exec --json --skip-git-repo-check -s workspace-write [-m model] <prompt>
|
|
8
|
+
* (resume: codex exec resume <thread_id> --json --skip-git-repo-check <prompt>)
|
|
9
|
+
* with stdin ignored and cwd set to the project path. Output is JSONL
|
|
10
|
+
* (thread.started / item.* / turn.*) normalized into BackendEvents.
|
|
11
|
+
*
|
|
12
|
+
* File changes are not trusted from the agent's report — the orchestrator's
|
|
13
|
+
* WorkspaceGuard diffs the project after the run and restores it before review.
|
|
14
|
+
*
|
|
15
|
+
* Reference: https://github.com/openai/codex
|
|
16
|
+
*/
|
|
17
|
+
import { LocalAgentAdapter, LocalAgentConfig, CommandSpec, EventSink } from "./local-agent-adapter";
|
|
18
|
+
import { BackendCapabilities, TaskContext } from "../backend-interface";
|
|
19
|
+
interface CodexConfig extends LocalAgentConfig {
|
|
20
|
+
provider: "codex_cli";
|
|
21
|
+
}
|
|
22
|
+
export declare class CodexCLIAdapter extends LocalAgentAdapter {
|
|
23
|
+
readonly provider: "codex_cli";
|
|
24
|
+
constructor(config: CodexConfig);
|
|
25
|
+
getCapabilities(): BackendCapabilities;
|
|
26
|
+
protected get binary(): string;
|
|
27
|
+
protected get installInstructions(): string;
|
|
28
|
+
protected get versionArgs(): string[];
|
|
29
|
+
protected get supportsResume(): boolean;
|
|
30
|
+
protected checkAuthentication(): Promise<{
|
|
31
|
+
authenticated: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
fixInstructions?: string;
|
|
34
|
+
}>;
|
|
35
|
+
protected buildCommand(context: TaskContext): CommandSpec;
|
|
36
|
+
protected handleLine(line: string, context: TaskContext, sink: EventSink): boolean;
|
|
37
|
+
private handleItemCompleted;
|
|
38
|
+
private recordFailure;
|
|
39
|
+
protected inspectFailureText(text: string): {
|
|
40
|
+
message: string;
|
|
41
|
+
authFailure?: boolean;
|
|
42
|
+
} | null;
|
|
43
|
+
}
|
|
44
|
+
export {};
|