letagents 0.8.0 â 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/local-state.js +22 -0
- package/dist/mcp/server.js +703 -86
- 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;
|
|
@@ -103,6 +124,7 @@ export function saveRoomSession(input) {
|
|
|
103
124
|
room_id: input.room_id,
|
|
104
125
|
project_id: input.project_id ?? existing?.project_id ?? null,
|
|
105
126
|
code: input.code ?? existing?.code ?? null,
|
|
127
|
+
display_name: input.display_name ?? existing?.display_name ?? null,
|
|
106
128
|
joined_via: input.joined_via,
|
|
107
129
|
joined_at: existing?.joined_at ?? now,
|
|
108
130
|
last_seen_at: now,
|
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.
|
|
@@ -64,6 +499,16 @@ class ApiError extends Error {
|
|
|
64
499
|
this.body = body;
|
|
65
500
|
}
|
|
66
501
|
}
|
|
502
|
+
class RepoRoomAuthRequiredError extends Error {
|
|
503
|
+
roomId;
|
|
504
|
+
pendingAuth;
|
|
505
|
+
constructor(roomId, pendingAuth) {
|
|
506
|
+
super(`Repo room '${roomId}' requires authentication. Device flow started: open ${pendingAuth.verification_uri} and enter code ${pendingAuth.user_code}, then run poll_device_auth.`);
|
|
507
|
+
this.name = "RepoRoomAuthRequiredError";
|
|
508
|
+
this.roomId = roomId;
|
|
509
|
+
this.pendingAuth = pendingAuth;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
67
512
|
function getLetagentsToken() {
|
|
68
513
|
return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
|
|
69
514
|
}
|
|
@@ -79,6 +524,34 @@ function isMissingRouteError(error) {
|
|
|
79
524
|
(error.status === 404 || error.status === 405) &&
|
|
80
525
|
/Cannot (GET|POST|PATCH)|Not Found|Cannot GET \/rooms|Cannot POST \/rooms/i.test(error.body));
|
|
81
526
|
}
|
|
527
|
+
function parseApiErrorPayload(error) {
|
|
528
|
+
if (!(error instanceof ApiError)) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const parsed = JSON.parse(error.body);
|
|
533
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function resolveApiPath(urlOrPath) {
|
|
540
|
+
if (!urlOrPath) {
|
|
541
|
+
return "/auth/device/start";
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const parsed = new URL(urlOrPath, `${API_URL}/`);
|
|
545
|
+
const apiBase = new URL(`${API_URL}/`);
|
|
546
|
+
if (parsed.origin !== apiBase.origin) {
|
|
547
|
+
return "/auth/device/start";
|
|
548
|
+
}
|
|
549
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return "/auth/device/start";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
82
555
|
async function apiCall(path, options) {
|
|
83
556
|
const headers = {
|
|
84
557
|
"Content-Type": "application/json",
|
|
@@ -98,6 +571,9 @@ async function apiCall(path, options) {
|
|
|
98
571
|
// Only clear on 401 (invalid/expired credential), NOT on 403
|
|
99
572
|
// (valid credential but insufficient permissions, e.g., private repo access)
|
|
100
573
|
clearStoredAuth();
|
|
574
|
+
currentAuthenticatedAccount = undefined;
|
|
575
|
+
currentAuthenticatedAccountSource = null;
|
|
576
|
+
currentAuthenticatedEnvToken = null;
|
|
101
577
|
}
|
|
102
578
|
throw new ApiError(res.status, body);
|
|
103
579
|
}
|
|
@@ -107,11 +583,48 @@ async function apiCall(path, options) {
|
|
|
107
583
|
}
|
|
108
584
|
return JSON.parse(body);
|
|
109
585
|
}
|
|
586
|
+
async function startPendingDeviceAuth(roomId, deviceFlowUrl) {
|
|
587
|
+
const existing = getPendingDeviceAuth();
|
|
588
|
+
if (existing?.suggested_room_id === roomId) {
|
|
589
|
+
return existing;
|
|
590
|
+
}
|
|
591
|
+
const response = await apiCall(resolveApiPath(deviceFlowUrl), {
|
|
592
|
+
method: "POST",
|
|
593
|
+
});
|
|
594
|
+
return setPendingDeviceAuth({
|
|
595
|
+
request_id: response.request_id,
|
|
596
|
+
user_code: response.user_code,
|
|
597
|
+
verification_uri: response.verification_uri,
|
|
598
|
+
interval_seconds: response.interval,
|
|
599
|
+
expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
|
|
600
|
+
started_at: new Date().toISOString(),
|
|
601
|
+
suggested_room_id: roomId,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function maybeHandleRepoRoomAuthRequired(error, roomId) {
|
|
605
|
+
const payload = parseApiErrorPayload(error);
|
|
606
|
+
if (!(error instanceof ApiError) || error.status !== 401 || payload?.error !== "auth_required") {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const pendingAuth = await startPendingDeviceAuth(roomId, typeof payload.device_flow_url === "string" ? payload.device_flow_url : undefined);
|
|
610
|
+
throw new RepoRoomAuthRequiredError(roomId, pendingAuth);
|
|
611
|
+
}
|
|
612
|
+
function toRepoRoomAuthRequiredResult(error) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: "auth_required",
|
|
616
|
+
room_id: error.roomId,
|
|
617
|
+
next_step: "poll_device_auth",
|
|
618
|
+
pending_device_auth: error.pendingAuth,
|
|
619
|
+
message: error.message,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
110
622
|
function toRoomState(input) {
|
|
111
623
|
return {
|
|
112
624
|
room_id: input.room_id,
|
|
113
625
|
project_id: input.project_id ?? null,
|
|
114
626
|
code: input.code ?? null,
|
|
627
|
+
display_name: input.display_name ?? null,
|
|
115
628
|
joined_via: input.joined_via,
|
|
116
629
|
};
|
|
117
630
|
}
|
|
@@ -122,6 +635,7 @@ function toPublicRoomState(state) {
|
|
|
122
635
|
return {
|
|
123
636
|
room_id: state.room_id,
|
|
124
637
|
code: state.code ?? null,
|
|
638
|
+
display_name: state.display_name ?? null,
|
|
125
639
|
joined_via: state.joined_via,
|
|
126
640
|
};
|
|
127
641
|
}
|
|
@@ -132,6 +646,7 @@ function toPublicStoredRoomSession(session) {
|
|
|
132
646
|
return {
|
|
133
647
|
room_id: session.room_id,
|
|
134
648
|
code: session.code ?? null,
|
|
649
|
+
display_name: session.display_name ?? null,
|
|
135
650
|
joined_via: session.joined_via,
|
|
136
651
|
joined_at: session.joined_at,
|
|
137
652
|
last_seen_at: session.last_seen_at,
|
|
@@ -145,12 +660,19 @@ function toPublicRoomResponse(response, fallbackRoomId) {
|
|
|
145
660
|
room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
|
|
146
661
|
};
|
|
147
662
|
}
|
|
663
|
+
async function withAgentIdentity(payload) {
|
|
664
|
+
return {
|
|
665
|
+
...payload,
|
|
666
|
+
agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
148
669
|
function rememberRoom(state, lastMessageId) {
|
|
149
670
|
currentRoom = state;
|
|
150
671
|
saveRoomSession({
|
|
151
672
|
room_id: state.room_id,
|
|
152
673
|
project_id: state.project_id ?? null,
|
|
153
674
|
code: state.code ?? null,
|
|
675
|
+
display_name: state.display_name ?? null,
|
|
154
676
|
joined_via: state.joined_via,
|
|
155
677
|
last_message_id: lastMessageId,
|
|
156
678
|
});
|
|
@@ -192,6 +714,7 @@ async function roomScopedApiCall(input) {
|
|
|
192
714
|
return result;
|
|
193
715
|
}
|
|
194
716
|
catch (error) {
|
|
717
|
+
await maybeHandleRepoRoomAuthRequired(error, input.room_id);
|
|
195
718
|
if (!input.project_id || !isMissingRouteError(error)) {
|
|
196
719
|
throw error;
|
|
197
720
|
}
|
|
@@ -221,14 +744,21 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
221
744
|
: looksLikeInviteCode(joinedRoomId)
|
|
222
745
|
? joinedRoomId
|
|
223
746
|
: null,
|
|
747
|
+
display_name: typeof response.display_name === "string" ? response.display_name : null,
|
|
224
748
|
joined_via: joinedVia,
|
|
225
749
|
}));
|
|
750
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
226
751
|
return {
|
|
227
752
|
room,
|
|
228
|
-
response: {
|
|
753
|
+
response: {
|
|
754
|
+
...response,
|
|
755
|
+
room_id: joinedRoomId,
|
|
756
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
757
|
+
},
|
|
229
758
|
};
|
|
230
759
|
}
|
|
231
760
|
catch (error) {
|
|
761
|
+
await maybeHandleRepoRoomAuthRequired(error, roomId);
|
|
232
762
|
if (!isMissingRouteError(error)) {
|
|
233
763
|
throw error;
|
|
234
764
|
}
|
|
@@ -242,14 +772,17 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
242
772
|
room_id: legacyRoomId,
|
|
243
773
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
244
774
|
code: typeof project.code === "string" ? project.code : legacyRoomId,
|
|
775
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
245
776
|
joined_via: joinedVia,
|
|
246
777
|
}));
|
|
778
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
247
779
|
return {
|
|
248
780
|
room,
|
|
249
781
|
response: {
|
|
250
782
|
...project,
|
|
251
783
|
room_id: legacyRoomId,
|
|
252
784
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
785
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
253
786
|
},
|
|
254
787
|
};
|
|
255
788
|
}
|
|
@@ -267,14 +800,17 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
267
800
|
: looksLikeInviteCode(legacyRoomId)
|
|
268
801
|
? legacyRoomId
|
|
269
802
|
: null,
|
|
803
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
270
804
|
joined_via: joinedVia,
|
|
271
805
|
}));
|
|
806
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
272
807
|
return {
|
|
273
808
|
room,
|
|
274
809
|
response: {
|
|
275
810
|
...project,
|
|
276
811
|
room_id: legacyRoomId,
|
|
277
812
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
813
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
278
814
|
},
|
|
279
815
|
};
|
|
280
816
|
}
|
|
@@ -289,47 +825,31 @@ async function createInviteRoom() {
|
|
|
289
825
|
room_id: roomId,
|
|
290
826
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
291
827
|
code: typeof project.code === "string" ? project.code : roomId,
|
|
828
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
292
829
|
joined_via: "join_code",
|
|
293
830
|
}));
|
|
294
|
-
await
|
|
831
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
295
832
|
return {
|
|
296
833
|
room,
|
|
297
|
-
response:
|
|
834
|
+
response: {
|
|
835
|
+
...toPublicRoomResponse(project, roomId),
|
|
836
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
837
|
+
},
|
|
298
838
|
};
|
|
299
839
|
}
|
|
300
840
|
async function joinInviteCode(code) {
|
|
301
841
|
const joined = await joinRoomIdentifier(code, "join_code");
|
|
302
|
-
|
|
303
|
-
return {
|
|
842
|
+
return withAgentIdentity({
|
|
304
843
|
...toPublicRoomResponse(joined.response, joined.room.room_id),
|
|
305
844
|
joined_via: "join_code",
|
|
306
|
-
};
|
|
845
|
+
});
|
|
307
846
|
}
|
|
308
847
|
async function joinNamedRoom(name) {
|
|
309
848
|
const joined = await joinRoomIdentifier(name, "join_room");
|
|
310
|
-
|
|
311
|
-
return {
|
|
849
|
+
return withAgentIdentity({
|
|
312
850
|
...toPublicRoomResponse(joined.response, joined.room.room_id),
|
|
313
851
|
joined_via: "join_room",
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
async function autoRegisterAgentIdentity() {
|
|
317
|
-
if (!AGENT_NAME || !currentRoom) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
await apiCall("/agents", {
|
|
322
|
-
method: "POST",
|
|
323
|
-
body: JSON.stringify({
|
|
324
|
-
name: AGENT_NAME,
|
|
325
|
-
display_name: AGENT_DISPLAY_NAME || AGENT_NAME,
|
|
326
|
-
owner_label: AGENT_OWNER_LABEL || undefined,
|
|
327
|
-
}),
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
console.error("Agent identity registration failed:", error);
|
|
332
|
-
}
|
|
852
|
+
});
|
|
333
853
|
}
|
|
334
854
|
// ---------------------------------------------------------------------------
|
|
335
855
|
// MCP Server
|
|
@@ -418,42 +938,50 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
|
|
|
418
938
|
server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
|
|
419
939
|
name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
|
|
420
940
|
}, async ({ name }) => {
|
|
421
|
-
|
|
422
|
-
content: [
|
|
423
|
-
{
|
|
424
|
-
type: "text",
|
|
425
|
-
text: JSON.stringify(await joinNamedRoom(name), null, 2),
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
};
|
|
429
|
-
});
|
|
430
|
-
// -- get_current_room -------------------------------------------------------
|
|
431
|
-
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
|
|
432
|
-
if (!currentRoom) {
|
|
941
|
+
try {
|
|
433
942
|
return {
|
|
434
943
|
content: [
|
|
435
944
|
{
|
|
436
945
|
type: "text",
|
|
437
|
-
text: JSON.stringify(
|
|
946
|
+
text: JSON.stringify(await joinNamedRoom(name), null, 2),
|
|
438
947
|
},
|
|
439
948
|
],
|
|
440
949
|
};
|
|
441
950
|
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
953
|
+
return {
|
|
954
|
+
content: [
|
|
955
|
+
{
|
|
956
|
+
type: "text",
|
|
957
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
958
|
+
},
|
|
959
|
+
],
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
// -- get_current_room -------------------------------------------------------
|
|
966
|
+
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
|
|
442
967
|
return {
|
|
443
968
|
content: [
|
|
444
969
|
{
|
|
445
970
|
type: "text",
|
|
446
|
-
text: JSON.stringify(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
971
|
+
text: JSON.stringify(currentRoom
|
|
972
|
+
? {
|
|
973
|
+
connected: true,
|
|
974
|
+
...toPublicRoomState(currentRoom),
|
|
975
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
|
|
976
|
+
auth: getStoredAuth()
|
|
977
|
+
? {
|
|
978
|
+
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
979
|
+
expires_at: getStoredAuth()?.expires_at ?? null,
|
|
980
|
+
account: getStoredAuth()?.account ?? null,
|
|
981
|
+
}
|
|
982
|
+
: null,
|
|
983
|
+
}
|
|
984
|
+
: { connected: false, message: "Not currently in any room" }, null, 2),
|
|
457
985
|
},
|
|
458
986
|
],
|
|
459
987
|
};
|
|
@@ -508,13 +1036,16 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
508
1036
|
"Use this to let other agents and humans know what you are currently doing, " +
|
|
509
1037
|
"e.g. 'reviewing PR #2', 'waiting for tests', 'writing WISHLIST.md'. " +
|
|
510
1038
|
"Status updates are distinct from chat messages and can be filtered separately.", {
|
|
511
|
-
sender: z
|
|
1039
|
+
sender: z
|
|
1040
|
+
.string()
|
|
1041
|
+
.optional()
|
|
1042
|
+
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
512
1043
|
status: z.string().describe("Short status description (e.g. 'reviewing PR #2', 'idle', 'thinking...')"),
|
|
513
1044
|
room_id: z
|
|
514
1045
|
.string()
|
|
515
1046
|
.optional()
|
|
516
1047
|
.describe("Canonical room ID. Defaults to the current room."),
|
|
517
|
-
}, async ({ sender, status, room_id }) => {
|
|
1048
|
+
}, async ({ sender: _sender, status, room_id }) => {
|
|
518
1049
|
const targetRoomId = getTargetRoomId(room_id);
|
|
519
1050
|
const targetProjectId = getFallbackProjectId();
|
|
520
1051
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -533,6 +1064,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
533
1064
|
}
|
|
534
1065
|
// Status messages use a reserved prefix so the UI (and agents) can distinguish
|
|
535
1066
|
// them from normal chat messages without changing the data model.
|
|
1067
|
+
const identity = await ensureAgentIdentity();
|
|
1068
|
+
const sender = identity.actor_label;
|
|
536
1069
|
const statusText = `[status] ${status}`;
|
|
537
1070
|
const message = await roomScopedApiCall({
|
|
538
1071
|
room_id: targetRoomId,
|
|
@@ -553,6 +1086,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
553
1086
|
success: true,
|
|
554
1087
|
status_posted: status,
|
|
555
1088
|
sender,
|
|
1089
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
556
1090
|
message_id: typeof message.id === "string" ? message.id : null,
|
|
557
1091
|
timestamp: typeof message.timestamp === "string" ? message.timestamp : null,
|
|
558
1092
|
}, null, 2),
|
|
@@ -571,10 +1105,13 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
571
1105
|
"work that needs to be done.", {
|
|
572
1106
|
title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
|
|
573
1107
|
description: z.string().optional().describe("Longer description of what needs to be done"),
|
|
574
|
-
created_by: z
|
|
1108
|
+
created_by: z
|
|
1109
|
+
.string()
|
|
1110
|
+
.optional()
|
|
1111
|
+
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
575
1112
|
source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
|
|
576
1113
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
577
|
-
}, async ({ title, description, created_by, source_message_id, room_id }) => {
|
|
1114
|
+
}, async ({ title, description, created_by: _createdBy, source_message_id, room_id }) => {
|
|
578
1115
|
const targetRoomId = getTargetRoomId(room_id);
|
|
579
1116
|
const targetProjectId = getFallbackProjectId();
|
|
580
1117
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -582,6 +1119,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
582
1119
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
583
1120
|
};
|
|
584
1121
|
}
|
|
1122
|
+
const identity = await ensureAgentIdentity();
|
|
585
1123
|
const task = await roomScopedApiCall({
|
|
586
1124
|
room_id: targetRoomId,
|
|
587
1125
|
project_id: targetProjectId,
|
|
@@ -589,11 +1127,21 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
589
1127
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks`,
|
|
590
1128
|
options: {
|
|
591
1129
|
method: "POST",
|
|
592
|
-
body: JSON.stringify({
|
|
1130
|
+
body: JSON.stringify({
|
|
1131
|
+
title,
|
|
1132
|
+
description,
|
|
1133
|
+
created_by: identity.actor_label,
|
|
1134
|
+
source_message_id,
|
|
1135
|
+
}),
|
|
593
1136
|
},
|
|
594
1137
|
});
|
|
595
1138
|
return {
|
|
596
|
-
content: [
|
|
1139
|
+
content: [
|
|
1140
|
+
{
|
|
1141
|
+
type: "text",
|
|
1142
|
+
text: JSON.stringify({ success: true, task, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
597
1145
|
};
|
|
598
1146
|
});
|
|
599
1147
|
server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
|
|
@@ -630,9 +1178,12 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
630
1178
|
"status. This sets the assignee to you and moves the status to 'assigned'. " +
|
|
631
1179
|
"Do NOT claim proposed tasks â they need to be accepted first.", {
|
|
632
1180
|
task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
|
|
633
|
-
assignee: z
|
|
1181
|
+
assignee: z
|
|
1182
|
+
.string()
|
|
1183
|
+
.optional()
|
|
1184
|
+
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
634
1185
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
635
|
-
}, async ({ task_id, assignee, room_id }) => {
|
|
1186
|
+
}, async ({ task_id, assignee: _assignee, room_id }) => {
|
|
636
1187
|
const targetRoomId = getTargetRoomId(room_id);
|
|
637
1188
|
const targetProjectId = getFallbackProjectId();
|
|
638
1189
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -641,6 +1192,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
641
1192
|
};
|
|
642
1193
|
}
|
|
643
1194
|
try {
|
|
1195
|
+
const identity = await ensureAgentIdentity();
|
|
644
1196
|
const updated = await roomScopedApiCall({
|
|
645
1197
|
room_id: targetRoomId,
|
|
646
1198
|
project_id: targetProjectId,
|
|
@@ -648,11 +1200,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
648
1200
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
|
|
649
1201
|
options: {
|
|
650
1202
|
method: "PATCH",
|
|
651
|
-
body: JSON.stringify({ status: "assigned", assignee }),
|
|
1203
|
+
body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
|
|
652
1204
|
},
|
|
653
1205
|
});
|
|
654
1206
|
return {
|
|
655
|
-
content: [
|
|
1207
|
+
content: [
|
|
1208
|
+
{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: JSON.stringify({ success: true, task: updated, agent_identity: toPublicAgentIdentity(identity) }, null, 2),
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
656
1213
|
};
|
|
657
1214
|
}
|
|
658
1215
|
catch (error) {
|
|
@@ -666,7 +1223,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
666
1223
|
"but NOT proposed â in_progress).", {
|
|
667
1224
|
task_id: z.string().describe("The task ID to update"),
|
|
668
1225
|
status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
|
|
669
|
-
assignee: z
|
|
1226
|
+
assignee: z
|
|
1227
|
+
.string()
|
|
1228
|
+
.optional()
|
|
1229
|
+
.describe("New assignee for the task. Defaults to the current agent when status=assigned."),
|
|
670
1230
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
671
1231
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
672
1232
|
}, async ({ task_id, status, assignee, pr_url, room_id }) => {
|
|
@@ -678,6 +1238,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
678
1238
|
};
|
|
679
1239
|
}
|
|
680
1240
|
try {
|
|
1241
|
+
const identity = status === "assigned" && !assignee
|
|
1242
|
+
? await ensureAgentIdentity()
|
|
1243
|
+
: null;
|
|
681
1244
|
const updated = await roomScopedApiCall({
|
|
682
1245
|
room_id: targetRoomId,
|
|
683
1246
|
project_id: targetProjectId,
|
|
@@ -685,11 +1248,24 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
685
1248
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`,
|
|
686
1249
|
options: {
|
|
687
1250
|
method: "PATCH",
|
|
688
|
-
body: JSON.stringify({
|
|
1251
|
+
body: JSON.stringify({
|
|
1252
|
+
status,
|
|
1253
|
+
assignee: assignee ?? identity?.actor_label,
|
|
1254
|
+
pr_url,
|
|
1255
|
+
}),
|
|
689
1256
|
},
|
|
690
1257
|
});
|
|
691
1258
|
return {
|
|
692
|
-
content: [
|
|
1259
|
+
content: [
|
|
1260
|
+
{
|
|
1261
|
+
type: "text",
|
|
1262
|
+
text: JSON.stringify({
|
|
1263
|
+
success: true,
|
|
1264
|
+
task: updated,
|
|
1265
|
+
agent_identity: identity ? toPublicAgentIdentity(identity) : null,
|
|
1266
|
+
}, null, 2),
|
|
1267
|
+
},
|
|
1268
|
+
],
|
|
693
1269
|
};
|
|
694
1270
|
}
|
|
695
1271
|
catch (error) {
|
|
@@ -880,14 +1456,18 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
|
|
|
880
1456
|
// -- send_message -----------------------------------------------------------
|
|
881
1457
|
server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
882
1458
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
883
|
-
sender: z
|
|
1459
|
+
sender: z
|
|
1460
|
+
.string()
|
|
1461
|
+
.optional()
|
|
1462
|
+
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
884
1463
|
text: z.string().describe("The message text to send"),
|
|
885
|
-
}, async ({ room_id, sender, text }) => {
|
|
1464
|
+
}, async ({ room_id, sender: _sender, text }) => {
|
|
886
1465
|
const targetRoomId = getTargetRoomId(room_id);
|
|
887
1466
|
const targetProjectId = getFallbackProjectId();
|
|
888
1467
|
if (!targetRoomId && !targetProjectId) {
|
|
889
1468
|
throw new Error("No room is currently selected. Join a room first or pass room_id.");
|
|
890
1469
|
}
|
|
1470
|
+
const identity = await ensureAgentIdentity();
|
|
891
1471
|
const message = await roomScopedApiCall({
|
|
892
1472
|
room_id: targetRoomId,
|
|
893
1473
|
project_id: targetProjectId,
|
|
@@ -895,7 +1475,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
895
1475
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
896
1476
|
options: {
|
|
897
1477
|
method: "POST",
|
|
898
|
-
body: JSON.stringify({ sender, text }),
|
|
1478
|
+
body: JSON.stringify({ sender: identity.actor_label, text }),
|
|
899
1479
|
},
|
|
900
1480
|
});
|
|
901
1481
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
@@ -903,7 +1483,10 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
903
1483
|
content: [
|
|
904
1484
|
{
|
|
905
1485
|
type: "text",
|
|
906
|
-
text: JSON.stringify(
|
|
1486
|
+
text: JSON.stringify({
|
|
1487
|
+
...message,
|
|
1488
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
1489
|
+
}, null, 2),
|
|
907
1490
|
},
|
|
908
1491
|
],
|
|
909
1492
|
};
|
|
@@ -1009,6 +1592,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
|
|
|
1009
1592
|
account: storedAuth?.account ?? null,
|
|
1010
1593
|
token_expires_at: storedAuth?.expires_at ?? null,
|
|
1011
1594
|
pending_device_auth: pendingAuth,
|
|
1595
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
|
|
1012
1596
|
current_room: toPublicRoomState(currentRoom),
|
|
1013
1597
|
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
1014
1598
|
detected_room_from_context: detectedRoom,
|
|
@@ -1131,6 +1715,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1131
1715
|
if (result.status === "denied" || result.status === "expired") {
|
|
1132
1716
|
clearPendingDeviceAuth();
|
|
1133
1717
|
clearStoredAuth();
|
|
1718
|
+
currentAuthenticatedAccount = undefined;
|
|
1719
|
+
currentAuthenticatedAccountSource = null;
|
|
1720
|
+
currentAuthenticatedEnvToken = null;
|
|
1134
1721
|
return {
|
|
1135
1722
|
content: [
|
|
1136
1723
|
{
|
|
@@ -1151,6 +1738,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1151
1738
|
stored_at: new Date().toISOString(),
|
|
1152
1739
|
source: "device_flow",
|
|
1153
1740
|
});
|
|
1741
|
+
currentAuthenticatedAccount = storedAuth.account ?? undefined;
|
|
1742
|
+
currentAuthenticatedAccountSource = storedAuth.account ? "stored" : null;
|
|
1743
|
+
currentAuthenticatedEnvToken = null;
|
|
1154
1744
|
let joinedRoom = null;
|
|
1155
1745
|
const roomToJoin = room_id ||
|
|
1156
1746
|
pendingAuth?.suggested_room_id ||
|
|
@@ -1161,8 +1751,8 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1161
1751
|
const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
|
|
1162
1752
|
const joined = await joinRoomIdentifier(roomToJoin, joinedVia);
|
|
1163
1753
|
joinedRoom = joined.room;
|
|
1164
|
-
await autoRegisterAgentIdentity();
|
|
1165
1754
|
}
|
|
1755
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
1166
1756
|
return {
|
|
1167
1757
|
content: [
|
|
1168
1758
|
{
|
|
@@ -1173,6 +1763,7 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1173
1763
|
account: storedAuth.account ?? null,
|
|
1174
1764
|
expires_at: storedAuth.expires_at ?? null,
|
|
1175
1765
|
auto_joined_room: joinedRoom,
|
|
1766
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
1176
1767
|
}, null, 2),
|
|
1177
1768
|
},
|
|
1178
1769
|
],
|
|
@@ -1181,6 +1772,9 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1181
1772
|
server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
|
|
1182
1773
|
clearPendingDeviceAuth();
|
|
1183
1774
|
clearStoredAuth();
|
|
1775
|
+
currentAuthenticatedAccount = undefined;
|
|
1776
|
+
currentAuthenticatedAccountSource = null;
|
|
1777
|
+
currentAuthenticatedEnvToken = null;
|
|
1184
1778
|
return {
|
|
1185
1779
|
content: [
|
|
1186
1780
|
{
|
|
@@ -1211,22 +1805,38 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
1211
1805
|
],
|
|
1212
1806
|
};
|
|
1213
1807
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1808
|
+
try {
|
|
1809
|
+
const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
|
|
1810
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
1811
|
+
return {
|
|
1812
|
+
content: [
|
|
1813
|
+
{
|
|
1814
|
+
type: "text",
|
|
1815
|
+
text: JSON.stringify({
|
|
1816
|
+
success: true,
|
|
1817
|
+
rejoined_from_local_state: true,
|
|
1818
|
+
server_session_resumed: false,
|
|
1819
|
+
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
1820
|
+
room: joined.room,
|
|
1821
|
+
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
1822
|
+
}, null, 2),
|
|
1823
|
+
},
|
|
1824
|
+
],
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
catch (error) {
|
|
1828
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
1829
|
+
return {
|
|
1830
|
+
content: [
|
|
1831
|
+
{
|
|
1832
|
+
type: "text",
|
|
1833
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
1834
|
+
},
|
|
1835
|
+
],
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1230
1840
|
});
|
|
1231
1841
|
// -- check_repo_visibility --------------------------------------------------
|
|
1232
1842
|
server.tool("check_repo_visibility", "Auto-detect the current repo's git remote and check if it's public or private. Returns the canonical key, provider, visibility, and suggested room type (discoverable for public, invite for private/unknown). Useful for deciding whether to auto-join a discoverable room or create an invite room.", {
|
|
@@ -1272,6 +1882,7 @@ async function main() {
|
|
|
1272
1882
|
const configRoom = getRoomFromConfig();
|
|
1273
1883
|
if (configRoom) {
|
|
1274
1884
|
await joinRoomIdentifier(configRoom, "config");
|
|
1885
|
+
await ensureAgentIdentity();
|
|
1275
1886
|
console.error(`đ Auto-joined room '${configRoom}' (from .letagents.json)`);
|
|
1276
1887
|
return;
|
|
1277
1888
|
}
|
|
@@ -1279,6 +1890,7 @@ async function main() {
|
|
|
1279
1890
|
const gitRoom = getGitRemoteIdentity();
|
|
1280
1891
|
if (gitRoom) {
|
|
1281
1892
|
await joinRoomIdentifier(gitRoom, "git-remote");
|
|
1893
|
+
await ensureAgentIdentity();
|
|
1282
1894
|
console.error(`đ Auto-joined room '${gitRoom}' (inferred from git remote â consider adding a .letagents.json)`);
|
|
1283
1895
|
return;
|
|
1284
1896
|
}
|
|
@@ -1286,6 +1898,7 @@ async function main() {
|
|
|
1286
1898
|
const savedCurrentRoom = getStoredCurrentRoom();
|
|
1287
1899
|
if (savedCurrentRoom) {
|
|
1288
1900
|
await joinRoomIdentifier(savedCurrentRoom.room_id, savedCurrentRoom.joined_via);
|
|
1901
|
+
await ensureAgentIdentity();
|
|
1289
1902
|
console.error(`đ Rejoined saved room '${savedCurrentRoom.room_id}' (from local state)`);
|
|
1290
1903
|
return;
|
|
1291
1904
|
}
|
|
@@ -1293,6 +1906,10 @@ async function main() {
|
|
|
1293
1906
|
console.error("âšī¸ No .letagents.json, git remote, or saved room found â use create_room, join_code, or join_room to connect.");
|
|
1294
1907
|
}
|
|
1295
1908
|
catch (err) {
|
|
1909
|
+
if (err instanceof RepoRoomAuthRequiredError) {
|
|
1910
|
+
console.error(`đ Repo room auth required for '${err.roomId}'. Open ${err.pendingAuth.verification_uri} and enter code ${err.pendingAuth.user_code}, then run poll_device_auth.`);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1296
1913
|
// Auto-join failure should never block the MCP server
|
|
1297
1914
|
console.error("â ī¸ Auto-join failed (server still running):", err instanceof Error ? err.message : err);
|
|
1298
1915
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const STRUCTURED_AGENT_LABEL_SEPARATOR = " | ";
|
|
2
|
+
const IDE_LABELS = new Map([
|
|
3
|
+
["agent", "Agent"],
|
|
4
|
+
["antigravity", "Antigravity"],
|
|
5
|
+
["claude", "Claude"],
|
|
6
|
+
["codex", "Codex"],
|
|
7
|
+
["orchestrator", "Orchestrator"],
|
|
8
|
+
]);
|
|
9
|
+
function normalizeWhitespace(value) {
|
|
10
|
+
return value.trim().replace(/\s+/g, " ");
|
|
11
|
+
}
|
|
12
|
+
function normalizeIdeLabel(value) {
|
|
13
|
+
const normalized = normalizeWhitespace(String(value ?? ""))
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
16
|
+
.replace(/^-+|-+$/g, "");
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return IDE_LABELS.get(normalized) ?? toTitleCaseCodename(normalized);
|
|
21
|
+
}
|
|
22
|
+
export function toTitleCaseCodename(value) {
|
|
23
|
+
const normalized = normalizeWhitespace(value);
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
return normalized
|
|
28
|
+
.split(/[-\s]+/)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
31
|
+
.join(" ");
|
|
32
|
+
}
|
|
33
|
+
export function formatOwnerAttribution(ownerLabel) {
|
|
34
|
+
const trimmed = normalizeWhitespace(ownerLabel) || "Owner";
|
|
35
|
+
return /s$/i.test(trimmed) ? `${trimmed}' agent` : `${trimmed}'s agent`;
|
|
36
|
+
}
|
|
37
|
+
export function inferAgentIdeLabel(value) {
|
|
38
|
+
const normalized = normalizeWhitespace(String(value ?? "")).toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (normalized === "codex" || normalized.startsWith("codex-")) {
|
|
43
|
+
return "Codex";
|
|
44
|
+
}
|
|
45
|
+
if (normalized === "antigravity" || normalized.startsWith("antigravity-")) {
|
|
46
|
+
return "Antigravity";
|
|
47
|
+
}
|
|
48
|
+
if (normalized === "claude" || normalized.startsWith("claude-")) {
|
|
49
|
+
return "Claude";
|
|
50
|
+
}
|
|
51
|
+
if (normalized === "orchestrator" || normalized.startsWith("orchestrator-")) {
|
|
52
|
+
return "Orchestrator";
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export function buildAgentActorLabel(input) {
|
|
57
|
+
const displayName = normalizeWhitespace(input.display_name) || "Agent";
|
|
58
|
+
const ownerAttribution = formatOwnerAttribution(input.owner_label);
|
|
59
|
+
const ideLabel = normalizeIdeLabel(input.ide_label) ?? "Agent";
|
|
60
|
+
return [displayName, ownerAttribution, ideLabel].join(STRUCTURED_AGENT_LABEL_SEPARATOR);
|
|
61
|
+
}
|
|
62
|
+
export function parseAgentActorLabel(value) {
|
|
63
|
+
const raw = normalizeWhitespace(String(value ?? ""));
|
|
64
|
+
if (!raw) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const structuredParts = raw
|
|
68
|
+
.split(STRUCTURED_AGENT_LABEL_SEPARATOR)
|
|
69
|
+
.map((part) => normalizeWhitespace(part))
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
if (structuredParts.length === 3 &&
|
|
72
|
+
/agent$/i.test(structuredParts[1]) &&
|
|
73
|
+
normalizeIdeLabel(structuredParts[2])) {
|
|
74
|
+
return {
|
|
75
|
+
raw,
|
|
76
|
+
display_name: structuredParts[0],
|
|
77
|
+
owner_attribution: structuredParts[1],
|
|
78
|
+
ide_label: normalizeIdeLabel(structuredParts[2]),
|
|
79
|
+
structured: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const legacyMatch = raw.match(/^(.*?)\s*\(([^)]+agent)\)$/i);
|
|
83
|
+
if (legacyMatch) {
|
|
84
|
+
const displayName = normalizeWhitespace(legacyMatch[1] ?? "") || raw;
|
|
85
|
+
const ownerAttribution = normalizeWhitespace(legacyMatch[2] ?? "") || null;
|
|
86
|
+
return {
|
|
87
|
+
raw,
|
|
88
|
+
display_name: displayName,
|
|
89
|
+
owner_attribution: ownerAttribution,
|
|
90
|
+
ide_label: inferAgentIdeLabel(displayName),
|
|
91
|
+
structured: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
raw,
|
|
96
|
+
display_name: raw,
|
|
97
|
+
owner_attribution: null,
|
|
98
|
+
ide_label: inferAgentIdeLabel(raw),
|
|
99
|
+
structured: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function getAgentPrimaryLabel(value) {
|
|
103
|
+
return parseAgentActorLabel(value)?.display_name ?? normalizeWhitespace(String(value ?? ""));
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letagents",
|
|
3
|
-
"version": "0.
|
|
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": {
|