letagents 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,27 @@ export function clearPendingDeviceAuth() {
88
88
  return state;
89
89
  });
90
90
  }
91
+ export function getStoredAgentIdentity(identityKey) {
92
+ const state = readLocalState();
93
+ if (identityKey?.trim()) {
94
+ const scoped = state.agent_identities?.[identityKey.trim()];
95
+ if (scoped) {
96
+ return scoped;
97
+ }
98
+ }
99
+ return state.agent_identity ?? null;
100
+ }
101
+ export function setStoredAgentIdentity(agentIdentity, identityKey) {
102
+ updateLocalState((state) => {
103
+ state.agent_identity = agentIdentity;
104
+ if (identityKey?.trim()) {
105
+ state.agent_identities = state.agent_identities ?? {};
106
+ state.agent_identities[identityKey.trim()] = agentIdentity;
107
+ }
108
+ return state;
109
+ });
110
+ return agentIdentity;
111
+ }
91
112
  export function getStoredCurrentRoom() {
92
113
  const state = readLocalState();
93
114
  return state.current_room ?? null;
@@ -103,6 +124,7 @@ export function saveRoomSession(input) {
103
124
  room_id: input.room_id,
104
125
  project_id: input.project_id ?? existing?.project_id ?? null,
105
126
  code: input.code ?? existing?.code ?? null,
127
+ display_name: input.display_name ?? existing?.display_name ?? null,
106
128
  joined_via: input.joined_via,
107
129
  joined_at: existing?.joined_at ?? now,
108
130
  last_seen_at: now,
@@ -2,25 +2,460 @@
2
2
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ import { createHash } from "crypto";
5
6
  import { writeFileSync, existsSync } from "fs";
7
+ import { userInfo } from "os";
6
8
  import { join, dirname } from "path";
7
9
  import { execSync } from "child_process";
8
10
  import { SseClient } from "./sse-client.js";
9
11
  import { getRoomFromConfig } from "./config-reader.js";
10
12
  import { getGitRemoteIdentity } from "./git-remote.js";
11
- import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setPendingDeviceAuth, setStoredAuth, touchRoomSession, } from "./local-state.js";
13
+ import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, } from "./local-state.js";
12
14
  import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
15
+ import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
16
+ const CURRENT_AGENT_IDENTITY_KEY = getAgentIdentityStorageKey();
13
17
  let currentRoom = null;
18
+ let currentAgentIdentity = getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY);
19
+ let currentAuthenticatedAccount = undefined;
20
+ let currentAuthenticatedAccountSource = null;
21
+ let currentAuthenticatedEnvToken = null;
14
22
  // ---------------------------------------------------------------------------
15
23
  // Config
16
24
  // ---------------------------------------------------------------------------
