letagents 0.12.9 → 0.12.10

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.
Files changed (117) hide show
  1. package/README.md +16 -15
  2. package/dist/mcp/agent-prompt-delivery.js +20 -0
  3. package/dist/mcp/codex-session/app-server.js +89 -0
  4. package/dist/mcp/codex-session/index.js +10 -0
  5. package/dist/mcp/codex-session/rpc-client.js +85 -0
  6. package/dist/mcp/codex-session/runtime-bridge.js +254 -0
  7. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  8. package/dist/mcp/codex-session/session-inspection.js +100 -0
  9. package/dist/mcp/codex-session/session-mapper.js +51 -0
  10. package/dist/mcp/codex-session/session-start.js +182 -0
  11. package/dist/mcp/codex-session/session-status.js +77 -0
  12. package/dist/mcp/codex-session/session-stop.js +43 -0
  13. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  14. package/dist/mcp/codex-session/start-prompt.js +49 -0
  15. package/dist/mcp/codex-session/types.js +1 -0
  16. package/dist/mcp/codex-session.js +1 -672
  17. package/dist/mcp/config-reader.js +10 -2
  18. package/dist/mcp/git-remote.js +90 -14
  19. package/dist/mcp/local-state/agent-identity.js +26 -0
  20. package/dist/mcp/local-state/agent-sessions.js +66 -0
  21. package/dist/mcp/local-state/auth.js +57 -0
  22. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  23. package/dist/mcp/local-state/index.js +8 -0
  24. package/dist/mcp/local-state/local-chat.js +808 -0
  25. package/dist/mcp/local-state/room-sessions.js +51 -0
  26. package/dist/mcp/local-state/storage.js +95 -0
  27. package/dist/mcp/local-state/types.js +1 -0
  28. package/dist/mcp/local-state.js +1 -351
  29. package/dist/mcp/managed-agent-providers.js +65 -0
  30. package/dist/mcp/rental-tools/activity.js +37 -0
  31. package/dist/mcp/rental-tools/budget.js +36 -0
  32. package/dist/mcp/rental-tools/context.js +62 -0
  33. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  34. package/dist/mcp/rental-tools/metering.js +73 -0
  35. package/dist/mcp/rental-tools/patches.js +100 -0
  36. package/dist/mcp/rental-tools/provider.js +117 -0
  37. package/dist/mcp/rental-tools/shared.js +19 -0
  38. package/dist/mcp/rental-tools/types.js +1 -0
  39. package/dist/mcp/rental-tools.js +7 -0
  40. package/dist/mcp/repo-visibility.js +8 -6
  41. package/dist/mcp/server/register-tools.js +19 -0
  42. package/dist/mcp/server/repo-context.js +28 -0
  43. package/dist/mcp/server/resources.js +77 -0
  44. package/dist/mcp/server/runtime/agent-sessions.js +134 -0
  45. package/dist/mcp/server/runtime/api.js +82 -0
  46. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  47. package/dist/mcp/server/runtime/device-auth.js +48 -0
  48. package/dist/mcp/server/runtime/identity/config.js +58 -0
  49. package/dist/mcp/server/runtime/identity/directory.js +87 -0
  50. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  51. package/dist/mcp/server/runtime/identity/names.js +74 -0
  52. package/dist/mcp/server/runtime/identity/public.js +19 -0
  53. package/dist/mcp/server/runtime/identity/state.js +32 -0
  54. package/dist/mcp/server/runtime/identity.js +96 -0
  55. package/dist/mcp/server/runtime/messages.js +160 -0
  56. package/dist/mcp/server/runtime/presence.js +52 -0
  57. package/dist/mcp/server/runtime/room-api.js +42 -0
  58. package/dist/mcp/server/runtime/room-state.js +122 -0
  59. package/dist/mcp/server/runtime/rooms.js +318 -0
  60. package/dist/mcp/server/runtime.js +14 -0
  61. package/dist/mcp/server/tools/agent-sessions.js +374 -0
  62. package/dist/mcp/server/tools/messages/index.js +14 -0
  63. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  64. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  65. package/dist/mcp/server/tools/messages/response.js +17 -0
  66. package/dist/mcp/server/tools/messages/send-tool.js +203 -0
  67. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  68. package/dist/mcp/server/tools/messages/wait-tool.js +333 -0
  69. package/dist/mcp/server/tools/messages.js +1 -0
  70. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +148 -0
  71. package/dist/mcp/server/tools/onboarding/name-tool.js +89 -0
  72. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  73. package/dist/mcp/server/tools/onboarding/status-tool.js +108 -0
  74. package/dist/mcp/server/tools/onboarding.js +8 -0
  75. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  76. package/dist/mcp/server/tools/rental/context-tools.js +35 -0
  77. package/dist/mcp/server/tools/rental/index.js +17 -0
  78. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  79. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  80. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  81. package/dist/mcp/server/tools/rental/response.js +10 -0
  82. package/dist/mcp/server/tools/rental/types.js +1 -0
  83. package/dist/mcp/server/tools/rental.js +1 -0
  84. package/dist/mcp/server/tools/rooms/index.js +5 -0
  85. package/dist/mcp/server/tools/rooms/inspection-tools.js +124 -0
  86. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  87. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  88. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  89. package/dist/mcp/server/tools/rooms/response.js +10 -0
  90. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  91. package/dist/mcp/server/tools/rooms.js +1 -0
  92. package/dist/mcp/server/tools/tasks/api.js +222 -0
  93. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  94. package/dist/mcp/server/tools/tasks/board-intent-tools.js +321 -0
  95. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  96. package/dist/mcp/server/tools/tasks/context.js +35 -0
  97. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  98. package/dist/mcp/server/tools/tasks/index.js +14 -0
  99. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  100. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  101. package/dist/mcp/server/tools/tasks/response.js +13 -0
  102. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  103. package/dist/mcp/server/tools/tasks.js +1 -0
  104. package/dist/mcp/server.js +10 -3157
  105. package/dist/mcp/sse-client.js +6 -14
  106. package/dist/shared/activation-routing.js +263 -0
  107. package/dist/shared/agent-identity.js +21 -5
  108. package/dist/shared/agent-presence.js +4 -3
  109. package/dist/shared/rental/lrt.js +214 -0
  110. package/dist/shared/rental/meter-types.js +14 -0
  111. package/dist/shared/room-agent-prompts.js +14 -1
  112. package/package.json +15 -5
  113. package/dist/mcp/__tests__/config-reader.test.js +0 -93
  114. package/dist/mcp/__tests__/git-remote.test.js +0 -37
  115. package/dist/mcp/__tests__/server-helpers.test.js +0 -121
  116. package/dist/shared/agent-reasoning.js +0 -49
  117. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -1,672 +1 @@
