@geteigen/cli 0.0.0-stage → 3.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/README.md +154 -2
- package/dist/agent.d.ts +32 -0
- package/dist/agent.js +246 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +33 -0
- package/dist/cli/src/agent.d.ts +24 -0
- package/dist/cli/src/agent.js +260 -0
- package/dist/cli/src/agentTools.d.ts +10 -0
- package/dist/cli/src/agentTools.js +70 -0
- package/dist/cli/src/auth.d.ts +5 -0
- package/dist/cli/src/auth.js +33 -0
- package/dist/cli/src/config.d.ts +11 -0
- package/dist/cli/src/config.js +51 -0
- package/dist/cli/src/cursorAuth.d.ts +10 -0
- package/dist/cli/src/cursorAuth.js +92 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +244 -0
- package/dist/cli/src/listModels.d.ts +14 -0
- package/dist/cli/src/listModels.js +196 -0
- package/dist/cli/src/listSessions.d.ts +9 -0
- package/dist/cli/src/listSessions.js +37 -0
- package/dist/cli/src/machineConfig.d.ts +20 -0
- package/dist/cli/src/machineConfig.js +66 -0
- package/dist/cli/src/pickFolder.d.ts +5 -0
- package/dist/cli/src/pickFolder.js +65 -0
- package/dist/cli/src/proc.d.ts +23 -0
- package/dist/cli/src/proc.js +77 -0
- package/dist/cli/src/service.d.ts +17 -0
- package/dist/cli/src/service.js +246 -0
- package/dist/cli/src/workRoots.d.ts +30 -0
- package/dist/cli/src/workRoots.js +221 -0
- package/dist/cli/src/worker.d.ts +8 -0
- package/dist/cli/src/worker.js +51 -0
- package/dist/cli/src/workerCore.d.ts +47 -0
- package/dist/cli/src/workerCore.js +702 -0
- package/dist/cli/src/workspace.d.ts +8 -0
- package/dist/cli/src/workspace.js +269 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +51 -0
- package/dist/cursorAuth.d.ts +10 -0
- package/dist/cursorAuth.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/listModels.d.ts +6 -0
- package/dist/listModels.js +62 -0
- package/dist/listSessions.d.ts +9 -0
- package/dist/listSessions.js +37 -0
- package/dist/pickFolder.d.ts +5 -0
- package/dist/pickFolder.js +65 -0
- package/dist/shared/actionDrafts.d.ts +83 -0
- package/dist/shared/actionDrafts.js +169 -0
- package/dist/shared/agentArtifacts.d.ts +49 -0
- package/dist/shared/agentArtifacts.js +169 -0
- package/dist/shared/agentLabel.d.ts +18 -0
- package/dist/shared/agentLabel.js +22 -0
- package/dist/shared/agentMentions.d.ts +38 -0
- package/dist/shared/agentMentions.js +110 -0
- package/dist/shared/agentRepos.d.ts +28 -0
- package/dist/shared/agentRepos.js +60 -0
- package/dist/shared/agentRunStatus.d.ts +12 -0
- package/dist/shared/agentRunStatus.js +17 -0
- package/dist/shared/agentUsage.d.ts +24 -0
- package/dist/shared/agentUsage.js +139 -0
- package/dist/shared/approvals.d.ts +44 -0
- package/dist/shared/approvals.js +97 -0
- package/dist/shared/askEigen.d.ts +190 -0
- package/dist/shared/askEigen.js +76 -0
- package/dist/shared/attribution.d.ts +25 -0
- package/dist/shared/attribution.js +63 -0
- package/dist/shared/automations.d.ts +136 -0
- package/dist/shared/automations.js +546 -0
- package/dist/shared/backends/claudeCode.d.ts +42 -0
- package/dist/shared/backends/claudeCode.js +540 -0
- package/dist/shared/backends/codex.d.ts +15 -0
- package/dist/shared/backends/codex.js +287 -0
- package/dist/shared/backends/cursor.d.ts +102 -0
- package/dist/shared/backends/cursor.js +1197 -0
- package/dist/shared/backends/index.d.ts +18 -0
- package/dist/shared/backends/index.js +58 -0
- package/dist/shared/backends/opencode.d.ts +40 -0
- package/dist/shared/backends/opencode.js +397 -0
- package/dist/shared/backends/types.d.ts +171 -0
- package/dist/shared/backends/types.js +19 -0
- package/dist/shared/backends/usage.d.ts +8 -0
- package/dist/shared/backends/usage.js +71 -0
- package/dist/shared/billingPlans.d.ts +60 -0
- package/dist/shared/billingPlans.js +262 -0
- package/dist/shared/brain.d.ts +353 -0
- package/dist/shared/brain.js +487 -0
- package/dist/shared/clarifyingQuestions.d.ts +13 -0
- package/dist/shared/clarifyingQuestions.js +207 -0
- package/dist/shared/claudeModels.d.ts +10 -0
- package/dist/shared/claudeModels.js +52 -0
- package/dist/shared/cloudBackends.d.ts +21 -0
- package/dist/shared/cloudBackends.js +29 -0
- package/dist/shared/codexModels.d.ts +10 -0
- package/dist/shared/codexModels.js +28 -0
- package/dist/shared/contextWindows.d.ts +2 -0
- package/dist/shared/contextWindows.js +30 -0
- package/dist/shared/cursorMcpConfig.d.ts +14 -0
- package/dist/shared/cursorMcpConfig.js +43 -0
- package/dist/shared/cursorProjectMcp.d.ts +10 -0
- package/dist/shared/cursorProjectMcp.js +42 -0
- package/dist/shared/docs.d.ts +59 -0
- package/dist/shared/docs.js +87 -0
- package/dist/shared/events.d.ts +929 -0
- package/dist/shared/events.js +59 -0
- package/dist/shared/integrations.d.ts +266 -0
- package/dist/shared/integrations.js +449 -0
- package/dist/shared/issueTranscript.d.ts +64 -0
- package/dist/shared/issueTranscript.js +387 -0
- package/dist/shared/issues.d.ts +139 -0
- package/dist/shared/issues.js +217 -0
- package/dist/shared/machines.d.ts +115 -0
- package/dist/shared/machines.js +58 -0
- package/dist/shared/markdownMedia.d.ts +13 -0
- package/dist/shared/markdownMedia.js +41 -0
- package/dist/shared/notificationText.d.ts +6 -0
- package/dist/shared/notificationText.js +38 -0
- package/dist/shared/opencodeModels.d.ts +19 -0
- package/dist/shared/opencodeModels.js +62 -0
- package/dist/shared/opencodeSession.d.ts +7 -0
- package/dist/shared/opencodeSession.js +30 -0
- package/dist/shared/orchestration.d.ts +58 -0
- package/dist/shared/orchestration.js +12 -0
- package/dist/shared/orgs.d.ts +47 -0
- package/dist/shared/orgs.js +49 -0
- package/dist/shared/personalOrg.d.ts +5 -0
- package/dist/shared/personalOrg.js +12 -0
- package/dist/shared/plans.d.ts +3 -0
- package/dist/shared/plans.js +19 -0
- package/dist/shared/providerModels.d.ts +40 -0
- package/dist/shared/providerModels.js +503 -0
- package/dist/shared/roomAttention.d.ts +17 -0
- package/dist/shared/roomAttention.js +36 -0
- package/dist/shared/roomContext.d.ts +120 -0
- package/dist/shared/roomContext.js +133 -0
- package/dist/shared/roomPermissions.d.ts +56 -0
- package/dist/shared/roomPermissions.js +133 -0
- package/dist/shared/sessionSeats.d.ts +29 -0
- package/dist/shared/sessionSeats.js +57 -0
- package/dist/shared/slashCommands.d.ts +39 -0
- package/dist/shared/slashCommands.js +166 -0
- package/dist/shared/space.d.ts +33 -0
- package/dist/shared/space.js +54 -0
- package/dist/shared/spaceFeed.d.ts +78 -0
- package/dist/shared/spaceFeed.js +194 -0
- package/dist/shared/stringifyUnknown.d.ts +1 -0
- package/dist/shared/stringifyUnknown.js +1 -0
- package/dist/shared/swarm.d.ts +320 -0
- package/dist/shared/swarm.js +328 -0
- package/dist/shared/swarmVisual.d.ts +79 -0
- package/dist/shared/swarmVisual.js +212 -0
- package/dist/shared/terminalFilter.d.ts +13 -0
- package/dist/shared/terminalFilter.js +46 -0
- package/dist/shared/toolIntegration.d.ts +56 -0
- package/dist/shared/toolIntegration.js +180 -0
- package/dist/shared/typing.d.ts +9 -0
- package/dist/shared/typing.js +37 -0
- package/dist/shared/uploads.d.ts +31 -0
- package/dist/shared/uploads.js +65 -0
- package/dist/shared/voiceApprovals.d.ts +36 -0
- package/dist/shared/voiceApprovals.js +229 -0
- package/dist/shared/workspace.d.ts +47 -0
- package/dist/shared/workspace.js +40 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +266 -0
- package/package.json +51 -4
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import { arch, hostname, platform } from "os";
|
|
3
|
+
import { detectAgentTools } from "./agentTools.js";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, resolve } from "path";
|
|
7
|
+
import { listLocalModelsForBackend } from "./listModels.js";
|
|
8
|
+
import { runAgentWithHandle, abortRun, abortAll, isEditTool, getFileDiff, revertFiles, } from "./agent.js";
|
|
9
|
+
import { listChatSessions } from "./listSessions.js";
|
|
10
|
+
import { WorkRoots, finalizeGit, prepareRepo } from "./workRoots.js";
|
|
11
|
+
import { checkoutWorkspaceGit, commitWorkspaceGit, getWorkspaceGit, listWorkspaceDir, pushWorkspaceGit, readWorkspaceFile, searchWorkspaceFiles, } from "./workspace.js";
|
|
12
|
+
import { isSafeAttachmentRelPath, } from "../../shared/uploads.js";
|
|
13
|
+
/**
|
|
14
|
+
* 6: worker:list-models accepts optional `backend` (cursor | claude-code | codex).
|
|
15
|
+
* 7: worker:run-prompt carries `mcpServers` (room tools + Integrations Hub).
|
|
16
|
+
* 8: worker:ready reports os / arch / client / detected agent CLIs.
|
|
17
|
+
* 9: machine credentials, worker:settings (allowed roots), list-dirs,
|
|
18
|
+
* prepare-repo, finalize-git, per-run env, worker:revoked.
|
|
19
|
+
* 10: OpenCode backend + model listing.
|
|
20
|
+
* 11: workspace git snapshot and repo file browsing.
|
|
21
|
+
* 12: commit, push, and branch switching from the git panel.
|
|
22
|
+
*/
|
|
23
|
+
const WORKER_PROTOCOL = 12;
|
|
24
|
+
const MAX_CONCURRENT = Number(process.env.STEER_MAX_CONCURRENT_AGENTS || 4);
|
|
25
|
+
/** Match server attachFileDiff — wait for the working tree to flush. */
|
|
26
|
+
const DIFF_WAIT_MS = 120;
|
|
27
|
+
function writeWorkerAttachments(cwd, attachments) {
|
|
28
|
+
const root = resolve(cwd);
|
|
29
|
+
for (const file of attachments) {
|
|
30
|
+
if (!file?.relPath || !isSafeAttachmentRelPath(file.relPath)) {
|
|
31
|
+
throw new Error(`Invalid attachment path: ${file?.relPath || "(empty)"}`);
|
|
32
|
+
}
|
|
33
|
+
const abs = resolve(root, file.relPath);
|
|
34
|
+
if (abs !== root && !abs.startsWith(root + "/") && !abs.startsWith(root + "\\")) {
|
|
35
|
+
throw new Error(`Attachment escaped workspace: ${file.relPath}`);
|
|
36
|
+
}
|
|
37
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
38
|
+
writeFileSync(abs, Buffer.from(file.data, "base64"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function sleep(ms) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
}
|
|
44
|
+
function generateWorkerId(email) {
|
|
45
|
+
const raw = `${hostname()}-${email}`;
|
|
46
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
47
|
+
}
|
|
48
|
+
function makeRunKey(roomId, agentId) {
|
|
49
|
+
return `${roomId}:${agentId}`;
|
|
50
|
+
}
|
|
51
|
+
/** Connect a local agent worker to the Steer server. */
|
|
52
|
+
export function createWorker(options) {
|
|
53
|
+
const { serverUrl, token, repoPathOverride, pickFolder, log } = options;
|
|
54
|
+
const workerId = options.machineId || generateWorkerId(options.email);
|
|
55
|
+
const roots = new WorkRoots(options.workRoots ?? []);
|
|
56
|
+
let maxConcurrent = MAX_CONCURRENT;
|
|
57
|
+
log.info(`Connecting to ${serverUrl}`);
|
|
58
|
+
log.detail(`Worker ID: ${workerId}`);
|
|
59
|
+
log.detail(`Protocol: ${WORKER_PROTOCOL}`);
|
|
60
|
+
const activeRuns = new Map();
|
|
61
|
+
/** Last known repo root per room/agent — used for revert after runs finish. */
|
|
62
|
+
const revertRootByRunKey = new Map();
|
|
63
|
+
let runSeqCounter = 0;
|
|
64
|
+
const offlineQueue = [];
|
|
65
|
+
const lastSessionByAgent = new Map();
|
|
66
|
+
const MAX_QUEUE = 500;
|
|
67
|
+
const listActiveRuns = () => [...activeRuns.values()].map((r) => ({
|
|
68
|
+
roomId: r.roomId,
|
|
69
|
+
agentId: r.agentId,
|
|
70
|
+
}));
|
|
71
|
+
let state = "connecting";
|
|
72
|
+
let stateMessage;
|
|
73
|
+
const status = () => ({
|
|
74
|
+
state,
|
|
75
|
+
message: stateMessage,
|
|
76
|
+
workerId,
|
|
77
|
+
serverUrl,
|
|
78
|
+
activeRuns: listActiveRuns(),
|
|
79
|
+
});
|
|
80
|
+
const setState = (next, message) => {
|
|
81
|
+
state = next;
|
|
82
|
+
stateMessage = message;
|
|
83
|
+
options.onStatus?.(status());
|
|
84
|
+
};
|
|
85
|
+
/** Active runs changed — same connection state, new run list. */
|
|
86
|
+
const notifyRuns = () => options.onStatus?.(status());
|
|
87
|
+
const socket = io(`${serverUrl}/worker`, {
|
|
88
|
+
auth: { token },
|
|
89
|
+
reconnection: true,
|
|
90
|
+
reconnectionAttempts: Infinity,
|
|
91
|
+
reconnectionDelay: 2000,
|
|
92
|
+
reconnectionDelayMax: 30000,
|
|
93
|
+
transports: ["websocket", "polling"],
|
|
94
|
+
});
|
|
95
|
+
const emitOrQueue = (event, payload) => {
|
|
96
|
+
if (socket.connected) {
|
|
97
|
+
socket.emit(event, payload);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (offlineQueue.length >= MAX_QUEUE)
|
|
101
|
+
offlineQueue.shift();
|
|
102
|
+
offlineQueue.push({ event, payload });
|
|
103
|
+
};
|
|
104
|
+
const flushQueue = () => {
|
|
105
|
+
if (!offlineQueue.length)
|
|
106
|
+
return;
|
|
107
|
+
log.detail(` Flushing ${offlineQueue.length} buffered event(s)…`);
|
|
108
|
+
const batch = offlineQueue.splice(0, offlineQueue.length);
|
|
109
|
+
for (const item of batch) {
|
|
110
|
+
socket.emit(item.event, item.payload);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// Which agent CLIs this machine has; refreshed on each (re)connect.
|
|
114
|
+
let agentTools = [];
|
|
115
|
+
const refreshAgentTools = () => detectAgentTools()
|
|
116
|
+
.then((tools) => {
|
|
117
|
+
agentTools = tools;
|
|
118
|
+
})
|
|
119
|
+
.catch(() => undefined);
|
|
120
|
+
const announceReady = () => {
|
|
121
|
+
const runs = listActiveRuns();
|
|
122
|
+
socket.emit("worker:ready", {
|
|
123
|
+
workerId,
|
|
124
|
+
machineName: hostname(),
|
|
125
|
+
os: platform(),
|
|
126
|
+
arch: arch(),
|
|
127
|
+
client: options.client ?? "cli",
|
|
128
|
+
agents: agentTools,
|
|
129
|
+
maxConcurrent,
|
|
130
|
+
protocol: WORKER_PROTOCOL,
|
|
131
|
+
activeRuns: runs,
|
|
132
|
+
// Legacy fields for older servers
|
|
133
|
+
activeRoomId: runs[0]?.roomId ?? null,
|
|
134
|
+
busy: runs.length > 0,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
socket.on("connect", () => {
|
|
138
|
+
log.success("✓ Connected to server");
|
|
139
|
+
setState("connected");
|
|
140
|
+
if (activeRuns.size > 0) {
|
|
141
|
+
log.info(` ${activeRuns.size} agent run(s) still going — resuming stream`);
|
|
142
|
+
}
|
|
143
|
+
announceReady();
|
|
144
|
+
// Tool detection can take a few seconds; send it once it's in.
|
|
145
|
+
void refreshAgentTools().then(() => {
|
|
146
|
+
if (socket.connected)
|
|
147
|
+
socket.emit("worker:info", { agents: agentTools });
|
|
148
|
+
});
|
|
149
|
+
flushQueue();
|
|
150
|
+
});
|
|
151
|
+
socket.on("disconnect", (reason) => {
|
|
152
|
+
log.warn(`Disconnected: ${reason}`);
|
|
153
|
+
setState("disconnected", reason);
|
|
154
|
+
if (activeRuns.size > 0) {
|
|
155
|
+
log.warn(` ${activeRuns.size} agent(s) keep running locally (will sync on reconnect)`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
socket.on("connect_error", (err) => {
|
|
159
|
+
log.error(`Connection error: ${err.message}`);
|
|
160
|
+
setState("error", err.message);
|
|
161
|
+
// A revoked machine credential is rejected at the handshake — stop retrying.
|
|
162
|
+
if (options.machineId && /was removed|setup token/i.test(err.message)) {
|
|
163
|
+
socket.io.opts.reconnection = false;
|
|
164
|
+
socket.disconnect();
|
|
165
|
+
options.onRevoked?.(err.message);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
socket.on("worker:run-prompt", (payload) => {
|
|
169
|
+
const { roomId, prompt, repoPath: payloadRepoPath, cwd: payloadCwd, modelId, } = payload;
|
|
170
|
+
const agentId = payload.agentId || "default";
|
|
171
|
+
const backendKind = payload.backend === "claude-code" ||
|
|
172
|
+
payload.backend === "codex" ||
|
|
173
|
+
payload.backend === "opencode"
|
|
174
|
+
? payload.backend
|
|
175
|
+
: "cursor";
|
|
176
|
+
const mode = payload.mode === "plan" ? "plan" : "agent";
|
|
177
|
+
const runKey = makeRunKey(roomId, agentId);
|
|
178
|
+
const repoPath = repoPathOverride || payloadCwd || payloadRepoPath;
|
|
179
|
+
const cwd = payloadCwd || repoPath;
|
|
180
|
+
revertRootByRunKey.set(runKey, repoPath);
|
|
181
|
+
const sessionId = payload.sessionId || lastSessionByAgent.get(runKey) || null;
|
|
182
|
+
if (activeRuns.has(runKey)) {
|
|
183
|
+
log.warn(` Rejecting new prompt — agent ${agentId} already running in room ${roomId}`);
|
|
184
|
+
emitOrQueue("worker:agent-event", {
|
|
185
|
+
roomId,
|
|
186
|
+
agentId,
|
|
187
|
+
event: {
|
|
188
|
+
kind: "error",
|
|
189
|
+
message: "This agent is already running another prompt",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
emitOrQueue("worker:agent-event", {
|
|
193
|
+
roomId,
|
|
194
|
+
agentId,
|
|
195
|
+
event: { kind: "done", result: "" },
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const outside = [repoPath, cwd].find((p) => p && !roots.isAllowed(p));
|
|
200
|
+
if (outside) {
|
|
201
|
+
log.warn(` Rejecting new prompt — ${outside} is outside this machine's allowed folders`);
|
|
202
|
+
emitOrQueue("worker:agent-event", {
|
|
203
|
+
roomId,
|
|
204
|
+
agentId,
|
|
205
|
+
event: {
|
|
206
|
+
kind: "error",
|
|
207
|
+
message: `This machine only runs agents inside ${roots.effective().join(", ")}. Pick a folder there.`,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
emitOrQueue("worker:agent-event", {
|
|
211
|
+
roomId,
|
|
212
|
+
agentId,
|
|
213
|
+
event: { kind: "done", result: "" },
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (activeRuns.size >= maxConcurrent) {
|
|
218
|
+
log.warn(` Rejecting new prompt — max concurrent agents (${maxConcurrent}) reached`);
|
|
219
|
+
emitOrQueue("worker:agent-event", {
|
|
220
|
+
roomId,
|
|
221
|
+
agentId,
|
|
222
|
+
event: {
|
|
223
|
+
kind: "error",
|
|
224
|
+
message: `Worker is at max concurrent agents (${maxConcurrent})`,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
emitOrQueue("worker:agent-event", {
|
|
228
|
+
roomId,
|
|
229
|
+
agentId,
|
|
230
|
+
event: { kind: "done", result: "" },
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const thisRun = ++runSeqCounter;
|
|
235
|
+
const runState = {
|
|
236
|
+
roomId,
|
|
237
|
+
agentId,
|
|
238
|
+
runSeq: thisRun,
|
|
239
|
+
editedPaths: new Map(),
|
|
240
|
+
pendingCallPaths: new Map(),
|
|
241
|
+
emittedTerminal: false,
|
|
242
|
+
abort: () => { },
|
|
243
|
+
};
|
|
244
|
+
activeRuns.set(runKey, runState);
|
|
245
|
+
notifyRuns();
|
|
246
|
+
if (payload.attachments?.length) {
|
|
247
|
+
try {
|
|
248
|
+
writeWorkerAttachments(cwd, payload.attachments);
|
|
249
|
+
log.detail(` Saved ${payload.attachments.length} attachment(s) to ${cwd}/.steer-uploads`);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const message = err.message || "Failed to save attached files";
|
|
253
|
+
log.error(` ✗ ${message}`);
|
|
254
|
+
activeRuns.delete(runKey);
|
|
255
|
+
notifyRuns();
|
|
256
|
+
emitOrQueue("worker:agent-event", {
|
|
257
|
+
roomId,
|
|
258
|
+
agentId,
|
|
259
|
+
event: { kind: "error", message },
|
|
260
|
+
});
|
|
261
|
+
emitOrQueue("worker:agent-event", {
|
|
262
|
+
roomId,
|
|
263
|
+
agentId,
|
|
264
|
+
event: { kind: "done", result: "" },
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
log.info(`\n━━━ Running prompt in room ${roomId} / agent ${agentId} ━━━`);
|
|
270
|
+
log.detail(`Cwd: ${cwd}`);
|
|
271
|
+
log.detail(`Backend: ${backendKind}`);
|
|
272
|
+
log.detail(`Model: ${modelId}`);
|
|
273
|
+
const mcpNames = Object.keys(payload.mcpServers || {});
|
|
274
|
+
if (mcpNames.length) {
|
|
275
|
+
// Names only — the entries carry bearer tokens.
|
|
276
|
+
log.detail(`MCP: ${mcpNames.join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
if (payload.env && Object.keys(payload.env).length) {
|
|
279
|
+
// Names only — values are provider keys.
|
|
280
|
+
log.detail(`Env: ${Object.keys(payload.env).join(", ")} (this run only)`);
|
|
281
|
+
}
|
|
282
|
+
log.detail(`Prompt: ${prompt.slice(0, 100)}${prompt.length > 100 ? "…" : ""}`);
|
|
283
|
+
// Process stream events in order. Fire-and-forget handlers used to race
|
|
284
|
+
// lock acquisition / git diffs ahead of later tool_done rows, so chat
|
|
285
|
+
// grouped cards often missed path + patch until the whole run finished.
|
|
286
|
+
let eventQueue = Promise.resolve();
|
|
287
|
+
const onEvent = (event) => {
|
|
288
|
+
eventQueue = eventQueue
|
|
289
|
+
.then(() => handleEvent(event))
|
|
290
|
+
.catch((err) => {
|
|
291
|
+
log.error(` ✗ Event handler: ${err.message || String(err)}`);
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
const handleEvent = async (event) => {
|
|
295
|
+
const current = activeRuns.get(runKey);
|
|
296
|
+
if (!current || current.runSeq !== thisRun)
|
|
297
|
+
return;
|
|
298
|
+
let outgoing = event;
|
|
299
|
+
if (event.kind === "tool_start") {
|
|
300
|
+
log.warn(` ▸ ${event.name} ${event.detail}`);
|
|
301
|
+
if (event.path && event.callId) {
|
|
302
|
+
current.pendingCallPaths.set(event.callId, event.path);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (event.kind === "tool_done") {
|
|
306
|
+
const path = event.path ||
|
|
307
|
+
(event.callId
|
|
308
|
+
? current.pendingCallPaths.get(event.callId)
|
|
309
|
+
: undefined);
|
|
310
|
+
log.success(` ✓ ${event.name}${path ? ` ${path}` : ` ${event.detail}`}`);
|
|
311
|
+
if (isEditTool(event.name) && path && event.callId) {
|
|
312
|
+
current.editedPaths.set(path, event.callId);
|
|
313
|
+
await sleep(DIFF_WAIT_MS);
|
|
314
|
+
if (activeRuns.get(runKey)?.runSeq !== thisRun)
|
|
315
|
+
return;
|
|
316
|
+
let patch = event.diffPatch?.trim() || "";
|
|
317
|
+
try {
|
|
318
|
+
const git = (await getFileDiff(cwd, path)).trim();
|
|
319
|
+
if (git)
|
|
320
|
+
patch = git;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// synthetic parser patch is still useful
|
|
324
|
+
}
|
|
325
|
+
outgoing = {
|
|
326
|
+
...event,
|
|
327
|
+
path,
|
|
328
|
+
diffPatch: patch || event.diffPatch,
|
|
329
|
+
};
|
|
330
|
+
if (patch) {
|
|
331
|
+
emitOrQueue("worker:file-diff", {
|
|
332
|
+
roomId,
|
|
333
|
+
agentId,
|
|
334
|
+
callId: event.callId,
|
|
335
|
+
toolName: event.name || "edit",
|
|
336
|
+
path,
|
|
337
|
+
patch,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (event.kind === "assistant_final") {
|
|
343
|
+
log.info(` Assistant: ${event.text.slice(0, 120)}…`);
|
|
344
|
+
}
|
|
345
|
+
else if (event.kind === "session") {
|
|
346
|
+
if (event.sessionId)
|
|
347
|
+
lastSessionByAgent.set(runKey, event.sessionId);
|
|
348
|
+
log.detail(` session ${event.sessionId}`);
|
|
349
|
+
}
|
|
350
|
+
else if (event.kind === "error") {
|
|
351
|
+
log.error(` ✗ Error: ${event.message}`);
|
|
352
|
+
}
|
|
353
|
+
else if (event.kind === "done") {
|
|
354
|
+
log.success(" ✓ Agent finished");
|
|
355
|
+
}
|
|
356
|
+
if (outgoing.kind === "done" || outgoing.kind === "error") {
|
|
357
|
+
current.emittedTerminal = true;
|
|
358
|
+
}
|
|
359
|
+
emitOrQueue("worker:agent-event", { roomId, agentId, event: outgoing });
|
|
360
|
+
};
|
|
361
|
+
const finishRun = (terminal) => {
|
|
362
|
+
const current = activeRuns.get(runKey);
|
|
363
|
+
if (!current || current.runSeq !== thisRun)
|
|
364
|
+
return;
|
|
365
|
+
if (terminal && !current.emittedTerminal) {
|
|
366
|
+
emitOrQueue("worker:agent-event", {
|
|
367
|
+
roomId,
|
|
368
|
+
agentId,
|
|
369
|
+
event: terminal,
|
|
370
|
+
});
|
|
371
|
+
current.emittedTerminal = true;
|
|
372
|
+
}
|
|
373
|
+
else if (!current.emittedTerminal) {
|
|
374
|
+
emitOrQueue("worker:agent-event", {
|
|
375
|
+
roomId,
|
|
376
|
+
agentId,
|
|
377
|
+
event: { kind: "done", result: "" },
|
|
378
|
+
});
|
|
379
|
+
current.emittedTerminal = true;
|
|
380
|
+
}
|
|
381
|
+
activeRuns.delete(runKey);
|
|
382
|
+
notifyRuns();
|
|
383
|
+
};
|
|
384
|
+
const handle = runAgentWithHandle(cwd, prompt, modelId, onEvent, sessionId, runKey, backendKind, mode, payload.mcpServers, payload.env);
|
|
385
|
+
runState.abort = handle.abort;
|
|
386
|
+
handle.promise
|
|
387
|
+
.then(async () => {
|
|
388
|
+
await eventQueue;
|
|
389
|
+
const current = activeRuns.get(runKey);
|
|
390
|
+
if (!current || current.runSeq !== thisRun)
|
|
391
|
+
return;
|
|
392
|
+
for (const [filePath, callId] of current.editedPaths) {
|
|
393
|
+
try {
|
|
394
|
+
const patch = await getFileDiff(cwd, filePath);
|
|
395
|
+
if (patch) {
|
|
396
|
+
emitOrQueue("worker:file-diff", {
|
|
397
|
+
roomId,
|
|
398
|
+
agentId,
|
|
399
|
+
callId,
|
|
400
|
+
toolName: "edit",
|
|
401
|
+
path: filePath,
|
|
402
|
+
patch,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// diff failures are non-fatal
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
finishRun({ kind: "done", result: "" });
|
|
411
|
+
})
|
|
412
|
+
.catch(async (err) => {
|
|
413
|
+
await eventQueue;
|
|
414
|
+
const current = activeRuns.get(runKey);
|
|
415
|
+
if (!current || current.runSeq !== thisRun)
|
|
416
|
+
return;
|
|
417
|
+
const message = err.message || "Agent error";
|
|
418
|
+
log.error(`Agent error: ${message}`);
|
|
419
|
+
finishRun({ kind: "error", message });
|
|
420
|
+
emitOrQueue("worker:agent-event", {
|
|
421
|
+
roomId,
|
|
422
|
+
agentId,
|
|
423
|
+
event: { kind: "done", result: "" },
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
socket.on("worker:abort", (payload) => {
|
|
428
|
+
const agentId = payload.agentId;
|
|
429
|
+
if (agentId) {
|
|
430
|
+
const runKey = makeRunKey(payload.roomId, agentId);
|
|
431
|
+
log.warn(` ⚠ Abort requested for ${payload.roomId}/${agentId}`);
|
|
432
|
+
const run = activeRuns.get(runKey);
|
|
433
|
+
run?.abort();
|
|
434
|
+
abortRun(runKey);
|
|
435
|
+
activeRuns.delete(runKey);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// Legacy: abort all runs for the room
|
|
439
|
+
log.warn(` ⚠ Abort requested for room ${payload.roomId}`);
|
|
440
|
+
for (const [key, run] of [...activeRuns.entries()]) {
|
|
441
|
+
if (run.roomId === payload.roomId) {
|
|
442
|
+
run.abort();
|
|
443
|
+
abortRun(key);
|
|
444
|
+
activeRuns.delete(key);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
notifyRuns();
|
|
449
|
+
});
|
|
450
|
+
socket.on("worker:revert-files", async (payload) => {
|
|
451
|
+
log.warn(` ↺ Revert requested for ${payload.filePaths.length} file(s) in room ${payload.roomId}`);
|
|
452
|
+
const agentId = payload.agentId || "default";
|
|
453
|
+
const runKey = makeRunKey(payload.roomId, agentId);
|
|
454
|
+
const repoPath = payload.repoPath ||
|
|
455
|
+
revertRootByRunKey.get(runKey) ||
|
|
456
|
+
repoPathOverride ||
|
|
457
|
+
"";
|
|
458
|
+
if (!repoPath) {
|
|
459
|
+
emitOrQueue("worker:files-reverted", {
|
|
460
|
+
roomId: payload.roomId,
|
|
461
|
+
agentId: payload.agentId,
|
|
462
|
+
filePaths: [],
|
|
463
|
+
messageId: payload.messageId,
|
|
464
|
+
error: "No local repo path for this room — run an agent once or reconnect the worker with a repo",
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const { reverted, errors } = await revertFiles(repoPath, payload.filePaths);
|
|
470
|
+
emitOrQueue("worker:files-reverted", {
|
|
471
|
+
roomId: payload.roomId,
|
|
472
|
+
agentId: payload.agentId,
|
|
473
|
+
filePaths: reverted,
|
|
474
|
+
messageId: payload.messageId,
|
|
475
|
+
error: errors.length ? errors.join("; ") : undefined,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
emitOrQueue("worker:files-reverted", {
|
|
480
|
+
roomId: payload.roomId,
|
|
481
|
+
agentId: payload.agentId,
|
|
482
|
+
filePaths: [],
|
|
483
|
+
messageId: payload.messageId,
|
|
484
|
+
error: err instanceof Error ? err.message : String(err),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
socket.on("worker:pick-folder", async (payload) => {
|
|
489
|
+
log.info(" 📂 Opening folder picker…");
|
|
490
|
+
try {
|
|
491
|
+
const path = await pickFolder();
|
|
492
|
+
if (path) {
|
|
493
|
+
log.success(` ✓ Selected: ${path}`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
log.warn(" Folder pick cancelled");
|
|
497
|
+
}
|
|
498
|
+
emitOrQueue("worker:folder-picked", {
|
|
499
|
+
requestId: payload.requestId,
|
|
500
|
+
path,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
const message = err.message;
|
|
505
|
+
log.error(` ✗ Folder picker error: ${message}`);
|
|
506
|
+
emitOrQueue("worker:folder-picked", {
|
|
507
|
+
requestId: payload.requestId,
|
|
508
|
+
path: null,
|
|
509
|
+
error: message,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
socket.on("worker:list-models", async (payload) => {
|
|
514
|
+
const backend = payload.backend || "cursor";
|
|
515
|
+
const label = backend === "claude-code"
|
|
516
|
+
? "Claude Code"
|
|
517
|
+
: backend === "codex"
|
|
518
|
+
? "Codex"
|
|
519
|
+
: backend === "opencode"
|
|
520
|
+
? "OpenCode"
|
|
521
|
+
: "Cursor";
|
|
522
|
+
log.info(` Listing ${label} models…`);
|
|
523
|
+
try {
|
|
524
|
+
const models = await listLocalModelsForBackend(backend);
|
|
525
|
+
log.success(` ✓ ${models.length} models`);
|
|
526
|
+
emitOrQueue("worker:models-listed", {
|
|
527
|
+
requestId: payload.requestId,
|
|
528
|
+
models,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
const message = err.message;
|
|
533
|
+
log.error(` ✗ List models error: ${message}`);
|
|
534
|
+
emitOrQueue("worker:models-listed", {
|
|
535
|
+
requestId: payload.requestId,
|
|
536
|
+
error: message,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
socket.on("worker:list-sessions", (payload) => {
|
|
541
|
+
log.info(` Listing Cursor chats for ${payload.repoPath}…`);
|
|
542
|
+
try {
|
|
543
|
+
const sessions = listChatSessions(payload.repoPath);
|
|
544
|
+
log.success(` ✓ ${sessions.length} chat(s)`);
|
|
545
|
+
emitOrQueue("worker:sessions-listed", {
|
|
546
|
+
requestId: payload.requestId,
|
|
547
|
+
sessions,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
const message = err.message;
|
|
552
|
+
log.error(` ✗ List sessions error: ${message}`);
|
|
553
|
+
emitOrQueue("worker:sessions-listed", {
|
|
554
|
+
requestId: payload.requestId,
|
|
555
|
+
sessions: [],
|
|
556
|
+
error: message,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
socket.on("worker:workspace-git", async (payload) => {
|
|
561
|
+
try {
|
|
562
|
+
if (!roots.isAllowed(payload.repoPath))
|
|
563
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
564
|
+
const snapshot = await getWorkspaceGit(payload.repoPath);
|
|
565
|
+
emitOrQueue("worker:workspace-git-result", { requestId: payload.requestId, snapshot });
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
emitOrQueue("worker:workspace-git-result", {
|
|
569
|
+
requestId: payload.requestId,
|
|
570
|
+
error: err.message || "Couldn't read git state",
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
socket.on("worker:workspace-git-action", async (payload) => {
|
|
575
|
+
try {
|
|
576
|
+
if (!roots.isAllowed(payload.repoPath))
|
|
577
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
578
|
+
if (payload.action === "commit")
|
|
579
|
+
await commitWorkspaceGit(payload.repoPath, payload.message || "");
|
|
580
|
+
else if (payload.action === "push")
|
|
581
|
+
await pushWorkspaceGit(payload.repoPath);
|
|
582
|
+
else if (payload.action === "checkout")
|
|
583
|
+
await checkoutWorkspaceGit(payload.repoPath, payload.branch || "");
|
|
584
|
+
else
|
|
585
|
+
throw new Error("Unknown git action");
|
|
586
|
+
emitOrQueue("worker:workspace-git-action-result", { requestId: payload.requestId, ok: true });
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
emitOrQueue("worker:workspace-git-action-result", {
|
|
590
|
+
requestId: payload.requestId,
|
|
591
|
+
error: err.message || "Git failed",
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
socket.on("worker:workspace-dir", async (payload) => {
|
|
596
|
+
try {
|
|
597
|
+
if (!roots.isAllowed(payload.repoPath))
|
|
598
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
599
|
+
const entries = await listWorkspaceDir(payload.repoPath, payload.path);
|
|
600
|
+
emitOrQueue("worker:workspace-dir-result", { requestId: payload.requestId, entries });
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
emitOrQueue("worker:workspace-dir-result", {
|
|
604
|
+
requestId: payload.requestId,
|
|
605
|
+
error: err.message || "Couldn't list files",
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
socket.on("worker:workspace-file", async (payload) => {
|
|
610
|
+
try {
|
|
611
|
+
if (!roots.isAllowed(payload.repoPath))
|
|
612
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
613
|
+
const file = await readWorkspaceFile(payload.repoPath, payload.path);
|
|
614
|
+
emitOrQueue("worker:workspace-file-result", { requestId: payload.requestId, file });
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
emitOrQueue("worker:workspace-file-result", {
|
|
618
|
+
requestId: payload.requestId,
|
|
619
|
+
error: err.message || "Couldn't read file",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
socket.on("worker:workspace-search", async (payload) => {
|
|
624
|
+
try {
|
|
625
|
+
if (!roots.isAllowed(payload.repoPath))
|
|
626
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
627
|
+
const matches = await searchWorkspaceFiles(payload.repoPath, payload.query);
|
|
628
|
+
emitOrQueue("worker:workspace-search-result", { requestId: payload.requestId, matches });
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
emitOrQueue("worker:workspace-search-result", {
|
|
632
|
+
requestId: payload.requestId,
|
|
633
|
+
error: err.message || "Couldn't search files",
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
socket.on("worker:settings", (data) => {
|
|
638
|
+
roots.setServerRoots(Array.isArray(data?.allowedRoots) ? data.allowedRoots : []);
|
|
639
|
+
if (typeof data?.maxConcurrent === "number" && data.maxConcurrent > 0) {
|
|
640
|
+
maxConcurrent = Math.min(16, Math.floor(data.maxConcurrent));
|
|
641
|
+
}
|
|
642
|
+
const allowed = roots.effective();
|
|
643
|
+
log.detail(` Settings: up to ${maxConcurrent} agents${allowed.length ? ` · folders: ${allowed.join(", ")}` : ""}`);
|
|
644
|
+
});
|
|
645
|
+
socket.on("worker:list-dirs", (payload) => {
|
|
646
|
+
try {
|
|
647
|
+
const listing = roots.listDirs(payload.path || undefined);
|
|
648
|
+
emitOrQueue("worker:dirs-listed", { requestId: payload.requestId, listing });
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
emitOrQueue("worker:dirs-listed", {
|
|
652
|
+
requestId: payload.requestId,
|
|
653
|
+
error: err.message || "Couldn't list folders",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
socket.on("worker:prepare-repo", async (payload) => {
|
|
658
|
+
log.info(payload.repoUrl ? ` ⤓ Preparing ${payload.repoUrl} (${payload.ref || "main"})…` : " Preparing a workspace…");
|
|
659
|
+
try {
|
|
660
|
+
const path = await prepareRepo(roots, payload);
|
|
661
|
+
log.success(` ✓ Ready at ${path}`);
|
|
662
|
+
emitOrQueue("worker:repo-prepared", { requestId: payload.requestId, path });
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
const message = err.message || "Couldn't prepare the repository";
|
|
666
|
+
log.error(` ✗ ${message}`);
|
|
667
|
+
emitOrQueue("worker:repo-prepared", { requestId: payload.requestId, error: message });
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
socket.on("worker:finalize-git", async (payload) => {
|
|
671
|
+
log.info(` ⤒ Pushing ${payload.branch}…`);
|
|
672
|
+
try {
|
|
673
|
+
const result = await finalizeGit(roots, payload);
|
|
674
|
+
log.success(result.pushed ? ` ✓ Pushed ${result.branch}` : " Nothing to push");
|
|
675
|
+
emitOrQueue("worker:git-finalized", { requestId: payload.requestId, ...result });
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
const message = err.message || "Couldn't push changes";
|
|
679
|
+
log.error(` ✗ ${message}`);
|
|
680
|
+
emitOrQueue("worker:git-finalized", { requestId: payload.requestId, error: message });
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
socket.on("worker:revoked", (data) => {
|
|
684
|
+
const message = data?.message || "This machine was removed in Eigen.";
|
|
685
|
+
log.warn(` ${message}`);
|
|
686
|
+
abortAll();
|
|
687
|
+
activeRuns.clear();
|
|
688
|
+
socket.io.opts.reconnection = false;
|
|
689
|
+
socket.disconnect();
|
|
690
|
+
setState("error", message);
|
|
691
|
+
options.onRevoked?.(message);
|
|
692
|
+
});
|
|
693
|
+
return {
|
|
694
|
+
workerId,
|
|
695
|
+
status,
|
|
696
|
+
stop() {
|
|
697
|
+
abortAll();
|
|
698
|
+
activeRuns.clear();
|
|
699
|
+
socket.disconnect();
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|