@lasso-ai/cli 1.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/ARCHITECTURE.md +342 -0
- package/CHANGELOG.md +102 -0
- package/CODE_OF_CONDUCT.md +129 -0
- package/CONTRIBUTING.md +122 -0
- package/LICENSE +7 -0
- package/README.md +548 -0
- package/SECURITY.md +65 -0
- package/SUPPORT.md +31 -0
- package/dist/cli/agent.d.ts +61 -0
- package/dist/cli/agent.js +418 -0
- package/dist/cli/auth.d.ts +38 -0
- package/dist/cli/auth.js +162 -0
- package/dist/cli/bridge.d.ts +160 -0
- package/dist/cli/bridge.js +450 -0
- package/dist/cli/host/certificates.d.ts +12 -0
- package/dist/cli/host/certificates.js +52 -0
- package/dist/cli/host/client.d.ts +49 -0
- package/dist/cli/host/client.js +141 -0
- package/dist/cli/host/daemon.d.ts +50 -0
- package/dist/cli/host/daemon.js +368 -0
- package/dist/cli/host/dns.d.ts +80 -0
- package/dist/cli/host/dns.js +255 -0
- package/dist/cli/host/install.d.ts +32 -0
- package/dist/cli/host/install.js +177 -0
- package/dist/cli/host/next-host-entry.d.ts +1 -0
- package/dist/cli/host/next-host-entry.js +82 -0
- package/dist/cli/host/paths.d.ts +15 -0
- package/dist/cli/host/paths.js +55 -0
- package/dist/cli/host/registry.d.ts +31 -0
- package/dist/cli/host/registry.js +103 -0
- package/dist/cli/host/runtime.d.ts +23 -0
- package/dist/cli/host/runtime.js +134 -0
- package/dist/cli/host/vite-host-entry.d.ts +1 -0
- package/dist/cli/host/vite-host-entry.js +59 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +322 -0
- package/dist/cli/project.d.ts +56 -0
- package/dist/cli/project.js +298 -0
- package/dist/cli/server/next.d.ts +6 -0
- package/dist/cli/server/next.js +76 -0
- package/dist/cli/server/vite.d.ts +1 -0
- package/dist/cli/server/vite.js +51 -0
- package/dist/cli/utils/framework.d.ts +2 -0
- package/dist/cli/utils/framework.js +26 -0
- package/dist/overlay.js +20365 -0
- package/docs/PUBLISHING.md +63 -0
- package/package.json +106 -0
|
@@ -0,0 +1,450 @@
|
|
|
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
|
+
exports.startBridge = startBridge;
|
|
7
|
+
// src/cli/bridge.ts
|
|
8
|
+
const ws_1 = require("ws");
|
|
9
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const agent_1 = require("./agent");
|
|
16
|
+
const auth_1 = require("./auth");
|
|
17
|
+
const DEFAULT_BRIDGE_PORT = 3056;
|
|
18
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
19
|
+
async function gitCommand(cwd, args) {
|
|
20
|
+
const result = await execFileAsync("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
21
|
+
return result.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
async function getGitState(cwd) {
|
|
24
|
+
try {
|
|
25
|
+
await gitCommand(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { isRepo: false };
|
|
29
|
+
}
|
|
30
|
+
const status = (await gitCommand(cwd, ["status", "--short"])).split("\n").filter(Boolean);
|
|
31
|
+
const branch = await gitCommand(cwd, ["branch", "--show-current"]).catch(() => "");
|
|
32
|
+
const remote = await gitCommand(cwd, ["remote", "get-url", "origin"]).catch(() => "");
|
|
33
|
+
return { isRepo: true, branch, status, hasChanges: status.length > 0, hasRemote: Boolean(remote), remote };
|
|
34
|
+
}
|
|
35
|
+
function fileLineEnding(content) {
|
|
36
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
37
|
+
}
|
|
38
|
+
function withLineEnding(value, lineEnding) {
|
|
39
|
+
return value.replace(/\r\n?|\n/g, lineEnding);
|
|
40
|
+
}
|
|
41
|
+
function occurrenceCount(content, needle) {
|
|
42
|
+
if (!needle)
|
|
43
|
+
return 0;
|
|
44
|
+
let count = 0;
|
|
45
|
+
let from = 0;
|
|
46
|
+
while (true) {
|
|
47
|
+
const index = content.indexOf(needle, from);
|
|
48
|
+
if (index < 0)
|
|
49
|
+
return count;
|
|
50
|
+
count += 1;
|
|
51
|
+
from = index + needle.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function prepareChange(content, change) {
|
|
55
|
+
const lineEnding = fileLineEnding(content);
|
|
56
|
+
const oldString = withLineEnding(change.oldString, lineEnding);
|
|
57
|
+
const newString = withLineEnding(change.newString, lineEnding);
|
|
58
|
+
const matches = occurrenceCount(content, oldString);
|
|
59
|
+
if (matches === 0) {
|
|
60
|
+
throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated.`);
|
|
61
|
+
}
|
|
62
|
+
if (matches > 1) {
|
|
63
|
+
throw new Error(`Could not safely apply ${change.filePath}. The selected code is not unique (${matches} matches).`);
|
|
64
|
+
}
|
|
65
|
+
const start = content.indexOf(oldString);
|
|
66
|
+
return { start, end: start + oldString.length, oldString, newString };
|
|
67
|
+
}
|
|
68
|
+
function readEnvFile(cwd, filename) {
|
|
69
|
+
try {
|
|
70
|
+
return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
|
|
71
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
let value = match[2].trim();
|
|
74
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
values[match[1]] = value.trim();
|
|
77
|
+
}
|
|
78
|
+
return values;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = DEFAULT_BRIDGE_PORT) {
|
|
86
|
+
const bridgeServer = node_http_1.default.createServer(); // dedicated, empty HTTP server
|
|
87
|
+
const wss = new ws_1.WebSocketServer({ server: bridgeServer });
|
|
88
|
+
let overlaySocket = null;
|
|
89
|
+
let lastSnapshot = [];
|
|
90
|
+
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
91
|
+
const fileEnv = envRoots.reduce((values, root) => ({
|
|
92
|
+
...values,
|
|
93
|
+
...readEnvFile(root, ".env"),
|
|
94
|
+
...readEnvFile(root, ".env.local"),
|
|
95
|
+
...readEnvFile(root, ".env.production"),
|
|
96
|
+
}), {});
|
|
97
|
+
const lassoKeyConfigured = Boolean(process.env.VITE_LASSO_API_KEY || process.env.NEXT_LASSO_API_KEY || fileEnv.VITE_LASSO_API_KEY || fileEnv.NEXT_LASSO_API_KEY);
|
|
98
|
+
let agentConfig = (() => {
|
|
99
|
+
const provider = (process.env.LASSO_AGENT_PROVIDER || fileEnv.LASSO_AGENT_PROVIDER || process.env.AI_PROVIDER || fileEnv.AI_PROVIDER || "").toLowerCase();
|
|
100
|
+
const keys = {
|
|
101
|
+
google: process.env.GOOGLE_GENERATIVE_AI_API_KEY || fileEnv.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY || fileEnv.GEMINI_API_KEY,
|
|
102
|
+
openai: process.env.OPENAI_API_KEY || fileEnv.OPENAI_API_KEY,
|
|
103
|
+
anthropic: process.env.ANTHROPIC_API_KEY || fileEnv.ANTHROPIC_API_KEY,
|
|
104
|
+
ollama: process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL,
|
|
105
|
+
};
|
|
106
|
+
const selectedProvider = provider === "google" || provider === "gemini" ? "google" : provider === "openai" ? "openai" : provider === "ollama" ? "ollama" : provider === "anthropic" ? "anthropic" : keys.ollama ? "ollama" : keys.google ? "google" : keys.openai ? "openai" : "anthropic";
|
|
107
|
+
const apiKey = selectedProvider === "ollama" ? "ollama" : keys[selectedProvider];
|
|
108
|
+
if (!apiKey)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
provider: selectedProvider,
|
|
112
|
+
apiKey,
|
|
113
|
+
model: process.env.LASSO_AGENT_MODEL || fileEnv.LASSO_AGENT_MODEL || process.env.AI_MODEL || fileEnv.AI_MODEL,
|
|
114
|
+
baseUrl: process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL ? `${(process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api").replace(/\/api\/?$/, "")}/v1` : undefined,
|
|
115
|
+
};
|
|
116
|
+
})();
|
|
117
|
+
let activeAgentController = null;
|
|
118
|
+
let localAgents = new Set();
|
|
119
|
+
async function localModels() {
|
|
120
|
+
const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(`${base.replace(/\/$/, "")}/tags`);
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return [];
|
|
125
|
+
const payload = await response.json();
|
|
126
|
+
return (payload.models || []).filter((model) => model.name).map((model) => ({ id: model.name, label: `${model.name} · Local`, provider: "ollama" }));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function openCodeModels() {
|
|
133
|
+
if (!localAgents.has("opencode"))
|
|
134
|
+
return [];
|
|
135
|
+
try {
|
|
136
|
+
const result = await execFileAsync("opencode", ["models"], { maxBuffer: 1024 * 1024 });
|
|
137
|
+
return result.stdout
|
|
138
|
+
.split(/\r?\n/)
|
|
139
|
+
.map((line) => line.trim())
|
|
140
|
+
.filter((line) => /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.:@/-]+$/.test(line))
|
|
141
|
+
.map((model) => ({ id: `opencode:${model}`, label: `OpenCode · ${model}`, provider: "cli" }));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const availableModels = async () => {
|
|
148
|
+
localAgents = await (0, agent_1.detectLocalAgents)();
|
|
149
|
+
const locals = await localModels();
|
|
150
|
+
const discoveredOpenCodeModels = await openCodeModels();
|
|
151
|
+
if (locals.length && !agentConfig) {
|
|
152
|
+
const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
|
|
153
|
+
agentConfig = { provider: "ollama", apiKey: "ollama", model: process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL, baseUrl: `${base.replace(/\/api\/?$/, "")}/v1` };
|
|
154
|
+
}
|
|
155
|
+
return [
|
|
156
|
+
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
157
|
+
{ id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
|
|
158
|
+
{ id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
|
|
159
|
+
{ id: "gpt-4.1-mini", label: "GPT-4.1 mini", provider: "openai" },
|
|
160
|
+
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "google" },
|
|
161
|
+
{ id: "gemini-3.7-flash", label: "Gemini 3.7 Flash", provider: "google" },
|
|
162
|
+
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview", provider: "google" },
|
|
163
|
+
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview", provider: "google" },
|
|
164
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "google" },
|
|
165
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google" },
|
|
166
|
+
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash-Lite", provider: "google" },
|
|
167
|
+
...(localAgents.has("claude-code") ? [
|
|
168
|
+
{ id: "claude-code:sonnet", label: "Claude Code · Sonnet", provider: "cli" },
|
|
169
|
+
{ id: "claude-code:opus", label: "Claude Code · Opus", provider: "cli" },
|
|
170
|
+
{ id: "claude-code:haiku", label: "Claude Code · Haiku", provider: "cli" },
|
|
171
|
+
] : []),
|
|
172
|
+
...(localAgents.has("codex") ? [
|
|
173
|
+
{ id: "codex:gpt-5", label: "Codex · GPT-5", provider: "cli" },
|
|
174
|
+
{ id: "codex:gpt-5-codex", label: "Codex · GPT-5-Codex", provider: "cli" },
|
|
175
|
+
] : []),
|
|
176
|
+
...(localAgents.has("opencode") ? [
|
|
177
|
+
{ id: "opencode:opencode/big-pickle", label: "OpenCode · Big Pickle", provider: "cli" },
|
|
178
|
+
{ id: "opencode:deepseek/deepseek-chat", label: "OpenCode · DeepSeek Chat", provider: "cli" },
|
|
179
|
+
{ id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet", provider: "cli" },
|
|
180
|
+
{ id: "opencode:openai/gpt-5", label: "OpenCode · GPT-5", provider: "cli" },
|
|
181
|
+
] : []),
|
|
182
|
+
...discoveredOpenCodeModels,
|
|
183
|
+
...locals,
|
|
184
|
+
];
|
|
185
|
+
};
|
|
186
|
+
wss.on("connection", (socket) => {
|
|
187
|
+
overlaySocket = socket;
|
|
188
|
+
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
189
|
+
socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? collabConfig : null }));
|
|
190
|
+
void availableModels().then((models) => {
|
|
191
|
+
if (socket.readyState === socket.OPEN)
|
|
192
|
+
socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? collabConfig : null }));
|
|
193
|
+
});
|
|
194
|
+
void getGitState(cwd).then((git) => {
|
|
195
|
+
if (socket.readyState === socket.OPEN)
|
|
196
|
+
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
197
|
+
});
|
|
198
|
+
socket.on("message", async (raw) => {
|
|
199
|
+
const msg = JSON.parse(raw.toString());
|
|
200
|
+
if (msg.type === "edit" || msg.type === "ask") {
|
|
201
|
+
console.log(`[lasso] received ${msg.type}:`, { model: msg.model, provider: msg.provider, selectionId: msg.context?.selectionId, messages: msg.messages?.length || 0, changes: msg.type === "edit" ? msg.changesHistory?.length || 0 : 0, screenshots: Boolean(msg.context?.screenshots?.full || msg.context?.screenshots?.element) });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log("[lasso] received from overlay:", msg);
|
|
205
|
+
}
|
|
206
|
+
if (msg.type === "ask") {
|
|
207
|
+
const localProvider = msg.provider === "cli";
|
|
208
|
+
const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
|
|
209
|
+
if (!lassoKeyConfigured && !localProvider) {
|
|
210
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!agentConfig && !localProvider) {
|
|
214
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "No hosted agent is configured. Select Claude Code/Codex or add a provider key." }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
activeAgentController?.abort();
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
activeAgentController = controller;
|
|
220
|
+
const selectedConfig = localProvider
|
|
221
|
+
? { provider: cliProvider, model: msg.model }
|
|
222
|
+
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
223
|
+
void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message) => {
|
|
224
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
225
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
|
|
226
|
+
}).then((answer) => {
|
|
227
|
+
if (!controller.signal.aborted)
|
|
228
|
+
socket.send(JSON.stringify({ type: "assistant_message", message: answer }));
|
|
229
|
+
}).catch((error) => {
|
|
230
|
+
if (!controller.signal.aborted)
|
|
231
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
232
|
+
}).finally(() => {
|
|
233
|
+
if (activeAgentController === controller)
|
|
234
|
+
activeAgentController = null;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else if (msg.type === "edit") {
|
|
238
|
+
const localProvider = msg.provider === "cli";
|
|
239
|
+
const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
|
|
240
|
+
if (!lassoKeyConfigured && !localProvider) {
|
|
241
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!agentConfig && !localProvider) {
|
|
245
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Add a supported agent key: GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." }));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
activeAgentController?.abort();
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
activeAgentController = controller;
|
|
251
|
+
const selectedConfig = localProvider
|
|
252
|
+
? { provider: cliProvider, model: msg.model }
|
|
253
|
+
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
254
|
+
void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message) => {
|
|
255
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
256
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
.then((proposal) => {
|
|
260
|
+
if (!controller.signal.aborted)
|
|
261
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
262
|
+
})
|
|
263
|
+
.catch((error) => {
|
|
264
|
+
if (controller.signal.aborted)
|
|
265
|
+
return;
|
|
266
|
+
const message = error instanceof Error ? error.message : "The agent could not prepare a change.";
|
|
267
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message }));
|
|
268
|
+
})
|
|
269
|
+
.finally(() => {
|
|
270
|
+
if (activeAgentController === controller)
|
|
271
|
+
activeAgentController = null;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else if (msg.type === "stop") {
|
|
275
|
+
activeAgentController?.abort();
|
|
276
|
+
activeAgentController = null;
|
|
277
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "stopped", message: "Agent stopped." }));
|
|
278
|
+
}
|
|
279
|
+
else if (msg.type === "runtime_error") {
|
|
280
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: `Runtime error detected${msg.selectionId ? ` for selection ${msg.selectionId}` : ""}: ${msg.details}` }));
|
|
281
|
+
}
|
|
282
|
+
else if (msg.type === "git_status") {
|
|
283
|
+
socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
|
|
284
|
+
}
|
|
285
|
+
else if (msg.type === "git_generate_message") {
|
|
286
|
+
const localProvider = msg.provider === "cli";
|
|
287
|
+
const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
|
|
288
|
+
if (!agentConfig && !localProvider) {
|
|
289
|
+
socket.send(JSON.stringify({ type: "git_commit_message", error: "No AI provider is configured." }));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const selectedConfig = localProvider
|
|
293
|
+
? { provider: cliProvider, model: msg.model }
|
|
294
|
+
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
activeAgentController?.abort();
|
|
297
|
+
activeAgentController = controller;
|
|
298
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "thinking", message: "Generating a commit message…" }));
|
|
299
|
+
void getGitState(cwd).then((git) => (0, agent_1.generateCommitMessage)(cwd, git.status || [], selectedConfig, controller.signal, (message) => {
|
|
300
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
301
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
|
|
302
|
+
})).then((message) => {
|
|
303
|
+
if (!controller.signal.aborted)
|
|
304
|
+
socket.send(JSON.stringify({ type: "git_commit_message", message }));
|
|
305
|
+
}).catch((error) => {
|
|
306
|
+
if (!controller.signal.aborted)
|
|
307
|
+
socket.send(JSON.stringify({ type: "git_commit_message", error: error instanceof Error ? error.message : "Unable to generate a commit message." }));
|
|
308
|
+
}).finally(() => { if (activeAgentController === controller)
|
|
309
|
+
activeAgentController = null; });
|
|
310
|
+
}
|
|
311
|
+
else if (msg.type === "git_init") {
|
|
312
|
+
try {
|
|
313
|
+
await gitCommand(cwd, ["init"]);
|
|
314
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Git repository initialized." }));
|
|
315
|
+
socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Git could not be initialized." }));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else if (msg.type === "git_commit") {
|
|
322
|
+
try {
|
|
323
|
+
const commitMessage = msg.message.trim().slice(0, 120);
|
|
324
|
+
if (!commitMessage)
|
|
325
|
+
throw new Error("Enter a commit message first.");
|
|
326
|
+
await gitCommand(cwd, ["add", "-A"]);
|
|
327
|
+
await gitCommand(cwd, ["commit", "-m", commitMessage]);
|
|
328
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Changes committed." }));
|
|
329
|
+
socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Changes could not be committed." }));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (msg.type === "git_push") {
|
|
336
|
+
try {
|
|
337
|
+
await gitCommand(cwd, ["push"]);
|
|
338
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Changes pushed to the configured remote." }));
|
|
339
|
+
socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Changes could not be pushed." }));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (msg.type === "transcribe") {
|
|
346
|
+
const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv);
|
|
347
|
+
try {
|
|
348
|
+
const resp = await fetch(`${serverUrl}/api/v1/transcribe`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: { "Content-Type": "application/json" },
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
audio: msg.audio,
|
|
353
|
+
mimeType: msg.mimeType,
|
|
354
|
+
language: msg.language,
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
const data = await resp.json();
|
|
358
|
+
socket.send(JSON.stringify({
|
|
359
|
+
type: "transcribe_result",
|
|
360
|
+
requestId: msg.requestId,
|
|
361
|
+
success: Boolean(data.success),
|
|
362
|
+
text: data.text || "",
|
|
363
|
+
provider: data.provider || "gradium",
|
|
364
|
+
error: data.message,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
socket.send(JSON.stringify({
|
|
369
|
+
type: "transcribe_result",
|
|
370
|
+
requestId: msg.requestId,
|
|
371
|
+
success: false,
|
|
372
|
+
error: error instanceof Error ? error.message : "Failed to contact transcription service",
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (msg.type === "apply") {
|
|
377
|
+
try {
|
|
378
|
+
lastSnapshot = [];
|
|
379
|
+
const planned = new Map();
|
|
380
|
+
for (const change of msg.changes) {
|
|
381
|
+
const filePath = node_path_1.default.resolve(cwd, change.filePath);
|
|
382
|
+
if (!filePath.startsWith(`${node_path_1.default.resolve(cwd)}${node_path_1.default.sep}`))
|
|
383
|
+
throw new Error("A proposed file was outside the project.");
|
|
384
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
385
|
+
const prepared = prepareChange(content, change);
|
|
386
|
+
const fileChanges = planned.get(filePath) || [];
|
|
387
|
+
if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
|
|
388
|
+
throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
|
|
389
|
+
}
|
|
390
|
+
fileChanges.push({ filePath, content, ...prepared });
|
|
391
|
+
planned.set(filePath, fileChanges);
|
|
392
|
+
}
|
|
393
|
+
// Validate every change before writing any file, then apply each
|
|
394
|
+
// file's replacements from the end toward the beginning.
|
|
395
|
+
for (const [filePath, changes] of planned) {
|
|
396
|
+
const content = changes[0].content;
|
|
397
|
+
lastSnapshot.push({ filePath, content });
|
|
398
|
+
const nextContent = [...changes]
|
|
399
|
+
.sort((a, b) => b.start - a.start)
|
|
400
|
+
.reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content);
|
|
401
|
+
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
402
|
+
}
|
|
403
|
+
socket.send(JSON.stringify({ type: "applied", message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.` }));
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
for (const snapshot of lastSnapshot)
|
|
407
|
+
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
408
|
+
lastSnapshot = [];
|
|
409
|
+
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The change could not be applied." }));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else if (msg.type === "undo") {
|
|
413
|
+
for (const snapshot of lastSnapshot)
|
|
414
|
+
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
415
|
+
const count = lastSnapshot.length;
|
|
416
|
+
lastSnapshot = [];
|
|
417
|
+
socket.send(JSON.stringify({ type: "undone", message: count ? "The accepted change was reverted." : "There is no accepted change to undo." }));
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
socket.on("close", () => {
|
|
421
|
+
overlaySocket = null;
|
|
422
|
+
console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
let bridgeListening = false;
|
|
426
|
+
bridgeServer.on("listening", () => {
|
|
427
|
+
bridgeListening = true;
|
|
428
|
+
});
|
|
429
|
+
bridgeServer.on("error", (error) => {
|
|
430
|
+
if (error.code === "EADDRINUSE") {
|
|
431
|
+
console.warn(chalk_1.default.yellow("!") + ` Lasso bridge port ${bridgePort} is already in use; reusing the existing bridge.`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.error(chalk_1.default.red("Lasso bridge error:"), error.message);
|
|
435
|
+
});
|
|
436
|
+
bridgeServer.listen(bridgePort, "127.0.0.1");
|
|
437
|
+
function send(msg) {
|
|
438
|
+
if (overlaySocket?.readyState === overlaySocket?.OPEN) {
|
|
439
|
+
overlaySocket.send(JSON.stringify(msg));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
send,
|
|
444
|
+
close() {
|
|
445
|
+
if (bridgeListening)
|
|
446
|
+
bridgeServer.close();
|
|
447
|
+
wss.close();
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const TLS_KEY_FILE: string;
|
|
2
|
+
export declare const TLS_CERT_FILE: string;
|
|
3
|
+
export declare function ensureHostCertificate(): {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
trusted: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function hostCertificateStatus(): {
|
|
9
|
+
present: boolean;
|
|
10
|
+
certificate: string;
|
|
11
|
+
key: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
exports.TLS_CERT_FILE = exports.TLS_KEY_FILE = void 0;
|
|
7
|
+
exports.ensureHostCertificate = ensureHostCertificate;
|
|
8
|
+
exports.hostCertificateStatus = hostCertificateStatus;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const paths_1 = require("./paths");
|
|
14
|
+
exports.TLS_KEY_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.key");
|
|
15
|
+
exports.TLS_CERT_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.crt");
|
|
16
|
+
function hasCertificate() {
|
|
17
|
+
return node_fs_1.default.existsSync(exports.TLS_KEY_FILE) && node_fs_1.default.existsSync(exports.TLS_CERT_FILE);
|
|
18
|
+
}
|
|
19
|
+
function ensureHostCertificate() {
|
|
20
|
+
node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
if (hasCertificate())
|
|
22
|
+
return { ok: true, trusted: false };
|
|
23
|
+
try {
|
|
24
|
+
(0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
|
|
25
|
+
(0, node_child_process_1.execFileSync)("mkcert", ["-cert-file", exports.TLS_CERT_FILE, "-key-file", exports.TLS_KEY_FILE, "localhost", "127.0.0.1", "::1", "*.lasso"], { stdio: "ignore" });
|
|
26
|
+
node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
|
|
27
|
+
return { ok: true, trusted: true };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// mkcert is optional; OpenSSL still provides a usable local certificate.
|
|
31
|
+
}
|
|
32
|
+
const configFile = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-host-${process.pid}.cnf`);
|
|
33
|
+
try {
|
|
34
|
+
node_fs_1.default.writeFileSync(configFile, [
|
|
35
|
+
"[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no",
|
|
36
|
+
"[req_distinguished_name]", "CN = *.lasso", "[v3_req]",
|
|
37
|
+
"subjectAltName = DNS:*.lasso,DNS:localhost,IP:127.0.0.1,IP:::1", "",
|
|
38
|
+
].join("\n"), { mode: 0o600 });
|
|
39
|
+
(0, node_child_process_1.execFileSync)("openssl", ["req", "-x509", "-nodes", "-newkey", "rsa:2048", "-keyout", exports.TLS_KEY_FILE, "-out", exports.TLS_CERT_FILE, "-days", "825", "-config", configFile], { stdio: "ignore" });
|
|
40
|
+
node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
|
|
41
|
+
return { ok: true, trusted: false };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return { ok: false, trusted: false, error: error instanceof Error ? error.message : String(error) };
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
node_fs_1.default.rmSync(configFile, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function hostCertificateStatus() {
|
|
51
|
+
return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE };
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type HostProject } from "./registry";
|
|
2
|
+
export interface HostHealth {
|
|
3
|
+
ok?: boolean;
|
|
4
|
+
service?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
pid?: number;
|
|
7
|
+
proxyPort?: number | null;
|
|
8
|
+
httpsPort?: number | null;
|
|
9
|
+
https?: boolean;
|
|
10
|
+
dnsPort?: number | null;
|
|
11
|
+
running?: number;
|
|
12
|
+
registered?: number;
|
|
13
|
+
projects?: Array<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
export declare function hostAlive(port: number): Promise<HostHealth | null>;
|
|
16
|
+
export declare function readDaemonPid(): number | null;
|
|
17
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
18
|
+
export interface RegisterResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
domain: string;
|
|
21
|
+
directory: string;
|
|
22
|
+
replaced?: boolean;
|
|
23
|
+
viaHost?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function registerWithHost(domain: string, directory: string, projectId: string, port: number): Promise<RegisterResult>;
|
|
27
|
+
export declare function unregisterFromHost(domain: string, port: number): Promise<{
|
|
28
|
+
ok: boolean;
|
|
29
|
+
removed?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
export interface ProjectWithStatus extends HostProject {
|
|
33
|
+
domain: string;
|
|
34
|
+
status: "running" | "stopped";
|
|
35
|
+
port: number | null;
|
|
36
|
+
pid: number | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function listHostProjects(port: number): Promise<{
|
|
39
|
+
health: HostHealth | null;
|
|
40
|
+
projects: ProjectWithStatus[];
|
|
41
|
+
}>;
|
|
42
|
+
export declare function stopHostProject(domain: string, port: number): Promise<{
|
|
43
|
+
ok: boolean;
|
|
44
|
+
error?: string;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function restartHost(port: number): Promise<{
|
|
47
|
+
ok: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|