letagents 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Agent codenames — extracted from server.ts per Emmy's directive.
3
+ *
4
+ * Each agent instance gets a two-word codename (e.g. "River Valley")
5
+ * deterministically derived from its runtime key via SHA-256 hashing.
6
+ */
7
+ import { createHash } from "crypto";
8
+ import { toTitleCaseCodename } from "../shared/agent-identity.js";
9
+ // ---------------------------------------------------------------------------
10
+ // Codenames
11
+ // ---------------------------------------------------------------------------
12
+ export const AGENT_CODENAMES = [
13
+ "amber",
14
+ "anchor",
15
+ "autumn",
16
+ "badger",
17
+ "bay",
18
+ "bear",
19
+ "brook",
20
+ "calm",
21
+ "canyon",
22
+ "cedar",
23
+ "clear",
24
+ "cloud",
25
+ "comet",
26
+ "copper",
27
+ "creek",
28
+ "crisp",
29
+ "crest",
30
+ "dawn",
31
+ "delta",
32
+ "dune",
33
+ "ember",
34
+ "falcon",
35
+ "fern",
36
+ "field",
37
+ "firefly",
38
+ "fjord",
39
+ "forest",
40
+ "fox",
41
+ "garden",
42
+ "glade",
43
+ "golden",
44
+ "granite",
45
+ "grove",
46
+ "harbor",
47
+ "hawk",
48
+ "hollow",
49
+ "indigo",
50
+ "ivory",
51
+ "jade",
52
+ "juniper",
53
+ "lagoon",
54
+ "lake",
55
+ "lantern",
56
+ "leaf",
57
+ "lively",
58
+ "lunar",
59
+ "lynx",
60
+ "maple",
61
+ "marsh",
62
+ "meadow",
63
+ "mesa",
64
+ "misty",
65
+ "moon",
66
+ "morrow",
67
+ "moss",
68
+ "noble",
69
+ "oak",
70
+ "olive",
71
+ "opal",
72
+ "otter",
73
+ "owl",
74
+ "peak",
75
+ "pearl",
76
+ "pine",
77
+ "quiet",
78
+ "raven",
79
+ "reef",
80
+ "ridge",
81
+ "river",
82
+ "rook",
83
+ "sage",
84
+ "scarlet",
85
+ "shore",
86
+ "silver",
87
+ "sky",
88
+ "solar",
89
+ "sparrow",
90
+ "spring",
91
+ "star",
92
+ "stone",
93
+ "storm",
94
+ "summit",
95
+ "sun",
96
+ "sunlit",
97
+ "swift",
98
+ "thicket",
99
+ "tidal",
100
+ "timber",
101
+ "trail",
102
+ "valley",
103
+ "verdant",
104
+ "vista",
105
+ "warm",
106
+ "wave",
107
+ "west",
108
+ "wild",
109
+ "willow",
110
+ "wind",
111
+ "winter",
112
+ "wolf",
113
+ "wood",
114
+ "wren",
115
+ ];
116
+ export const AGENT_CODENAME_SPACE = AGENT_CODENAMES.length * AGENT_CODENAMES.length;
117
+ // ---------------------------------------------------------------------------
118
+ // Slug helpers
119
+ // ---------------------------------------------------------------------------
120
+ export function normalizeSlugSegment(input, fallback) {
121
+ const normalized = input
122
+ .normalize("NFKD")
123
+ .replace(/[^\x00-\x7F]/g, "")
124
+ .toLowerCase()
125
+ .replace(/[^a-z0-9]+/g, "-")
126
+ .replace(/^-+|-+$/g, "")
127
+ .replace(/-{2,}/g, "-");
128
+ return normalized || fallback;
129
+ }
130
+ export function normalizeAgentBaseName(input) {
131
+ return normalizeSlugSegment(input, "agent").replace(/-agent$/, "") || "agent";
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Codename derivation
135
+ // ---------------------------------------------------------------------------
136
+ export function hashStringToIndex(value, modulo) {
137
+ const digest = createHash("sha256").update(value).digest();
138
+ return digest.readUInt32BE(0) % modulo;
139
+ }
140
+ export function codenameFromIndex(index) {
141
+ const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
142
+ const firstIndex = Math.floor(normalizedIndex / AGENT_CODENAMES.length);
143
+ const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
144
+ const first = AGENT_CODENAMES[firstIndex];
145
+ const second = AGENT_CODENAMES[secondIndex];
146
+ return {
147
+ name: normalizeAgentBaseName(`${first}-${second}`),
148
+ display_name: `${toTitleCaseCodename(first)} ${toTitleCaseCodename(second)}`,
149
+ };
150
+ }
151
+ export function pickLocalCodename(runtimeKey, offset = 0) {
152
+ const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
153
+ return codenameFromIndex(index);
154
+ }
@@ -157,6 +157,12 @@ export function getStoredAgentIdentity(identityKey) {
157
157
  if (scoped) {
158
158
  return scoped;
159
159
  }
160
+ // For UUID-based identity keys, do NOT fall back to the shared global
161
+ // agent_identity — that would cause different processes to inherit each
162
+ // other's identity. Only legacy (non-UUID) keys use the global fallback.
163
+ if (identityKey.startsWith("instance:")) {
164
+ return null;
165
+ }
160
166
  }
161
167
  return state.agent_identity ?? null;
162
168
  }
