letagents 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/local-state.js +73 -11
- package/dist/mcp/room-id.js +3 -0
- package/dist/mcp/server.js +285 -57
- package/package.json +1 -1
package/dist/mcp/local-state.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
2
3
|
import { homedir } from "os";
|
|
3
4
|
import { dirname, join } from "path";
|
|
4
5
|
const DEFAULT_STATE_PATH = join(homedir(), ".letagents", "mcp-state.json");
|
|
6
|
+
const STATE_LOCK_WAIT_MS = 25;
|
|
7
|
+
const STATE_LOCK_TIMEOUT_MS = 2_000;
|
|
8
|
+
const STATE_LOCK_STALE_MS = 10_000;
|
|
9
|
+
const STATE_LOCK_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
5
10
|
export function getLocalStatePath() {
|
|
6
11
|
return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
|
|
7
12
|
}
|
|
8
|
-
|
|
9
|
-
const statePath = getLocalStatePath();
|
|
13
|
+
function readLocalStateFromPath(statePath) {
|
|
10
14
|
if (!existsSync(statePath)) {
|
|
11
15
|
return {};
|
|
12
16
|
}
|
|
@@ -19,18 +23,76 @@ export function readLocalState() {
|
|
|
19
23
|
return {};
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
|
-
export function
|
|
26
|
+
export function readLocalState() {
|
|
27
|
+
const statePath = getLocalStatePath();
|
|
28
|
+
return readLocalStateFromPath(statePath);
|
|
29
|
+
}
|
|
30
|
+
function sleepSync(ms) {
|
|
31
|
+
if (ms > 0) {
|
|
32
|
+
Atomics.wait(STATE_LOCK_SLEEP_BUFFER, 0, 0, ms);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeLocalStateUnlocked(statePath, state) {
|
|
36
|
+
const tempPath = `${statePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
39
|
+
renameSync(tempPath, statePath);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rmSync(tempPath, { force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function withStateLock(callback) {
|
|
23
46
|
const statePath = getLocalStatePath();
|
|
24
47
|
mkdirSync(dirname(statePath), { recursive: true });
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
const lockPath = `${statePath}.lock`;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
while (true) {
|
|
51
|
+
let lockFd = null;
|
|
52
|
+
try {
|
|
53
|
+
lockFd = openSync(lockPath, "wx");
|
|
54
|
+
return callback(statePath);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const err = error;
|
|
58
|
+
if (err.code !== "EEXIST") {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const stats = statSync(lockPath);
|
|
63
|
+
if (Date.now() - stats.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
64
|
+
rmSync(lockPath, { force: true });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (Date.now() - startedAt >= STATE_LOCK_TIMEOUT_MS) {
|
|
72
|
+
throw new Error(`Timed out acquiring local state lock at ${lockPath}`);
|
|
73
|
+
}
|
|
74
|
+
sleepSync(STATE_LOCK_WAIT_MS);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (lockFd !== null) {
|
|
78
|
+
closeSync(lockFd);
|
|
79
|
+
rmSync(lockPath, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function writeLocalState(state) {
|
|
85
|
+
withStateLock((statePath) => {
|
|
86
|
+
writeLocalStateUnlocked(statePath, state);
|
|
87
|
+
});
|
|
28
88
|
}
|
|
29
89
|
export function updateLocalState(updater) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
90
|
+
return withStateLock((statePath) => {
|
|
91
|
+
const current = readLocalStateFromPath(statePath);
|
|
92
|
+
const updated = updater(current) ?? current;
|
|
93
|
+
writeLocalStateUnlocked(statePath, updated);
|
|
94
|
+
return updated;
|
|
95
|
+
});
|
|
34
96
|
}
|
|
35
97
|
function isExpired(expiresAt) {
|
|
36
98
|
if (!expiresAt) {
|
package/dist/mcp/room-id.js
CHANGED
|
@@ -4,6 +4,9 @@ export function encodeRoomIdPath(roomId) {
|
|
|
4
4
|
.map((segment) => encodeURIComponent(segment))
|
|
5
5
|
.join("/");
|
|
6
6
|
}
|
|
7
|
+
export function getCanonicalRoomWebPath(roomId) {
|
|
8
|
+
return `/in/${encodeRoomIdPath(roomId)}`;
|
|
9
|
+
}
|
|
7
10
|
export function looksLikeInviteCode(value) {
|
|
8
11
|
return /^[A-Z0-9]{4}(?:-[A-Z0-9]{4})+$/.test(value.trim().toUpperCase());
|
|
9
12
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -10,12 +10,12 @@ import { execSync } from "child_process";
|
|
|
10
10
|
import { SseClient } from "./sse-client.js";
|
|
11
11
|
import { getRoomFromConfig } from "./config-reader.js";
|
|
12
12
|
import { getGitRemoteIdentity } from "./git-remote.js";
|
|
13
|
-
import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, } from "./local-state.js";
|
|
14
|
-
import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
|
|
13
|
+
import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
14
|
+
import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
|
|
15
15
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
16
|
-
const CURRENT_AGENT_IDENTITY_KEY = getAgentIdentityStorageKey();
|
|
17
16
|
let currentRoom = null;
|
|
18
|
-
let
|
|
17
|
+
let currentAgentIdentityKey = "";
|
|
18
|
+
let currentAgentIdentity = null;
|
|
19
19
|
let currentAuthenticatedAccount = undefined;
|
|
20
20
|
let currentAuthenticatedAccountSource = null;
|
|
21
21
|
let currentAuthenticatedEnvToken = null;
|
|
@@ -27,6 +27,10 @@ const AGENT_NAME = (process.env.LETAGENTS_AGENT_NAME || process.env.AGENT_NAME |
|
|
|
27
27
|
const AGENT_DISPLAY_NAME = (process.env.LETAGENTS_AGENT_DISPLAY_NAME || "").trim();
|
|
28
28
|
const AGENT_IDE_LABEL = (process.env.LETAGENTS_AGENT_IDE || process.env.AGENT_IDE || "").trim();
|
|
29
29
|
const AGENT_OWNER_LABEL = (process.env.LETAGENTS_AGENT_OWNER_LABEL || "").trim();
|
|
30
|
+
const EXPLICIT_AGENT_IDENTITY_KEY = getExplicitAgentIdentityStorageKey();
|
|
31
|
+
currentAgentIdentityKey =
|
|
32
|
+
EXPLICIT_AGENT_IDENTITY_KEY ?? getFallbackAgentIdentityNamespaceKey(detectAgentIdeLabel());
|
|
33
|
+
currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
|
|
30
34
|
// ---------------------------------------------------------------------------
|
|
31
35
|
// Helpers
|
|
32
36
|
// ---------------------------------------------------------------------------
|
|
@@ -166,7 +170,9 @@ const AGENT_CODENAMES = [
|
|
|
166
170
|
"wood",
|
|
167
171
|
"wren",
|
|
168
172
|
];
|
|
169
|
-
|
|
173
|
+
const AGENT_CODENAME_SPACE = AGENT_CODENAMES.length * AGENT_CODENAMES.length;
|
|
174
|
+
const AGENT_IDENTITY_SLOT_SUFFIX = ":slot:";
|
|
175
|
+
function getExplicitAgentIdentityStorageKey() {
|
|
170
176
|
const runtimeSignals = [
|
|
171
177
|
process.env.LETAGENTS_AGENT_INSTANCE_ID,
|
|
172
178
|
process.env.CODEX_THREAD_ID && `codex:${process.env.CODEX_THREAD_ID}`,
|
|
@@ -177,12 +183,26 @@ function getAgentIdentityStorageKey() {
|
|
|
177
183
|
if (runtimeSignals.length) {
|
|
178
184
|
return runtimeSignals[0];
|
|
179
185
|
}
|
|
180
|
-
return
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function getFallbackAgentIdentityNamespaceKey(ideLabel) {
|
|
189
|
+
return `cwd:${process.cwd()}:ide:${normalizeAgentBaseName(ideLabel)}`;
|
|
181
190
|
}
|
|
182
191
|
function hashStringToIndex(value, modulo) {
|
|
183
192
|
const digest = createHash("sha256").update(value).digest();
|
|
184
193
|
return digest.readUInt16BE(0) % modulo;
|
|
185
194
|
}
|
|
195
|
+
function codenameFromIndex(index) {
|
|
196
|
+
const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
|
|
197
|
+
const firstIndex = Math.floor(normalizedIndex / AGENT_CODENAMES.length);
|
|
198
|
+
const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
|
|
199
|
+
const first = AGENT_CODENAMES[firstIndex];
|
|
200
|
+
const second = AGENT_CODENAMES[secondIndex];
|
|
201
|
+
return {
|
|
202
|
+
name: normalizeAgentBaseName(`${first}-${second}`),
|
|
203
|
+
display_name: `${toTitleCaseCodename(first)} ${toTitleCaseCodename(second)}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
186
206
|
function detectAgentIdeLabel() {
|
|
187
207
|
if (AGENT_IDE_LABEL) {
|
|
188
208
|
return toTitleCaseCodename(AGENT_IDE_LABEL);
|
|
@@ -194,6 +214,122 @@ function detectAgentIdeLabel() {
|
|
|
194
214
|
const inferred = inferAgentIdeLabel(explicitName);
|
|
195
215
|
return inferred || "Agent";
|
|
196
216
|
}
|
|
217
|
+
function getFallbackSlotPrefix(namespaceKey) {
|
|
218
|
+
return `${namespaceKey}${AGENT_IDENTITY_SLOT_SUFFIX}`;
|
|
219
|
+
}
|
|
220
|
+
function getFallbackSlotKey(namespaceKey, slotIndex) {
|
|
221
|
+
return `${getFallbackSlotPrefix(namespaceKey)}${slotIndex}`;
|
|
222
|
+
}
|
|
223
|
+
function parseFallbackSlotIndex(identityKey, namespaceKey) {
|
|
224
|
+
const prefix = getFallbackSlotPrefix(namespaceKey);
|
|
225
|
+
if (!identityKey.startsWith(prefix)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const slotIndex = Number.parseInt(identityKey.slice(prefix.length), 10);
|
|
229
|
+
return Number.isInteger(slotIndex) && slotIndex >= 0 ? slotIndex : null;
|
|
230
|
+
}
|
|
231
|
+
function isProcessAlive(pid) {
|
|
232
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
process.kill(pid, 0);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const err = error;
|
|
241
|
+
return err.code === "EPERM";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function claimFallbackIdentityKey(namespaceKey) {
|
|
245
|
+
let claimedKey = getFallbackSlotKey(namespaceKey, 0);
|
|
246
|
+
let claimedIdentity = null;
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
updateLocalState((state) => {
|
|
249
|
+
const identities = state.agent_identities ?? {};
|
|
250
|
+
const leases = state.agent_identity_leases ?? {};
|
|
251
|
+
const namespacePrefix = getFallbackSlotPrefix(namespaceKey);
|
|
252
|
+
for (const [identityKey, lease] of Object.entries(leases)) {
|
|
253
|
+
if (!identityKey.startsWith(namespacePrefix) || lease.namespace_key !== namespaceKey) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!isProcessAlive(lease.pid)) {
|
|
257
|
+
delete leases[identityKey];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const activeLeaseForPid = Object.entries(leases).find(([identityKey, lease]) => identityKey.startsWith(namespacePrefix) &&
|
|
261
|
+
lease.namespace_key === namespaceKey &&
|
|
262
|
+
lease.pid === process.pid);
|
|
263
|
+
if (activeLeaseForPid) {
|
|
264
|
+
const [identityKey, lease] = activeLeaseForPid;
|
|
265
|
+
lease.updated_at = now;
|
|
266
|
+
claimedKey = identityKey;
|
|
267
|
+
claimedIdentity = identities[identityKey] ?? null;
|
|
268
|
+
state.agent_identity_leases = leases;
|
|
269
|
+
return state;
|
|
270
|
+
}
|
|
271
|
+
const slotKeys = new Set();
|
|
272
|
+
for (const identityKey of Object.keys(identities)) {
|
|
273
|
+
if (identityKey.startsWith(namespacePrefix)) {
|
|
274
|
+
slotKeys.add(identityKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const identityKey of Object.keys(leases)) {
|
|
278
|
+
if (identityKey.startsWith(namespacePrefix)) {
|
|
279
|
+
slotKeys.add(identityKey);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const sortedSlotKeys = [...slotKeys].sort((left, right) => {
|
|
283
|
+
const leftIndex = parseFallbackSlotIndex(left, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
|
|
284
|
+
const rightIndex = parseFallbackSlotIndex(right, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
|
|
285
|
+
return leftIndex - rightIndex;
|
|
286
|
+
});
|
|
287
|
+
for (const identityKey of sortedSlotKeys) {
|
|
288
|
+
if (leases[identityKey]) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
leases[identityKey] = {
|
|
292
|
+
namespace_key: namespaceKey,
|
|
293
|
+
pid: process.pid,
|
|
294
|
+
acquired_at: now,
|
|
295
|
+
updated_at: now,
|
|
296
|
+
};
|
|
297
|
+
claimedKey = identityKey;
|
|
298
|
+
claimedIdentity = identities[identityKey] ?? null;
|
|
299
|
+
state.agent_identity_leases = leases;
|
|
300
|
+
return state;
|
|
301
|
+
}
|
|
302
|
+
let nextSlotIndex = 0;
|
|
303
|
+
while (slotKeys.has(getFallbackSlotKey(namespaceKey, nextSlotIndex))) {
|
|
304
|
+
nextSlotIndex += 1;
|
|
305
|
+
}
|
|
306
|
+
claimedKey = getFallbackSlotKey(namespaceKey, nextSlotIndex);
|
|
307
|
+
leases[claimedKey] = {
|
|
308
|
+
namespace_key: namespaceKey,
|
|
309
|
+
pid: process.pid,
|
|
310
|
+
acquired_at: now,
|
|
311
|
+
updated_at: now,
|
|
312
|
+
};
|
|
313
|
+
claimedIdentity = identities[claimedKey] ?? null;
|
|
314
|
+
state.agent_identity_leases = leases;
|
|
315
|
+
return state;
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
identityKey: claimedKey,
|
|
319
|
+
identity: claimedIdentity,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function ensureAgentIdentityKey(ideLabel) {
|
|
323
|
+
if (EXPLICIT_AGENT_IDENTITY_KEY) {
|
|
324
|
+
currentAgentIdentityKey = EXPLICIT_AGENT_IDENTITY_KEY;
|
|
325
|
+
currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
|
|
326
|
+
return currentAgentIdentityKey;
|
|
327
|
+
}
|
|
328
|
+
const claimed = claimFallbackIdentityKey(getFallbackAgentIdentityNamespaceKey(ideLabel));
|
|
329
|
+
currentAgentIdentityKey = claimed.identityKey;
|
|
330
|
+
currentAgentIdentity = claimed.identity;
|
|
331
|
+
return currentAgentIdentityKey;
|
|
332
|
+
}
|
|
197
333
|
async function getAuthenticatedAgentDirectory() {
|
|
198
334
|
try {
|
|
199
335
|
const result = await apiCall("/agents/me");
|
|
@@ -213,9 +349,9 @@ async function getAuthenticatedAgentDirectory() {
|
|
|
213
349
|
return null;
|
|
214
350
|
}
|
|
215
351
|
}
|
|
216
|
-
function shouldReuseStoredIdentity(identity) {
|
|
352
|
+
function shouldReuseStoredIdentity(identity, identityKey) {
|
|
217
353
|
return Boolean(identity &&
|
|
218
|
-
identity.runtime_key ===
|
|
354
|
+
identity.runtime_key === identityKey &&
|
|
219
355
|
identity.display_name?.trim() &&
|
|
220
356
|
identity.ide_label?.trim() &&
|
|
221
357
|
identity.owner_attribution?.trim());
|
|
@@ -237,42 +373,38 @@ function resolveExplicitAgentIdentity() {
|
|
|
237
373
|
return null;
|
|
238
374
|
}
|
|
239
375
|
function pickLocalCodename(runtimeKey, offset = 0) {
|
|
240
|
-
const index =
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
name: normalizeAgentBaseName(word),
|
|
244
|
-
display_name: toTitleCaseCodename(word),
|
|
245
|
-
};
|
|
376
|
+
const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
|
|
377
|
+
return codenameFromIndex(index);
|
|
246
378
|
}
|
|
247
|
-
async function resolveAgentName(authAvailable) {
|
|
379
|
+
async function resolveAgentName(authAvailable, identityKey) {
|
|
248
380
|
const explicit = resolveExplicitAgentIdentity();
|
|
249
381
|
if (explicit) {
|
|
250
382
|
return explicit;
|
|
251
383
|
}
|
|
252
|
-
if (shouldReuseStoredIdentity(currentAgentIdentity)) {
|
|
384
|
+
if (shouldReuseStoredIdentity(currentAgentIdentity, identityKey)) {
|
|
253
385
|
return {
|
|
254
386
|
name: currentAgentIdentity.name,
|
|
255
387
|
display_name: currentAgentIdentity.display_name,
|
|
256
388
|
};
|
|
257
389
|
}
|
|
258
390
|
if (!authAvailable) {
|
|
259
|
-
return pickLocalCodename(
|
|
391
|
+
return pickLocalCodename(identityKey);
|
|
260
392
|
}
|
|
261
393
|
const directory = await getAuthenticatedAgentDirectory();
|
|
262
394
|
const existingNames = new Set((directory?.agents ?? [])
|
|
263
395
|
.map((agent) => normalizeAgentBaseName(agent.name || ""))
|
|
264
396
|
.filter(Boolean));
|
|
265
|
-
for (let offset = 0; offset <
|
|
266
|
-
const candidate = pickLocalCodename(
|
|
397
|
+
for (let offset = 0; offset < AGENT_CODENAME_SPACE; offset += 1) {
|
|
398
|
+
const candidate = pickLocalCodename(identityKey, offset);
|
|
267
399
|
if (!existingNames.has(candidate.name)) {
|
|
268
400
|
return candidate;
|
|
269
401
|
}
|
|
270
402
|
}
|
|
271
403
|
const fallbackHash = createHash("sha256")
|
|
272
|
-
.update(
|
|
404
|
+
.update(identityKey)
|
|
273
405
|
.digest("hex")
|
|
274
406
|
.slice(0, 4);
|
|
275
|
-
const fallback = pickLocalCodename(
|
|
407
|
+
const fallback = pickLocalCodename(identityKey);
|
|
276
408
|
return {
|
|
277
409
|
name: `${fallback.name}-${fallbackHash}`,
|
|
278
410
|
display_name: `${fallback.display_name} ${fallbackHash.toUpperCase()}`,
|
|
@@ -384,8 +516,9 @@ async function ensureAgentIdentity() {
|
|
|
384
516
|
const owner = await resolveOwnerContext();
|
|
385
517
|
const authAvailable = Boolean(getLetagentsToken());
|
|
386
518
|
const ideLabel = detectAgentIdeLabel();
|
|
519
|
+
const identityKey = ensureAgentIdentityKey(ideLabel);
|
|
387
520
|
const ownerAttribution = formatOwnerAttribution(owner.label);
|
|
388
|
-
const { name, display_name: displayName } = await resolveAgentName(authAvailable);
|
|
521
|
+
const { name, display_name: displayName } = await resolveAgentName(authAvailable, identityKey);
|
|
389
522
|
const actorLabel = buildAgentActorLabel({
|
|
390
523
|
display_name: displayName,
|
|
391
524
|
owner_label: owner.label,
|
|
@@ -399,7 +532,7 @@ async function ensureAgentIdentity() {
|
|
|
399
532
|
ide_label: ideLabel,
|
|
400
533
|
actor_label: actorLabel,
|
|
401
534
|
canonical_key: owner.login ? `${owner.login}/${name}` : null,
|
|
402
|
-
runtime_key:
|
|
535
|
+
runtime_key: identityKey,
|
|
403
536
|
source: "local",
|
|
404
537
|
resolved_at: new Date().toISOString(),
|
|
405
538
|
};
|
|
@@ -452,7 +585,7 @@ async function ensureAgentIdentity() {
|
|
|
452
585
|
currentAgentIdentity = setStoredAgentIdentity({
|
|
453
586
|
...resolved,
|
|
454
587
|
resolved_at: new Date().toISOString(),
|
|
455
|
-
},
|
|
588
|
+
}, identityKey);
|
|
456
589
|
}
|
|
457
590
|
return currentAgentIdentity ?? resolved;
|
|
458
591
|
}
|
|
@@ -628,22 +761,32 @@ function toRoomState(input) {
|
|
|
628
761
|
joined_via: input.joined_via,
|
|
629
762
|
};
|
|
630
763
|
}
|
|
764
|
+
function getCanonicalRoomWebUrl(roomId) {
|
|
765
|
+
return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
|
|
766
|
+
}
|
|
767
|
+
function withCanonicalRoomLink(roomId, payload) {
|
|
768
|
+
return {
|
|
769
|
+
...payload,
|
|
770
|
+
room_path: getCanonicalRoomWebPath(roomId),
|
|
771
|
+
room_url: getCanonicalRoomWebUrl(roomId),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
631
774
|
function toPublicRoomState(state) {
|
|
632
775
|
if (!state) {
|
|
633
776
|
return null;
|
|
634
777
|
}
|
|
635
|
-
return {
|
|
778
|
+
return withCanonicalRoomLink(state.room_id, {
|
|
636
779
|
room_id: state.room_id,
|
|
637
780
|
code: state.code ?? null,
|
|
638
781
|
display_name: state.display_name ?? null,
|
|
639
782
|
joined_via: state.joined_via,
|
|
640
|
-
};
|
|
783
|
+
});
|
|
641
784
|
}
|
|
642
785
|
function toPublicStoredRoomSession(session) {
|
|
643
786
|
if (!session) {
|
|
644
787
|
return null;
|
|
645
788
|
}
|
|
646
|
-
return {
|
|
789
|
+
return withCanonicalRoomLink(session.room_id, {
|
|
647
790
|
room_id: session.room_id,
|
|
648
791
|
code: session.code ?? null,
|
|
649
792
|
display_name: session.display_name ?? null,
|
|
@@ -651,12 +794,12 @@ function toPublicStoredRoomSession(session) {
|
|
|
651
794
|
joined_at: session.joined_at,
|
|
652
795
|
last_seen_at: session.last_seen_at,
|
|
653
796
|
last_message_id: session.last_message_id ?? null,
|
|
654
|
-
};
|
|
797
|
+
});
|
|
655
798
|
}
|
|
656
799
|
function toPublicRoomResponse(response, fallbackRoomId) {
|
|
657
800
|
const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
|
|
658
801
|
return {
|
|
659
|
-
...rest,
|
|
802
|
+
...withCanonicalRoomLink(typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId, rest),
|
|
660
803
|
room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
|
|
661
804
|
};
|
|
662
805
|
}
|
|
@@ -868,18 +1011,35 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
868
1011
|
const normalizedRoomId = String(room_id);
|
|
869
1012
|
const storedSession = getStoredRoomSession(normalizedRoomId) ??
|
|
870
1013
|
(currentRoom?.room_id === normalizedRoomId ? getStoredCurrentRoom() : null);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1014
|
+
// Paginate through all pages to return full message history
|
|
1015
|
+
const allMessages = [];
|
|
1016
|
+
let afterCursor;
|
|
1017
|
+
for (;;) {
|
|
1018
|
+
const query = new URLSearchParams();
|
|
1019
|
+
if (afterCursor)
|
|
1020
|
+
query.set("after", afterCursor);
|
|
1021
|
+
const qs = query.toString();
|
|
1022
|
+
const result = await roomScopedApiCall({
|
|
1023
|
+
room_id: normalizedRoomId,
|
|
1024
|
+
project_id: storedSession?.project_id ?? null,
|
|
1025
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1026
|
+
project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1027
|
+
});
|
|
1028
|
+
const msgs = result.messages ?? [];
|
|
1029
|
+
allMessages.push(...msgs);
|
|
1030
|
+
if (!result.has_more || msgs.length === 0)
|
|
1031
|
+
break;
|
|
1032
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
1033
|
+
if (!lastMsg?.id)
|
|
1034
|
+
break;
|
|
1035
|
+
afterCursor = lastMsg.id;
|
|
1036
|
+
}
|
|
877
1037
|
return {
|
|
878
1038
|
contents: [
|
|
879
1039
|
{
|
|
880
1040
|
uri: uri.href,
|
|
881
1041
|
mimeType: "application/json",
|
|
882
|
-
text: JSON.stringify(
|
|
1042
|
+
text: JSON.stringify({ messages: allMessages }, null, 2),
|
|
883
1043
|
},
|
|
884
1044
|
],
|
|
885
1045
|
};
|
|
@@ -972,7 +1132,7 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
972
1132
|
? {
|
|
973
1133
|
connected: true,
|
|
974
1134
|
...toPublicRoomState(currentRoom),
|
|
975
|
-
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(
|
|
1135
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
976
1136
|
auth: getStoredAuth()
|
|
977
1137
|
? {
|
|
978
1138
|
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
@@ -1163,15 +1323,31 @@ server.tool("get_board", "Get the current task board for the room. By default sh
|
|
|
1163
1323
|
params.set("status", status);
|
|
1164
1324
|
if (open_only !== false)
|
|
1165
1325
|
params.set("open", "true");
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1326
|
+
// Paginate through all pages to return the full board
|
|
1327
|
+
const allTasks = [];
|
|
1328
|
+
let afterCursor;
|
|
1329
|
+
for (;;) {
|
|
1330
|
+
const pageParams = new URLSearchParams(params);
|
|
1331
|
+
if (afterCursor)
|
|
1332
|
+
pageParams.set("after", afterCursor);
|
|
1333
|
+
const qs = pageParams.toString();
|
|
1334
|
+
const result = await roomScopedApiCall({
|
|
1335
|
+
room_id: targetRoomId,
|
|
1336
|
+
project_id: targetProjectId,
|
|
1337
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
|
|
1338
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
|
|
1339
|
+
});
|
|
1340
|
+
const tasks = result.tasks ?? [];
|
|
1341
|
+
allTasks.push(...tasks);
|
|
1342
|
+
if (!result.has_more || tasks.length === 0)
|
|
1343
|
+
break;
|
|
1344
|
+
const lastTask = tasks[tasks.length - 1];
|
|
1345
|
+
if (!lastTask?.id)
|
|
1346
|
+
break;
|
|
1347
|
+
afterCursor = lastTask.id;
|
|
1348
|
+
}
|
|
1173
1349
|
return {
|
|
1174
|
-
content: [{ type: "text", text: JSON.stringify({ success: true,
|
|
1350
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
|
|
1175
1351
|
};
|
|
1176
1352
|
});
|
|
1177
1353
|
server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
|
|
@@ -1497,17 +1673,41 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1497
1673
|
}, async ({ room_id }) => {
|
|
1498
1674
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1499
1675
|
const targetProjectId = getFallbackProjectId();
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1676
|
+
// Paginate through all pages to honor the "read all messages" contract
|
|
1677
|
+
const allMessages = [];
|
|
1678
|
+
let afterCursor;
|
|
1679
|
+
let roomIdFromResponse;
|
|
1680
|
+
for (;;) {
|
|
1681
|
+
const query = new URLSearchParams();
|
|
1682
|
+
if (afterCursor)
|
|
1683
|
+
query.set("after", afterCursor);
|
|
1684
|
+
const qs = query.toString();
|
|
1685
|
+
const result = await roomScopedApiCall({
|
|
1686
|
+
room_id: targetRoomId,
|
|
1687
|
+
project_id: targetProjectId,
|
|
1688
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1689
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1690
|
+
});
|
|
1691
|
+
roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
|
|
1692
|
+
const msgs = result.messages ?? [];
|
|
1693
|
+
allMessages.push(...msgs);
|
|
1694
|
+
if (!result.has_more || msgs.length === 0)
|
|
1695
|
+
break;
|
|
1696
|
+
// Use the last message ID as the cursor for the next page
|
|
1697
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
1698
|
+
if (!lastMsg?.id)
|
|
1699
|
+
break;
|
|
1700
|
+
afterCursor = lastMsg.id;
|
|
1701
|
+
}
|
|
1702
|
+
const output = { messages: allMessages };
|
|
1703
|
+
if (roomIdFromResponse) {
|
|
1704
|
+
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1705
|
+
}
|
|
1506
1706
|
return {
|
|
1507
1707
|
content: [
|
|
1508
1708
|
{
|
|
1509
1709
|
type: "text",
|
|
1510
|
-
text: JSON.stringify(
|
|
1710
|
+
text: JSON.stringify(output, null, 2),
|
|
1511
1711
|
},
|
|
1512
1712
|
],
|
|
1513
1713
|
};
|
|
@@ -1535,21 +1735,49 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1535
1735
|
params.set("after", after_message_id);
|
|
1536
1736
|
params.set("timeout", String(serverTimeout));
|
|
1537
1737
|
const queryString = params.toString();
|
|
1538
|
-
const
|
|
1738
|
+
const firstResult = await roomScopedApiCall({
|
|
1539
1739
|
room_id: targetRoomId,
|
|
1540
1740
|
project_id: targetProjectId,
|
|
1541
1741
|
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
|
|
1542
1742
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
|
|
1543
1743
|
options: { signal: AbortSignal.timeout(clientTimeout) },
|
|
1544
1744
|
});
|
|
1745
|
+
const allMessages = [...(firstResult.messages ?? [])];
|
|
1746
|
+
const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
|
|
1747
|
+
// If the immediate response has more pages, paginate through them
|
|
1748
|
+
if (firstResult.has_more && allMessages.length > 0) {
|
|
1749
|
+
let afterCursor = allMessages[allMessages.length - 1]?.id;
|
|
1750
|
+
while (afterCursor) {
|
|
1751
|
+
const pageParams = new URLSearchParams();
|
|
1752
|
+
pageParams.set("after", afterCursor);
|
|
1753
|
+
const qs = pageParams.toString();
|
|
1754
|
+
const page = await roomScopedApiCall({
|
|
1755
|
+
room_id: targetRoomId,
|
|
1756
|
+
project_id: targetProjectId,
|
|
1757
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`,
|
|
1758
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`,
|
|
1759
|
+
});
|
|
1760
|
+
const msgs = page.messages ?? [];
|
|
1761
|
+
allMessages.push(...msgs);
|
|
1762
|
+
if (!page.has_more || msgs.length === 0)
|
|
1763
|
+
break;
|
|
1764
|
+
afterCursor = msgs[msgs.length - 1]?.id;
|
|
1765
|
+
if (!afterCursor)
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const output = { messages: allMessages };
|
|
1770
|
+
if (roomIdFromResponse) {
|
|
1771
|
+
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1772
|
+
}
|
|
1545
1773
|
if (targetRoomId) {
|
|
1546
|
-
touchRoomSession(targetRoomId, getLastMessageId(
|
|
1774
|
+
touchRoomSession(targetRoomId, getLastMessageId(output));
|
|
1547
1775
|
}
|
|
1548
1776
|
return {
|
|
1549
1777
|
content: [
|
|
1550
1778
|
{
|
|
1551
1779
|
type: "text",
|
|
1552
|
-
text: JSON.stringify(
|
|
1780
|
+
text: JSON.stringify(output, null, 2),
|
|
1553
1781
|
},
|
|
1554
1782
|
],
|
|
1555
1783
|
};
|
|
@@ -1592,7 +1820,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
|
|
|
1592
1820
|
account: storedAuth?.account ?? null,
|
|
1593
1821
|
token_expires_at: storedAuth?.expires_at ?? null,
|
|
1594
1822
|
pending_device_auth: pendingAuth,
|
|
1595
|
-
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(
|
|
1823
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
1596
1824
|
current_room: toPublicRoomState(currentRoom),
|
|
1597
1825
|
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
1598
1826
|
detected_room_from_context: detectedRoom,
|
|
@@ -1817,7 +2045,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
1817
2045
|
rejoined_from_local_state: true,
|
|
1818
2046
|
server_session_resumed: false,
|
|
1819
2047
|
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
1820
|
-
room: joined.room,
|
|
2048
|
+
room: toPublicRoomState(joined.room),
|
|
1821
2049
|
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
1822
2050
|
}, null, 2),
|
|
1823
2051
|
},
|