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.
- package/dist/mcp/local-state.js +21 -0
- package/dist/mcp/server.js +554 -47
- package/dist/shared/agent-identity.js +104 -0
- package/package.json +2 -1
package/dist/mcp/local-state.js
CHANGED
|
@@ -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;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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: {
|
|
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
|
|
831
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
379
832
|
return {
|
|
380
833
|
room,
|
|
381
|
-
response:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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: [
|
|
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
|
|
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: [
|
|
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
|
|
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({
|
|
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: [
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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": {
|