17
25
  const API_URL = (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
18
26
  const AGENT_NAME = (process.env.LETAGENTS_AGENT_NAME || process.env.AGENT_NAME || "").trim();
19
27
  const AGENT_DISPLAY_NAME = (process.env.LETAGENTS_AGENT_DISPLAY_NAME || "").trim();
28
+ const AGENT_IDE_LABEL = (process.env.LETAGENTS_AGENT_IDE || process.env.AGENT_IDE || "").trim();
20
29
  const AGENT_OWNER_LABEL = (process.env.LETAGENTS_AGENT_OWNER_LABEL || "").trim();
21
30
  // ---------------------------------------------------------------------------
22
31
  // Helpers
23
32
  // ---------------------------------------------------------------------------
33
+ function readCommandOutput(command, cwd = process.cwd()) {
34
+ try {
35
+ const output = execSync(command, {
36
+ cwd,
37
+ stdio: ["pipe", "pipe", "pipe"],
38
+ encoding: "utf-8",
39
+ }).trim();
40
+ return output || null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function normalizeSlugSegment(input, fallback) {
47
+ const normalized = input
48
+ .normalize("NFKD")
49
+ .replace(/[^\x00-\x7F]/g, "")
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, "-")
52
+ .replace(/^-+|-+$/g, "")
53
+ .replace(/-{2,}/g, "-");
54
+ return normalized || fallback;
55
+ }
56
+ function normalizeAgentBaseName(input) {
57
+ return normalizeSlugSegment(input, "agent").replace(/-agent$/, "") || "agent";
58
+ }
59
+ function isCodexRuntime() {
60
+ return Boolean(process.env.CODEX_THREAD_ID ||
61
+ process.env.CODEX_SHELL ||
62
+ process.env.CODEX_CI ||
63
+ process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE);
64
+ }
65
+ const AGENT_CODENAMES = [
66
+ "amber",
67
+ "anchor",
68
+ "autumn",
69
+ "badger",
70
+ "bay",
71
+ "bear",
72
+ "brook",
73
+ "calm",
74
+ "canyon",
75
+ "cedar",
76
+ "clear",
77
+ "cloud",
78
+ "comet",
79
+ "copper",
80
+ "creek",
81
+ "crisp",
82
+ "crest",
83
+ "dawn",
84
+ "delta",
85
+ "dune",
86
+ "ember",
87
+ "falcon",
88
+ "fern",
89
+ "field",
90
+ "firefly",
91
+ "fjord",
92
+ "forest",
93
+ "fox",
94
+ "garden",
95
+ "glade",
96
+ "golden",
97
+ "granite",
98
+ "grove",
99
+ "harbor",
100
+ "hawk",
101
+ "hollow",
102
+ "indigo",
103
+ "ivory",
104
+ "jade",
105
+ "juniper",
106
+ "lagoon",
107
+ "lake",
108
+ "lantern",
109
+ "leaf",
110
+ "lively",
111
+ "lunar",
112
+ "lynx",
113
+ "maple",
114
+ "marsh",
115
+ "meadow",
116
+ "mesa",
117
+ "misty",
118
+ "moon",
119
+ "morrow",
120
+ "moss",
121
+ "noble",
122
+ "oak",
123
+ "olive",
124
+ "opal",
125
+ "otter",
126
+ "owl",
127
+ "peak",
128
+ "pearl",
129
+ "pine",
130
+ "quiet",
131
+ "raven",
132
+ "reef",
133
+ "ridge",
134
+ "river",
135
+ "rook",
136
+ "sage",
137
+ "scarlet",
138
+ "shore",
139
+ "silver",
140
+ "sky",
141
+ "solar",
142
+ "sparrow",
143
+ "spring",
144
+ "star",
145
+ "stone",
146
+ "storm",
147
+ "summit",
148
+ "sun",
149
+ "sunlit",
150
+ "swift",
151
+ "thicket",
152
+ "tidal",
153
+ "timber",
154
+ "trail",
155
+ "valley",
156
+ "verdant",
157
+ "vista",
158
+ "warm",
159
+ "wave",
160
+ "west",
161
+ "wild",
162
+ "willow",
163
+ "wind",
164
+ "winter",
165
+ "wolf",
166
+ "wood",
167
+ "wren",
168
+ ];
169
+ function getAgentIdentityStorageKey() {
170
+ const runtimeSignals = [
171
+ process.env.LETAGENTS_AGENT_INSTANCE_ID,
172
+ process.env.CODEX_THREAD_ID && `codex:${process.env.CODEX_THREAD_ID}`,
173
+ process.env.ANTIGRAVITY_THREAD_ID && `antigravity:${process.env.ANTIGRAVITY_THREAD_ID}`,
174
+ process.env.CLAUDECODE_SESSION_ID && `claude:${process.env.CLAUDECODE_SESSION_ID}`,
175
+ process.env.MCP_SESSION_ID && `mcp:${process.env.MCP_SESSION_ID}`,
176
+ ].filter((value) => Boolean(value?.trim()));
177
+ if (runtimeSignals.length) {
178
+ return runtimeSignals[0];
179
+ }
180
+ return `cwd:${process.cwd()}`;
181
+ }
182
+ function hashStringToIndex(value, modulo) {
183
+ const digest = createHash("sha256").update(value).digest();
184
+ return digest.readUInt16BE(0) % modulo;
185
+ }
186
+ function detectAgentIdeLabel() {
187
+ if (AGENT_IDE_LABEL) {
188
+ return toTitleCaseCodename(AGENT_IDE_LABEL);
189
+ }
190
+ if (isCodexRuntime()) {
191
+ return "Codex";
192
+ }
193
+ const explicitName = normalizeAgentBaseName(AGENT_NAME || AGENT_DISPLAY_NAME);
194
+ const inferred = inferAgentIdeLabel(explicitName);
195
+ return inferred || "Agent";
196
+ }
197
+ async function getAuthenticatedAgentDirectory() {
198
+ try {
199
+ const result = await apiCall("/agents/me");
200
+ const account = result?.account;
201
+ if (!account?.login?.trim()) {
202
+ return null;
203
+ }
204
+ currentAuthenticatedAccount = account;
205
+ currentAuthenticatedAccountSource = process.env.LETAGENTS_TOKEN?.trim() ? "env" : "stored";
206
+ currentAuthenticatedEnvToken = process.env.LETAGENTS_TOKEN?.trim() || null;
207
+ return {
208
+ account,
209
+ agents: Array.isArray(result?.agents) ? result.agents : [],
210
+ };
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ function shouldReuseStoredIdentity(identity) {
217
+ return Boolean(identity &&
218
+ identity.runtime_key === CURRENT_AGENT_IDENTITY_KEY &&
219
+ identity.display_name?.trim() &&
220
+ identity.ide_label?.trim() &&
221
+ identity.owner_attribution?.trim());
222
+ }
223
+ function resolveExplicitAgentIdentity() {
224
+ if (AGENT_NAME) {
225
+ const name = normalizeAgentBaseName(AGENT_NAME);
226
+ return {
227
+ name,
228
+ display_name: AGENT_DISPLAY_NAME || toTitleCaseCodename(AGENT_NAME),
229
+ };
230
+ }
231
+ if (AGENT_DISPLAY_NAME) {
232
+ return {
233
+ name: normalizeAgentBaseName(AGENT_DISPLAY_NAME),
234
+ display_name: AGENT_DISPLAY_NAME.trim(),
235
+ };
236
+ }
237
+ return null;
238
+ }
239
+ function pickLocalCodename(runtimeKey, offset = 0) {
240
+ const index = (hashStringToIndex(runtimeKey, AGENT_CODENAMES.length) + offset) % AGENT_CODENAMES.length;
241
+ const word = AGENT_CODENAMES[index];
242
+ return {
243
+ name: normalizeAgentBaseName(word),
244
+ display_name: toTitleCaseCodename(word),
245
+ };
246
+ }
247
+ async function resolveAgentName(authAvailable) {
248
+ const explicit = resolveExplicitAgentIdentity();
249
+ if (explicit) {
250
+ return explicit;
251
+ }
252
+ if (shouldReuseStoredIdentity(currentAgentIdentity)) {
253
+ return {
254
+ name: currentAgentIdentity.name,
255
+ display_name: currentAgentIdentity.display_name,
256
+ };
257
+ }
258
+ if (!authAvailable) {
259
+ return pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY);
260
+ }
261
+ const directory = await getAuthenticatedAgentDirectory();
262
+ const existingNames = new Set((directory?.agents ?? [])
263
+ .map((agent) => normalizeAgentBaseName(agent.name || ""))
264
+ .filter(Boolean));
265
+ for (let offset = 0; offset < AGENT_CODENAMES.length; offset += 1) {
266
+ const candidate = pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY, offset);
267
+ if (!existingNames.has(candidate.name)) {
268
+ return candidate;
269
+ }
270
+ }
271
+ const fallbackHash = createHash("sha256")
272
+ .update(CURRENT_AGENT_IDENTITY_KEY)
273
+ .digest("hex")
274
+ .slice(0, 4);
275
+ const fallback = pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY);
276
+ return {
277
+ name: `${fallback.name}-${fallbackHash}`,
278
+ display_name: `${fallback.display_name} ${fallbackHash.toUpperCase()}`,
279
+ };
280
+ }
281
+ async function getAuthenticatedAccountProfile() {
282
+ const envToken = (process.env.LETAGENTS_TOKEN || "").trim();
283
+ if (envToken) {
284
+ if (currentAuthenticatedAccountSource === "env" &&
285
+ currentAuthenticatedEnvToken === envToken &&
286
+ currentAuthenticatedAccount?.login?.trim()) {
287
+ return currentAuthenticatedAccount;
288
+ }
289
+ const directory = await getAuthenticatedAgentDirectory();
290
+ if (directory?.account?.login?.trim()) {
291
+ return directory.account;
292
+ }
293
+ return null;
294
+ }
295
+ const storedAccount = getStoredAuth()?.account;
296
+ if (storedAccount?.login?.trim()) {
297
+ currentAuthenticatedAccount = storedAccount;
298
+ currentAuthenticatedAccountSource = "stored";
299
+ currentAuthenticatedEnvToken = null;
300
+ return storedAccount;
301
+ }
302
+ if (!getLetagentsToken()) {
303
+ currentAuthenticatedAccount = undefined;
304
+ currentAuthenticatedAccountSource = null;
305
+ currentAuthenticatedEnvToken = null;
306
+ return null;
307
+ }
308
+ if (currentAuthenticatedAccountSource === "stored" &&
309
+ currentAuthenticatedAccount?.login?.trim()) {
310
+ return currentAuthenticatedAccount;
311
+ }
312
+ const directory = await getAuthenticatedAgentDirectory();
313
+ if (directory?.account?.login?.trim()) {
314
+ return directory.account;
315
+ }
316
+ return null;
317
+ }
318
+ async function resolveOwnerContext() {
319
+ const account = await getAuthenticatedAccountProfile();
320
+ const authLogin = account?.login?.trim() || null;
321
+ const authLabel = account?.display_name?.trim() || authLogin;
322
+ if (authLogin || authLabel || AGENT_OWNER_LABEL) {
323
+ const label = AGENT_OWNER_LABEL || authLabel || authLogin || "Owner";
324
+ const slug = normalizeSlugSegment(authLogin || label, "owner");
325
+ return { slug, label, login: authLogin };
326
+ }
327
+ const gitUserName = readCommandOutput("git config --get user.name");
328
+ const gitUserEmail = readCommandOutput("git config --get user.email");
329
+ const gitIdentity = gitUserName || gitUserEmail?.split("@")[0] || null;
330
+ if (gitIdentity) {
331
+ return {
332
+ slug: normalizeSlugSegment(gitIdentity, "owner"),
333
+ label: gitIdentity,
334
+ login: null,
335
+ };
336
+ }
337
+ const osIdentity = process.env.USER ||
338
+ process.env.LOGNAME ||
339
+ process.env.USERNAME ||
340
+ (() => {
341
+ try {
342
+ return userInfo().username;
343
+ }
344
+ catch {
345
+ return null;
346
+ }
347
+ })() ||
348
+ "owner";
349
+ return {
350
+ slug: normalizeSlugSegment(osIdentity, "owner"),
351
+ label: osIdentity,
352
+ login: null,
353
+ };
354
+ }
355
+ function sameAgentIdentity(left, right) {
356
+ return Boolean(left &&
357
+ left.name === right.name &&
358
+ left.display_name === right.display_name &&
359
+ left.owner_label === right.owner_label &&
360
+ left.owner_attribution === right.owner_attribution &&
361
+ left.ide_label === right.ide_label &&
362
+ left.actor_label === right.actor_label &&
363
+ left.canonical_key === right.canonical_key &&
364
+ left.runtime_key === right.runtime_key &&
365
+ left.source === right.source);
366
+ }
367
+ function toPublicAgentIdentity(identity) {
368
+ if (!identity) {
369
+ return null;
370
+ }
371
+ return {
372
+ name: identity.name,
373
+ display_name: identity.display_name,
374
+ owner_label: identity.owner_label,
375
+ owner_attribution: identity.owner_attribution ?? formatOwnerAttribution(identity.owner_label),
376
+ ide_label: identity.ide_label ?? inferAgentIdeLabel(identity.display_name) ?? "Agent",
377
+ actor_label: identity.actor_label,
378
+ canonical_key: identity.canonical_key ?? null,
379
+ runtime_key: identity.runtime_key ?? null,
380
+ source: identity.source,
381
+ };
382
+ }
383
+ async function ensureAgentIdentity() {
384
+ const owner = await resolveOwnerContext();
385
+ const authAvailable = Boolean(getLetagentsToken());
386
+ const ideLabel = detectAgentIdeLabel();
387
+ const ownerAttribution = formatOwnerAttribution(owner.label);
388
+ const { name, display_name: displayName } = await resolveAgentName(authAvailable);
389
+ const actorLabel = buildAgentActorLabel({
390
+ display_name: displayName,
391
+ owner_label: owner.label,
392
+ ide_label: ideLabel,
393
+ });
394
+ let resolved = {
395
+ name,
396
+ display_name: displayName,
397
+ owner_label: owner.label,
398
+ owner_attribution: ownerAttribution,
399
+ ide_label: ideLabel,
400
+ actor_label: actorLabel,
401
+ canonical_key: owner.login ? `${owner.login}/${name}` : null,
402
+ runtime_key: CURRENT_AGENT_IDENTITY_KEY,
403
+ source: "local",
404
+ resolved_at: new Date().toISOString(),
405
+ };
406
+ if (currentAgentIdentity &&
407
+ currentAgentIdentity.name === resolved.name &&
408
+ currentAgentIdentity.display_name === resolved.display_name &&
409
+ currentAgentIdentity.owner_label === resolved.owner_label &&
410
+ currentAgentIdentity.owner_attribution === resolved.owner_attribution &&
411
+ currentAgentIdentity.ide_label === resolved.ide_label &&
412
+ currentAgentIdentity.actor_label === resolved.actor_label &&
413
+ currentAgentIdentity.runtime_key === resolved.runtime_key &&
414
+ (!authAvailable || currentAgentIdentity.source === "api")) {
415
+ return currentAgentIdentity;
416
+ }
417
+ if (authAvailable) {
418
+ try {
419
+ const registered = await apiCall("/agents", {
420
+ method: "POST",
421
+ body: JSON.stringify({
422
+ name: resolved.name,
423
+ display_name: resolved.display_name,
424
+ owner_label: resolved.owner_label,
425
+ }),
426
+ });
427
+ resolved = {
428
+ ...resolved,
429
+ canonical_key: typeof registered.canonical_key === "string"
430
+ ? registered.canonical_key
431
+ : resolved.canonical_key,
432
+ display_name: typeof registered.display_name === "string"
433
+ ? registered.display_name
434
+ : resolved.display_name,
435
+ owner_label: typeof registered.owner_label === "string"
436
+ ? registered.owner_label
437
+ : resolved.owner_label,
438
+ source: "api",
439
+ };
440
+ resolved.owner_attribution = formatOwnerAttribution(resolved.owner_label);
441
+ resolved.actor_label = buildAgentActorLabel({
442
+ display_name: resolved.display_name,
443
+ owner_label: resolved.owner_label,
444
+ ide_label: resolved.ide_label,
445
+ });
446
+ }
447
+ catch (error) {
448
+ console.error("Agent identity registration failed:", error instanceof Error ? error.message : error);
449
+ }
450
+ }
451
+ if (!sameAgentIdentity(currentAgentIdentity, resolved)) {
452
+ currentAgentIdentity = setStoredAgentIdentity({
453
+ ...resolved,
454
+ resolved_at: new Date().toISOString(),
455
+ }, CURRENT_AGENT_IDENTITY_KEY);
456
+ }
457
+ return currentAgentIdentity ?? resolved;
458
+ }
24
459
  /**
25
460
  * Resolve the root of the git repository containing `dir`.
26
461
  * Returns null if `dir` is not inside a git repo.
@@ -64,6 +499,16 @@ class ApiError extends Error {
64
499
  this.body = body;
65
500
  }
66
501
  }
502
+ class RepoRoomAuthRequiredError extends Error {
503
+ roomId;
504
+ pendingAuth;
505
+ constructor(roomId, pendingAuth) {
506
+ super(`Repo room '${roomId}' requires authentication. Device flow started: open ${pendingAuth.verification_uri} and enter code ${pendingAuth.user_code}, then run poll_device_auth.`);
507
+ this.name = "RepoRoomAuthRequiredError";
508
+ this.roomId = roomId;
509
+ this.pendingAuth = pendingAuth;
510
+ }
511
+ }
67
512
  function getLetagentsToken() {
68
513
  return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
69
514
  }
@@ -79,6 +524,34 @@ function isMissingRouteError(error) {
79
524
  (error.status === 404 || error.status === 405) &&
80
525
  /Cannot (GET|POST|PATCH)|Not Found|Cannot GET \/rooms|Cannot POST \/rooms/i.test(error.body));
81
526
  }
527
+ function parseApiErrorPayload(error) {
528
+ if (!(error instanceof ApiError)) {
529
+ return null;
530
+ }
531
+ try {
532
+ const parsed = JSON.parse(error.body);
533
+ return parsed && typeof parsed === "object" ? parsed : null;
534
+ }
535
+ catch {
536
+ return null;
537
+ }
538
+ }
539
+ function resolveApiPath(urlOrPath) {
540
+ if (!urlOrPath) {
541
+ return "/auth/device/start";
542
+ }
543
+ try {
544
+ const parsed = new URL(urlOrPath, `${API_URL}/`);
545
+ const apiBase = new URL(`${API_URL}/`);
546
+ if (parsed.origin !== apiBase.origin) {
547
+ return "/auth/device/start";
548
+ }
549
+ return `${parsed.pathname}${parsed.search}`;
550
+ }
551
+ catch {
552
+ return "/auth/device/start";
553
+ }
554
+ }
82
555
  async function apiCall(path, options) {
83
556
  const headers = {
84
557
  "Content-Type": "application/json",
@@ -98,6 +571,9 @@ async function apiCall(path, options) {
98
571
  // Only clear on 401 (invalid/expired credential), NOT on 403
99
572
  // (valid credential but insufficient permissions, e.g., private repo access)
100
573
  clearStoredAuth();
574
+ currentAuthenticatedAccount = undefined;
575
+ currentAuthenticatedAccountSource = null;
576
+ currentAuthenticatedEnvToken = null;
101
577
  }
102
578
  throw new ApiError(res.status, body);
103
579
  }
@@ -107,11 +583,48 @@ async function apiCall(path, options) {
107
583
  }
108
584
  return JSON.parse(body);
109
585
  }
586
+ async function startPendingDeviceAuth(roomId, deviceFlowUrl) {
587
+ const existing = getPendingDeviceAuth();
588
+ if (existing?.suggested_room_id === roomId) {
589
+ return existing;
590
+ }
591
+ const response = await apiCall(resolveApiPath(deviceFlowUrl), {
592
+ method: "POST",
593
+ });
594
+ return setPendingDeviceAuth({
595
+ request_id: response.request_id,
596
+ user_code: response.user_code,
597
+ verification_uri: response.verification_uri,
598
+ interval_seconds: response.interval,
599
+ expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
600
+ started_at: new Date().toISOString(),
601
+ suggested_room_id: roomId,
602
+ });
603
+ }
604
+ async function maybeHandleRepoRoomAuthRequired(error, roomId) {
605
+ const payload = parseApiErrorPayload(error);
606
+ if (!(error instanceof ApiError) || error.status !== 401 || payload?.error !== "auth_required") {
607
+ return;
608
+ }
609
+ const pendingAuth = await startPendingDeviceAuth(roomId, typeof payload.device_flow_url === "string" ? payload.device_flow_url : undefined);
610
+ throw new RepoRoomAuthRequiredError(roomId, pendingAuth);
611
+ }
612
+ function toRepoRoomAuthRequiredResult(error) {
613
+ return {
614
+ success: false,
615
+ error: "auth_required",
616
+ room_id: error.roomId,
617
+ next_step: "poll_device_auth",
618
+ pending_device_auth: error.pendingAuth,
619
+ message: error.message,
620
+ };
621
+ }
110
622
  function toRoomState(input) {
111
623
  return {
112
624
  room_id: input.room_id,
113
625
  project_id: input.project_id ?? null,
114
626
  code: input.code ?? null,
627
+ display_name: input.display_name ?? null,
115
628
  joined_via: input.joined_via,
116
629
  };
117
630
  }
@@ -122,6 +635,7 @@ function toPublicRoomState(state) {
122
635
  return {
123
636
  room_id: state.room_id,
124
637
  code: state.code ?? null,
638
+ display_name: state.display_name ?? null,
125
639
  joined_via: state.joined_via,
126
640
  };
127
641
  }
@@ -132,6 +646,7 @@ function toPublicStoredRoomSession(session) {
132
646
  return {
133
647
  room_id: session.room_id,
134
648
  code: session.code ?? null,
649
+ display_name: session.display_name ?? null,
135
650
  joined_via: session.joined_via,
136
651
  joined_at: session.joined_at,
137
652
  last_seen_at: session.last_seen_at,
@@ -145,12 +660,19 @@ function toPublicRoomResponse(response, fallbackRoomId) {
145
660
  room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
146
661
  };
147
662
  }
663
+ async function withAgentIdentity(payload) {
664
+ return {
665
+ ...payload,
666
+ agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
667
+ };
668
+ }
148
669
  function rememberRoom(state, lastMessageId) {
149
670
  currentRoom = state;
150
671
  saveRoomSession({
151
672
  room_id: state.room_id,
152
673
  project_id: state.project_id ?? null,
153
674
  code: state.code ?? null,
675
+ display_name: state.display_name ?? null,
154
676
  joined_via: state.joined_via,
155
677
  last_message_id: lastMessageId,
156
678
  });
@@ -192,6 +714,7 @@ async function roomScopedApiCall(input) {
192
714
  return result;
193
715
  }
194
716
  catch (error) {
717
+ await maybeHandleRepoRoomAuthRequired(error, input.room_id);
195
718
  if (!input.project_id || !isMissingRouteError(error)) {
196
719
  throw error;
197
720
  }
@@ -221,14 +744,21 @@ async function joinRoomIdentifier(identifier, joinedVia) {
221
744
  : looksLikeInviteCode(joinedRoomId)
222
745
  ? joinedRoomId
223
746
  : null,
747
+ display_name: typeof response.display_name === "string" ? response.display_name : null,
224
748
  joined_via: joinedVia,
225
749
  }));
750
+ const agentIdentity = await ensureAgentIdentity();
226
751
  return {
227
752
  room,
228
- response: { ...response, room_id: joinedRoomId },
753
+ response: {
754
+ ...response,
755
+ room_id: joinedRoomId,
756
+ agent_identity: toPublicAgentIdentity(agentIdentity),
757
+ },
229
758
  };
230
759
  }
231
760
  catch (error) {
761
+ await maybeHandleRepoRoomAuthRequired(error, roomId);
232
762
  if (!isMissingRouteError(error)) {
233
763
  throw error;
234
764
  }
@@ -242,14 +772,17 @@ async function joinRoomIdentifier(identifier, joinedVia) {
242
772
  room_id: legacyRoomId,
243
773
  project_id: typeof project.id === "string" ? project.id : null,
244
774
  code: typeof project.code === "string" ? project.code : legacyRoomId,
775
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
245
776
  joined_via: joinedVia,
246
777
  }));
778
+ const agentIdentity = await ensureAgentIdentity();
247
779
  return {
248
780
  room,
249
781
  response: {
250
782
  ...project,
251
783
  room_id: legacyRoomId,
252
784
  project_id: typeof project.id === "string" ? project.id : null,
785
+ agent_identity: toPublicAgentIdentity(agentIdentity),
253
786
  },
254
787
  };
255
788
  }
@@ -267,14 +800,17 @@ async function joinRoomIdentifier(identifier, joinedVia) {
267
800
  : looksLikeInviteCode(legacyRoomId)
268
801
  ? legacyRoomId
269
802
  : null,
803
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
270
804
  joined_via: joinedVia,
271
805
  }));
806
+ const agentIdentity = await ensureAgentIdentity();
272
807
  return {
273
808
  room,
274
809
  response: {
275
810
  ...project,
276
811
  room_id: legacyRoomId,
277
812
  project_id: typeof project.id === "string" ? project.id : null,
813
+ agent_identity: toPublicAgentIdentity(agentIdentity),
278
814
  },
279
815
  };
280
816
  }
@@ -289,47 +825,31 @@ async function createInviteRoom() {
289
825
  room_id: roomId,
290
826
  project_id: typeof project.id === "string" ? project.id : null,
291
827
  code: typeof project.code === "string" ? project.code : roomId,
828
+ display_name: typeof project.display_name === "string" ? project.display_name : null,
292
829
  joined_via: "join_code",
293
830
  }));
294
- await autoRegisterAgentIdentity();
831
+ const agentIdentity = await ensureAgentIdentity();
295
832
  return {
296
833
  room,
297
- response: toPublicRoomResponse(project, roomId),
834
+ response: {
835
+ ...toPublicRoomResponse(project, roomId),
836
+ agent_identity: toPublicAgentIdentity(agentIdentity),
837
+ },
298
838
  };
299
839
  }
300
840
  async function joinInviteCode(code) {
301
841
  const joined = await joinRoomIdentifier(code, "join_code");
302
- await autoRegisterAgentIdentity();
303
- return {
842
+ return withAgentIdentity({
304
843
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
305
844
  joined_via: "join_code",
306
- };
845
+ });
307
846
  }
308
847
  async function joinNamedRoom(name) {
309
848
  const joined = await joinRoomIdentifier(name, "join_room");
310
- await autoRegisterAgentIdentity();
311
- return {
849
+ return withAgentIdentity({
312
850
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
313
851
  joined_via: "join_room",
314
- };
315
- }
316
- async function autoRegisterAgentIdentity() {
317
- if (!AGENT_NAME || !currentRoom) {
318
- return;
319
- }
320
- try {
321
- await apiCall("/agents", {
322
- method: "POST",
323
- body: JSON.stringify({
324
- name: AGENT_NAME,
325
- display_name: AGENT_DISPLAY_NAME || AGENT_NAME,
326
- owner_label: AGENT_OWNER_LABEL || undefined,
327
- }),
328
- });
329
- }
330
- catch (error) {
331
- console.error("Agent identity registration failed:", error);
332
- }
852
+ });
333
853
  }
334
854
  // ---------------------------------------------------------------------------
335
855
  // MCP Server
@@ -418,42 +938,50 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
418
938
  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
939
  name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
420
940
  }, 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) {
941
+ try {
433
942
  return {
434
943
  content: [
435
944
  {
436
945
  type: "text",
437
- text: JSON.stringify({ connected: false, message: "Not currently in any room" }, null, 2),
946
+ text: JSON.stringify(await joinNamedRoom(name), null, 2),
438
947
  },
439
948
  ],
440
949
  };
441
950
  }
951
+ catch (error) {
952
+ if (error instanceof RepoRoomAuthRequiredError) {
953
+ return {
954
+ content: [
955
+ {
956
+ type: "text",
957
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
958
+ },
959
+ ],
960
+ };
961
+ }
962
+ throw error;
963
+ }
964
+ });
965
+ // -- get_current_room -------------------------------------------------------
966
+ server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
442
967
  return {
443
968
  content: [
444
969
  {
445
970
  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),
971
+ text: JSON.stringify(currentRoom
972
+ ? {
973
+ connected: true,
974
+ ...toPublicRoomState(currentRoom),
975
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
976
+ auth: getStoredAuth()
977
+ ? {
978
+ source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
979
+ expires_at: getStoredAuth()?.expires_at ?? null,
980
+ account: getStoredAuth()?.account ?? null,
981
+ }
982
+ : null,
983
+ }
984
+ : { connected: false, message: "Not currently in any room" }, null, 2),
457
985
  },
458
986
  ],
459
987
  };
@@ -508,13 +1036,16 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
508
1036
  "Use this to let other agents and humans know what you are currently doing, " +
509
1037
  "e.g. 'reviewing PR #2', 'waiting for tests', 'writing WISHLIST.md'. " +
510
1038
  "Status updates are distinct from chat messages and can be filtered separately.", {
511
- sender: z.string().describe("Name of the agent posting the status (e.g. 'codex-agent')"),
1039
+ sender: z
1040
+ .string()
1041
+ .optional()
1042
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
512
1043
  status: z.string().describe("Short status description (e.g. 'reviewing PR #2', 'idle', 'thinking...')"),
513
1044
  room_id: z
514
1045
  .string()
515
1046
  .optional()
516
1047
  .describe("Canonical room ID. Defaults to the current room."),
517
- }, async ({ sender, status, room_id }) => {
1048
+ }, async ({ sender: _sender, status, room_id }) => {
518
1049
  const targetRoomId = getTargetRoomId(room_id);
519
1050
  const targetProjectId = getFallbackProjectId();
520
1051
  if (!targetRoomId && !targetProjectId) {
@@ -533,6 +1064,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
533
1064
  }
534
1065
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
535
1066
  // them from normal chat messages without changing the data model.
1067
+ const identity = await ensureAgentIdentity();
1068
+ const sender = identity.actor_label;
536
1069
  const statusText = `[status] ${status}`;
537
1070
  const message = await roomScopedApiCall({
538
1071
  room_id: targetRoomId,
@@ -553,6 +1086,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
553
1086
  success: true,
554
1087
  status_posted: status,
555
1088
  sender,
1089
+ agent_identity: toPublicAgentIdentity(identity),
556
1090
  message_id: typeof message.id === "string" ? message.id : null,
557
1091
  timestamp: typeof message.timestamp === "string" ? message.timestamp : null,
558
1092
  }, null, 2),
@@ -571,10 +1105,13 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
571
1105
  "work that needs to be done.", {
572
1106
  title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
573
1107
  description: z.string().optional().describe("Longer description of what needs to be done"),
574
- created_by: z.string().describe("Name of the agent or human creating the task"),
1108
+ created_by: z
1109
+ .string()
1110
+ .optional()
1111
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
575
1112
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
576
1113
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
577
- }, async ({ title, description, created_by, source_message_id, room_id }) => {
1114
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id }) => {
578
1115
  const targetRoomId = getTargetRoomId(room_id);
579
1116
  const targetProjectId = getFallbackProjectId();
580
1117
  if (!targetRoomId && !targetProjectId) {
@@ -582,6 +1119,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
582
1119
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
583
1120
  };
584
1121
  }
1122
+ const identity = await ensureAgentIdentity();
585
1123
  const task = await roomScopedApiCall({
586
1124
  room_id: targetRoomId,
587
1125
  project_id: targetProjectId,
@@ -589,11 +1127,21 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
589
1127
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks`,
590
1128
  options: {
591
1129
  method: "POST",
592
- body: JSON.stringify({ title, description, created_by, source_message_id }),
1130
+ body: JSON.stringify({
1131
+ title,
1132
+ description,
1133
+ created_by: identity.actor_label,
1134
+ source_message_id,
1135
+ }),
593
1136
  },
594
1137
  });
595
1138
  return {
596
- content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
1139
+ content: [
1140
+ {
1141
+ type: "text",
1142
+ text: JSON.stringify({ success: true, task, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
1143
+ },
1144
+ ],
597
1145
  };
598
1146
  });
599
1147
  server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
@@ -630,9 +1178,12 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
630
1178
  "status. This sets the assignee to you and moves the status to 'assigned'. " +
631
1179
  "Do NOT claim proposed tasks — they need to be accepted first.", {
632
1180
  task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
633
- assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
1181
+ assignee: z
1182
+ .string()
1183
+ .optional()
1184
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
634
1185
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
635
- }, async ({ task_id, assignee, room_id }) => {
1186
+ }, async ({ task_id, assignee: _assignee, room_id }) => {
636
1187
  const targetRoomId = getTargetRoomId(room_id);
637
1188
  const targetProjectId = getFallbackProjectId();
638
1189
  if (!targetRoomId && !targetProjectId) {
@@ -641,6 +1192,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
641
1192
  };
642
1193
  }
643
1194
  try {
1195
+ const identity = await ensureAgentIdentity();
644
1196
  const updated = await roomScopedApiCall({
645
1197
  room_id: targetRoomId,
646
1198
  project_id: targetProjectId,
@@ -648,11 +1200,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
648
1200
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
649
1201
  options: {
650
1202
  method: "PATCH",
651
- body: JSON.stringify({ status: "assigned", assignee }),
1203
+ body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
652
1204
  },
653
1205
  });
654
1206
  return {
655
- content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
1207
+ content: [
1208
+ {
1209
+ type: "text",
1210
+ text: JSON.stringify({ success: true, task: updated, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
1211
+ },
1212
+ ],
656
1213
  };
657
1214
  }
658
1215
  catch (error) {
@@ -666,7 +1223,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
666
1223
  "but NOT proposed → in_progress).", {
667
1224
  task_id: z.string().describe("The task ID to update"),
668
1225
  status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
669
- assignee: z.string().optional().describe("New assignee for the task"),
1226
+ assignee: z
1227
+ .string()
1228
+ .optional()
1229
+ .describe("New assignee for the task. Defaults to the current agent when status=assigned."),
670
1230
  pr_url: z.string().optional().describe("PR URL to link to the task"),
671
1231
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
672
1232
  }, async ({ task_id, status, assignee, pr_url, room_id }) => {
@@ -678,6 +1238,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
678
1238
  };
679
1239
  }
680
1240
  try {
1241
+ const identity = status === "assigned" && !assignee
1242
+ ? await ensureAgentIdentity()
1243
+ : null;
681
1244
  const updated = await roomScopedApiCall({
682
1245
  room_id: targetRoomId,
683
1246
  project_id: targetProjectId,
@@ -685,11 +1248,24 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
685
1248
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
686
1249
  options: {
687
1250
  method: "PATCH",
688
- body: JSON.stringify({ status, assignee, pr_url }),
1251
+ body: JSON.stringify({
1252
+ status,
1253
+ assignee: assignee ?? identity?.actor_label,
1254
+ pr_url,
1255
+ }),
689
1256
  },
690
1257
  });
691
1258
  return {
692
- content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: JSON.stringify({
1263
+ success: true,
1264
+ task: updated,
1265
+ agent_identity: identity ? toPublicAgentIdentity(identity) : null,
1266
+ }, null, 2),
1267
+ },
1268
+ ],
693
1269
  };
694
1270
  }
695
1271
  catch (error) {
@@ -880,14 +1456,18 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
880
1456
  // -- send_message -----------------------------------------------------------
881
1457
  server.tool("send_message", "Send a message to a Let Agents Chat room.", {
882
1458
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
883
- sender: z.string().describe("Name identifying the sending agent (e.g. 'antigravity-agent')"),
1459
+ sender: z
1460
+ .string()
1461
+ .optional()
1462
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
884
1463
  text: z.string().describe("The message text to send"),
885
- }, async ({ room_id, sender, text }) => {
1464
+ }, async ({ room_id, sender: _sender, text }) => {
886
1465
  const targetRoomId = getTargetRoomId(room_id);
887
1466
  const targetProjectId = getFallbackProjectId();
888
1467
  if (!targetRoomId && !targetProjectId) {
889
1468
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
890
1469
  }
1470
+ const identity = await ensureAgentIdentity();
891
1471
  const message = await roomScopedApiCall({
892
1472
  room_id: targetRoomId,
893
1473
  project_id: targetProjectId,
@@ -895,7 +1475,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
895
1475
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
896
1476
  options: {
897
1477
  method: "POST",
898
- body: JSON.stringify({ sender, text }),
1478
+ body: JSON.stringify({ sender: identity.actor_label, text }),
899
1479
  },
900
1480
  });
901
1481
  touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
@@ -903,7 +1483,10 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
903
1483
  content: [
904
1484
  {
905
1485
  type: "text",
906
- text: JSON.stringify(message, null, 2),
1486
+ text: JSON.stringify({
1487
+ ...message,
1488
+ agent_identity: toPublicAgentIdentity(identity),
1489
+ }, null, 2),
907
1490
  },
908
1491
  ],
909
1492
  };
@@ -1009,6 +1592,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
1009
1592
  account: storedAuth?.account ?? null,
1010
1593
  token_expires_at: storedAuth?.expires_at ?? null,
1011
1594
  pending_device_auth: pendingAuth,
1595
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
1012
1596
  current_room: toPublicRoomState(currentRoom),
1013
1597
  saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
1014
1598
  detected_room_from_context: detectedRoom,
@@ -1131,6 +1715,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1131
1715
  if (result.status === "denied" || result.status === "expired") {
1132
1716
  clearPendingDeviceAuth();
1133
1717
  clearStoredAuth();
1718
+ currentAuthenticatedAccount = undefined;
1719
+ currentAuthenticatedAccountSource = null;
1720
+ currentAuthenticatedEnvToken = null;
1134
1721
  return {
1135
1722
  content: [
1136
1723
  {
@@ -1151,6 +1738,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1151
1738
  stored_at: new Date().toISOString(),
1152
1739
  source: "device_flow",
1153
1740
  });
1741
+ currentAuthenticatedAccount = storedAuth.account ?? undefined;
1742
+ currentAuthenticatedAccountSource = storedAuth.account ? "stored" : null;
1743
+ currentAuthenticatedEnvToken = null;
1154
1744
  let joinedRoom = null;
1155
1745
  const roomToJoin = room_id ||
1156
1746
  pendingAuth?.suggested_room_id ||
@@ -1161,8 +1751,8 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1161
1751
  const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
1162
1752
  const joined = await joinRoomIdentifier(roomToJoin, joinedVia);
1163
1753
  joinedRoom = joined.room;
1164
- await autoRegisterAgentIdentity();
1165
1754
  }
1755
+ const agentIdentity = await ensureAgentIdentity();
1166
1756
  return {
1167
1757
  content: [
1168
1758
  {
@@ -1173,6 +1763,7 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1173
1763
  account: storedAuth.account ?? null,
1174
1764
  expires_at: storedAuth.expires_at ?? null,
1175
1765
  auto_joined_room: joinedRoom,
1766
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1176
1767
  }, null, 2),
1177
1768
  },
1178
1769
  ],
@@ -1181,6 +1772,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1181
1772
  server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
1182
1773
  clearPendingDeviceAuth();
1183
1774
  clearStoredAuth();
1775
+ currentAuthenticatedAccount = undefined;
1776
+ currentAuthenticatedAccountSource = null;
1777
+ currentAuthenticatedEnvToken = null;
1184
1778
  return {
1185
1779
  content: [
1186
1780
  {
@@ -1211,22 +1805,38 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1211
1805
  ],
1212
1806
  };
1213
1807
  }
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
- };
1808
+ try {
1809
+ const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1810
+ const agentIdentity = await ensureAgentIdentity();
1811
+ return {
1812
+ content: [
1813
+ {
1814
+ type: "text",
1815
+ text: JSON.stringify({
1816
+ success: true,
1817
+ rejoined_from_local_state: true,
1818
+ server_session_resumed: false,
1819
+ last_message_id_before_restart: savedRoom.last_message_id ?? null,
1820
+ room: joined.room,
1821
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1822
+ }, null, 2),
1823
+ },
1824
+ ],
1825
+ };
1826
+ }
1827
+ catch (error) {
1828
+ if (error instanceof RepoRoomAuthRequiredError) {
1829
+ return {
1830
+ content: [
1831
+ {
1832
+ type: "text",
1833
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
1834
+ },
1835
+ ],
1836
+ };
1837
+ }
1838
+ throw error;
1839
+ }
1230
1840
  });
1231
1841
  // -- check_repo_visibility --------------------------------------------------
1232
1842
  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.", {
@@ -1272,6 +1882,7 @@ async function main() {
1272
1882
  const configRoom = getRoomFromConfig();
1273
1883
  if (configRoom) {
1274
1884
  await joinRoomIdentifier(configRoom, "config");
1885
+ await ensureAgentIdentity();
1275
1886
  console.error(`🏠 Auto-joined room '${configRoom}' (from .letagents.json)`);
1276
1887
  return;
1277
1888
  }
@@ -1279,6 +1890,7 @@ async function main() {
1279
1890
  const gitRoom = getGitRemoteIdentity();
1280
1891
  if (gitRoom) {
1281
1892
  await joinRoomIdentifier(gitRoom, "git-remote");
1893
+ await ensureAgentIdentity();
1282
1894
  console.error(`🏠 Auto-joined room '${gitRoom}' (inferred from git remote — consider adding a .letagents.json)`);
1283
1895
  return;
1284
1896
  }
@@ -1286,6 +1898,7 @@ async function main() {
1286
1898
  const savedCurrentRoom = getStoredCurrentRoom();
1287
1899
  if (savedCurrentRoom) {
1288
1900
  await joinRoomIdentifier(savedCurrentRoom.room_id, savedCurrentRoom.joined_via);
1901
+ await ensureAgentIdentity();
1289
1902
  console.error(`🏠 Rejoined saved room '${savedCurrentRoom.room_id}' (from local state)`);
1290
1903
  return;
1291
1904
  }
@@ -1293,6 +1906,10 @@ async function main() {
1293
1906
  console.error("â„šī¸ No .letagents.json, git remote, or saved room found — use create_room, join_code, or join_room to connect.");
1294
1907
  }
1295
1908
  catch (err) {
1909
+ if (err instanceof RepoRoomAuthRequiredError) {
1910
+ 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.`);
1911
+ return;
1912
+ }
1296
1913
  // Auto-join failure should never block the MCP server
1297
1914
  console.error("âš ī¸ Auto-join failed (server still running):", err instanceof Error ? err.message : err);
1298
1915
  }
@@ -0,0 +1,104 @@
1
+ const STRUCTURED_AGENT_LABEL_SEPARATOR = " | ";
2
+ const IDE_LABELS = new Map([
3
+ ["agent", "Agent"],
4
+ ["antigravity", "Antigravity"],
5
+ ["claude", "Claude"],
6
+ ["codex", "Codex"],
7
+ ["orchestrator", "Orchestrator"],
8
+ ]);
9
+ function normalizeWhitespace(value) {
10
+ return value.trim().replace(/\s+/g, " ");
11
+ }
12
+ function normalizeIdeLabel(value) {
13
+ const normalized = normalizeWhitespace(String(value ?? ""))
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, "-")
16
+ .replace(/^-+|-+$/g, "");
17
+ if (!normalized) {
18
+ return null;
19
+ }
20
+ return IDE_LABELS.get(normalized) ?? toTitleCaseCodename(normalized);
21
+ }
22
+ export function toTitleCaseCodename(value) {
23
+ const normalized = normalizeWhitespace(value);
24
+ if (!normalized) {
25
+ return "";
26
+ }
27
+ return normalized
28
+ .split(/[-\s]+/)
29
+ .filter(Boolean)
30
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
31
+ .join(" ");
32
+ }
33
+ export function formatOwnerAttribution(ownerLabel) {
34
+ const trimmed = normalizeWhitespace(ownerLabel) || "Owner";
35
+ return /s$/i.test(trimmed) ? `${trimmed}' agent` : `${trimmed}'s agent`;
36
+ }
37
+ export function inferAgentIdeLabel(value) {
38
+ const normalized = normalizeWhitespace(String(value ?? "")).toLowerCase();
39
+ if (!normalized) {
40
+ return null;
41
+ }
42
+ if (normalized === "codex" || normalized.startsWith("codex-")) {
43
+ return "Codex";
44
+ }
45
+ if (normalized === "antigravity" || normalized.startsWith("antigravity-")) {
46
+ return "Antigravity";
47
+ }
48
+ if (normalized === "claude" || normalized.startsWith("claude-")) {
49
+ return "Claude";
50
+ }
51
+ if (normalized === "orchestrator" || normalized.startsWith("orchestrator-")) {
52
+ return "Orchestrator";
53
+ }
54
+ return null;
55
+ }
56
+ export function buildAgentActorLabel(input) {
57
+ const displayName = normalizeWhitespace(input.display_name) || "Agent";
58
+ const ownerAttribution = formatOwnerAttribution(input.owner_label);
59
+ const ideLabel = normalizeIdeLabel(input.ide_label) ?? "Agent";
60
+ return [displayName, ownerAttribution, ideLabel].join(STRUCTURED_AGENT_LABEL_SEPARATOR);
61
+ }
62
+ export function parseAgentActorLabel(value) {
63
+ const raw = normalizeWhitespace(String(value ?? ""));
64
+ if (!raw) {
65
+ return null;
66
+ }
67
+ const structuredParts = raw
68
+ .split(STRUCTURED_AGENT_LABEL_SEPARATOR)
69
+ .map((part) => normalizeWhitespace(part))
70
+ .filter(Boolean);
71
+ if (structuredParts.length === 3 &&
72
+ /agent$/i.test(structuredParts[1]) &&
73
+ normalizeIdeLabel(structuredParts[2])) {
74
+ return {
75
+ raw,
76
+ display_name: structuredParts[0],
77
+ owner_attribution: structuredParts[1],
78
+ ide_label: normalizeIdeLabel(structuredParts[2]),
79
+ structured: true,
80
+ };
81
+ }
82
+ const legacyMatch = raw.match(/^(.*?)\s*\(([^)]+agent)\)$/i);
83
+ if (legacyMatch) {
84
+ const displayName = normalizeWhitespace(legacyMatch[1] ?? "") || raw;
85
+ const ownerAttribution = normalizeWhitespace(legacyMatch[2] ?? "") || null;
86
+ return {
87
+ raw,
88
+ display_name: displayName,
89
+ owner_attribution: ownerAttribution,
90
+ ide_label: inferAgentIdeLabel(displayName),
91
+ structured: false,
92
+ };
93
+ }
94
+ return {
95
+ raw,
96
+ display_name: raw,
97
+ owner_attribution: null,
98
+ ide_label: inferAgentIdeLabel(raw),
99
+ structured: false,
100
+ };
101
+ }
102
+ export function getAgentPrimaryLabel(value) {
103
+ return parseAgentActorLabel(value)?.display_name ?? normalizeWhitespace(String(value ?? ""));
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/mcp/**",
12
+ "dist/shared/**",
12
13
  "README.md"
13
14
  ],
14
15
  "scripts": {