@@ -2,7 +2,7 @@
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
+ import { createHash, randomUUID } from "crypto";
6
6
  import { writeFileSync, existsSync } from "fs";
7
7
  import { userInfo } from "os";
8
8
  import { join, dirname } from "path";
@@ -10,6 +10,7 @@ import { execSync } from "child_process";
10
10
  import { SseClient } from "./sse-client.js";
11
11
  import { getRoomFromConfig } from "./config-reader.js";
12
12
  import { getGitRemoteIdentity } from "./git-remote.js";
13
+ import { AGENT_CODENAME_SPACE, normalizeSlugSegment, normalizeAgentBaseName, pickLocalCodename, } from "./codenames.js";
13
14
  import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
14
15
  import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
15
16
  import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
@@ -28,8 +29,9 @@ const AGENT_DISPLAY_NAME = (process.env.LETAGENTS_AGENT_DISPLAY_NAME || "").trim
28
29
  const AGENT_IDE_LABEL = (process.env.LETAGENTS_AGENT_IDE || process.env.AGENT_IDE || "").trim();
29
30
  const AGENT_OWNER_LABEL = (process.env.LETAGENTS_AGENT_OWNER_LABEL || "").trim();
30
31
  const EXPLICIT_AGENT_IDENTITY_KEY = getExplicitAgentIdentityStorageKey();
32
+ const AGENT_INSTANCE_UUID = randomUUID();
31
33
  currentAgentIdentityKey =
32
- EXPLICIT_AGENT_IDENTITY_KEY ?? getFallbackAgentIdentityNamespaceKey(detectAgentIdeLabel());
34
+ EXPLICIT_AGENT_IDENTITY_KEY ?? `instance:${AGENT_INSTANCE_UUID}`;
33
35
  currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
34
36
  // ---------------------------------------------------------------------------
35
37
  // Helpers
@@ -47,130 +49,15 @@ function readCommandOutput(command, cwd = process.cwd()) {
47
49
  return null;
48
50
  }
49
51
  }
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
- }
52
+ // normalizeSlugSegment, normalizeAgentBaseName, AGENT_CODENAMES,
53
+ // AGENT_CODENAME_SPACE, hashStringToIndex, codenameFromIndex,
54
+ // pickLocalCodename — all imported from ./codenames.js
63
55
  function isCodexRuntime() {
64
56
  return Boolean(process.env.CODEX_THREAD_ID ||
65
57
  process.env.CODEX_SHELL ||
66
58
  process.env.CODEX_CI ||
67
59
  process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE);
68
60
  }
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
61
  const AGENT_IDENTITY_SLOT_SUFFIX = ":slot:";
175
62
  function getExplicitAgentIdentityStorageKey() {
176
63
  const runtimeSignals = [
@@ -188,21 +75,6 @@ function getExplicitAgentIdentityStorageKey() {
188
75
  function getFallbackAgentIdentityNamespaceKey(ideLabel) {
189
76
  return `cwd:${process.cwd()}:ide:${normalizeAgentBaseName(ideLabel)}`;
190
77
  }
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
78
  function detectAgentIdeLabel() {
207
79
  if (AGENT_IDE_LABEL) {
208
80
  return toTitleCaseCodename(AGENT_IDE_LABEL);
@@ -325,9 +197,12 @@ function ensureAgentIdentityKey(ideLabel) {
325
197
  currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
326
198
  return currentAgentIdentityKey;
327
199
  }
328
- const claimed = claimFallbackIdentityKey(getFallbackAgentIdentityNamespaceKey(ideLabel));
329
- currentAgentIdentityKey = claimed.identityKey;
330
- currentAgentIdentity = claimed.identity;
200
+ // v1: Use per-instance UUID for guaranteed distinct identity.
201
+ // Each MCP process gets a unique UUID — no slot/PID coordination needed.
202
+ // Trade-off: process restart = new identity (accepted for v1).
203
+ const uuidKey = `instance:${AGENT_INSTANCE_UUID}`;
204
+ currentAgentIdentityKey = uuidKey;
205
+ currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
331
206
  return currentAgentIdentityKey;
332
207
  }
333
208
  async function getAuthenticatedAgentDirectory() {
@@ -372,10 +247,7 @@ function resolveExplicitAgentIdentity() {
372
247
  }
373
248
  return null;
374
249
  }
375
- function pickLocalCodename(runtimeKey, offset = 0) {
376
- const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
377
- return codenameFromIndex(index);
378
- }
250
+ // pickLocalCodename imported from ./codenames.js
379
251
  async function resolveAgentName(authAvailable, identityKey) {
380
252
  const explicit = resolveExplicitAgentIdentity();
381
253
  if (explicit) {
@@ -509,6 +381,7 @@ function toPublicAgentIdentity(identity) {
509
381
  actor_label: identity.actor_label,
510
382
  canonical_key: identity.canonical_key ?? null,
511
383
  runtime_key: identity.runtime_key ?? null,
384
+ agent_instance_id: AGENT_INSTANCE_UUID,
512
385
  source: identity.source,
513
386
  };
514
387
  }
@@ -1991,7 +1864,11 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
1991
1864
  account: storedAuth.account ?? null,
1992
1865
  expires_at: storedAuth.expires_at ?? null,
1993
1866
  auto_joined_room: joinedRoom,
1867
+ ...(joinedRoom?.room_id
1868
+ ? withCanonicalRoomLink(joinedRoom.room_id, {})
1869
+ : {}),
1994
1870
  agent_identity: toPublicAgentIdentity(agentIdentity),
1871
+ hint: "You can change your agent's display name anytime using the set_agent_name tool.",
1995
1872
  }, null, 2),
1996
1873
  },
1997
1874
  ],
@@ -2015,6 +1892,95 @@ server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token an
2015
1892
  ],
