letagents 0.8.1 → 0.10.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.
@@ -2,25 +2,593 @@
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";
12
- import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
13
+ import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
14
+ import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
15
+ import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
13
16
  let currentRoom = null;
17
+ let currentAgentIdentityKey = "";
18
+ let currentAgentIdentity = null;
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();
30
+ const EXPLICIT_AGENT_IDENTITY_KEY = getExplicitAgentIdentityStorageKey();
31
+ currentAgentIdentityKey =
32
+ EXPLICIT_AGENT_IDENTITY_KEY ?? getFallbackAgentIdentityNamespaceKey(detectAgentIdeLabel());
33
+ currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
21
34
  // ---------------------------------------------------------------------------
22
35
  // Helpers
23
36
  // ---------------------------------------------------------------------------
37
+ function readCommandOutput(command, cwd = process.cwd()) {
38
+ try {
39
+ const output = execSync(command, {
40
+ cwd,
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ encoding: "utf-8",
43
+ }).trim();
44
+ return output || null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function normalizeSlugSegment(input, fallback) {
51
+ const normalized = input
52
+ .normalize("NFKD")
53
+ .replace(/[^\x00-\x7F]/g, "")
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, "-")
56
+ .replace(/^-+|-+$/g, "")
57
+ .replace(/-{2,}/g, "-");
58
+ return normalized || fallback;
59
+ }
60
+ function normalizeAgentBaseName(input) {
61
+ return normalizeSlugSegment(input, "agent").replace(/-agent$/, "") || "agent";
62
+ }
63
+ function isCodexRuntime() {
64
+ return Boolean(process.env.CODEX_THREAD_ID ||
65
+ process.env.CODEX_SHELL ||
66
+ process.env.CODEX_CI ||
67
+ process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE);
68
+ }
69
+ const AGENT_CODENAMES = [
70
+ "amber",
71
+ "anchor",
72
+ "autumn",
73
+ "badger",
74
+ "bay",
75
+ "bear",
76
+ "brook",
77
+ "calm",
78
+ "canyon",
79
+ "cedar",
80
+ "clear",
81
+ "cloud",
82
+ "comet",
83
+ "copper",
84
+ "creek",
85
+ "crisp",
86
+ "crest",
87
+ "dawn",
88
+ "delta",
89
+ "dune",
90
+ "ember",
91
+ "falcon",
92
+ "fern",
93
+ "field",
94
+ "firefly",
95
+ "fjord",
96
+ "forest",
97
+ "fox",
98
+ "garden",
99
+ "glade",
100
+ "golden",
101
+ "granite",
102
+ "grove",
103
+ "harbor",
104
+ "hawk",
105
+ "hollow",
106
+ "indigo",
107
+ "ivory",
108
+ "jade",
109
+ "juniper",
110
+ "lagoon",
111
+ "lake",
112
+ "lantern",
113
+ "leaf",
114
+ "lively",
115
+ "lunar",
116
+ "lynx",
117
+ "maple",
118
+ "marsh",
119
+ "meadow",
120
+ "mesa",
121
+ "misty",
122
+ "moon",
123
+ "morrow",
124
+ "moss",
125
+ "noble",
126
+ "oak",
127
+ "olive",
128
+ "opal",
129
+ "otter",
130
+ "owl",
131
+ "peak",
132
+ "pearl",
133
+ "pine",
134
+ "quiet",
135
+ "raven",
136
+ "reef",
137
+ "ridge",
138
+ "river",
139
+ "rook",
140
+ "sage",
141
+ "scarlet",
142
+ "shore",
143
+ "silver",
144
+ "sky",
145
+ "solar",
146
+ "sparrow",
147
+ "spring",
148
+ "star",
149
+ "stone",
150
+ "storm",
151
+ "summit",
152
+ "sun",
153
+ "sunlit",
154
+ "swift",
155
+ "thicket",
156
+ "tidal",
157
+ "timber",
158
+ "trail",
159
+ "valley",
160
+ "verdant",
161
+ "vista",
162
+ "warm",
163
+ "wave",
164
+ "west",
165
+ "wild",
166
+ "willow",
167
+ "wind",
168
+ "winter",
169
+ "wolf",
170
+ "wood",
171
+ "wren",
172
+ ];
173
+ const AGENT_CODENAME_SPACE = AGENT_CODENAMES.length * AGENT_CODENAMES.length;
174
+ const AGENT_IDENTITY_SLOT_SUFFIX = ":slot:";
175
+ function getExplicitAgentIdentityStorageKey() {
176
+ const runtimeSignals = [
177
+ process.env.LETAGENTS_AGENT_INSTANCE_ID,
178
+ process.env.CODEX_THREAD_ID && `codex:${process.env.CODEX_THREAD_ID}`,
179
+ process.env.ANTIGRAVITY_THREAD_ID && `antigravity:${process.env.ANTIGRAVITY_THREAD_ID}`,
180
+ process.env.CLAUDECODE_SESSION_ID && `claude:${process.env.CLAUDECODE_SESSION_ID}`,
181
+ process.env.MCP_SESSION_ID && `mcp:${process.env.MCP_SESSION_ID}`,
182
+ ].filter((value) => Boolean(value?.trim()));
183
+ if (runtimeSignals.length) {
184
+ return runtimeSignals[0];
185
+ }
186
+ return null;
187
+ }
188
+ function getFallbackAgentIdentityNamespaceKey(ideLabel) {
189
+ return `cwd:${process.cwd()}:ide:${normalizeAgentBaseName(ideLabel)}`;
190
+ }
191
+ function hashStringToIndex(value, modulo) {
192
+ const digest = createHash("sha256").update(value).digest();
193
+ return digest.readUInt16BE(0) % modulo;
194
+ }
195
+ function codenameFromIndex(index) {
196
+ const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
197
+ const firstIndex = Math.floor(normalizedIndex / AGENT_CODENAMES.length);
198
+ const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
199
+ const first = AGENT_CODENAMES[firstIndex];
200
+ const second = AGENT_CODENAMES[secondIndex];
201
+ return {
202
+ name: normalizeAgentBaseName(`${first}-${second}`),
203
+ display_name: `${toTitleCaseCodename(first)} ${toTitleCaseCodename(second)}`,
204
+ };
205
+ }
206
+ function detectAgentIdeLabel() {
207
+ if (AGENT_IDE_LABEL) {
208
+ return toTitleCaseCodename(AGENT_IDE_LABEL);
209
+ }
210
+ if (isCodexRuntime()) {
211
+ return "Codex";
212
+ }
213
+ const explicitName = normalizeAgentBaseName(AGENT_NAME || AGENT_DISPLAY_NAME);
214
+ const inferred = inferAgentIdeLabel(explicitName);
215
+ return inferred || "Agent";
216
+ }
217
+ function getFallbackSlotPrefix(namespaceKey) {
218
+ return `${namespaceKey}${AGENT_IDENTITY_SLOT_SUFFIX}`;
219
+ }
220
+ function getFallbackSlotKey(namespaceKey, slotIndex) {
221
+ return `${getFallbackSlotPrefix(namespaceKey)}${slotIndex}`;
222
+ }
223
+ function parseFallbackSlotIndex(identityKey, namespaceKey) {
224
+ const prefix = getFallbackSlotPrefix(namespaceKey);
225
+ if (!identityKey.startsWith(prefix)) {
226
+ return null;
227
+ }
228
+ const slotIndex = Number.parseInt(identityKey.slice(prefix.length), 10);
229
+ return Number.isInteger(slotIndex) && slotIndex >= 0 ? slotIndex : null;
230
+ }
231
+ function isProcessAlive(pid) {
232
+ if (!pid || !Number.isInteger(pid) || pid <= 0) {
233
+ return false;
234
+ }
235
+ try {
236
+ process.kill(pid, 0);
237
+ return true;
238
+ }
239
+ catch (error) {
240
+ const err = error;
241
+ return err.code === "EPERM";
242
+ }
243
+ }
244
+ function claimFallbackIdentityKey(namespaceKey) {
245
+ let claimedKey = getFallbackSlotKey(namespaceKey, 0);
246
+ let claimedIdentity = null;
247
+ const now = new Date().toISOString();
248
+ updateLocalState((state) => {
249
+ const identities = state.agent_identities ?? {};
250
+ const leases = state.agent_identity_leases ?? {};
251
+ const namespacePrefix = getFallbackSlotPrefix(namespaceKey);
252
+ for (const [identityKey, lease] of Object.entries(leases)) {
253
+ if (!identityKey.startsWith(namespacePrefix) || lease.namespace_key !== namespaceKey) {
254
+ continue;
255
+ }
256
+ if (!isProcessAlive(lease.pid)) {
257
+ delete leases[identityKey];
258
+ }
259
+ }
260
+ const activeLeaseForPid = Object.entries(leases).find(([identityKey, lease]) => identityKey.startsWith(namespacePrefix) &&
261
+ lease.namespace_key === namespaceKey &&
262
+ lease.pid === process.pid);
263
+ if (activeLeaseForPid) {
264
+ const [identityKey, lease] = activeLeaseForPid;
265
+ lease.updated_at = now;
266
+ claimedKey = identityKey;
267
+ claimedIdentity = identities[identityKey] ?? null;
268
+ state.agent_identity_leases = leases;
269
+ return state;
270
+ }
271
+ const slotKeys = new Set();
272
+ for (const identityKey of Object.keys(identities)) {
273
+ if (identityKey.startsWith(namespacePrefix)) {
274
+ slotKeys.add(identityKey);
275
+ }
276
+ }
277
+ for (const identityKey of Object.keys(leases)) {
278
+ if (identityKey.startsWith(namespacePrefix)) {
279
+ slotKeys.add(identityKey);
280
+ }
281
+ }
282
+ const sortedSlotKeys = [...slotKeys].sort((left, right) => {
283
+ const leftIndex = parseFallbackSlotIndex(left, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
284
+ const rightIndex = parseFallbackSlotIndex(right, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
285
+ return leftIndex - rightIndex;
286
+ });
287
+ for (const identityKey of sortedSlotKeys) {
288
+ if (leases[identityKey]) {
289
+ continue;
290
+ }
291
+ leases[identityKey] = {
292
+ namespace_key: namespaceKey,
293
+ pid: process.pid,
294
+ acquired_at: now,
295
+ updated_at: now,
296
+ };
297
+ claimedKey = identityKey;
298
+ claimedIdentity = identities[identityKey] ?? null;
299
+ state.agent_identity_leases = leases;
300
+ return state;
301
+ }
302
+ let nextSlotIndex = 0;
303
+ while (slotKeys.has(getFallbackSlotKey(namespaceKey, nextSlotIndex))) {
304
+ nextSlotIndex += 1;
305
+ }
306
+ claimedKey = getFallbackSlotKey(namespaceKey, nextSlotIndex);
307
+ leases[claimedKey] = {
308
+ namespace_key: namespaceKey,
309
+ pid: process.pid,
310
+ acquired_at: now,
311
+ updated_at: now,
312
+ };
313
+ claimedIdentity = identities[claimedKey] ?? null;
314
+ state.agent_identity_leases = leases;
315
+ return state;
316
+ });
317
+ return {
318
+ identityKey: claimedKey,
319
+ identity: claimedIdentity,
320
+ };
321
+ }
322
+ function ensureAgentIdentityKey(ideLabel) {
323
+ if (EXPLICIT_AGENT_IDENTITY_KEY) {
324
+ currentAgentIdentityKey = EXPLICIT_AGENT_IDENTITY_KEY;
325
+ currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
326
+ return currentAgentIdentityKey;
327
+ }
328
+ const claimed = claimFallbackIdentityKey(getFallbackAgentIdentityNamespaceKey(ideLabel));
329
+ currentAgentIdentityKey = claimed.identityKey;
330
+ currentAgentIdentity = claimed.identity;
331
+ return currentAgentIdentityKey;
332
+ }
333
+ async function getAuthenticatedAgentDirectory() {
334
+ try {
335
+ const result = await apiCall("/agents/me");
336
+ const account = result?.account;
337
+ if (!account?.login?.trim()) {
338
+ return null;
339
+ }
340
+ currentAuthenticatedAccount = account;
341
+ currentAuthenticatedAccountSource = process.env.LETAGENTS_TOKEN?.trim() ? "env" : "stored";
342
+ currentAuthenticatedEnvToken = process.env.LETAGENTS_TOKEN?.trim() || null;
343
+ return {
344
+ account,
345
+ agents: Array.isArray(result?.agents) ? result.agents : [],
346
+ };
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
352
+ function shouldReuseStoredIdentity(identity, identityKey) {
353
+ return Boolean(identity &&
354
+ identity.runtime_key === identityKey &&
355
+ identity.display_name?.trim() &&
356
+ identity.ide_label?.trim() &&
357
+ identity.owner_attribution?.trim());
358
+ }
359
+ function resolveExplicitAgentIdentity() {
360
+ if (AGENT_NAME) {
361
+ const name = normalizeAgentBaseName(AGENT_NAME);
362
+ return {
363
+ name,
364
+ display_name: AGENT_DISPLAY_NAME || toTitleCaseCodename(AGENT_NAME),
365
+ };
366
+ }
367
+ if (AGENT_DISPLAY_NAME) {
368
+ return {
369
+ name: normalizeAgentBaseName(AGENT_DISPLAY_NAME),
370
+ display_name: AGENT_DISPLAY_NAME.trim(),
371
+ };
372
+ }
373
+ return null;
374
+ }
375
+ function pickLocalCodename(runtimeKey, offset = 0) {
376
+ const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
377
+ return codenameFromIndex(index);
378
+ }
379
+ async function resolveAgentName(authAvailable, identityKey) {
380
+ const explicit = resolveExplicitAgentIdentity();
381
+ if (explicit) {
382
+ return explicit;
383
+ }
384
+ if (shouldReuseStoredIdentity(currentAgentIdentity, identityKey)) {
385
+ return {
386
+ name: currentAgentIdentity.name,
387
+ display_name: currentAgentIdentity.display_name,
388
+ };
389
+ }
390
+ if (!authAvailable) {
391
+ return pickLocalCodename(identityKey);
392
+ }
393
+ const directory = await getAuthenticatedAgentDirectory();
394
+ const existingNames = new Set((directory?.agents ?? [])
395
+ .map((agent) => normalizeAgentBaseName(agent.name || ""))
396
+ .filter(Boolean));
397
+ for (let offset = 0; offset < AGENT_CODENAME_SPACE; offset += 1) {
398
+ const candidate = pickLocalCodename(identityKey, offset);
399
+ if (!existingNames.has(candidate.name)) {
400
+ return candidate;
401
+ }
402
+ }
403
+ const fallbackHash = createHash("sha256")
404
+ .update(identityKey)
405
+ .digest("hex")
406
+ .slice(0, 4);
407
+ const fallback = pickLocalCodename(identityKey);
408
+ return {
409
+ name: `${fallback.name}-${fallbackHash}`,
410
+ display_name: `${fallback.display_name} ${fallbackHash.toUpperCase()}`,
411
+ };
412
+ }
413
+ async function getAuthenticatedAccountProfile() {
414
+ const envToken = (process.env.LETAGENTS_TOKEN || "").trim();
415
+ if (envToken) {
416
+ if (currentAuthenticatedAccountSource === "env" &&
417
+ currentAuthenticatedEnvToken === envToken &&
418
+ currentAuthenticatedAccount?.login?.trim()) {
419
+ return currentAuthenticatedAccount;
420
+ }
421
+ const directory = await getAuthenticatedAgentDirectory();
422
+ if (directory?.account?.login?.trim()) {
423
+ return directory.account;
424
+ }
425
+ return null;
426
+ }
427
+ const storedAccount = getStoredAuth()?.account;
428
+ if (storedAccount?.login?.trim()) {
429
+ currentAuthenticatedAccount = storedAccount;
430
+ currentAuthenticatedAccountSource = "stored";
431
+ currentAuthenticatedEnvToken = null;
432
+ return storedAccount;
433
+ }
434
+ if (!getLetagentsToken()) {
435
+ currentAuthenticatedAccount = undefined;
436
+ currentAuthenticatedAccountSource = null;
437
+ currentAuthenticatedEnvToken = null;
438
+ return null;
439
+ }
440
+ if (currentAuthenticatedAccountSource === "stored" &&
441
+ currentAuthenticatedAccount?.login?.trim()) {
442
+ return currentAuthenticatedAccount;
443
+ }
444
+ const directory = await getAuthenticatedAgentDirectory();
445
+ if (directory?.account?.login?.trim()) {
446
+ return directory.account;
447
+ }
448
+ return null;
449
+ }
450
+ async function resolveOwnerContext() {
451
+ const account = await getAuthenticatedAccountProfile();
452
+ const authLogin = account?.login?.trim() || null;
453
+ const authLabel = account?.display_name?.trim() || authLogin;
454
+ if (authLogin || authLabel || AGENT_OWNER_LABEL) {
455
+ const label = AGENT_OWNER_LABEL || authLabel || authLogin || "Owner";
456
+ const slug = normalizeSlugSegment(authLogin || label, "owner");
457
+ return { slug, label, login: authLogin };
458
+ }
459
+ const gitUserName = readCommandOutput("git config --get user.name");
460
+ const gitUserEmail = readCommandOutput("git config --get user.email");
461
+ const gitIdentity = gitUserName || gitUserEmail?.split("@")[0] || null;
462
+ if (gitIdentity) {
463
+ return {
464
+ slug: normalizeSlugSegment(gitIdentity, "owner"),
465
+ label: gitIdentity,
466
+ login: null,
467
+ };
468
+ }
469
+ const osIdentity = process.env.USER ||
470
+ process.env.LOGNAME ||
471
+ process.env.USERNAME ||
472
+ (() => {
473
+ try {
474
+ return userInfo().username;
475
+ }
476
+ catch {
477
+ return null;
478
+ }
479
+ })() ||
480
+ "owner";
481
+ return {
482
+ slug: normalizeSlugSegment(osIdentity, "owner"),
483
+ label: osIdentity,
484
+ login: null,
485
+ };
486
+ }
487
+ function sameAgentIdentity(left, right) {
488
+ return Boolean(left &&
489
+ left.name === right.name &&
490
+ left.display_name === right.display_name &&
491
+ left.owner_label === right.owner_label &&
492
+ left.owner_attribution === right.owner_attribution &&
493
+ left.ide_label === right.ide_label &&
494
+ left.actor_label === right.actor_label &&
495
+ left.canonical_key === right.canonical_key &&
496
+ left.runtime_key === right.runtime_key &&
497
+ left.source === right.source);
498
+ }
499
+ function toPublicAgentIdentity(identity) {
500
+ if (!identity) {
501
+ return null;
502
+ }
503
+ return {
504
+ name: identity.name,
505
+ display_name: identity.display_name,
506
+ owner_label: identity.owner_label,
507
+ owner_attribution: identity.owner_attribution ?? formatOwnerAttribution(identity.owner_label),
508
+ ide_label: identity.ide_label ?? inferAgentIdeLabel(identity.display_name) ?? "Agent",
509
+ actor_label: identity.actor_label,
510
+ canonical_key: identity.canonical_key ?? null,
511
+ runtime_key: identity.runtime_key ?? null,
512
+ source: identity.source,
513
+ };
514
+ }
515
+ async function ensureAgentIdentity() {
516
+ const owner = await resolveOwnerContext();
517
+ const authAvailable = Boolean(getLetagentsToken());
518
+ const ideLabel = detectAgentIdeLabel();
519
+ const identityKey = ensureAgentIdentityKey(ideLabel);
520
+ const ownerAttribution = formatOwnerAttribution(owner.label);
521
+ const { name, display_name: displayName } = await resolveAgentName(authAvailable, identityKey);
522
+ const actorLabel = buildAgentActorLabel({
523
+ display_name: displayName,
524
+ owner_label: owner.label,
525
+ ide_label: ideLabel,
526
+ });
527
+ let resolved = {
528
+ name,
529
+ display_name: displayName,
530
+ owner_label: owner.label,
531
+ owner_attribution: ownerAttribution,
532
+ ide_label: ideLabel,
533
+ actor_label: actorLabel,
534
+ canonical_key: owner.login ? `${owner.login}/${name}` : null,
535
+ runtime_key: identityKey,
536
+ source: "local",
537
+ resolved_at: new Date().toISOString(),
538
+ };
539
+ if (currentAgentIdentity &&
540
+ currentAgentIdentity.name === resolved.name &&
541
+ currentAgentIdentity.display_name === resolved.display_name &&
542
+ currentAgentIdentity.owner_label === resolved.owner_label &&
543
+ currentAgentIdentity.owner_attribution === resolved.owner_attribution &&
544
+ currentAgentIdentity.ide_label === resolved.ide_label &&
545
+ currentAgentIdentity.actor_label === resolved.actor_label &&
546
+ currentAgentIdentity.runtime_key === resolved.runtime_key &&
547
+ (!authAvailable || currentAgentIdentity.source === "api")) {
548
+ return currentAgentIdentity;
549
+ }
550
+ if (authAvailable) {
551
+ try {
552
+ const registered = await apiCall("/agents", {
553
+ method: "POST",
554
+ body: JSON.stringify({
555
+ name: resolved.name,
556
+ display_name: resolved.display_name,
557
+ owner_label: resolved.owner_label,
558
+ }),
559
+ });
560
+ resolved = {
561
+ ...resolved,
562
+ canonical_key: typeof registered.canonical_key === "string"
563
+ ? registered.canonical_key
564
+ : resolved.canonical_key,
565
+ display_name: typeof registered.display_name === "string"
566
+ ? registered.display_name
567
+ : resolved.display_name,
568
+ owner_label: typeof registered.owner_label === "string"
569
+ ? registered.owner_label
570
+ : resolved.owner_label,
571
+ source: "api",
572
+ };
573
+ resolved.owner_attribution = formatOwnerAttribution(resolved.owner_label);
574
+ resolved.actor_label = buildAgentActorLabel({
575
+ display_name: resolved.display_name,
576
+ owner_label: resolved.owner_label,
577
+ ide_label: resolved.ide_label,
578
+ });
579
+ }
580
+ catch (error) {
581
+ console.error("Agent identity registration failed:", error instanceof Error ? error.message : error);
582
+ }
583
+ }
584
+ if (!sameAgentIdentity(currentAgentIdentity, resolved)) {
585
+ currentAgentIdentity = setStoredAgentIdentity({
586
+ ...resolved,
587
+ resolved_at: new Date().toISOString(),
588
+ }, identityKey);
589
+ }
590
+ return currentAgentIdentity ?? resolved;
591
+ }
24
592
  /**
25
593
  * Resolve the root of the git repository containing `dir`.
26
594
  * Returns null if `dir` is not inside a git repo.
@@ -136,6 +704,9 @@ async function apiCall(path, options) {
136
704
  // Only clear on 401 (invalid/expired credential), NOT on 403
137
705
  // (valid credential but insufficient permissions, e.g., private repo access)
138
706
  clearStoredAuth();
707
+ currentAuthenticatedAccount = undefined;
708
+ currentAuthenticatedAccountSource = null;
709
+ currentAuthenticatedEnvToken = null;
139
710
  }
140
711
  throw new ApiError(res.status, body);
141
712
  }
@@ -190,22 +761,32 @@ function toRoomState(input) {
190
761
  joined_via: input.joined_via,
191
762
  };
192
763
  }
764
+ function getCanonicalRoomWebUrl(roomId) {
765
+ return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
766
+ }
767
+ function withCanonicalRoomLink(roomId, payload) {
768
+ return {
769
+ ...payload,
770
+ room_path: getCanonicalRoomWebPath(roomId),
771
+ room_url: getCanonicalRoomWebUrl(roomId),
772
+ };
773
+ }
193
774
  function toPublicRoomState(state) {
194
775
  if (!state) {
195
776
  return null;
196
777
  }
197
- return {
778
+ return withCanonicalRoomLink(state.room_id, {
198
779
  room_id: state.room_id,
199
780
  code: state.code ?? null,
200
781
  display_name: state.display_name ?? null,
201
782
  joined_via: state.joined_via,
202
- };
783
+ });
203
784
  }
204
785
  function toPublicStoredRoomSession(session) {
205
786
  if (!session) {
206
787
  return null;
207
788
  }
208
- return {
789
+ return withCanonicalRoomLink(session.room_id, {
209
790
  room_id: session.room_id,
210
791
  code: session.code ?? null,
211
792
  display_name: session.display_name ?? null,
@@ -213,15 +794,21 @@ function toPublicStoredRoomSession(session) {
213
794
  joined_at: session.joined_at,
214
795
  last_seen_at: session.last_seen_at,
215
796
  last_message_id: session.last_message_id ?? null,
216
- };
797
+ });
217
798
  }
218
799
  function toPublicRoomResponse(response, fallbackRoomId) {
219
800
  const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
220
801
  return {
221
- ...rest,
802
+ ...withCanonicalRoomLink(typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId, rest),
222
803
  room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
223
804
  };
224
805
  }
806
+ async function withAgentIdentity(payload) {
807
+ return {
808
+ ...payload,
809
+ agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
810
+ };
811
+ }
225
812
  function rememberRoom(state, lastMessageId) {
226
813
  currentRoom = state;
227
814
  saveRoomSession({
@@ -303,9 +890,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
303
890
  display_name: typeof response.display_name === "string" ? response.display_name : null,
304
891
  joined_via: joinedVia,
305
892
  }));
893
+ const agentIdentity = await ensureAgentIdentity();
306
894
  return {
307
895
  room,
308
- response: { ...response, room_id: joinedRoomId },
896
+ response: {
897
+ ...response,
898
+ room_id: joinedRoomId,
899
+ agent_identity: toPublicAgentIdentity(agentIdentity),
900
+ },
309
901
  };
310
902
  }
311
903
  catch (error) {
@@ -326,12 +918,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
326
918
  display_name: typeof project.display_name === "string" ? project.display_name : null,
327
919
  joined_via: joinedVia,
328
920
  }));
921
+ const agentIdentity = await ensureAgentIdentity();
329
922
  return {
330
923
  room,
331
924
  response: {
332
925
  ...project,
333
926
  room_id: legacyRoomId,
334
927
  project_id: typeof project.id === "string" ? project.id : null,
928
+ agent_identity: toPublicAgentIdentity(agentIdentity),
335
929
  },
336
930
  };
337
931
  }
@@ -352,12 +946,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
352
946
  display_name: typeof project.display_name === "string" ? project.display_name : null,
353
947
  joined_via: joinedVia,
354
948
  }));
949
+ const agentIdentity = await ensureAgentIdentity();
355
950
  return {
356
951
  room,
357
952
  response: {
358
953
  ...project,
359
954
  room_id: legacyRoomId,
360
955
  project_id: typeof project.id === "string" ? project.id : null,
956
+ agent_identity: toPublicAgentIdentity(agentIdentity),
361
957
  },
362
958
  };
363
959
  }
@@ -375,45 +971,28 @@ async function createInviteRoom() {
375
971
  display_name: typeof project.display_name === "string" ? project.display_name : null,
376
972
  joined_via: "join_code",
377
973
  }));
378
- await autoRegisterAgentIdentity();
974
+ const agentIdentity = await ensureAgentIdentity();
379
975
  return {
380
976
  room,
381
- response: toPublicRoomResponse(project, roomId),
977
+ response: {
978
+ ...toPublicRoomResponse(project, roomId),
979
+ agent_identity: toPublicAgentIdentity(agentIdentity),
980
+ },
382
981
  };
383
982
  }
384
983
  async function joinInviteCode(code) {
385
984
  const joined = await joinRoomIdentifier(code, "join_code");
386
- await autoRegisterAgentIdentity();
387
- return {
985
+ return withAgentIdentity({
388
986
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
389
987
  joined_via: "join_code",
390
- };
988
+ });
391
989
  }
392
990
  async function joinNamedRoom(name) {
393
991
  const joined = await joinRoomIdentifier(name, "join_room");
394
- await autoRegisterAgentIdentity();
395
- return {
992
+ return withAgentIdentity({
396
993
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
397
994
  joined_via: "join_room",
398
- };
399
- }
400
- async function autoRegisterAgentIdentity() {
401
- if (!AGENT_NAME || !currentRoom) {
402
- return;
403
- }
404
- try {
405
- await apiCall("/agents", {
406
- method: "POST",
407
- body: JSON.stringify({
408
- name: AGENT_NAME,
409
- display_name: AGENT_DISPLAY_NAME || AGENT_NAME,
410
- owner_label: AGENT_OWNER_LABEL || undefined,
411
- }),
412
- });
413
- }
414
- catch (error) {
415
- console.error("Agent identity registration failed:", error);
416
- }
995
+ });
417
996
  }
418
997
  // ---------------------------------------------------------------------------
419
998
  // MCP Server
@@ -432,18 +1011,35 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
432
1011
  const normalizedRoomId = String(room_id);
433
1012
  const storedSession = getStoredRoomSession(normalizedRoomId) ??
434
1013
  (currentRoom?.room_id === normalizedRoomId ? getStoredCurrentRoom() : null);
435
- const result = await roomScopedApiCall({
436
- room_id: normalizedRoomId,
437
- project_id: storedSession?.project_id ?? null,
438
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
439
- project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages`,
440
- });
1014
+ // Paginate through all pages to return full message history
1015
+ const allMessages = [];
1016
+ let afterCursor;
1017
+ for (;;) {
1018
+ const query = new URLSearchParams();
1019
+ if (afterCursor)
1020
+ query.set("after", afterCursor);
1021
+ const qs = query.toString();
1022
+ const result = await roomScopedApiCall({
1023
+ room_id: normalizedRoomId,
1024
+ project_id: storedSession?.project_id ?? null,
1025
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
1026
+ project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`,
1027
+ });
1028
+ const msgs = result.messages ?? [];
1029
+ allMessages.push(...msgs);
1030
+ if (!result.has_more || msgs.length === 0)
1031
+ break;
1032
+ const lastMsg = msgs[msgs.length - 1];
1033
+ if (!lastMsg?.id)
1034
+ break;
1035
+ afterCursor = lastMsg.id;
1036
+ }
441
1037
  return {
442
1038
  contents: [
443
1039
  {
444
1040
  uri: uri.href,
445
1041
  mimeType: "application/json",
446
- text: JSON.stringify(result, null, 2),
1042
+ text: JSON.stringify({ messages: allMessages }, null, 2),
447
1043
  },
448
1044
  ],
449
1045
  };
@@ -536,6 +1132,7 @@ server.tool("get_current_room", "Get information about the currently joined room
536
1132
  ? {
537
1133
  connected: true,
538
1134
  ...toPublicRoomState(currentRoom),
1135
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
539
1136
  auth: getStoredAuth()
540
1137
  ? {
541
1138
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -599,13 +1196,16 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
599
1196
  "Use this to let other agents and humans know what you are currently doing, " +
600
1197
  "e.g. 'reviewing PR #2', 'waiting for tests', 'writing WISHLIST.md'. " +
601
1198
  "Status updates are distinct from chat messages and can be filtered separately.", {
602
- sender: z.string().describe("Name of the agent posting the status (e.g. 'codex-agent')"),
1199
+ sender: z
1200
+ .string()
1201
+ .optional()
1202
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
603
1203
  status: z.string().describe("Short status description (e.g. 'reviewing PR #2', 'idle', 'thinking...')"),
604
1204
  room_id: z
605
1205
  .string()
606
1206
  .optional()
607
1207
  .describe("Canonical room ID. Defaults to the current room."),
608
- }, async ({ sender, status, room_id }) => {
1208
+ }, async ({ sender: _sender, status, room_id }) => {
609
1209
  const targetRoomId = getTargetRoomId(room_id);
610
1210
  const targetProjectId = getFallbackProjectId();
611
1211
  if (!targetRoomId && !targetProjectId) {
@@ -624,6 +1224,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
624
1224
  }
625
1225
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
626
1226
  // them from normal chat messages without changing the data model.
1227
+ const identity = await ensureAgentIdentity();
1228
+ const sender = identity.actor_label;
627
1229
  const statusText = `[status] ${status}`;
628
1230
  const message = await roomScopedApiCall({
629
1231
  room_id: targetRoomId,
@@ -644,6 +1246,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
644
1246
  success: true,
645
1247
  status_posted: status,
646
1248
  sender,
1249
+ agent_identity: toPublicAgentIdentity(identity),
647
1250
  message_id: typeof message.id === "string" ? message.id : null,
648
1251
  timestamp: typeof message.timestamp === "string" ? message.timestamp : null,
649
1252
  }, null, 2),
@@ -662,10 +1265,13 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
662
1265
  "work that needs to be done.", {
663
1266
  title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
664
1267
  description: z.string().optional().describe("Longer description of what needs to be done"),
665
- created_by: z.string().describe("Name of the agent or human creating the task"),
1268
+ created_by: z
1269
+ .string()
1270
+ .optional()
1271
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
666
1272
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
667
1273
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
668
- }, async ({ title, description, created_by, source_message_id, room_id }) => {
1274
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id }) => {
669
1275
  const targetRoomId = getTargetRoomId(room_id);
670
1276
  const targetProjectId = getFallbackProjectId();
671
1277
  if (!targetRoomId && !targetProjectId) {
@@ -673,6 +1279,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
673
1279
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
674
1280
  };
675
1281
  }
1282
+ const identity = await ensureAgentIdentity();
676
1283
  const task = await roomScopedApiCall({
677
1284
  room_id: targetRoomId,
678
1285
  project_id: targetProjectId,
@@ -680,11 +1287,21 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
680
1287
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks`,
681
1288
  options: {
682
1289
  method: "POST",
683
- body: JSON.stringify({ title, description, created_by, source_message_id }),
1290
+ body: JSON.stringify({
1291
+ title,
1292
+ description,
1293
+ created_by: identity.actor_label,
1294
+ source_message_id,
1295
+ }),
684
1296
  },
685
1297
  });
686
1298
  return {
687
- content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
1299
+ content: [
1300
+ {
1301
+ type: "text",
1302
+ text: JSON.stringify({ success: true, task, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
1303
+ },
1304
+ ],
688
1305
  };
689
1306
  });
690
1307
  server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
@@ -706,24 +1323,43 @@ server.tool("get_board", "Get the current task board for the room. By default sh
706
1323
  params.set("status", status);
707
1324
  if (open_only !== false)
708
1325
  params.set("open", "true");
709
- const qs = params.toString();
710
- const result = await roomScopedApiCall({
711
- room_id: targetRoomId,
712
- project_id: targetProjectId,
713
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
714
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
715
- });
1326
+ // Paginate through all pages to return the full board
1327
+ const allTasks = [];
1328
+ let afterCursor;
1329
+ for (;;) {
1330
+ const pageParams = new URLSearchParams(params);
1331
+ if (afterCursor)
1332
+ pageParams.set("after", afterCursor);
1333
+ const qs = pageParams.toString();
1334
+ const result = await roomScopedApiCall({
1335
+ room_id: targetRoomId,
1336
+ project_id: targetProjectId,
1337
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
1338
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
1339
+ });
1340
+ const tasks = result.tasks ?? [];
1341
+ allTasks.push(...tasks);
1342
+ if (!result.has_more || tasks.length === 0)
1343
+ break;
1344
+ const lastTask = tasks[tasks.length - 1];
1345
+ if (!lastTask?.id)
1346
+ break;
1347
+ afterCursor = lastTask.id;
1348
+ }
716
1349
  return {
717
- content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }],
1350
+ content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
718
1351
  };
719
1352
  });
720
1353
  server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
721
1354
  "status. This sets the assignee to you and moves the status to 'assigned'. " +
722
1355
  "Do NOT claim proposed tasks — they need to be accepted first.", {
723
1356
  task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
724
- assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
1357
+ assignee: z
1358
+ .string()
1359
+ .optional()
1360
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
725
1361
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
726
- }, async ({ task_id, assignee, room_id }) => {
1362
+ }, async ({ task_id, assignee: _assignee, room_id }) => {
727
1363
  const targetRoomId = getTargetRoomId(room_id);
728
1364
  const targetProjectId = getFallbackProjectId();
729
1365
  if (!targetRoomId && !targetProjectId) {
@@ -732,6 +1368,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
732
1368
  };
733
1369
  }
734
1370
  try {
1371
+ const identity = await ensureAgentIdentity();
735
1372
  const updated = await roomScopedApiCall({
736
1373
  room_id: targetRoomId,
737
1374
  project_id: targetProjectId,
@@ -739,11 +1376,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
739
1376
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
740
1377
  options: {
741
1378
  method: "PATCH",
742
- body: JSON.stringify({ status: "assigned", assignee }),
1379
+ body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
743
1380
  },
744
1381
  });
745
1382
  return {
746
- content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
1383
+ content: [
1384
+ {
1385
+ type: "text",
1386
+ text: JSON.stringify({ success: true, task: updated, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
1387
+ },
1388
+ ],
747
1389
  };
748
1390
  }
749
1391
  catch (error) {
@@ -757,7 +1399,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
757
1399
  "but NOT proposed → in_progress).", {
758
1400
  task_id: z.string().describe("The task ID to update"),
759
1401
  status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
760
- assignee: z.string().optional().describe("New assignee for the task"),
1402
+ assignee: z
1403
+ .string()
1404
+ .optional()
1405
+ .describe("New assignee for the task. Defaults to the current agent when status=assigned."),
761
1406
  pr_url: z.string().optional().describe("PR URL to link to the task"),
762
1407
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
763
1408
  }, async ({ task_id, status, assignee, pr_url, room_id }) => {
@@ -769,6 +1414,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
769
1414
  };
770
1415
  }
771
1416
  try {
1417
+ const identity = status === "assigned" && !assignee
1418
+ ? await ensureAgentIdentity()
1419
+ : null;
772
1420
  const updated = await roomScopedApiCall({
773
1421
  room_id: targetRoomId,
774
1422
  project_id: targetProjectId,
@@ -776,11 +1424,24 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
776
1424
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
777
1425
  options: {
778
1426
  method: "PATCH",
779
- body: JSON.stringify({ status, assignee, pr_url }),
1427
+ body: JSON.stringify({
1428
+ status,
1429
+ assignee: assignee ?? identity?.actor_label,
1430
+ pr_url,
1431
+ }),
780
1432
  },
781
1433
  });
782
1434
  return {
783
- content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
1435
+ content: [
1436
+ {
1437
+ type: "text",
1438
+ text: JSON.stringify({
1439
+ success: true,
1440
+ task: updated,
1441
+ agent_identity: identity ? toPublicAgentIdentity(identity) : null,
1442
+ }, null, 2),
1443
+ },
1444
+ ],
784
1445
  };
785
1446
  }
786
1447
  catch (error) {
@@ -971,14 +1632,18 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
971
1632
  // -- send_message -----------------------------------------------------------
972
1633
  server.tool("send_message", "Send a message to a Let Agents Chat room.", {
973
1634
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
974
- sender: z.string().describe("Name identifying the sending agent (e.g. 'antigravity-agent')"),
1635
+ sender: z
1636
+ .string()
1637
+ .optional()
1638
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
975
1639
  text: z.string().describe("The message text to send"),
976
- }, async ({ room_id, sender, text }) => {
1640
+ }, async ({ room_id, sender: _sender, text }) => {
977
1641
  const targetRoomId = getTargetRoomId(room_id);
978
1642
  const targetProjectId = getFallbackProjectId();
979
1643
  if (!targetRoomId && !targetProjectId) {
980
1644
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
981
1645
  }
1646
+ const identity = await ensureAgentIdentity();
982
1647
  const message = await roomScopedApiCall({
983
1648
  room_id: targetRoomId,
984
1649
  project_id: targetProjectId,
@@ -986,7 +1651,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
986
1651
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
987
1652
  options: {
988
1653
  method: "POST",
989
- body: JSON.stringify({ sender, text }),
1654
+ body: JSON.stringify({ sender: identity.actor_label, text }),
990
1655
  },
991
1656
  });
992
1657
  touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
@@ -994,7 +1659,10 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
994
1659
  content: [
995
1660
  {
996
1661
  type: "text",
997
- text: JSON.stringify(message, null, 2),
1662
+ text: JSON.stringify({
1663
+ ...message,
1664
+ agent_identity: toPublicAgentIdentity(identity),
1665
+ }, null, 2),
998
1666
  },
999
1667
  ],
1000
1668
  };
@@ -1005,17 +1673,41 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
1005
1673
  }, async ({ room_id }) => {
1006
1674
  const targetRoomId = getTargetRoomId(room_id);
1007
1675
  const targetProjectId = getFallbackProjectId();
1008
- const result = await roomScopedApiCall({
1009
- room_id: targetRoomId,
1010
- project_id: targetProjectId,
1011
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
1012
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
1013
- });
1676
+ // Paginate through all pages to honor the "read all messages" contract
1677
+ const allMessages = [];
1678
+ let afterCursor;
1679
+ let roomIdFromResponse;
1680
+ for (;;) {
1681
+ const query = new URLSearchParams();
1682
+ if (afterCursor)
1683
+ query.set("after", afterCursor);
1684
+ const qs = query.toString();
1685
+ const result = await roomScopedApiCall({
1686
+ room_id: targetRoomId,
1687
+ project_id: targetProjectId,
1688
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
1689
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`,
1690
+ });
1691
+ roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
1692
+ const msgs = result.messages ?? [];
1693
+ allMessages.push(...msgs);
1694
+ if (!result.has_more || msgs.length === 0)
1695
+ break;
1696
+ // Use the last message ID as the cursor for the next page
1697
+ const lastMsg = msgs[msgs.length - 1];
1698
+ if (!lastMsg?.id)
1699
+ break;
1700
+ afterCursor = lastMsg.id;
1701
+ }
1702
+ const output = { messages: allMessages };
1703
+ if (roomIdFromResponse) {
1704
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1705
+ }
1014
1706
  return {
1015
1707
  content: [
1016
1708
  {
1017
1709
  type: "text",
1018
- text: JSON.stringify(result, null, 2),
1710
+ text: JSON.stringify(output, null, 2),
1019
1711
  },
1020
1712
  ],
1021
1713
  };
@@ -1043,21 +1735,49 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
1043
1735
  params.set("after", after_message_id);
1044
1736
  params.set("timeout", String(serverTimeout));
1045
1737
  const queryString = params.toString();
1046
- const result = await roomScopedApiCall({
1738
+ const firstResult = await roomScopedApiCall({
1047
1739
  room_id: targetRoomId,
1048
1740
  project_id: targetProjectId,
1049
1741
  room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
1050
1742
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
1051
1743
  options: { signal: AbortSignal.timeout(clientTimeout) },
1052
1744
  });
1745
+ const allMessages = [...(firstResult.messages ?? [])];
1746
+ const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
1747
+ // If the immediate response has more pages, paginate through them
1748
+ if (firstResult.has_more && allMessages.length > 0) {
1749
+ let afterCursor = allMessages[allMessages.length - 1]?.id;
1750
+ while (afterCursor) {
1751
+ const pageParams = new URLSearchParams();
1752
+ pageParams.set("after", afterCursor);
1753
+ const qs = pageParams.toString();
1754
+ const page = await roomScopedApiCall({
1755
+ room_id: targetRoomId,
1756
+ project_id: targetProjectId,
1757
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`,
1758
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`,
1759
+ });
1760
+ const msgs = page.messages ?? [];
1761
+ allMessages.push(...msgs);
1762
+ if (!page.has_more || msgs.length === 0)
1763
+ break;
1764
+ afterCursor = msgs[msgs.length - 1]?.id;
1765
+ if (!afterCursor)
1766
+ break;
1767
+ }
1768
+ }
1769
+ const output = { messages: allMessages };
1770
+ if (roomIdFromResponse) {
1771
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1772
+ }
1053
1773
  if (targetRoomId) {
1054
- touchRoomSession(targetRoomId, getLastMessageId(result));
1774
+ touchRoomSession(targetRoomId, getLastMessageId(output));
1055
1775
  }
1056
1776
  return {
1057
1777
  content: [
1058
1778
  {
1059
1779
  type: "text",
1060
- text: JSON.stringify(result, null, 2),
1780
+ text: JSON.stringify(output, null, 2),
1061
1781
  },
1062
1782
  ],
1063
1783
  };
@@ -1100,6 +1820,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
1100
1820
  account: storedAuth?.account ?? null,
1101
1821
  token_expires_at: storedAuth?.expires_at ?? null,
1102
1822
  pending_device_auth: pendingAuth,
1823
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1103
1824
  current_room: toPublicRoomState(currentRoom),
1104
1825
  saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
1105
1826
  detected_room_from_context: detectedRoom,
@@ -1222,6 +1943,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1222
1943
  if (result.status === "denied" || result.status === "expired") {
1223
1944
  clearPendingDeviceAuth();
1224
1945
  clearStoredAuth();
1946
+ currentAuthenticatedAccount = undefined;
1947
+ currentAuthenticatedAccountSource = null;
1948
+ currentAuthenticatedEnvToken = null;
1225
1949
  return {
1226
1950
  content: [
1227
1951
  {
@@ -1242,6 +1966,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1242
1966
  stored_at: new Date().toISOString(),
1243
1967
  source: "device_flow",
1244
1968
  });
1969
+ currentAuthenticatedAccount = storedAuth.account ?? undefined;
1970
+ currentAuthenticatedAccountSource = storedAuth.account ? "stored" : null;
1971
+ currentAuthenticatedEnvToken = null;
1245
1972
  let joinedRoom = null;
1246
1973
  const roomToJoin = room_id ||
1247
1974
  pendingAuth?.suggested_room_id ||
@@ -1252,8 +1979,8 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1252
1979
  const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
1253
1980
  const joined = await joinRoomIdentifier(roomToJoin, joinedVia);
1254
1981
  joinedRoom = joined.room;
1255
- await autoRegisterAgentIdentity();
1256
1982
  }
1983
+ const agentIdentity = await ensureAgentIdentity();
1257
1984
  return {
1258
1985
  content: [
1259
1986
  {
@@ -1264,6 +1991,7 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1264
1991
  account: storedAuth.account ?? null,
1265
1992
  expires_at: storedAuth.expires_at ?? null,
1266
1993
  auto_joined_room: joinedRoom,
1994
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1267
1995
  }, null, 2),
1268
1996
  },
1269
1997
  ],
@@ -1272,6 +2000,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1272
2000
  server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
1273
2001
  clearPendingDeviceAuth();
1274
2002
  clearStoredAuth();
2003
+ currentAuthenticatedAccount = undefined;
2004
+ currentAuthenticatedAccountSource = null;
2005
+ currentAuthenticatedEnvToken = null;
1275
2006
  return {
1276
2007
  content: [
1277
2008
  {
@@ -1304,7 +2035,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1304
2035
  }
1305
2036
  try {
1306
2037
  const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1307
- await autoRegisterAgentIdentity();
2038
+ const agentIdentity = await ensureAgentIdentity();
1308
2039
  return {
1309
2040
  content: [
1310
2041
  {
@@ -1314,7 +2045,8 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1314
2045
  rejoined_from_local_state: true,
1315
2046
  server_session_resumed: false,
1316
2047
  last_message_id_before_restart: savedRoom.last_message_id ?? null,
1317
- room: joined.room,
2048
+ room: toPublicRoomState(joined.room),
2049
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1318
2050
  }, null, 2),
1319
2051
  },
1320
2052
  ],
@@ -1378,6 +2110,7 @@ async function main() {
1378
2110
  const configRoom = getRoomFromConfig();
1379
2111
  if (configRoom) {
1380
2112
  await joinRoomIdentifier(configRoom, "config");
2113
+ await ensureAgentIdentity();
1381
2114
  console.error(`🏠 Auto-joined room '${configRoom}' (from .letagents.json)`);
1382
2115
  return;
1383
2116
  }
@@ -1385,6 +2118,7 @@ async function main() {
1385
2118
  const gitRoom = getGitRemoteIdentity();
1386
2119
  if (gitRoom) {
1387
2120
  await joinRoomIdentifier(gitRoom, "git-remote");
2121
+ await ensureAgentIdentity();
1388
2122
  console.error(`🏠 Auto-joined room '${gitRoom}' (inferred from git remote — consider adding a .letagents.json)`);
1389
2123
  return;
1390
2124
  }
@@ -1392,6 +2126,7 @@ async function main() {
1392
2126
  const savedCurrentRoom = getStoredCurrentRoom();
1393
2127
  if (savedCurrentRoom) {
1394
2128
  await joinRoomIdentifier(savedCurrentRoom.room_id, savedCurrentRoom.joined_via);
2129
+ await ensureAgentIdentity();
1395
2130
  console.error(`🏠 Rejoined saved room '${savedCurrentRoom.room_id}' (from local state)`);
1396
2131
  return;
1397
2132
  }