letagents 0.7.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,20 +145,90 @@ 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
  }
193
+ function toPublicRoomState(state) {
194
+ if (!state) {
195
+ return null;
196
+ }
197
+ return {
198
+ room_id: state.room_id,
199
+ code: state.code ?? null,
200
+ display_name: state.display_name ?? null,
201
+ joined_via: state.joined_via,
202
+ };
203
+ }
204
+ function toPublicStoredRoomSession(session) {
205
+ if (!session) {
206
+ return null;
207
+ }
208
+ return {
209
+ room_id: session.room_id,
210
+ code: session.code ?? null,
211
+ display_name: session.display_name ?? null,
212
+ joined_via: session.joined_via,
213
+ joined_at: session.joined_at,
214
+ last_seen_at: session.last_seen_at,
215
+ last_message_id: session.last_message_id ?? null,
216
+ };
217
+ }
218
+ function toPublicRoomResponse(response, fallbackRoomId) {
219
+ const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
220
+ return {
221
+ ...rest,
222
+ room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
223
+ };
224
+ }
118
225
  function rememberRoom(state, lastMessageId) {
119
226
  currentRoom = state;
120
227
  saveRoomSession({
121
228
  room_id: state.room_id,
122
229
  project_id: state.project_id ?? null,
123
230
  code: state.code ?? null,
231
+ display_name: state.display_name ?? null,
124
232
  joined_via: state.joined_via,
125
233
  last_message_id: lastMessageId,
126
234
  });
@@ -143,8 +251,8 @@ function touchCurrentRoom(lastMessageId) {
143
251
  function getTargetRoomId(roomId) {
144
252
  return roomId || currentRoom?.room_id || null;
145
253
  }
146
- function getTargetProjectId(projectId) {
147
- return projectId || currentRoom?.project_id || null;
254
+ function getFallbackProjectId() {
255
+ return currentRoom?.project_id ?? null;
148
256
  }
149
257
  function getLastMessageId(payload) {
150
258
  if (!payload || typeof payload !== "object") {
@@ -162,13 +270,14 @@ async function roomScopedApiCall(input) {
162
270
  return result;
163
271
  }
164
272
  catch (error) {
273
+ await maybeHandleRepoRoomAuthRequired(error, input.room_id);
165
274
  if (!input.project_id || !isMissingRouteError(error)) {
166
275
  throw error;
167
276
  }
168
277
  }
169
278
  }
170
279
  if (!input.project_id) {
171
- throw new Error("No room_id or project_id is available for this request.");
280
+ throw new Error("No room is available for this request.");
172
281
  }
173
282
  const result = await apiCall(input.project_path(input.project_id), input.options);
174
283
  if (input.room_id) {
@@ -191,6 +300,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
191
300
  : looksLikeInviteCode(joinedRoomId)
192
301
  ? joinedRoomId
193
302
  : null,
303
+ display_name: typeof response.display_name === "string" ? response.display_name : null,
194
304
  joined_via: joinedVia,
195
305
  }));
196
306
  return {
@@ -199,6 +309,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
199
309
  };
200
310
  }
201
311
  catch (error) {
312
+ await maybeHandleRepoRoomAuthRequired(error, roomId);
202
313
  if (!isMissingRouteError(error)) {
203
314
  throw error;
204
315
  }
@@ -212,6 +323,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
212
323
  room_id: legacyRoomId,
213
324
  project_id: typeof project.id === "string" ? project.id : null,
214
325
  code: typeof project.code === "string" ? project.code : legacyRoomId,
326
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
215
327
  joined_via: joinedVia,
216
328
  }));
217
329
  return {
@@ -237,6 +349,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
237
349
  : looksLikeInviteCode(legacyRoomId)
238
350
  ? legacyRoomId
239
351
  : null,
352
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
240
353
  joined_via: joinedVia,
241
354
  }));
242
355
  return {
@@ -248,6 +361,42 @@ async function joinRoomIdentifier(identifier, joinedVia) {
248
361
  },
249
362
  };
250
363
  }