2016
1893
  };
2017
1894
  });
1895
+ server.tool("set_agent_name", "Set or change the agent's display name. The agent will be known by this name in the room. Use this to pick a custom name instead of the auto-generated codename.", {
1896
+ name: z
1897
+ .string()
1898
+ .min(2)
1899
+ .max(64)
1900
+ .describe("The desired display name for this agent (2-64 characters)."),
1901
+ }, async ({ name: desiredName }) => {
1902
+ const trimmedName = desiredName.trim();
1903
+ if (trimmedName.length < 2 || trimmedName.length > 64) {
1904
+ return {
1905
+ content: [
1906
+ {
1907
+ type: "text",
1908
+ text: JSON.stringify({ success: false, error: "Name must be between 2 and 64 characters." }, null, 2),
1909
+ },
1910
+ ],
1911
+ };
1912
+ }
1913
+ const authAvailable = Boolean(getLetagentsToken());
1914
+ if (!authAvailable) {
1915
+ return {
1916
+ content: [
1917
+ {
1918
+ type: "text",
1919
+ text: JSON.stringify({ success: false, error: "Authentication required. Run start_device_auth first." }, null, 2),
1920
+ },
1921
+ ],
1922
+ };
1923
+ }
1924
+ const slugName = normalizeAgentBaseName(trimmedName);
1925
+ try {
1926
+ const owner = await resolveOwnerContext();
1927
+ const registered = await apiCall("/agents", {
1928
+ method: "POST",
1929
+ body: JSON.stringify({
1930
+ name: slugName,
1931
+ display_name: trimmedName,
1932
+ owner_label: owner.label,
1933
+ }),
1934
+ });
1935
+ const ideLabel = detectAgentIdeLabel();
1936
+ const ownerAttribution = formatOwnerAttribution(owner.label);
1937
+ const actorLabel = buildAgentActorLabel({
1938
+ display_name: trimmedName,
1939
+ owner_label: owner.label,
1940
+ ide_label: ideLabel,
1941
+ });
1942
+ const updatedIdentity = {
1943
+ name: slugName,
1944
+ display_name: trimmedName,
1945
+ owner_label: owner.label,
1946
+ owner_attribution: ownerAttribution,
1947
+ ide_label: ideLabel,
1948
+ actor_label: actorLabel,
1949
+ canonical_key: typeof registered.canonical_key === "string"
1950
+ ? registered.canonical_key
1951
+ : owner.login ? `${owner.login}/${slugName}` : null,
1952
+ runtime_key: currentAgentIdentityKey,
1953
+ source: "api",
1954
+ resolved_at: new Date().toISOString(),
1955
+ };
1956
+ currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
1957
+ return {
1958
+ content: [
1959
+ {
1960
+ type: "text",
1961
+ text: JSON.stringify({
1962
+ success: true,
1963
+ message: `Agent name changed to "${trimmedName}".`,
1964
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity),
1965
+ }, null, 2),
1966
+ },
1967
+ ],
1968
+ };
1969
+ }
1970
+ catch (error) {
1971
+ return {
1972
+ content: [
1973
+ {
1974
+ type: "text",
1975
+ text: JSON.stringify({
1976
+ success: false,
1977
+ error: `Failed to set name: ${error instanceof Error ? error.message : String(error)}`,
1978
+ }, null, 2),
1979
+ },
1980
+ ],
1981
+ };
1982
+ }
1983
+ });
2018
1984
  server.tool("resume_room_session", "Rejoin the last locally saved room context, or a specific saved room, after a restart. This recreates participation in the room; it does not preserve a prior server-side session ID.", {
2019
1985
  room_id: z
2020
1986
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",