letagents 0.8.1 → 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;
@@ -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.
@@ -136,6 +571,9 @@ async function apiCall(path, options) {
136
571
  // Only clear on 401 (invalid/expired credential), NOT on 403
137
572
  // (valid credential but insufficient permissions, e.g., private repo access)
138
573
  clearStoredAuth();
574
+ currentAuthenticatedAccount = undefined;
575
+ currentAuthenticatedAccountSource = null;
576
+ currentAuthenticatedEnvToken = null;
139
577
  }
140
578
  throw new ApiError(res.status, body);
141
579
  }
@@ -222,6 +660,12 @@ function toPublicRoomResponse(response, fallbackRoomId) {
222
660
  room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
223
661
  };
224
662
  }
663
+ async function withAgentIdentity(payload) {
664
+ return {
665
+ ...payload,
666
+ agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
667
+ };
668
+ }
225
669
  function rememberRoom(state, lastMessageId) {
226
670
  currentRoom = state;
227
671
  saveRoomSession({
@@ -303,9 +747,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
303
747
  display_name: typeof response.display_name === "string" ? response.display_name : null,
304
748
  joined_via: joinedVia,
305
749
  }));
750
+ const agentIdentity = await ensureAgentIdentity();
306
751
  return {
307
752
  room,
308
- response: { ...response, room_id: joinedRoomId },
753
+ response: {
754
+ ...response,
755
+ room_id: joinedRoomId,
756
+ agent_identity: toPublicAgentIdentity(agentIdentity),
757
+ },
309
758
  };
310
759
  }
311
760
  catch (error) {
@@ -326,12 +775,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
326
775
  display_name: typeof project.display_name === "string" ? project.display_name : null,
327
776
  joined_via: joinedVia,
328
777
  }));
778
+ const agentIdentity = await ensureAgentIdentity();
329
779
  return {
330
780
  room,
331
781
  response: {
332
782
  ...project,
333
783
  room_id: legacyRoomId,
334
784
  project_id: typeof project.id === "string" ? project.id : null,
785
+ agent_identity: toPublicAgentIdentity(agentIdentity),
335
786
  },
336
787
  };
337
788
  }
@@ -352,12 +803,14 @@ async function joinRoomIdentifier(identifier, joinedVia) {
352
803
  display_name: typeof project.display_name === "string" ? project.display_name : null,
353
804
  joined_via: joinedVia,
354
805
  }));
806
+ const agentIdentity = await ensureAgentIdentity();
355
807
  return {
356
808
  room,
357
809
  response: {
358
810
  ...project,
359
811
  room_id: legacyRoomId,
360
812
  project_id: typeof project.id === "string" ? project.id : null,
813
+ agent_identity: toPublicAgentIdentity(agentIdentity),
361
814
  },
362
815
  };
363
816
  }
@@ -375,45 +828,28 @@ async function createInviteRoom() {
375
828
  display_name: typeof project.display_name === "string" ? project.display_name : null,
376
829
  joined_via: "join_code",
377
830
  }));
378
- await autoRegisterAgentIdentity();
831
+ const agentIdentity = await ensureAgentIdentity();
379
832
  return {
380
833
  room,
381
- response: toPublicRoomResponse(project, roomId),
834
+ response: {
835
+ ...toPublicRoomResponse(project, roomId),
836
+ agent_identity: toPublicAgentIdentity(agentIdentity),
837
+ },
382
838
  };
383
839
  }
384
840
  async function joinInviteCode(code) {
385
841
  const joined = await joinRoomIdentifier(code, "join_code");
386
- await autoRegisterAgentIdentity();
387
- return {
842
+ return withAgentIdentity({
388
843
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
389
844
  joined_via: "join_code",
390
- };
845
+ });
391
846
  }
392
847
  async function joinNamedRoom(name) {
393
848
  const joined = await joinRoomIdentifier(name, "join_room");
394
- await autoRegisterAgentIdentity();
395
- return {
849
+ return withAgentIdentity({
396
850
  ...toPublicRoomResponse(joined.response, joined.room.room_id),
397
851
  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
- }
852
+ });
417
853
  }