1
- import { randomUUID } from "crypto";
2
- import { spawn } from "child_process";
3
- import { createServer } from "net";
4
- import { resolve } from "path";
5
- import { getCurrentCodexLiveSession, getStoredCodexLiveSession, saveCodexLiveSession, updateCodexLiveSession, } from "./local-state.js";
6
- const DEFAULT_SERVER_HOST = "127.0.0.1";
7
- const DEFAULT_STOP_PHRASE = "/stop-codex-room";
8
- const DEFAULT_TIMEOUT_MS = 15_000;
9
- const DEFAULT_STARTUP_OBSERVATION_MS = 8_000;
10
- const STARTUP_POLL_INTERVAL_MS = 500;
11
- const SESSION_MONITOR_INTERVAL_MS = 30_000;
12
- function getWebSocketCtor() {
13
- const ctor = globalThis.WebSocket;
14
- if (!ctor) {
15
- throw new Error("Codex live sessions require a Node runtime with global WebSocket support (Node >= 22).");
16
- }
17
- return ctor;
18
- }
19
- /** Track spawned server PIDs for cleanup on process exit. */
20
- const spawnedServerPids = new Set();
21
- const sessionMonitorTimers = new Map();
22
- function terminateSpawnedProcess(pid) {
23
- try {
24
- if (process.platform !== "win32") {
25
- process.kill(-pid, "SIGTERM");
26
- return;
27
- }
28
- }
29
- catch {
30
- // Fall back to the direct process below.
31
- }
32
- try {
33
- process.kill(pid, "SIGTERM");
34
- }
35
- catch {
36
- // Already gone.
37
- }
38
- }
39
- let cleanupRegistered = false;
40
- function registerProcessCleanup() {
41
- if (cleanupRegistered)
42
- return;
43
- cleanupRegistered = true;
44
- const cleanup = () => {
45
- for (const timer of sessionMonitorTimers.values()) {
46
- clearInterval(timer);
47
- }
48
- sessionMonitorTimers.clear();
49
- for (const pid of spawnedServerPids) {
50
- terminateSpawnedProcess(pid);
51
- }
52
- spawnedServerPids.clear();
53
- };
54
- process.on("exit", cleanup);
55
- process.on("SIGINT", () => { cleanup(); process.exit(130); });
56
- process.on("SIGTERM", () => { cleanup(); process.exit(143); });
57
- }
58
- class RpcClient {
59
- serverUrl;
60
- ws = null;
61
- nextId = 1;
62
- pending = new Map();
63
- constructor(serverUrl) {
64
- this.serverUrl = serverUrl;
65
- }
66
- async connect() {
67
- const WS = getWebSocketCtor();
68
- await new Promise((resolve, reject) => {
69
- const ws = new WS(this.serverUrl);
70
- this.ws = ws;
71
- ws.onopen = () => resolve();
72
- ws.onerror = () => reject(new Error(`WebSocket error connecting to ${this.serverUrl}`));
73
- ws.onmessage = (event) => this.handleMessage(String(event.data));
74
- ws.onclose = () => {
75
- for (const pending of this.pending.values()) {
76
- pending.reject(new Error("WebSocket closed"));
77
- }
78
- this.pending.clear();
79
- };
80
- });
81
- await this.request("initialize", {
82
- clientInfo: { name: "letagents-local-codex-session", version: "0.1.0" },
83
- capabilities: { experimentalApi: true },
84
- });
85
- this.ws?.send(JSON.stringify({ method: "initialized" }));
86
- }
87
- async request(method, params) {
88
- const id = this.nextId++;
89
- const payload = { jsonrpc: "2.0", id, method };
90
- if (params !== undefined) {
91
- payload.params = params;
92
- }
93
- return new Promise((resolve, reject) => {
94
- this.pending.set(id, {
95
- resolve: (value) => resolve(value),
96
- reject,
97
- });
98
- this.ws?.send(JSON.stringify(payload));
99
- });
100
- }
101
- close() {
102
- if (this.ws?.readyState === getWebSocketCtor().OPEN) {
103
- this.ws.close();
104
- }
105
- }
106
- handleMessage(raw) {
107
- let message;
108
- try {
109
- message = JSON.parse(raw);
110
- }
111
- catch {
112
- return;
113
- }
114
- if (message.id === undefined) {
115
- return;
116
- }
117
- const pending = this.pending.get(message.id);
118
- if (!pending) {
119
- return;
120
- }
121
- this.pending.delete(message.id);
122
- if (message.error) {
123
- pending.reject(new Error(typeof message.error === "object" && message.error && "message" in message.error
124
- ? String(message.error.message || JSON.stringify(message.error))
125
- : JSON.stringify(message.error)));
126
- return;
127
- }
128
- pending.resolve(message.result);
129
- }
130
- }
131
- function readyUrlFromServerUrl(serverUrl) {
132
- const url = new URL(serverUrl);
133
- url.protocol = url.protocol === "wss:" ? "https:" : "http:";
134
- url.pathname = "/readyz";
135
- url.search = "";
136
- url.hash = "";
137
- return url.toString();
138
- }
139
- async function isServerReady(serverUrl) {
140
- try {
141
- const response = await fetch(readyUrlFromServerUrl(serverUrl), {
142
- signal: AbortSignal.timeout(1_000),
143
- });
144
- return response.ok;
145
- }
146
- catch {
147
- return false;
148
- }
149
- }
150
- async function waitForServer(serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
151
- const startedAt = Date.now();
152
- while (Date.now() - startedAt < timeoutMs) {
153
- if (await isServerReady(serverUrl)) {
154
- return true;
155
- }
156
- await new Promise((resolve) => setTimeout(resolve, 250));
157
- }
158
- return false;
159
- }
160
- async function allocateLoopbackServerUrl() {
161
- const server = createServer();
162
- await new Promise((resolve, reject) => {
163
- server.once("error", reject);
164
- server.listen(0, DEFAULT_SERVER_HOST, () => resolve());
165
- });
166
- const address = server.address();
167
- await new Promise((resolve, reject) => {
168
- server.close((error) => {
169
- if (error) {
170
- reject(error);
171
- return;
172
- }
173
- resolve();
174
- });
175
- });
176
- if (!address || typeof address === "string") {
177
- throw new Error("Unable to allocate a loopback Codex app-server port.");
178
- }
179
- return `ws://${DEFAULT_SERVER_HOST}:${address.port}`;
180
- }
181
- async function resolveCodexServerUrl(explicitServerUrl) {
182
- if (explicitServerUrl) {
183
- return explicitServerUrl;
184
- }
185
- const configuredServerUrl = process.env.LETAGENTS_CODEX_SERVER_URL?.trim();
186
- if (configuredServerUrl) {
187
- return configuredServerUrl;
188
- }
189
- return allocateLoopbackServerUrl();
190
- }
191
- function launchAppServer(serverUrl, codexBin) {
192
- const child = spawn(codexBin, ["app-server", "--listen", serverUrl], {
193
- detached: true,
194
- stdio: "ignore",
195
- });
196
- child.unref();
197
- return child.pid ?? null;
198
- }
199
- function makeToken() {
200
- return `LOCAL_CODEX_ROOM_${randomUUID()}`;
201
- }
202
- function formatDeadline(minutes) {
203
- if (minutes <= 0) {
204
- return { utc: null };
205
- }
206
- const deadline = new Date(Date.now() + minutes * 60 * 1000);
207
- return {
208
- utc: deadline.toISOString().replace("T", " ").replace(".000Z", " UTC"),
209
- };
210
- }
211
- function buildJoinInstruction(joinedVia, roomIdentifier) {
212
- if (joinedVia === "join_code") {
213
- return `Call the LetAgents MCP tool join_code with {"code":"${roomIdentifier}","session_mode":"current"}.`;
214
- }
215
- return `Call the LetAgents MCP tool join_room with {"name":"${roomIdentifier}","session_mode":"current"}.`;
216
- }
217
- function buildStartPrompt(input) {
218
- const deadlineInstruction = input.max_minutes > 0 && input.deadline_utc
219
- ? `Hard stop deadline: ${input.deadline_utc}. Stop when the stop phrase appears or when that deadline is reached, whichever comes first.`
220
- : "There is no hard deadline. Stop only when the stop phrase appears or when you are interrupted.";
221
- return [
222
- "Run as a persistent local Codex worker for a LetAgents room.",
223
- `Primary working directory: ${input.cwd}. Use this repository/worktree when the room asks for implementation or repo work.`,
224
- deadlineInstruction,
225
- "",
226
- "Instructions:",
227
- `1. ${buildJoinInstruction(input.joined_via, input.room_identifier)}`,
228
- "2. Call register_agent_session with session_kind=\"worker\" and runtime=\"codex\". Keep the returned agent_session_id.",
229
- "3. Pass that agent_session_id to wait_for_messages, send_message, post_status, and task tools whenever the tool accepts it.",
230
- "4. Do not start another live session. Join the room inline in this worker only.",
231
- "5. Read the room and task board before contributing so you have current context.",
232
- "6. Keep polling with wait_for_messages using a 30000 ms timeout and track the latest seen message id.",
233
- "7. When new messages arrive, contribute when useful. Be concise, thoughtful, and non-repetitive.",
234
- "8. 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.",
235
- "9. Post short status updates to the room when you start meaningful work, when you are blocked, and when you finish meaningful work.",
236
- `10. Stop immediately if a browser/user room message text exactly equals: ${input.stop_phrase}`,
237
- `11. When stopping, reply in this thread with exactly: ${input.token}_DONE`,
238
- "",
239
- "Constraints:",
240
- "- Do not narrate hidden chain-of-thought.",
241
- "- Do not spam the room with keepalive messages.",
242
- "- Stay in the room continuously until stopped.",
243
- ].join("\n");
244
- }
245
- function extractTurnStatus(turn) {
246
- if (!turn) {
247
- return null;
248
- }
249
- if (typeof turn.status === "string") {
250
- return turn.status;
251
- }
252
- if (turn.status && typeof turn.status === "object" && "status" in turn.status) {
253
- return typeof turn.status.status === "string" ? turn.status.status : null;
254
- }
255
- return null;
256
- }
257
- function extractThreadStatus(thread) {
258
- if (!thread?.status) {
259
- return null;
260
- }
261
- if (typeof thread.status === "string") {
262
- return thread.status;
263
- }
264
- return typeof thread.status.type === "string" ? thread.status.type : null;
265
- }
266
- function summarizeItems(items) {
267
- return (items ?? []).slice(-6).map((item) => {
268
- if (item.type === "agentMessage") {
269
- return { type: item.type, phase: item.phase, text: item.text ?? null };
270
- }
271
- if (item.type === "userMessage") {
272
- return {
273
- type: item.type,
274
- text: (item.content ?? []).map((part) => part.text ?? "").join("\n"),
275
- };
276
- }
277
- return { type: item.type ?? "unknown" };
278
- });
279
- }
280
- export function deriveCodexLiveSessionStatus(session, serverReachable, threadStatus, turnStatus) {
281
- if (threadStatus === "systemError" || threadStatus === "error" || turnStatus === "failed") {
282
- return "failed";
283
- }
284
- if (turnStatus === "completed") {
285
- return "completed";
286
- }
287
- if (turnStatus === "interrupted") {
288
- return "interrupted";
289
- }
290
- if (turnStatus === "inProgress" || threadStatus === "active") {
291
- return "running";
292
- }
293
- if (!serverReachable) {
294
- return session.status === "completed" || session.status === "interrupted"
295
- ? session.status
296
- : "unknown";
297
- }
298
- if (session.status === "starting") {
299
- return "starting";
300
- }
301
- return session.status;
302
- }
303
- function isTerminalCodexSessionStatus(status) {
304
- return status === "completed" || status === "interrupted" || status === "failed";
305
- }
306
- function parseStartupObservationMs() {
307
- const parsed = Number.parseInt(process.env.LETAGENTS_CODEX_STARTUP_OBSERVATION_MS ?? "", 10);
308
- if (!Number.isFinite(parsed) || parsed < 0) {
309
- return DEFAULT_STARTUP_OBSERVATION_MS;
310
- }
311
- return parsed;
312
- }
313
- function sleep(ms) {
314
- return new Promise((resolve) => setTimeout(resolve, ms));
315
- }
316
- function clearSessionMonitor(sessionId) {
317
- const timer = sessionMonitorTimers.get(sessionId);
318
- if (!timer) {
319
- return;
320
- }
321
- clearInterval(timer);
322
- sessionMonitorTimers.delete(sessionId);
323
- }
324
- function killOwnedAppServer(session) {
325
- if (!session.launched_server || !session.server_pid) {
326
- return;
327
- }
328
- terminateSpawnedProcess(session.server_pid);
329
- spawnedServerPids.delete(session.server_pid);
330
- }
331
- function scheduleOwnedSessionMonitor(session) {
332
- if (!session.launched_server || sessionMonitorTimers.has(session.session_id)) {
333
- return;
334
- }
335
- const timer = setInterval(() => {
336
- void inspectLocalCodexSession(session.session_id)
337
- .then((status) => {
338
- if (!status ||
339
- !status.server_reachable ||
340
- isTerminalCodexSessionStatus(status.session.status)) {
341
- clearSessionMonitor(session.session_id);
342
- }
343
- })
344
- .catch(() => {
345
- const latest = getStoredCodexLiveSession(session.session_id);
346
- if (latest?.launched_server) {
347
- killOwnedAppServer(latest);
348
- }
349
- clearSessionMonitor(session.session_id);
350
- });
351
- }, SESSION_MONITOR_INTERVAL_MS);
352
- timer.unref?.();
353
- sessionMonitorTimers.set(session.session_id, timer);
354
- }
355
- async function waitForWorkerStartup(session) {
356
- const observationMs = parseStartupObservationMs();
357
- const deadline = Date.now() + observationMs;
358
- let latest = session;
359
- while (Date.now() < deadline) {
360
- await sleep(Math.min(STARTUP_POLL_INTERVAL_MS, Math.max(deadline - Date.now(), 0)));
361
- const inspected = await inspectLocalCodexSession(session.session_id);
362
- if (!inspected) {
363
- continue;
364
- }
365
- latest = inspected.session;
366
- if (!inspected.server_reachable || latest.status === "unknown") {
367
- const reason = !inspected.server_reachable
368
- ? "app-server became unreachable during startup"
369
- : "worker status became unknown during startup";
370
- const failed = updateCodexLiveSession(session.session_id, (current) => ({
371
- ...current,
372
- status: "failed",
373
- last_error: reason,
374
- updated_at: new Date().toISOString(),
375
- })) ?? latest;
376
- throw new Error(`Codex worker exited during startup: ${failed.last_error ?? reason}`);
377
- }
378
- if (isTerminalCodexSessionStatus(latest.status)) {
379
- const reason = latest.status === "completed"
380
- ? "turn completed before entering the room polling loop"
381
- : `turn entered ${latest.status}`;
382
- const failed = updateCodexLiveSession(session.session_id, (current) => ({
383
- ...current,
384
- status: "failed",
385
- last_error: reason,
386
- updated_at: new Date().toISOString(),
387
- })) ?? latest;
388
- throw new Error(`Codex worker exited during startup: ${failed.last_error ?? reason}`);
389
- }
390
- }
391
- return latest;
392
- }
393
- function toSessionState(input) {
394
- const now = new Date().toISOString();
395
- return {
396
- session_id: input.session_id,
397
- room_id: input.room_id,
398
- room_identifier: input.room_identifier,
399
- room_code: input.room_code ?? null,
400
- room_display_name: input.room_display_name ?? null,
401
- joined_via: input.joined_via,
402
- cwd: input.cwd,
403
- stop_phrase: input.stop_phrase,
404
- max_minutes: input.max_minutes,
405
- deadline_utc: input.deadline_utc,
406
- token: input.token,
407
- thread_id: input.thread_id,
408
- turn_id: input.turn_id,
409
- server_url: input.server_url,
410
- server_pid: input.server_pid,
411
- launched_server: input.launched_server,
412
- codex_bin: input.codex_bin,
413
- status: "running",
414
- last_error: null,
415
- started_at: now,
416
- updated_at: now,
417
- };
418
- }
419
- function isLikelyMaterializingError(error) {
420
- const message = error instanceof Error ? error.message : String(error);
421
- return message.includes("not materialized yet");
422
- }
423
- export function toPublicCodexLiveSession(session) {
424
- return {
425
- session_id: session.session_id,
426
- room_id: session.room_id,
427
- room_code: session.room_code ?? null,
428
- room_display_name: session.room_display_name ?? null,
429
- joined_via: session.joined_via,
430
- cwd: session.cwd,
431
- stop_phrase: session.stop_phrase,
432
- max_minutes: session.max_minutes,
433
- deadline_utc: session.deadline_utc ?? null,
434
- thread_id: session.thread_id,
435
- turn_id: session.turn_id,
436
- server_url: session.server_url,
437
- server_pid: session.server_pid ?? null,
438
- launched_server: session.launched_server,
439
- status: session.status,
440
- last_error: session.last_error ?? null,
441
- started_at: session.started_at,
442
- updated_at: session.updated_at,
443
- };
444
- }
445
- export async function inspectLocalCodexSession(sessionId, roomId) {
446
- const session = sessionId
447
- ? getStoredCodexLiveSession(sessionId)
448
- : getCurrentCodexLiveSession(roomId ?? undefined);
449
- if (!session) {
450
- return null;
451
- }
452
- const serverReachable = await isServerReady(session.server_url);
453
- if (!serverReachable) {
454
- const updated = updateCodexLiveSession(session.session_id, (current) => ({
455
- ...current,
456
- status: deriveCodexLiveSessionStatus(current, false, null, null),
457
- updated_at: new Date().toISOString(),
458
- })) ?? session;
459
- if (updated.launched_server) {
460
- killOwnedAppServer(updated);
461
- clearSessionMonitor(updated.session_id);
462
- }
463
- return {
464
- session: updated,
465
- server_reachable: false,
466
- thread_status: null,
467
- turn_status: null,
468
- recent_items: [],
469
- };
470
- }
471
- const client = new RpcClient(session.server_url);
472
- try {
473
- await client.connect();
474
- let read = null;
475
- try {
476
- read = await client.request("thread/read", {
477
- threadId: session.thread_id,
478
- includeTurns: true,
479
- });
480
- }
481
- catch (error) {
482
- if (!isLikelyMaterializingError(error)) {
483
- throw error;
484
- }
485
- }
486
- const turns = read?.thread?.turns ?? [];
487
- const turn = turns.find((candidate) => candidate.id === session.turn_id) ?? turns[turns.length - 1];
488
- const threadStatus = extractThreadStatus(read?.thread);
489
- const turnStatus = extractTurnStatus(turn);
490
- const updated = updateCodexLiveSession(session.session_id, (current) => ({
491
- ...current,
492
- status: deriveCodexLiveSessionStatus(current, true, threadStatus, turnStatus),
493
- last_error: null,
494
- updated_at: new Date().toISOString(),
495
- })) ?? session;
496
- if (isTerminalCodexSessionStatus(updated.status)) {
497
- killOwnedAppServer(updated);
498
- clearSessionMonitor(updated.session_id);
499
- }
500
- return {
501
- session: updated,
502
- server_reachable: true,
503
- thread_status: read?.thread?.status ?? null,
504
- turn_status: turn?.status ?? null,
505
- recent_items: summarizeItems(turn?.items ?? turn?.output),
506
- };
507
- }
508
- catch (error) {
509
- const updated = updateCodexLiveSession(session.session_id, (current) => ({
510
- ...current,
511
- status: "unknown",
512
- last_error: error instanceof Error ? error.message : String(error),
513
- updated_at: new Date().toISOString(),
514
- })) ?? session;
515
- if (updated.launched_server) {
516
- killOwnedAppServer(updated);
517
- clearSessionMonitor(updated.session_id);
518
- }
519
- return {
520
- session: updated,
521
- server_reachable: true,
522
- thread_status: null,
523
- turn_status: null,
524
- recent_items: [],
525
- };
526
- }
527
- finally {
528
- client.close();
529
- }
530
- }
531
- export async function startLocalCodexSession(input) {
532
- const cwd = resolve(input.cwd || process.cwd());
533
- const currentSession = getCurrentCodexLiveSession(input.room_id);
534
- if (currentSession &&
535
- currentSession.room_id === input.room_id &&
536
- resolve(currentSession.cwd) === cwd) {
537
- const inspected = await inspectLocalCodexSession(currentSession.session_id);
538
- if (inspected &&
539
- (inspected.session.status === "running" || inspected.session.status === "starting")) {
540
- scheduleOwnedSessionMonitor(inspected.session);
541
- return { session: inspected.session, reused: true };
542
- }
543
- }
544
- const serverUrl = await resolveCodexServerUrl(input.server_url);
545
- const stopPhrase = input.stop_phrase || DEFAULT_STOP_PHRASE;
546
- const maxMinutes = Number.isFinite(input.max_minutes) ? Math.max(0, input.max_minutes ?? 0) : 0;
547
- const codexBin = input.codex_bin || process.env.LETAGENTS_CODEX_BIN || "codex";
548
- const token = makeToken();
549
- const deadline = formatDeadline(maxMinutes);
550
- const launchedServer = !(await isServerReady(serverUrl));
551
- let serverPid = null;
552
- let client = null;
553
- let startupSucceeded = false;
554
- try {
555
- if (launchedServer) {
556
- serverPid = launchAppServer(serverUrl, codexBin);
557
- if (serverPid) {
558
- spawnedServerPids.add(serverPid);
559
- registerProcessCleanup();
560
- }
561
- const ready = await waitForServer(serverUrl);
562
- if (!ready) {
563
- throw new Error(`Timed out waiting for codex app-server at ${serverUrl}`);
564
- }
565
- }
566
- client = new RpcClient(serverUrl);
567
- await client.connect();
568
- const threadStart = await client.request("thread/start", {});
569
- const threadId = threadStart.thread?.id;
570
- if (!threadId) {
571
- throw new Error("Codex app-server did not return a thread id.");
572
- }
573
- const prompt = buildStartPrompt({
574
- room_identifier: input.room_identifier,
575
- joined_via: input.joined_via,
576
- cwd,
577
- stop_phrase: stopPhrase,
578
- token,
579
- deadline_utc: deadline.utc,
580
- max_minutes: maxMinutes,
581
- });
582
- const turnStart = await client.request("turn/start", {
583
- threadId,
584
- cwd,
585
- approvalPolicy: "never",
586
- sandboxPolicy: { type: "dangerFullAccess" },
587
- input: [{ type: "text", text: prompt, text_elements: [] }],
588
- });
589
- const turnId = turnStart.turn?.id;
590
- if (!turnId) {
591
- throw new Error("Codex app-server did not return a turn id.");
592
- }
593
- const session = saveCodexLiveSession(toSessionState({
594
- session_id: randomUUID(),
595
- room_id: input.room_id,
596
- room_identifier: input.room_identifier,
597
- room_code: input.room_code ?? null,
598
- room_display_name: input.room_display_name ?? null,
599
- joined_via: input.joined_via,
600
- cwd,
601
- stop_phrase: stopPhrase,
602
- max_minutes: maxMinutes,
603
- deadline_utc: deadline.utc,
604
- token,
605
- thread_id: threadId,
606
- turn_id: turnId,
607
- server_url: serverUrl,
608
- server_pid: serverPid,
609
- launched_server: launchedServer,
610
- codex_bin: codexBin,
611
- }));
612
- try {
613
- const verifiedSession = await waitForWorkerStartup(session);
614
- scheduleOwnedSessionMonitor(verifiedSession);
615
- startupSucceeded = true;
616
- return { session: verifiedSession, reused: false };
617
- }
618
- catch (error) {
619
- killOwnedAppServer(session);
620
- throw error;
621
- }
622
- }
623
- catch (error) {
624
- if (!startupSucceeded && launchedServer && serverPid) {
625
- terminateSpawnedProcess(serverPid);
626
- spawnedServerPids.delete(serverPid);
627
- }
628
- throw error;
629
- }
630
- finally {
631
- client?.close();
632
- }
633
- }
634
- export async function stopLocalCodexSession(options) {
635
- const session = options?.session_id
636
- ? getStoredCodexLiveSession(options.session_id)
637
- : getCurrentCodexLiveSession(options?.room_id ?? undefined);
638
- if (!session) {
639
- return null;
640
- }
641
- // Attempt to interrupt the turn via RPC, but gracefully handle a dead server.
642
- const serverReachable = await isServerReady(session.server_url);
643
- if (serverReachable) {
644
- try {
645
- const client = new RpcClient(session.server_url);
646
- await client.connect();
647
- try {
648
- await client.request("turn/interrupt", {
649
- threadId: session.thread_id,
650
- turnId: session.turn_id,
651
- });
652
- }
653
- finally {
654
- client.close();
655
- }
656
- }
657
- catch {
658
- // Server may have died between the readiness check and the RPC call.
659
- }
660
- }
661
- const updated = updateCodexLiveSession(session.session_id, (current) => ({
662
- ...current,
663
- status: "interrupted",
664
- last_error: serverReachable ? null : "server unreachable at stop time",
665
- updated_at: new Date().toISOString(),
666
- })) ?? session;
667
- if (options?.shutdown_server || updated.launched_server) {
668
- killOwnedAppServer(updated);
669
- }
670
- clearSessionMonitor(updated.session_id);
671
- return updated;
672
- }
1
+ export * from "./codex-session/index.js";
@@ -7,10 +7,18 @@
7
7
  // Parses the config and returns the room name if found.