364
+ async function createInviteRoom() {
365
+ const project = await apiCall("/projects", { method: "POST" });
366
+ const roomId = typeof project.code === "string"
367
+ ? project.code
368
+ : typeof project.id === "string"
369
+ ? project.id
370
+ : "unknown-room";
371
+ const room = rememberRoom(toRoomState({
372
+ room_id: roomId,
373
+ project_id: typeof project.id === "string" ? project.id : null,
374
+ code: typeof project.code === "string" ? project.code : roomId,
375
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
376
+ joined_via: "join_code",
377
+ }));
378
+ await autoRegisterAgentIdentity();
379
+ return {
380
+ room,
381
+ response: toPublicRoomResponse(project, roomId),
382
+ };
383
+ }
384
+ async function joinInviteCode(code) {
385
+ const joined = await joinRoomIdentifier(code, "join_code");
386
+ await autoRegisterAgentIdentity();
387
+ return {
388
+ ...toPublicRoomResponse(joined.response, joined.room.room_id),
389
+ joined_via: "join_code",
390
+ };
391
+ }
392
+ async function joinNamedRoom(name) {
393
+ const joined = await joinRoomIdentifier(name, "join_room");
394
+ await autoRegisterAgentIdentity();
395
+ return {
396
+ ...toPublicRoomResponse(joined.response, joined.room.room_id),
397
+ joined_via: "join_room",
398
+ };
399
+ }
251
400
  async function autoRegisterAgentIdentity() {
252
401
  if (!AGENT_NAME || !currentRoom) {
253
402
  return;
@@ -299,101 +448,103 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
299
448
  ],
300
449
  };
301
450
  });