418
854
  // ---------------------------------------------------------------------------
419
855
  // MCP Server
@@ -536,6 +972,7 @@ server.tool("get_current_room", "Get information about the currently joined room
536
972
  ? {
537
973
  connected: true,
538
974
  ...toPublicRoomState(currentRoom),
975
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
539
976
  auth: getStoredAuth()
540
977
  ? {
541
978
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -599,13 +1036,16 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
599
1036
  "Use this to let other agents and humans know what you are currently doing, " +
600
1037
  "e.g. 'reviewing PR #2', 'waiting for tests', 'writing WISHLIST.md'. " +
601
1038
  "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')"),
1039
+ sender: z
1040
+ .string()
1041
+ .optional()
1042
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
603
1043
  status: z.string().describe("Short status description (e.g. 'reviewing PR #2', 'idle', 'thinking...')"),
604
1044
  room_id: z
605
1045
  .string()
606
1046
  .optional()
607
1047
  .describe("Canonical room ID. Defaults to the current room."),
608
- }, async ({ sender, status, room_id }) => {
1048
+ }, async ({ sender: _sender, status, room_id }) => {
609
1049
  const targetRoomId = getTargetRoomId(room_id);
610
1050
  const targetProjectId = getFallbackProjectId();
611
1051
  if (!targetRoomId && !targetProjectId) {
@@ -624,6 +1064,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
624
1064
  }
625
1065
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
626
1066
  // them from normal chat messages without changing the data model.
1067
+ const identity = await ensureAgentIdentity();
1068
+ const sender = identity.actor_label;
627
1069
  const statusText = `[status] ${status}`;
628
1070
  const message = await roomScopedApiCall({
629
1071
  room_id: targetRoomId,
@@ -644,6 +1086,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
644
1086
  success: true,
645
1087
  status_posted: status,
646
1088
  sender,
1089
+ agent_identity: toPublicAgentIdentity(identity),
647
1090
  message_id: typeof message.id === "string" ? message.id : null,
648
1091
  timestamp: typeof message.timestamp === "string" ? message.timestamp : null,
649
1092
  }, null, 2),
@@ -662,10 +1105,13 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
662
1105
  "work that needs to be done.", {
663
1106
  title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
664
1107
  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"),
1108
+ created_by: z
1109
+ .string()
1110
+ .optional()
1111
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
666
1112
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
667
1113
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
668
- }, async ({ title, description, created_by, source_message_id, room_id }) => {
1114
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id }) => {
669
1115
  const targetRoomId = getTargetRoomId(room_id);
670
1116
  const targetProjectId = getFallbackProjectId();
671
1117
  if (!targetRoomId && !targetProjectId) {
@@ -673,6 +1119,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
673
1119
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
674
1120
  };
675
1121
  }
1122
+ const identity = await ensureAgentIdentity();
676
1123
  const task = await roomScopedApiCall({
677
1124
  room_id: targetRoomId,
678
1125
  project_id: targetProjectId,
@@ -680,11 +1127,21 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
680
1127
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks`,
681
1128
  options: {
682
1129
  method: "POST",
683
- 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
+ }),
684
1136
  },
685
1137
  });
686
1138
  return {
687
- 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
+ ],
688
1145
  };
689
1146
  });
690
1147
  server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
@@ -721,9 +1178,12 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
721
1178
  "status. This sets the assignee to you and moves the status to 'assigned'. " +
722
1179
  "Do NOT claim proposed tasks — they need to be accepted first.", {
723
1180
  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'"),
1181
+ assignee: z
1182
+ .string()
1183
+ .optional()
1184
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
725
1185
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
726
- }, async ({ task_id, assignee, room_id }) => {
1186
+ }, async ({ task_id, assignee: _assignee, room_id }) => {
727
1187
  const targetRoomId = getTargetRoomId(room_id);
728
1188
  const targetProjectId = getFallbackProjectId();
729
1189
  if (!targetRoomId && !targetProjectId) {
@@ -732,6 +1192,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
732
1192
  };
733
1193
  }
734
1194
  try {
1195
+ const identity = await ensureAgentIdentity();
735
1196
  const updated = await roomScopedApiCall({
736
1197
  room_id: targetRoomId,
737
1198
  project_id: targetProjectId,
@@ -739,11 +1200,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
739
1200
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
740
1201
  options: {
741
1202
  method: "PATCH",
742
- body: JSON.stringify({ status: "assigned", assignee }),
1203
+ body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
743
1204
  },
744
1205
  });
745
1206
  return {
746
- 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
+ ],
747
1213
  };
748
1214
  }
749
1215
  catch (error) {
@@ -757,7 +1223,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
757
1223
  "but NOT proposed → in_progress).", {
758
1224
  task_id: z.string().describe("The task ID to update"),
759
1225
  status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
760
- 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."),
761
1230
  pr_url: z.string().optional().describe("PR URL to link to the task"),
762
1231
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
763
1232
  }, async ({ task_id, status, assignee, pr_url, room_id }) => {
@@ -769,6 +1238,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
769
1238
  };
770
1239
  }
771
1240
  try {
1241
+ const identity = status === "assigned" && !assignee
1242
+ ? await ensureAgentIdentity()
1243
+ : null;
772
1244
  const updated = await roomScopedApiCall({
773
1245
  room_id: targetRoomId,
774
1246
  project_id: targetProjectId,
@@ -776,11 +1248,24 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
776
1248
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
777
1249
  options: {
778
1250
  method: "PATCH",
779
- body: JSON.stringify({ status, assignee, pr_url }),
1251
+ body: JSON.stringify({
1252
+ status,
1253
+ assignee: assignee ?? identity?.actor_label,
1254
+ pr_url,
1255
+ }),
780
1256
  },
781
1257
  });
782
1258
  return {
783
- 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
+ ],
784
1269
  };
785
1270
  }
786
1271
  catch (error) {
@@ -971,14 +1456,18 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
971
1456
  // -- send_message -----------------------------------------------------------
972
1457
  server.tool("send_message", "Send a message to a Let Agents Chat room.", {
973
1458
  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')"),
1459
+ sender: z
1460
+ .string()
1461
+ .optional()
1462
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
975
1463
  text: z.string().describe("The message text to send"),
976
- }, async ({ room_id, sender, text }) => {
1464
+ }, async ({ room_id, sender: _sender, text }) => {
977
1465
  const targetRoomId = getTargetRoomId(room_id);
978
1466
  const targetProjectId = getFallbackProjectId();
979
1467
  if (!targetRoomId && !targetProjectId) {
980
1468
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
981
1469
  }
1470
+ const identity = await ensureAgentIdentity();
982
1471
  const message = await roomScopedApiCall({
983
1472
  room_id: targetRoomId,
984
1473
  project_id: targetProjectId,
@@ -986,7 +1475,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
986
1475
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
987
1476
  options: {
988
1477
  method: "POST",
989
- body: JSON.stringify({ sender, text }),
1478
+ body: JSON.stringify({ sender: identity.actor_label, text }),
990
1479
  },
991
1480
  });
992
1481
  touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
@@ -994,7 +1483,10 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
994
1483
  content: [
995
1484
  {
996
1485
  type: "text",
997
- text: JSON.stringify(message, null, 2),
1486
+ text: JSON.stringify({
1487
+ ...message,
1488
+ agent_identity: toPublicAgentIdentity(identity),
1489
+ }, null, 2),
998
1490
  },
999
1491
  ],
1000
1492
  };
@@ -1100,6 +1592,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
1100
1592
  account: storedAuth?.account ?? null,
1101
1593
  token_expires_at: storedAuth?.expires_at ?? null,
1102
1594
  pending_device_auth: pendingAuth,
1595
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
1103
1596
  current_room: toPublicRoomState(currentRoom),
1104
1597
  saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
1105
1598
  detected_room_from_context: detectedRoom,
@@ -1222,6 +1715,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1222
1715
  if (result.status === "denied" || result.status === "expired") {
1223
1716
  clearPendingDeviceAuth();
1224
1717
  clearStoredAuth();
1718
+ currentAuthenticatedAccount = undefined;
1719
+ currentAuthenticatedAccountSource = null;
1720
+ currentAuthenticatedEnvToken = null;
1225
1721
  return {
1226
1722
  content: [
1227
1723
  {
@@ -1242,6 +1738,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1242
1738
  stored_at: new Date().toISOString(),
1243
1739
  source: "device_flow",
1244
1740
  });
1741
+ currentAuthenticatedAccount = storedAuth.account ?? undefined;
1742
+ currentAuthenticatedAccountSource = storedAuth.account ? "stored" : null;
1743
+ currentAuthenticatedEnvToken = null;
1245
1744
  let joinedRoom = null;
1246
1745
  const roomToJoin = room_id ||
1247
1746
  pendingAuth?.suggested_room_id ||
@@ -1252,8 +1751,8 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1252
1751
  const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
1253
1752
  const joined = await joinRoomIdentifier(roomToJoin, joinedVia);
1254
1753
  joinedRoom = joined.room;
1255
- await autoRegisterAgentIdentity();
1256
1754
  }
1755
+ const agentIdentity = await ensureAgentIdentity();
1257
1756
  return {
1258
1757
  content: [
1259
1758
  {
@@ -1264,6 +1763,7 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1264
1763
  account: storedAuth.account ?? null,
1265
1764
  expires_at: storedAuth.expires_at ?? null,
1266
1765
  auto_joined_room: joinedRoom,
1766
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1267
1767
  }, null, 2),
1268
1768
  },
1269
1769
  ],
@@ -1272,6 +1772,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1272
1772
  server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
1273
1773
  clearPendingDeviceAuth();
1274
1774
  clearStoredAuth();
1775
+ currentAuthenticatedAccount = undefined;
1776
+ currentAuthenticatedAccountSource = null;
1777
+ currentAuthenticatedEnvToken = null;
1275
1778
  return {
1276
1779
  content: [
1277
1780
  {
@@ -1304,7 +1807,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1304
1807
  }
1305
1808
  try {
1306
1809
  const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
1307
- await autoRegisterAgentIdentity();
1810
+ const agentIdentity = await ensureAgentIdentity();
1308
1811
  return {
1309
1812
  content: [
1310
1813
  {
@@ -1315,6 +1818,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1315
1818
  server_session_resumed: false,
1316
1819
  last_message_id_before_restart: savedRoom.last_message_id ?? null,
1317
1820
  room: joined.room,
1821
+ agent_identity: toPublicAgentIdentity(agentIdentity),
1318
1822
  }, null, 2),
1319
1823
  },
1320
1824
  ],
@@ -1378,6 +1882,7 @@ async function main() {
1378
1882
  const configRoom = getRoomFromConfig();
1379
1883
  if (configRoom) {
1380
1884
  await joinRoomIdentifier(configRoom, "config");
1885
+ await ensureAgentIdentity();
1381
1886
  console.error(`🏠 Auto-joined room '${configRoom}' (from .letagents.json)`);
1382
1887
  return;
1383
1888
  }
@@ -1385,6 +1890,7 @@ async function main() {
1385
1890
  const gitRoom = getGitRemoteIdentity();
1386
1891
  if (gitRoom) {
1387
1892
  await joinRoomIdentifier(gitRoom, "git-remote");
1893
+ await ensureAgentIdentity();
1388
1894
  console.error(`🏠 Auto-joined room '${gitRoom}' (inferred from git remote — consider adding a .letagents.json)`);
1389
1895
  return;
1390
1896
  }
@@ -1392,6 +1898,7 @@ async function main() {
1392
1898
  const savedCurrentRoom = getStoredCurrentRoom();
1393
1899
  if (savedCurrentRoom) {
1394
1900
  await joinRoomIdentifier(savedCurrentRoom.room_id, savedCurrentRoom.joined_via);
1901
+ await ensureAgentIdentity();
1395
1902
  console.error(`🏠 Rejoined saved room '${savedCurrentRoom.room_id}' (from local state)`);
1396
1903
  return;
1397
1904
  }
@@ -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.1",
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": {