8
8
  //
9
9
  // Config format:
10
- // { "room": "github.com/BrosInCode/letagents" }
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";
14
+ function normalizeConfiguredRoom(room) {
15
+ const trimmed = room.trim();
16
+ const githubRepoRoom = /^github\.com\/([^/:\s]+)\/([^/:\s]+)$/i.exec(trimmed);
17
+ if (!githubRepoRoom) {
18
+ return trimmed;
19
+ }
20
+ return `github.com/${githubRepoRoom[1].toLowerCase()}/${githubRepoRoom[2].toLowerCase()}`;
21
+ }
14
22
  /**
15
23
  * Search for `.letagents.json` starting from `startDir` and walking up
16
24
  * to the filesystem root. Returns the parsed config or null if not found.
@@ -27,7 +35,7 @@ export function findLetagentsConfig(startDir) {
27
35
  const parsed = JSON.parse(raw);
28
36
  // Validate required fields
29
37
  if (typeof parsed.room === "string" && parsed.room.trim() !== "") {
30
- return { room: parsed.room.trim() };
38
+ return { room: normalizeConfiguredRoom(parsed.room) };
31
39
  }
32
40
  // Config exists but is malformed — log and continue
33
41
  console.error(`[letagents] Found ${configPath} but missing valid "room" field`);