letagents 0.8.0 → 0.8.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.
@@ -103,6 +103,7 @@ export function saveRoomSession(input) {
103
103
  room_id: input.room_id,
104
104
  project_id: input.project_id ?? existing?.project_id ?? null,
105
105
  code: input.code ?? existing?.code ?? null,
106
+ display_name: input.display_name ?? existing?.display_name ?? null,
106
107
  joined_via: input.joined_via,
107
108
  joined_at: existing?.joined_at ?? now,
108
109
  last_seen_at: now,
@@ -64,6 +64,16 @@ class ApiError extends Error {
64
64
  this.body = body;
65
65
  }
66
66
  }
67
+ class RepoRoomAuthRequiredError extends Error {
68
+ roomId;
69
+ pendingAuth;
70
+ constructor(roomId, pendingAuth) {
71
+ super(`Repo room '${roomId}' requires authentication. Device flow started: open ${pendingAuth.verification_uri} and enter code ${pendingAuth.user_code}, then run poll_device_auth.`);
72
+ this.name = "RepoRoomAuthRequiredError";
73
+ this.roomId = roomId;
74
+ this.pendingAuth = pendingAuth;
75
+ }
76
+ }
67
77
  function getLetagentsToken() {
68
78
  return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
69
79
  }
@@ -79,6 +89,34 @@ function isMissingRouteError(error) {
79
89
  (error.status === 404 || error.status === 405) &&
80
90
  /Cannot (GET|POST|PATCH)|Not Found|Cannot GET \/rooms|Cannot POST \/rooms/i.test(error.body));
81
91
  }
