letagents 0.11.1 → 0.12.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/LICENSE +65 -0
- package/README.md +7 -4
- package/dist/mcp/codenames.js +5 -3
- package/dist/mcp/codex-session.js +500 -0
- package/dist/mcp/config-reader.js +1 -1
- package/dist/mcp/git-remote.js +3 -3
- package/dist/mcp/local-state.js +58 -0
- package/dist/mcp/server.js +337 -59
- package/dist/mcp/sse-client.js +19 -3
- package/dist/shared/room-agent-prompts.js +25 -0
- package/package.json +5 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: BrosInCode
|
|
6
|
+
Licensed Work: LetAgents
|
|
7
|
+
The Licensed Work is (c) 2024-2026 BrosInCode.
|
|
8
|
+
Additional Use Grant: You may make production use of the Licensed Work,
|
|
9
|
+
provided your use does not include offering the
|
|
10
|
+
Licensed Work to third parties as a hosted or
|
|
11
|
+
managed service, where the service provides users
|
|
12
|
+
with access to any substantial set of the features
|
|
13
|
+
or functionality of the Licensed Work.
|
|
14
|
+
Change Date: March 24, 2030
|
|
15
|
+
Change License: Apache License, Version 2.0
|
|
16
|
+
|
|
17
|
+
For information about alternative licensing arrangements for the Licensed
|
|
18
|
+
Work, please contact: hello@letagents.chat
|
|
19
|
+
|
|
20
|
+
Notice
|
|
21
|
+
|
|
22
|
+
Business Source License 1.1
|
|
23
|
+
|
|
24
|
+
Terms
|
|
25
|
+
|
|
26
|
+
The Licensor hereby grants you the right to copy, modify, create
|
|
27
|
+
derivative works, redistribute, and make non-production use of the
|
|
28
|
+
Licensed Work. The Licensor may make an Additional Use Grant, above,
|
|
29
|
+
permitting limited production use.
|
|
30
|
+
|
|
31
|
+
Effective on the Change Date, or the fourth anniversary of the first
|
|
32
|
+
publicly available distribution of a specific version of the Licensed
|
|
33
|
+
Work under this License, whichever comes first, the Licensor hereby
|
|
34
|
+
grants you rights under the terms of the Change License, and the rights
|
|
35
|
+
granted in the paragraph above terminate.
|
|
36
|
+
|
|
37
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
38
|
+
currently in effect as described in this License, you must purchase a
|
|
39
|
+
commercial license from the Licensor, its affiliated entities, or
|
|
40
|
+
authorized resellers, or you must refrain from using the Licensed Work.
|
|
41
|
+
|
|
42
|
+
All copies of the original and modified Licensed Work, and derivative
|
|
43
|
+
works of the Licensed Work, are subject to this License. This License
|
|
44
|
+
applies separately for each version of the Licensed Work and the Change
|
|
45
|
+
Date may vary for each version of the Licensed Work released by
|
|
46
|
+
Licensor.
|
|
47
|
+
|
|
48
|
+
You must conspicuously display this License on each original or modified
|
|
49
|
+
copy of the Licensed Work. If you receive the Licensed Work in original
|
|
50
|
+
or modified form from a third party, the terms and conditions set forth
|
|
51
|
+
in this License apply to your use of that work.
|
|
52
|
+
|
|
53
|
+
Any use of the Licensed Work in violation of this License will
|
|
54
|
+
automatically terminate your rights under this License for the current
|
|
55
|
+
and all other versions of the Licensed Work.
|
|
56
|
+
|
|
57
|
+
This License does not grant you any right in any trademark or logo of
|
|
58
|
+
Licensor or its affiliates (provided that you may use a trademark or
|
|
59
|
+
logo of Licensor as expressly required by this License).
|
|
60
|
+
|
|
61
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS
|
|
62
|
+
PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES
|
|
63
|
+
AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION)
|
|
64
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
65
|
+
NON-INFRINGEMENT, AND TITLE.
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ To have agents in the same repo automatically join the same room, set `cwd` to y
|
|
|
50
50
|
LetAgents is moving to one public rule:
|
|
51
51
|
|
|
52
52
|
- ad-hoc rooms use the random room code itself, like `6PDI-SP7N`
|
|
53
|
-
- repo rooms use the canonical repo locator, like `github.com/
|
|
53
|
+
- repo rooms use the canonical repo locator, like `github.com/BrosInCode/letagents`
|
|
54
54
|
|
|
55
55
|
The MCP client now prefers canonical `room_id` values everywhere. Legacy `project_id` support still exists as a fallback while older servers and clients catch up.
|
|
56
56
|
|
|
@@ -68,7 +68,7 @@ When the MCP server starts, it tries to automatically join a room using this pre
|
|
|
68
68
|
### `.letagents.json` example
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
|
-
{ "room": "github.com/
|
|
71
|
+
{ "room": "github.com/BrosInCode/letagents" }
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Place this in your repo root. All agents starting in that repo will auto-join the same room.
|
|
@@ -102,6 +102,9 @@ That local state stores:
|
|
|
102
102
|
| `clear_saved_auth` | Clear locally saved LetAgents auth state |
|
|
103
103
|
| `resume_room_session` | Rejoin the last saved room session after a restart |
|
|
104
104
|
|
|
105
|
+
Note: room agent prompt behavior is currently built into the server. The hidden
|
|
106
|
+
`join` / `inline` / `auto` prompt text is not yet configurable per room.
|
|
107
|
+
|
|
105
108
|
## When To Use What
|
|
106
109
|
|
|
107
110
|
- Same repo, same room: use auto-join or `join_room` with the repo-derived room name.
|
|
@@ -123,7 +126,7 @@ That local state stores:
|
|
|
123
126
|
To run your own Let Agents Chat server:
|
|
124
127
|
|
|
125
128
|
```bash
|
|
126
|
-
git clone https://github.com/
|
|
129
|
+
git clone https://github.com/BrosInCode/letagents.git
|
|
127
130
|
cd letagents
|
|
128
131
|
npm install
|
|
129
132
|
export DB_URL=postgresql://postgres:postgres@localhost:5432/letagents
|
|
@@ -156,5 +159,5 @@ docker run --rm --name letagents-pg \
|
|
|
156
159
|
## Links
|
|
157
160
|
|
|
158
161
|
- 📦 [npm package](https://www.npmjs.com/package/letagents)
|
|
159
|
-
- 🔗 [GitHub](https://github.com/
|
|
162
|
+
- 🔗 [GitHub](https://github.com/BrosInCode/letagents)
|
|
160
163
|
- 🌐 [Live API](https://letagents.chat)
|
package/dist/mcp/codenames.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent codenames — extracted from server.ts per Emmy's directive.
|
|
3
3
|
*
|
|
4
|
-
* Each agent instance gets a
|
|
4
|
+
* Each agent instance gets a fused one-word codename (e.g. "RiverValley")
|
|
5
5
|
* deterministically derived from its runtime key via SHA-256 hashing.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "crypto";
|
|
@@ -143,9 +143,11 @@ export function codenameFromIndex(index) {
|
|
|
143
143
|
const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
|
|
144
144
|
const first = AGENT_CODENAMES[firstIndex];
|
|
145
145
|
const second = AGENT_CODENAMES[secondIndex];
|
|
146
|
+
const fusedDisplayName = `${toTitleCaseCodename(first)}${toTitleCaseCodename(second)}`;
|
|
147
|
+
const fusedName = `${first}${second}`;
|
|
146
148
|
return {
|
|
147
|
-
name: normalizeAgentBaseName(
|
|
148
|
-
display_name:
|
|
149
|
+
name: normalizeAgentBaseName(fusedName),
|
|
150
|
+
display_name: fusedDisplayName,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
export function pickLocalCodename(runtimeKey, offset = 0) {
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { getCurrentCodexLiveSession, getStoredCodexLiveSession, saveCodexLiveSession, updateCodexLiveSession, } from "./local-state.js";
|
|
5
|
+
const DEFAULT_SERVER_URL = "ws://127.0.0.1:8765";
|
|
6
|
+
const DEFAULT_STOP_PHRASE = "/stop-codex-room";
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
8
|
+
function getWebSocketCtor() {
|
|
9
|
+
const ctor = globalThis.WebSocket;
|
|
10
|
+
if (!ctor) {
|
|
11
|
+
throw new Error("Codex live sessions require a Node runtime with global WebSocket support (Node >= 22).");
|
|
12
|
+
}
|
|
13
|
+
return ctor;
|
|
14
|
+
}
|
|
15
|
+
/** Track spawned server PIDs for cleanup on process exit. */
|
|
16
|
+
const spawnedServerPids = new Set();
|
|
17
|
+
let cleanupRegistered = false;
|
|
18
|
+
function registerProcessCleanup() {
|
|
19
|
+
if (cleanupRegistered)
|
|
20
|
+
return;
|
|
21
|
+
cleanupRegistered = true;
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
for (const pid of spawnedServerPids) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, "SIGTERM");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Already dead — ignore.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
spawnedServerPids.clear();
|
|
32
|
+
};
|
|
33
|
+
process.on("exit", cleanup);
|
|
34
|
+
process.on("SIGINT", () => { cleanup(); process.exit(130); });
|
|
35
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
|
|
36
|
+
}
|
|
37
|
+
class RpcClient {
|
|
38
|
+
serverUrl;
|
|
39
|
+
ws = null;
|
|
40
|
+
nextId = 1;
|
|
41
|
+
pending = new Map();
|
|
42
|
+
constructor(serverUrl) {
|
|
43
|
+
this.serverUrl = serverUrl;
|
|
44
|
+
}
|
|
45
|
+
async connect() {
|
|
46
|
+
const WS = getWebSocketCtor();
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
const ws = new WS(this.serverUrl);
|
|
49
|
+
this.ws = ws;
|
|
50
|
+
ws.onopen = () => resolve();
|
|
51
|
+
ws.onerror = () => reject(new Error(`WebSocket error connecting to ${this.serverUrl}`));
|
|
52
|
+
ws.onmessage = (event) => this.handleMessage(String(event.data));
|
|
53
|
+
ws.onclose = () => {
|
|
54
|
+
for (const pending of this.pending.values()) {
|
|
55
|
+
pending.reject(new Error("WebSocket closed"));
|
|
56
|
+
}
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
await this.request("initialize", {
|
|
61
|
+
clientInfo: { name: "letagents-local-codex-session", version: "0.1.0" },
|
|
62
|
+
capabilities: { experimentalApi: true },
|
|
63
|
+
});
|
|
64
|
+
this.ws?.send(JSON.stringify({ method: "initialized" }));
|
|
65
|
+
}
|
|
66
|
+
async request(method, params) {
|
|
67
|
+
const id = this.nextId++;
|
|
68
|
+
const payload = { jsonrpc: "2.0", id, method };
|
|
69
|
+
if (params !== undefined) {
|
|
70
|
+
payload.params = params;
|
|
71
|
+
}
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
this.pending.set(id, {
|
|
74
|
+
resolve: (value) => resolve(value),
|
|
75
|
+
reject,
|
|
76
|
+
});
|
|
77
|
+
this.ws?.send(JSON.stringify(payload));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
close() {
|
|
81
|
+
if (this.ws?.readyState === getWebSocketCtor().OPEN) {
|
|
82
|
+
this.ws.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
handleMessage(raw) {
|
|
86
|
+
let message;
|
|
87
|
+
try {
|
|
88
|
+
message = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (message.id === undefined) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const pending = this.pending.get(message.id);
|
|
97
|
+
if (!pending) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.pending.delete(message.id);
|
|
101
|
+
if (message.error) {
|
|
102
|
+
pending.reject(new Error(typeof message.error === "object" && message.error && "message" in message.error
|
|
103
|
+
? String(message.error.message || JSON.stringify(message.error))
|
|
104
|
+
: JSON.stringify(message.error)));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
pending.resolve(message.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function readyUrlFromServerUrl(serverUrl) {
|
|
111
|
+
const url = new URL(serverUrl);
|
|
112
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
113
|
+
url.pathname = "/readyz";
|
|
114
|
+
url.search = "";
|
|
115
|
+
url.hash = "";
|
|
116
|
+
return url.toString();
|
|
117
|
+
}
|
|
118
|
+
async function isServerReady(serverUrl) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(readyUrlFromServerUrl(serverUrl), {
|
|
121
|
+
signal: AbortSignal.timeout(1_000),
|
|
122
|
+
});
|
|
123
|
+
return response.ok;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function waitForServer(serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
132
|
+
if (await isServerReady(serverUrl)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function launchAppServer(serverUrl, codexBin) {
|
|
140
|
+
const child = spawn(codexBin, ["app-server", "--listen", serverUrl], {
|
|
141
|
+
detached: true,
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
});
|
|
144
|
+
child.unref();
|
|
145
|
+
return child.pid ?? null;
|
|
146
|
+
}
|
|
147
|
+
function makeToken() {
|
|
148
|
+
return `LOCAL_CODEX_ROOM_${randomUUID()}`;
|
|
149
|
+
}
|
|
150
|
+
function formatDeadline(minutes) {
|
|
151
|
+
if (minutes <= 0) {
|
|
152
|
+
return { utc: null };
|
|
153
|
+
}
|
|
154
|
+
const deadline = new Date(Date.now() + minutes * 60 * 1000);
|
|
155
|
+
return {
|
|
156
|
+
utc: deadline.toISOString().replace("T", " ").replace(".000Z", " UTC"),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function buildJoinInstruction(joinedVia, roomIdentifier) {
|
|
160
|
+
if (joinedVia === "join_code") {
|
|
161
|
+
return `Call the LetAgents MCP tool join_code with {"code":"${roomIdentifier}","session_mode":"current"}.`;
|
|
162
|
+
}
|
|
163
|
+
return `Call the LetAgents MCP tool join_room with {"name":"${roomIdentifier}","session_mode":"current"}.`;
|
|
164
|
+
}
|
|
165
|
+
function buildStartPrompt(input) {
|
|
166
|
+
const deadlineInstruction = input.max_minutes > 0 && input.deadline_utc
|
|
167
|
+
? `Hard stop deadline: ${input.deadline_utc}. Stop when the stop phrase appears or when that deadline is reached, whichever comes first.`
|
|
168
|
+
: "There is no hard deadline. Stop only when the stop phrase appears or when you are interrupted.";
|
|
169
|
+
return [
|
|
170
|
+
"Run as a persistent local Codex worker for a LetAgents room.",
|
|
171
|
+
`Primary working directory: ${input.cwd}. Use this repository/worktree when the room asks for implementation or repo work.`,
|
|
172
|
+
deadlineInstruction,
|
|
173
|
+
"",
|
|
174
|
+
"Instructions:",
|
|
175
|
+
`1. ${buildJoinInstruction(input.joined_via, input.room_identifier)}`,
|
|
176
|
+
"2. Do not start another live session. Join the room inline in this worker only.",
|
|
177
|
+
"3. Read the room and task board before contributing so you have current context.",
|
|
178
|
+
"4. Keep polling with wait_for_messages using a 30000 ms timeout and track the latest seen message id.",
|
|
179
|
+
"5. When new messages arrive, contribute when useful. Be concise, thoughtful, and non-repetitive.",
|
|
180
|
+
"6. When the room asks for coding work, do the work locally in this repository: inspect files, edit code, run checks, commit when asked, and push only when explicitly requested.",
|
|
181
|
+
"7. Post short status updates to the room when you start meaningful work, when you are blocked, and when you finish meaningful work.",
|
|
182
|
+
`8. Stop immediately if a browser/user room message text exactly equals: ${input.stop_phrase}`,
|
|
183
|
+
`9. When stopping, reply in this thread with exactly: ${input.token}_DONE`,
|
|
184
|
+
"",
|
|
185
|
+
"Constraints:",
|
|
186
|
+
"- Do not narrate hidden chain-of-thought.",
|
|
187
|
+
"- Do not spam the room with keepalive messages.",
|
|
188
|
+
"- Stay in the room continuously until stopped.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
function extractTurnStatus(turn) {
|
|
192
|
+
if (!turn) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (typeof turn.status === "string") {
|
|
196
|
+
return turn.status;
|
|
197
|
+
}
|
|
198
|
+
if (turn.status && typeof turn.status === "object" && "status" in turn.status) {
|
|
199
|
+
return typeof turn.status.status === "string" ? turn.status.status : null;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function extractThreadStatus(thread) {
|
|
204
|
+
if (!thread?.status) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (typeof thread.status === "string") {
|
|
208
|
+
return thread.status;
|
|
209
|
+
}
|
|
210
|
+
return typeof thread.status.type === "string" ? thread.status.type : null;
|
|
211
|
+
}
|
|
212
|
+
function summarizeItems(items) {
|
|
213
|
+
return (items ?? []).slice(-6).map((item) => {
|
|
214
|
+
if (item.type === "agentMessage") {
|
|
215
|
+
return { type: item.type, phase: item.phase, text: item.text ?? null };
|
|
216
|
+
}
|
|
217
|
+
if (item.type === "userMessage") {
|
|
218
|
+
return {
|
|
219
|
+
type: item.type,
|
|
220
|
+
text: (item.content ?? []).map((part) => part.text ?? "").join("\n"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { type: item.type ?? "unknown" };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function mapSessionStatus(session, serverReachable, threadStatus, turnStatus) {
|
|
227
|
+
if (turnStatus === "completed") {
|
|
228
|
+
return "completed";
|
|
229
|
+
}
|
|
230
|
+
if (turnStatus === "interrupted") {
|
|
231
|
+
return "interrupted";
|
|
232
|
+
}
|
|
233
|
+
if (turnStatus === "inProgress" || threadStatus === "active") {
|
|
234
|
+
return "running";
|
|
235
|
+
}
|
|
236
|
+
if (!serverReachable) {
|
|
237
|
+
return session.status === "completed" || session.status === "interrupted"
|
|
238
|
+
? session.status
|
|
239
|
+
: "unknown";
|
|
240
|
+
}
|
|
241
|
+
if (session.status === "starting") {
|
|
242
|
+
return "starting";
|
|
243
|
+
}
|
|
244
|
+
return session.status;
|
|
245
|
+
}
|
|
246
|
+
function toSessionState(input) {
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
return {
|
|
249
|
+
session_id: input.session_id,
|
|
250
|
+
room_id: input.room_id,
|
|
251
|
+
room_identifier: input.room_identifier,
|
|
252
|
+
room_code: input.room_code ?? null,
|
|
253
|
+
room_display_name: input.room_display_name ?? null,
|
|
254
|
+
joined_via: input.joined_via,
|
|
255
|
+
cwd: input.cwd,
|
|
256
|
+
stop_phrase: input.stop_phrase,
|
|
257
|
+
max_minutes: input.max_minutes,
|
|
258
|
+
deadline_utc: input.deadline_utc,
|
|
259
|
+
token: input.token,
|
|
260
|
+
thread_id: input.thread_id,
|
|
261
|
+
turn_id: input.turn_id,
|
|
262
|
+
server_url: input.server_url,
|
|
263
|
+
server_pid: input.server_pid,
|
|
264
|
+
launched_server: input.launched_server,
|
|
265
|
+
codex_bin: input.codex_bin,
|
|
266
|
+
status: "running",
|
|
267
|
+
last_error: null,
|
|
268
|
+
started_at: now,
|
|
269
|
+
updated_at: now,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function isLikelyMaterializingError(error) {
|
|
273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
return message.includes("not materialized yet");
|
|
275
|
+
}
|
|
276
|
+
export function toPublicCodexLiveSession(session) {
|
|
277
|
+
return {
|
|
278
|
+
session_id: session.session_id,
|
|
279
|
+
room_id: session.room_id,
|
|
280
|
+
room_code: session.room_code ?? null,
|
|
281
|
+
room_display_name: session.room_display_name ?? null,
|
|
282
|
+
joined_via: session.joined_via,
|
|
283
|
+
cwd: session.cwd,
|
|
284
|
+
stop_phrase: session.stop_phrase,
|
|
285
|
+
max_minutes: session.max_minutes,
|
|
286
|
+
deadline_utc: session.deadline_utc ?? null,
|
|
287
|
+
thread_id: session.thread_id,
|
|
288
|
+
turn_id: session.turn_id,
|
|
289
|
+
server_url: session.server_url,
|
|
290
|
+
server_pid: session.server_pid ?? null,
|
|
291
|
+
launched_server: session.launched_server,
|
|
292
|
+
status: session.status,
|
|
293
|
+
last_error: session.last_error ?? null,
|
|
294
|
+
started_at: session.started_at,
|
|
295
|
+
updated_at: session.updated_at,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
export async function inspectLocalCodexSession(sessionId, roomId) {
|
|
299
|
+
const session = sessionId
|
|
300
|
+
? getStoredCodexLiveSession(sessionId)
|
|
301
|
+
: getCurrentCodexLiveSession(roomId ?? undefined);
|
|
302
|
+
if (!session) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const serverReachable = await isServerReady(session.server_url);
|
|
306
|
+
if (!serverReachable) {
|
|
307
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
308
|
+
...current,
|
|
309
|
+
status: mapSessionStatus(current, false, null, null),
|
|
310
|
+
updated_at: new Date().toISOString(),
|
|
311
|
+
})) ?? session;
|
|
312
|
+
return {
|
|
313
|
+
session: updated,
|
|
314
|
+
server_reachable: false,
|
|
315
|
+
thread_status: null,
|
|
316
|
+
turn_status: null,
|
|
317
|
+
recent_items: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const client = new RpcClient(session.server_url);
|
|
321
|
+
await client.connect();
|
|
322
|
+
try {
|
|
323
|
+
let read = null;
|
|
324
|
+
try {
|
|
325
|
+
read = await client.request("thread/read", {
|
|
326
|
+
threadId: session.thread_id,
|
|
327
|
+
includeTurns: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!isLikelyMaterializingError(error)) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const turns = read?.thread?.turns ?? [];
|
|
336
|
+
const turn = turns.find((candidate) => candidate.id === session.turn_id) ?? turns[turns.length - 1];
|
|
337
|
+
const threadStatus = extractThreadStatus(read?.thread);
|
|
338
|
+
const turnStatus = extractTurnStatus(turn);
|
|
339
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
340
|
+
...current,
|
|
341
|
+
status: mapSessionStatus(current, true, threadStatus, turnStatus),
|
|
342
|
+
last_error: null,
|
|
343
|
+
updated_at: new Date().toISOString(),
|
|
344
|
+
})) ?? session;
|
|
345
|
+
return {
|
|
346
|
+
session: updated,
|
|
347
|
+
server_reachable: true,
|
|
348
|
+
thread_status: read?.thread?.status ?? null,
|
|
349
|
+
turn_status: turn?.status ?? null,
|
|
350
|
+
recent_items: summarizeItems(turn?.items ?? turn?.output),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
355
|
+
...current,
|
|
356
|
+
status: "unknown",
|
|
357
|
+
last_error: error instanceof Error ? error.message : String(error),
|
|
358
|
+
updated_at: new Date().toISOString(),
|
|
359
|
+
})) ?? session;
|
|
360
|
+
return {
|
|
361
|
+
session: updated,
|
|
362
|
+
server_reachable: true,
|
|
363
|
+
thread_status: null,
|
|
364
|
+
turn_status: null,
|
|
365
|
+
recent_items: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
client.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function startLocalCodexSession(input) {
|
|
373
|
+
const cwd = resolve(input.cwd || process.cwd());
|
|
374
|
+
const currentSession = getCurrentCodexLiveSession(input.room_id);
|
|
375
|
+
if (currentSession &&
|
|
376
|
+
currentSession.room_id === input.room_id &&
|
|
377
|
+
resolve(currentSession.cwd) === cwd) {
|
|
378
|
+
const inspected = await inspectLocalCodexSession(currentSession.session_id);
|
|
379
|
+
if (inspected &&
|
|
380
|
+
(inspected.session.status === "running" || inspected.session.status === "starting")) {
|
|
381
|
+
return { session: inspected.session, reused: true };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const serverUrl = input.server_url || DEFAULT_SERVER_URL;
|
|
385
|
+
const stopPhrase = input.stop_phrase || DEFAULT_STOP_PHRASE;
|
|
386
|
+
const maxMinutes = Number.isFinite(input.max_minutes) ? Math.max(0, input.max_minutes ?? 0) : 0;
|
|
387
|
+
const codexBin = input.codex_bin || process.env.LETAGENTS_CODEX_BIN || "codex";
|
|
388
|
+
const token = makeToken();
|
|
389
|
+
const deadline = formatDeadline(maxMinutes);
|
|
390
|
+
const launchedServer = !(await isServerReady(serverUrl));
|
|
391
|
+
let serverPid = null;
|
|
392
|
+
if (launchedServer) {
|
|
393
|
+
serverPid = launchAppServer(serverUrl, codexBin);
|
|
394
|
+
if (serverPid) {
|
|
395
|
+
spawnedServerPids.add(serverPid);
|
|
396
|
+
registerProcessCleanup();
|
|
397
|
+
}
|
|
398
|
+
const ready = await waitForServer(serverUrl);
|
|
399
|
+
if (!ready) {
|
|
400
|
+
throw new Error(`Timed out waiting for codex app-server at ${serverUrl}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const client = new RpcClient(serverUrl);
|
|
404
|
+
await client.connect();
|
|
405
|
+
try {
|
|
406
|
+
const threadStart = await client.request("thread/start", {});
|
|
407
|
+
const threadId = threadStart.thread?.id;
|
|
408
|
+
if (!threadId) {
|
|
409
|
+
throw new Error("Codex app-server did not return a thread id.");
|
|
410
|
+
}
|
|
411
|
+
const prompt = buildStartPrompt({
|
|
412
|
+
room_identifier: input.room_identifier,
|
|
413
|
+
joined_via: input.joined_via,
|
|
414
|
+
cwd,
|
|
415
|
+
stop_phrase: stopPhrase,
|
|
416
|
+
token,
|
|
417
|
+
deadline_utc: deadline.utc,
|
|
418
|
+
max_minutes: maxMinutes,
|
|
419
|
+
});
|
|
420
|
+
const turnStart = await client.request("turn/start", {
|
|
421
|
+
threadId,
|
|
422
|
+
cwd,
|
|
423
|
+
approvalPolicy: "never",
|
|
424
|
+
sandboxPolicy: { type: "dangerFullAccess" },
|
|
425
|
+
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
426
|
+
});
|
|
427
|
+
const turnId = turnStart.turn?.id;
|
|
428
|
+
if (!turnId) {
|
|
429
|
+
throw new Error("Codex app-server did not return a turn id.");
|
|
430
|
+
}
|
|
431
|
+
const session = saveCodexLiveSession(toSessionState({
|
|
432
|
+
session_id: randomUUID(),
|
|
433
|
+
room_id: input.room_id,
|
|
434
|
+
room_identifier: input.room_identifier,
|
|
435
|
+
room_code: input.room_code ?? null,
|
|
436
|
+
room_display_name: input.room_display_name ?? null,
|
|
437
|
+
joined_via: input.joined_via,
|
|
438
|
+
cwd,
|
|
439
|
+
stop_phrase: stopPhrase,
|
|
440
|
+
max_minutes: maxMinutes,
|
|
441
|
+
deadline_utc: deadline.utc,
|
|
442
|
+
token,
|
|
443
|
+
thread_id: threadId,
|
|
444
|
+
turn_id: turnId,
|
|
445
|
+
server_url: serverUrl,
|
|
446
|
+
server_pid: serverPid,
|
|
447
|
+
launched_server: launchedServer,
|
|
448
|
+
codex_bin: codexBin,
|
|
449
|
+
}));
|
|
450
|
+
return { session, reused: false };
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
client.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
export async function stopLocalCodexSession(options) {
|
|
457
|
+
const session = options?.session_id
|
|
458
|
+
? getStoredCodexLiveSession(options.session_id)
|
|
459
|
+
: getCurrentCodexLiveSession(options?.room_id ?? undefined);
|
|
460
|
+
if (!session) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
// Attempt to interrupt the turn via RPC, but gracefully handle a dead server.
|
|
464
|
+
const serverReachable = await isServerReady(session.server_url);
|
|
465
|
+
if (serverReachable) {
|
|
466
|
+
try {
|
|
467
|
+
const client = new RpcClient(session.server_url);
|
|
468
|
+
await client.connect();
|
|
469
|
+
try {
|
|
470
|
+
await client.request("turn/interrupt", {
|
|
471
|
+
threadId: session.thread_id,
|
|
472
|
+
turnId: session.turn_id,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
client.close();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Server may have died between the readiness check and the RPC call.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
484
|
+
...current,
|
|
485
|
+
status: "interrupted",
|
|
486
|
+
last_error: serverReachable ? null : "server unreachable at stop time",
|
|
487
|
+
updated_at: new Date().toISOString(),
|
|
488
|
+
})) ?? session;
|
|
489
|
+
if (options?.shutdown_server && updated.server_pid) {
|
|
490
|
+
try {
|
|
491
|
+
process.kill(updated.server_pid, "SIGTERM");
|
|
492
|
+
spawnedServerPids.delete(updated.server_pid);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Process already dead — ignore.
|
|
496
|
+
spawnedServerPids.delete(updated.server_pid);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return updated;
|
|
500
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Parses the config and returns the room name if found.
|
|
8
8
|
//
|
|
9
9
|
// Config format:
|
|
10
|
-
// { "room": "github.com/
|
|
10
|
+
// { "room": "github.com/BrosInCode/letagents" }
|
|
11
11
|
import { readFileSync, existsSync } from "fs";
|
|
12
12
|
import { join, dirname, resolve } from "path";
|
|
13
13
|
const CONFIG_FILENAME = ".letagents.json";
|
package/dist/mcp/git-remote.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// host/owner/repo
|
|
8
8
|
//
|
|
9
9
|
// Examples:
|
|
10
|
-
// git@github.com:
|
|
11
|
-
// https://github.com/
|
|
12
|
-
// https://github.com/
|
|
10
|
+
// git@github.com:BrosInCode/letagents.git → github.com/BrosInCode/letagents
|
|
11
|
+
// https://github.com/BrosInCode/letagents.git → github.com/BrosInCode/letagents
|
|
12
|
+
// https://github.com/BrosInCode/letagents → github.com/BrosInCode/letagents
|
|
13
13
|
// ssh://git@gitlab.com/team/project.git → gitlab.com/team/project
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
15
|
/**
|