302
- server.resource("project_messages", new ResourceTemplate("letagents://projects/{project_id}/messages", {
303
- list: undefined,
304
- }), async (uri, { project_id }) => {
305
- const result = await apiCall(`/projects/${encodeURIComponent(project_id)}/messages`);
451
+ // -- create_room ------------------------------------------------------------
452
+ server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns the room ID and join code.", {}, async () => {
453
+ const created = await createInviteRoom();
306
454
  return {
307
- contents: [
455
+ content: [
308
456
  {
309
- uri: uri.href,
310
- mimeType: "application/json",
311
- text: JSON.stringify(result, null, 2),
457
+ type: "text",
458
+ text: JSON.stringify(created.response, null, 2),
312
459
  },
313
460
  ],
314
461
  };
315
462
  });
316
463
  // -- create_project ---------------------------------------------------------
317
- server.tool("create_project", "Create a new project on Let Agents Chat. Returns a project ID and a join code that other agents can use to join.", {}, async () => {
318
- const project = await apiCall("/projects", { method: "POST" });
319
- const roomId = typeof project.code === "string"
320
- ? project.code
321
- : typeof project.id === "string"
322
- ? project.id
323
- : "unknown-room";
324
- rememberRoom(toRoomState({
325
- room_id: roomId,
326
- project_id: typeof project.id === "string" ? project.id : null,
327
- code: typeof project.code === "string" ? project.code : roomId,
328
- joined_via: "join_code",
329
- }));
330
- await autoRegisterAgentIdentity();
464
+ server.tool("create_project", "Legacy alias for create_room. Creates a new invite room and returns its join code.", {}, async () => {
465
+ const created = await createInviteRoom();
331
466
  return {
332
467
  content: [
333
468
  {
334
469
  type: "text",
335
- text: JSON.stringify({ ...project, room_id: roomId }, null, 2),
470
+ text: JSON.stringify(created.response, null, 2),
336
471
  },
337
472
  ],
338
473
  };
339
474
  });
340
- // -- join_project -----------------------------------------------------------
341
- server.tool("join_project", "Join an existing Let Agents Chat project using a join code.", {
342
- code: z.string().describe("The join code shared by the project creator (e.g. 'ABCX-7291')"),
475
+ // -- join_code --------------------------------------------------------------
476
+ server.tool("join_code", "Join an existing room using an invite code.", {
477
+ code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
343
478
  }, async ({ code }) => {
344
- const joined = await joinRoomIdentifier(code, "join_code");
345
- await autoRegisterAgentIdentity();
346
479
  return {
347
480
  content: [
348
481
  {
349
482
  type: "text",
350
- text: JSON.stringify({ ...joined.response, joined_via: "join_code" }, null, 2),
483
+ text: JSON.stringify(await joinInviteCode(code), null, 2),
351
484
  },
352
485
  ],
353
486
  };
354
487
  });
355
- // -- join_room --------------------------------------------------------------
356
- 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.", {
357
- name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
358
- }, async ({ name }) => {
359
- const joined = await joinRoomIdentifier(name, "join_room");
360
- await autoRegisterAgentIdentity();
488
+ // -- join_project -----------------------------------------------------------
489
+ server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
490
+ code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
491
+ }, async ({ code }) => {
361
492
  return {
362
493
  content: [
363
494
  {
364
495
  type: "text",
365
- text: JSON.stringify({ ...joined.response, joined_via: "join_room" }, null, 2),
496
+ text: JSON.stringify(await joinInviteCode(code), null, 2),
366
497
  },
367
498
  ],
368
499
  };
369
500
  });
370
- // -- get_current_room -------------------------------------------------------
371
- server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
372
- if (!currentRoom) {
501
+ // -- join_room --------------------------------------------------------------
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.", {
503
+ name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
504
+ }, async ({ name }) => {
505
+ try {
373
506
  return {
374
507
  content: [
375
508
  {
376
509
  type: "text",
377
- text: JSON.stringify({ connected: false, message: "Not currently in any room" }, null, 2),
510
+ text: JSON.stringify(await joinNamedRoom(name), null, 2),
378
511
  },
379
512
  ],
380
513
  };
381
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 () => {
382
531
  return {
383
532
  content: [
384
533
  {
385
534
  type: "text",
386
- text: JSON.stringify({
387
- connected: true,
388
- ...currentRoom,
389
- auth: getStoredAuth()
390
- ? {
391
- source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
392
- expires_at: getStoredAuth()?.expires_at ?? null,
393
- account: getStoredAuth()?.account ?? null,
394
- }
395
- : null,
396
- }, 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),
397
548
  },
398
549
  ],
399
550
  };
@@ -432,11 +583,11 @@ server.tool("check_repo", "Inspect the current repository context for Let Agents
432
583
  config_file: configPath ?? null,
433
584
  config_contents: configContents,
434
585
  derived_room_from_git: derivedRoom ?? null,
435
- current_room: currentRoom ?? null,
586
+ current_room: toPublicRoomState(currentRoom),
436
587
  join_hint: !currentRoom
437
588
  ? repoRoot
438
- ? "Run initialize_repo to set up .letagents.json, or join_room/join_project to connect."
439
- : "Not inside a git repo. Use join_project or join_room to connect manually."
589
+ ? "Run initialize_repo to set up .letagents.json, or use join_room/join_code to connect."
590
+ : "Not inside a git repo. Use create_room, join_code, or join_room to connect manually."
440
591
  : null,
441
592
  }, null, 2),
442
593
  },
@@ -454,13 +605,9 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
454
605
  .string()
455
606
  .optional()
456
607
  .describe("Canonical room ID. Defaults to the current room."),
457
- project_id: z
458
- .string()
459
- .optional()
460
- .describe("Legacy project ID. Defaults to the current room if joined."),
461
- }, async ({ sender, status, room_id, project_id }) => {
608
+ }, async ({ sender, status, room_id }) => {
462
609
  const targetRoomId = getTargetRoomId(room_id);
463
- const targetProjectId = getTargetProjectId(project_id);
610
+ const targetProjectId = getFallbackProjectId();
464
611
  if (!targetRoomId && !targetProjectId) {
465
612
  return {
466
613
  content: [
@@ -468,8 +615,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
468
615
  type: "text",
469
616
  text: JSON.stringify({
470
617
  success: false,
471
- error: "No room_id or project_id provided and not currently in a room.",
472
- hint: "Join a room first with join_project or join_room, or pass room_id explicitly.",
618
+ error: "No room_id provided and not currently in a room.",
619
+ hint: "Join or create a room first, or pass room_id explicitly.",
473
620
  }, null, 2),
474
621
  },
475
622
  ],
@@ -509,7 +656,7 @@ const TASK_STATUSES = [
509
656
  "proposed", "accepted", "assigned", "in_progress",
510
657
  "blocked", "in_review", "merged", "done", "cancelled",
511
658
  ];
512
- server.tool("add_task", "Add a new task to the project board. Tasks normally start as 'proposed' and must be " +
659
+ server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
513
660
  "accepted before an agent can claim them, but tasks created by trusted agents already " +
514
661
  "active in the room may be auto-accepted. Use this when a human or agent identifies " +
515
662
  "work that needs to be done.", {
@@ -518,10 +665,9 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
518
665
  created_by: z.string().describe("Name of the agent or human creating the task"),
519
666
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
520
667
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
521
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
522
- }, async ({ title, description, created_by, source_message_id, room_id, project_id }) => {
668
+ }, async ({ title, description, created_by, source_message_id, room_id }) => {
523
669
  const targetRoomId = getTargetRoomId(room_id);
524
- const targetProjectId = getTargetProjectId(project_id);
670
+ const targetProjectId = getFallbackProjectId();
525
671
  if (!targetRoomId && !targetProjectId) {
526
672
  return {
527
673
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
@@ -541,16 +687,15 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
541
687
  content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
542
688
  };
543
689
  });
544
- server.tool("get_board", "Get the current task board for the project. By default shows only open tasks " +
690
+ server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
545
691
  "(not done/cancelled). Agents should check this on startup and when idle to " +
546
692
  "see if there is unassigned work to claim.", {
547
693
  status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
548
694
  open_only: z.boolean().optional().describe("If true (default), only show tasks not done/cancelled"),
549
695
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
550
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
551
- }, async ({ status, open_only, room_id, project_id }) => {
696
+ }, async ({ status, open_only, room_id }) => {
552
697
  const targetRoomId = getTargetRoomId(room_id);
553
- const targetProjectId = getTargetProjectId(project_id);
698
+ const targetProjectId = getFallbackProjectId();
554
699
  if (!targetRoomId && !targetProjectId) {
555
700
  return {
556
701
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
@@ -578,10 +723,9 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
578
723
  task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
579
724
  assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
580
725
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
581
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
582
- }, async ({ task_id, assignee, room_id, project_id }) => {
726
+ }, async ({ task_id, assignee, room_id }) => {
583
727
  const targetRoomId = getTargetRoomId(room_id);
584
- const targetProjectId = getTargetProjectId(project_id);
728
+ const targetProjectId = getFallbackProjectId();
585
729
  if (!targetRoomId && !targetProjectId) {
586
730
  return {
587
731
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -616,10 +760,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
616
760
  assignee: z.string().optional().describe("New assignee for the task"),
617
761
  pr_url: z.string().optional().describe("PR URL to link to the task"),
618
762
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
619
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
620
- }, async ({ task_id, status, assignee, pr_url, room_id, project_id }) => {
763
+ }, async ({ task_id, status, assignee, pr_url, room_id }) => {
621
764
  const targetRoomId = getTargetRoomId(room_id);
622
- const targetProjectId = getTargetProjectId(project_id);
765
+ const targetProjectId = getFallbackProjectId();
623
766
  if (!targetRoomId && !targetProjectId) {
624
767
  return {
625
768
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -652,10 +795,9 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
652
795
  task_id: z.string().describe("The task ID to submit for review"),
653
796
  pr_url: z.string().optional().describe("GitHub PR URL for the work"),
654
797
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
655
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
656
- }, async ({ task_id, pr_url, room_id, project_id }) => {
798
+ }, async ({ task_id, pr_url, room_id }) => {
657
799
  const targetRoomId = getTargetRoomId(room_id);
658
- const targetProjectId = getTargetProjectId(project_id);
800
+ const targetProjectId = getFallbackProjectId();
659
801
  if (!targetRoomId && !targetProjectId) {
660
802
  return {
661
803
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -799,7 +941,6 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
799
941
  success: true,
800
942
  created: configPath,
801
943
  room_id: joined.room.room_id,
802
- project_id: joined.room.project_id ?? null,
803
944
  code: joined.room.code ?? null,
804
945
  joined: true,
805
946
  hint: "Consider adding .letagents.json to git so other contributors auto-join the same room.",
@@ -830,12 +971,11 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
830
971
  // -- send_message -----------------------------------------------------------
831
972
  server.tool("send_message", "Send a message to a Let Agents Chat room.", {
832
973
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
833
- project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
834
974
  sender: z.string().describe("Name identifying the sending agent (e.g. 'antigravity-agent')"),
835
975
  text: z.string().describe("The message text to send"),
836
- }, async ({ room_id, project_id, sender, text }) => {
976
+ }, async ({ room_id, sender, text }) => {
837
977
  const targetRoomId = getTargetRoomId(room_id);
838
- const targetProjectId = getTargetProjectId(project_id);
978
+ const targetProjectId = getFallbackProjectId();
839
979
  if (!targetRoomId && !targetProjectId) {
840
980
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
841
981
  }
@@ -862,10 +1002,9 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
862
1002
  // -- read_messages ----------------------------------------------------------
863
1003
  server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
864
1004
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
865
- project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
866
- }, async ({ room_id, project_id }) => {
1005
+ }, async ({ room_id }) => {
867
1006
  const targetRoomId = getTargetRoomId(room_id);
868
- const targetProjectId = getTargetProjectId(project_id);
1007
+ const targetProjectId = getFallbackProjectId();
869
1008
  const result = await roomScopedApiCall({
870
1009
  room_id: targetRoomId,
871
1010
  project_id: targetProjectId,
@@ -886,7 +1025,6 @@ const MAX_POLL_TIMEOUT_MS = 180000; // 3 minutes
886
1025
  const DEFAULT_POLL_TIMEOUT_MS = 30000; // 30 seconds
887
1026
  server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room. Blocks until new messages arrive or 30 seconds elapse. Use the after_message_id parameter to only receive messages newer than a specific message.", {
888
1027
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
889
- project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
890
1028
  after_message_id: z
891
1029
  .string()
892
1030
  .optional()
@@ -895,9 +1033,9 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
895
1033
  .number()
896
1034
  .optional()
897
1035
  .describe("Maximum wait time in milliseconds. If set to 0, the default timeout will be used."),
898
- }, async ({ room_id, project_id, after_message_id, timeout }) => {
1036
+ }, async ({ room_id, after_message_id, timeout }) => {
899
1037
  const targetRoomId = getTargetRoomId(room_id);
900
- const targetProjectId = getTargetProjectId(project_id);
1038
+ const targetProjectId = getFallbackProjectId();
901
1039
  const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), MAX_POLL_TIMEOUT_MS);
902
1040
  const clientTimeout = serverTimeout + 5000; // 5s buffer over server timeout
903
1041
  const params = new URLSearchParams();
@@ -962,8 +1100,8 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
962
1100
  account: storedAuth?.account ?? null,
963
1101
  token_expires_at: storedAuth?.expires_at ?? null,
964
1102
  pending_device_auth: pendingAuth,
965
- current_room: currentRoom,
966
- saved_current_room: savedCurrentRoom,
1103
+ current_room: toPublicRoomState(currentRoom),
1104
+ saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
967
1105
  detected_room_from_context: detectedRoom,
968
1106
  repo_root: repoRoot,
969
1107
  next_step: nextStep,
@@ -1164,22 +1302,37 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1164
1302
  ],
1165
1303
  };
1166
1304
  }
1167
- const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1168
- await autoRegisterAgentIdentity();
1169
- return {
1170
- content: [
1171
- {
1172
- type: "text",
1173
- text: JSON.stringify({
1174
- success: true,
1175
- rejoined_from_local_state: true,
1176
- server_session_resumed: false,
1177
- last_message_id_before_restart: savedRoom.last_message_id ?? null,
1178
- room: joined.room,
1179
- }, null, 2),
1180
- },
1181
- ],
1182
- };
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
+ }
1183
1336
  });
1184
1337
  // -- check_repo_visibility --------------------------------------------------
1185
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.", {
@@ -1197,7 +1350,7 @@ server.tool("check_repo_visibility", "Auto-detect the current repo's git remote
1197
1350
  type: "text",
1198
1351
  text: JSON.stringify({
1199
1352
  error: "Not in a git repository or no remote configured",
1200
- suggestion: "Use create_project to create an invite room instead",
1353
+ suggestion: "Use create_room to create an invite room instead",
1201
1354
  }, null, 2),
1202
1355
  },
1203
1356
  ],
@@ -1243,9 +1396,13 @@ async function main() {
1243
1396
  return;
1244
1397
  }
1245
1398
  // 4. No context found
1246
- console.error("ℹ️ No .letagents.json, git remote, or saved room found — use join_project or join_room to connect.");
1399
+ console.error("ℹ️ No .letagents.json, git remote, or saved room found — use create_room, join_code, or join_room to connect.");
1247
1400
  }
1248
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
+ }
1249
1406
  // Auto-join failure should never block the MCP server
1250
1407
  console.error("⚠️ Auto-join failed (server still running):", err instanceof Error ? err.message : err);
1251
1408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.7.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",