92
+ function parseApiErrorPayload(error) {
93
+ if (!(error instanceof ApiError)) {
94
+ return null;
95
+ }
96
+ try {
97
+ const parsed = JSON.parse(error.body);
98
+ return parsed && typeof parsed === "object" ? parsed : null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ function resolveApiPath(urlOrPath) {
105
+ if (!urlOrPath) {
106
+ return "/auth/device/start";
107
+ }
108
+ try {
109
+ const parsed = new URL(urlOrPath, `${API_URL}/`);
110
+ const apiBase = new URL(`${API_URL}/`);
111
+ if (parsed.origin !== apiBase.origin) {
112
+ return "/auth/device/start";
113
+ }
114
+ return `${parsed.pathname}${parsed.search}`;
115
+ }
116
+ catch {
117
+ return "/auth/device/start";
118
+ }
119
+ }
82
120
  async function apiCall(path, options) {
83
121
  const headers = {
84
122
  "Content-Type": "application/json",
@@ -107,11 +145,48 @@ async function apiCall(path, options) {
107
145
  }
108
146
  return JSON.parse(body);
109
147
  }
148
+ async function startPendingDeviceAuth(roomId, deviceFlowUrl) {
149
+ const existing = getPendingDeviceAuth();
150
+ if (existing?.suggested_room_id === roomId) {
151
+ return existing;
152
+ }
153
+ const response = await apiCall(resolveApiPath(deviceFlowUrl), {
154
+ method: "POST",
155
+ });
156
+ return setPendingDeviceAuth({
157
+ request_id: response.request_id,
158
+ user_code: response.user_code,
159
+ verification_uri: response.verification_uri,
160
+ interval_seconds: response.interval,
161
+ expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
162
+ started_at: new Date().toISOString(),
163
+ suggested_room_id: roomId,
164
+ });
165
+ }
166
+ async function maybeHandleRepoRoomAuthRequired(error, roomId) {
167
+ const payload = parseApiErrorPayload(error);
168
+ if (!(error instanceof ApiError) || error.status !== 401 || payload?.error !== "auth_required") {
169
+ return;
170
+ }
171
+ const pendingAuth = await startPendingDeviceAuth(roomId, typeof payload.device_flow_url === "string" ? payload.device_flow_url : undefined);
172
+ throw new RepoRoomAuthRequiredError(roomId, pendingAuth);
173
+ }
174
+ function toRepoRoomAuthRequiredResult(error) {
175
+ return {
176
+ success: false,
177
+ error: "auth_required",
178
+ room_id: error.roomId,
179
+ next_step: "poll_device_auth",
180
+ pending_device_auth: error.pendingAuth,
181
+ message: error.message,
182
+ };
183
+ }
110
184
  function toRoomState(input) {
111
185
  return {
112
186
  room_id: input.room_id,
113
187
  project_id: input.project_id ?? null,
114
188
  code: input.code ?? null,
189
+ display_name: input.display_name ?? null,
115
190
  joined_via: input.joined_via,
116
191
  };
117
192
  }
@@ -122,6 +197,7 @@ function toPublicRoomState(state) {
122
197
  return {
123
198
  room_id: state.room_id,
124
199
  code: state.code ?? null,
200
+ display_name: state.display_name ?? null,
125
201
  joined_via: state.joined_via,
126
202
  };
127
203
  }
@@ -132,6 +208,7 @@ function toPublicStoredRoomSession(session) {
132
208
  return {
133
209
  room_id: session.room_id,
134
210
  code: session.code ?? null,
211
+ display_name: session.display_name ?? null,
135
212
  joined_via: session.joined_via,
136
213
  joined_at: session.joined_at,
137
214
  last_seen_at: session.last_seen_at,
@@ -151,6 +228,7 @@ function rememberRoom(state, lastMessageId) {
151
228
  room_id: state.room_id,
152
229
  project_id: state.project_id ?? null,
153
230
  code: state.code ?? null,
231
+ display_name: state.display_name ?? null,
154
232
  joined_via: state.joined_via,
155
233
  last_message_id: lastMessageId,
156
234
  });
@@ -192,6 +270,7 @@ async function roomScopedApiCall(input) {
192
270
  return result;
193
271
  }
194
272
  catch (error) {
273
+ await maybeHandleRepoRoomAuthRequired(error, input.room_id);
195
274
  if (!input.project_id || !isMissingRouteError(error)) {
196
275
  throw error;
197
276
  }
@@ -221,6 +300,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
221
300
  : looksLikeInviteCode(joinedRoomId)
222
301
  ? joinedRoomId
223
302
  : null,
303
+ display_name: typeof response.display_name === "string" ? response.display_name : null,
224
304
  joined_via: joinedVia,
225
305
  }));
226
306
  return {
@@ -229,6 +309,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
229
309
  };
230
310
  }
231
311
  catch (error) {
312
+ await maybeHandleRepoRoomAuthRequired(error, roomId);
232
313
  if (!isMissingRouteError(error)) {
233
314
  throw error;
234
315
  }
@@ -242,6 +323,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
242
323
  room_id: legacyRoomId,
243
324
  project_id: typeof project.id === "string" ? project.id : null,
244
325
  code: typeof project.code === "string" ? project.code : legacyRoomId,
326
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
245
327
  joined_via: joinedVia,
246
328
  }));
247
329
  return {
@@ -267,6 +349,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
267
349
  : looksLikeInviteCode(legacyRoomId)
268
350
  ? legacyRoomId
269
351
  : null,
352
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
270
353
  joined_via: joinedVia,
271
354
  }));
272
355
  return {
@@ -289,6 +372,7 @@ async function createInviteRoom() {
289
372
  room_id: roomId,
290
373
  project_id: typeof project.id === "string" ? project.id : null,
291
374
  code: typeof project.code === "string" ? project.code : roomId,
375
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
292
376
  joined_via: "join_code",
293
377
  }));
294
378
  await autoRegisterAgentIdentity();
@@ -418,42 +502,49 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
418
502
  server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
419
503
  name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
420
504
  }, async ({ name }) => {
421
- return {
422
- content: [
423
- {
424
- type: "text",
425
- text: JSON.stringify(await joinNamedRoom(name), null, 2),
426
- },
427
- ],
428
- };
429
- });
430
- // -- get_current_room -------------------------------------------------------
431
- server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
432
- if (!currentRoom) {
505
+ try {
433
506
  return {
434
507
  content: [
435
508
  {
436
509
  type: "text",
437
- text: JSON.stringify({ connected: false, message: "Not currently in any room" }, null, 2),
510
+ text: JSON.stringify(await joinNamedRoom(name), null, 2),
438
511
  },
439
512
  ],
440
513
  };
441
514
  }
515
+ catch (error) {
516
+ if (error instanceof RepoRoomAuthRequiredError) {
517
+ return {
518
+ content: [
519
+ {
520
+ type: "text",
521
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
522
+ },
523
+ ],
524
+ };
525
+ }
526
+ throw error;
527
+ }
528
+ });
529
+ // -- get_current_room -------------------------------------------------------
530
+ server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
442
531
  return {
443
532
  content: [
444
533
  {
445
534
  type: "text",
446
- text: JSON.stringify({
447
- connected: true,
448
- ...toPublicRoomState(currentRoom),
449
- auth: getStoredAuth()
450
- ? {
451
- source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
452
- expires_at: getStoredAuth()?.expires_at ?? null,
453
- account: getStoredAuth()?.account ?? null,
454
- }
455
- : null,
456
- }, null, 2),
535
+ text: JSON.stringify(currentRoom
536
+ ? {
537
+ connected: true,
538
+ ...toPublicRoomState(currentRoom),
539
+ auth: getStoredAuth()
540
+ ? {
541
+ source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
542
+ expires_at: getStoredAuth()?.expires_at ?? null,
543
+ account: getStoredAuth()?.account ?? null,
544
+ }
545
+ : null,
546
+ }
547
+ : { connected: false, message: "Not currently in any room" }, null, 2),
457
548
  },
458
549
  ],
459
550
  };
@@ -1211,22 +1302,37 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1211
1302
  ],
1212
1303
  };
1213
1304
  }
1214
- const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1215
- await autoRegisterAgentIdentity();
1216
- return {
1217
- content: [
1218
- {
1219
- type: "text",
1220
- text: JSON.stringify({
1221
- success: true,
1222
- rejoined_from_local_state: true,
1223
- server_session_resumed: false,
1224
- last_message_id_before_restart: savedRoom.last_message_id ?? null,
1225
- room: joined.room,
1226
- }, null, 2),
1227
- },
1228
- ],
1229
- };
1305
+ try {
1306
+ const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1307
+ await autoRegisterAgentIdentity();
1308
+ return {
1309
+ content: [
1310
+ {
1311
+ type: "text",
1312
+ text: JSON.stringify({
1313
+ success: true,
1314
+ rejoined_from_local_state: true,
1315
+ server_session_resumed: false,
1316
+ last_message_id_before_restart: savedRoom.last_message_id ?? null,
1317
+ room: joined.room,
1318
+ }, null, 2),
1319
+ },
1320
+ ],
1321
+ };
1322
+ }
1323
+ catch (error) {
1324
+ if (error instanceof RepoRoomAuthRequiredError) {
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
1330
+ },
1331
+ ],
1332
+ };
1333
+ }
1334
+ throw error;
1335
+ }
1230
1336
  });
1231
1337
  // -- check_repo_visibility --------------------------------------------------
1232
1338
  server.tool("check_repo_visibility", "Auto-detect the current repo's git remote and check if it's public or private. Returns the canonical key, provider, visibility, and suggested room type (discoverable for public, invite for private/unknown). Useful for deciding whether to auto-join a discoverable room or create an invite room.", {
@@ -1293,6 +1399,10 @@ async function main() {
1293
1399
  console.error("ℹ️ No .letagents.json, git remote, or saved room found — use create_room, join_code, or join_room to connect.");
1294
1400
  }
1295
1401
  catch (err) {
1402
+ if (err instanceof RepoRoomAuthRequiredError) {
1403
+ console.error(`🔐 Repo room auth required for '${err.roomId}'. Open ${err.pendingAuth.verification_uri} and enter code ${err.pendingAuth.user_code}, then run poll_device_auth.`);
1404
+ return;
1405
+ }
1296
1406
  // Auto-join failure should never block the MCP server
1297
1407
  console.error("⚠️ Auto-join failed (server still running):", err instanceof Error ? err.message : err);
1298
1408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",