letagents 0.9.0 → 0.11.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/codenames.js +154 -0
- package/dist/mcp/local-state.js +73 -11
- package/dist/mcp/room-id.js +3 -0
- package/dist/mcp/server.js +374 -180
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent codenames — extracted from server.ts per Emmy's directive.
|
|
3
|
+
*
|
|
4
|
+
* Each agent instance gets a two-word codename (e.g. "River Valley")
|
|
5
|
+
* deterministically derived from its runtime key via SHA-256 hashing.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
import { toTitleCaseCodename } from "../shared/agent-identity.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Codenames
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const AGENT_CODENAMES = [
|
|
13
|
+
"amber",
|
|
14
|
+
"anchor",
|
|
15
|
+
"autumn",
|
|
16
|
+
"badger",
|
|
17
|
+
"bay",
|
|
18
|
+
"bear",
|
|
19
|
+
"brook",
|
|
20
|
+
"calm",
|
|
21
|
+
"canyon",
|
|
22
|
+
"cedar",
|
|
23
|
+
"clear",
|
|
24
|
+
"cloud",
|
|
25
|
+
"comet",
|
|
26
|
+
"copper",
|
|
27
|
+
"creek",
|
|
28
|
+
"crisp",
|
|
29
|
+
"crest",
|
|
30
|
+
"dawn",
|
|
31
|
+
"delta",
|
|
32
|
+
"dune",
|
|
33
|
+
"ember",
|
|
34
|
+
"falcon",
|
|
35
|
+
"fern",
|
|
36
|
+
"field",
|
|
37
|
+
"firefly",
|
|
38
|
+
"fjord",
|
|
39
|
+
"forest",
|
|
40
|
+
"fox",
|
|
41
|
+
"garden",
|
|
42
|
+
"glade",
|
|
43
|
+
"golden",
|
|
44
|
+
"granite",
|
|
45
|
+
"grove",
|
|
46
|
+
"harbor",
|
|
47
|
+
"hawk",
|
|
48
|
+
"hollow",
|
|
49
|
+
"indigo",
|
|
50
|
+
"ivory",
|
|
51
|
+
"jade",
|
|
52
|
+
"juniper",
|
|
53
|
+
"lagoon",
|
|
54
|
+
"lake",
|
|
55
|
+
"lantern",
|
|
56
|
+
"leaf",
|
|
57
|
+
"lively",
|
|
58
|
+
"lunar",
|
|
59
|
+
"lynx",
|
|
60
|
+
"maple",
|
|
61
|
+
"marsh",
|
|
62
|
+
"meadow",
|
|
63
|
+
"mesa",
|
|
64
|
+
"misty",
|
|
65
|
+
"moon",
|
|
66
|
+
"morrow",
|
|
67
|
+
"moss",
|
|
68
|
+
"noble",
|
|
69
|
+
"oak",
|
|
70
|
+
"olive",
|
|
71
|
+
"opal",
|
|
72
|
+
"otter",
|
|
73
|
+
"owl",
|
|
74
|
+
"peak",
|
|
75
|
+
"pearl",
|
|
76
|
+
"pine",
|
|
77
|
+
"quiet",
|
|
78
|
+
"raven",
|
|
79
|
+
"reef",
|
|
80
|
+
"ridge",
|
|
81
|
+
"river",
|
|
82
|
+
"rook",
|
|
83
|
+
"sage",
|
|
84
|
+
"scarlet",
|
|
85
|
+
"shore",
|
|
86
|
+
"silver",
|
|
87
|
+
"sky",
|
|
88
|
+
"solar",
|
|
89
|
+
"sparrow",
|
|
90
|
+
"spring",
|
|
91
|
+
"star",
|
|
92
|
+
"stone",
|
|
93
|
+
"storm",
|
|
94
|
+
"summit",
|
|
95
|
+
"sun",
|
|
96
|
+
"sunlit",
|
|
97
|
+
"swift",
|
|
98
|
+
"thicket",
|
|
99
|
+
"tidal",
|
|
100
|
+
"timber",
|
|
101
|
+
"trail",
|
|
102
|
+
"valley",
|
|
103
|
+
"verdant",
|
|
104
|
+
"vista",
|
|
105
|
+
"warm",
|
|
106
|
+
"wave",
|
|
107
|
+
"west",
|
|
108
|
+
"wild",
|
|
109
|
+
"willow",
|
|
110
|
+
"wind",
|
|
111
|
+
"winter",
|
|
112
|
+
"wolf",
|
|
113
|
+
"wood",
|
|
114
|
+
"wren",
|
|
115
|
+
];
|
|
116
|
+
export const AGENT_CODENAME_SPACE = AGENT_CODENAMES.length * AGENT_CODENAMES.length;
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Slug helpers
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export function normalizeSlugSegment(input, fallback) {
|
|
121
|
+
const normalized = input
|
|
122
|
+
.normalize("NFKD")
|
|
123
|
+
.replace(/[^\x00-\x7F]/g, "")
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
126
|
+
.replace(/^-+|-+$/g, "")
|
|
127
|
+
.replace(/-{2,}/g, "-");
|
|
128
|
+
return normalized || fallback;
|
|
129
|
+
}
|
|
130
|
+
export function normalizeAgentBaseName(input) {
|
|
131
|
+
return normalizeSlugSegment(input, "agent").replace(/-agent$/, "") || "agent";
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Codename derivation
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
export function hashStringToIndex(value, modulo) {
|
|
137
|
+
const digest = createHash("sha256").update(value).digest();
|
|
138
|
+
return digest.readUInt16BE(0) % modulo;
|
|
139
|
+
}
|
|
140
|
+
export function codenameFromIndex(index) {
|
|
141
|
+
const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
|
|
142
|
+
const firstIndex = Math.floor(normalizedIndex / AGENT_CODENAMES.length);
|
|
143
|
+
const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
|
|
144
|
+
const first = AGENT_CODENAMES[firstIndex];
|
|
145
|
+
const second = AGENT_CODENAMES[secondIndex];
|
|
146
|
+
return {
|
|
147
|
+
name: normalizeAgentBaseName(`${first}-${second}`),
|
|
148
|
+
display_name: `${toTitleCaseCodename(first)} ${toTitleCaseCodename(second)}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function pickLocalCodename(runtimeKey, offset = 0) {
|
|
152
|
+
const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
|
|
153
|
+
return codenameFromIndex(index);
|
|
154
|
+
}
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { createHash } from "crypto";
|
|
5
|
+
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import { writeFileSync, existsSync } from "fs";
|
|
7
7
|
import { userInfo } from "os";
|
|
8
8
|
import { join, dirname } from "path";
|
|
@@ -10,12 +10,13 @@ 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 {
|
|
14
|
-
import {
|
|
13
|
+
import { AGENT_CODENAME_SPACE, normalizeSlugSegment, normalizeAgentBaseName, pickLocalCodename, } from "./codenames.js";
|
|
14
|
+
import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
15
|
+
import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
|
|
15
16
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
16
|
-
const CURRENT_AGENT_IDENTITY_KEY = getAgentIdentityStorageKey();
|
|
17
17
|
let currentRoom = null;
|
|
18
|
-
let
|
|
18
|
+
let currentAgentIdentityKey = "";
|
|
19
|
+
let currentAgentIdentity = null;
|
|
19
20
|
let currentAuthenticatedAccount = undefined;
|
|
20
21
|
let currentAuthenticatedAccountSource = null;
|
|
21
22
|
let currentAuthenticatedEnvToken = null;
|
|
@@ -27,6 +28,11 @@ const AGENT_NAME = (process.env.LETAGENTS_AGENT_NAME || process.env.AGENT_NAME |
|
|
|
27
28
|
const AGENT_DISPLAY_NAME = (process.env.LETAGENTS_AGENT_DISPLAY_NAME || "").trim();
|
|
28
29
|
const AGENT_IDE_LABEL = (process.env.LETAGENTS_AGENT_IDE || process.env.AGENT_IDE || "").trim();
|
|
29
30
|
const AGENT_OWNER_LABEL = (process.env.LETAGENTS_AGENT_OWNER_LABEL || "").trim();
|
|
31
|
+
const EXPLICIT_AGENT_IDENTITY_KEY = getExplicitAgentIdentityStorageKey();
|
|
32
|
+
const AGENT_INSTANCE_UUID = randomUUID();
|
|
33
|
+
currentAgentIdentityKey =
|
|
34
|
+
EXPLICIT_AGENT_IDENTITY_KEY ?? `instance:${AGENT_INSTANCE_UUID}`;
|
|
35
|
+
currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
|
|
30
36
|
// ---------------------------------------------------------------------------
|
|
31
37
|
// Helpers
|
|
32
38
|
// ---------------------------------------------------------------------------
|
|
@@ -43,130 +49,17 @@ function readCommandOutput(command, cwd = process.cwd()) {
|
|
|
43
49
|
return null;
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
}
|
|
52
|
+
// normalizeSlugSegment, normalizeAgentBaseName, AGENT_CODENAMES,
|
|
53
|
+
// AGENT_CODENAME_SPACE, hashStringToIndex, codenameFromIndex,
|
|
54
|
+
// pickLocalCodename — all imported from ./codenames.js
|
|
59
55
|
function isCodexRuntime() {
|
|
60
56
|
return Boolean(process.env.CODEX_THREAD_ID ||
|
|
61
57
|
process.env.CODEX_SHELL ||
|
|
62
58
|
process.env.CODEX_CI ||
|
|
63
59
|
process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE);
|
|
64
60
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
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() {
|
|
61
|
+
const AGENT_IDENTITY_SLOT_SUFFIX = ":slot:";
|
|
62
|
+
function getExplicitAgentIdentityStorageKey() {
|
|
170
63
|
const runtimeSignals = [
|
|
171
64
|
process.env.LETAGENTS_AGENT_INSTANCE_ID,
|
|
172
65
|
process.env.CODEX_THREAD_ID && `codex:${process.env.CODEX_THREAD_ID}`,
|
|
@@ -177,11 +70,10 @@ function getAgentIdentityStorageKey() {
|
|
|
177
70
|
if (runtimeSignals.length) {
|
|
178
71
|
return runtimeSignals[0];
|
|
179
72
|
}
|
|
180
|
-
return
|
|
73
|
+
return null;
|
|
181
74
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
return digest.readUInt16BE(0) % modulo;
|
|
75
|
+
function getFallbackAgentIdentityNamespaceKey(ideLabel) {
|
|
76
|
+
return `cwd:${process.cwd()}:ide:${normalizeAgentBaseName(ideLabel)}`;
|
|
185
77
|
}
|
|
186
78
|
function detectAgentIdeLabel() {
|
|
187
79
|
if (AGENT_IDE_LABEL) {
|
|
@@ -194,6 +86,125 @@ function detectAgentIdeLabel() {
|
|
|
194
86
|
const inferred = inferAgentIdeLabel(explicitName);
|
|
195
87
|
return inferred || "Agent";
|
|
196
88
|
}
|
|
89
|
+
function getFallbackSlotPrefix(namespaceKey) {
|
|
90
|
+
return `${namespaceKey}${AGENT_IDENTITY_SLOT_SUFFIX}`;
|
|
91
|
+
}
|
|
92
|
+
function getFallbackSlotKey(namespaceKey, slotIndex) {
|
|
93
|
+
return `${getFallbackSlotPrefix(namespaceKey)}${slotIndex}`;
|
|
94
|
+
}
|
|
95
|
+
function parseFallbackSlotIndex(identityKey, namespaceKey) {
|
|
96
|
+
const prefix = getFallbackSlotPrefix(namespaceKey);
|
|
97
|
+
if (!identityKey.startsWith(prefix)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const slotIndex = Number.parseInt(identityKey.slice(prefix.length), 10);
|
|
101
|
+
return Number.isInteger(slotIndex) && slotIndex >= 0 ? slotIndex : null;
|
|
102
|
+
}
|
|
103
|
+
function isProcessAlive(pid) {
|
|
104
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 0);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const err = error;
|
|
113
|
+
return err.code === "EPERM";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function claimFallbackIdentityKey(namespaceKey) {
|
|
117
|
+
let claimedKey = getFallbackSlotKey(namespaceKey, 0);
|
|
118
|
+
let claimedIdentity = null;
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
updateLocalState((state) => {
|
|
121
|
+
const identities = state.agent_identities ?? {};
|
|
122
|
+
const leases = state.agent_identity_leases ?? {};
|
|
123
|
+
const namespacePrefix = getFallbackSlotPrefix(namespaceKey);
|
|
124
|
+
for (const [identityKey, lease] of Object.entries(leases)) {
|
|
125
|
+
if (!identityKey.startsWith(namespacePrefix) || lease.namespace_key !== namespaceKey) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!isProcessAlive(lease.pid)) {
|
|
129
|
+
delete leases[identityKey];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const activeLeaseForPid = Object.entries(leases).find(([identityKey, lease]) => identityKey.startsWith(namespacePrefix) &&
|
|
133
|
+
lease.namespace_key === namespaceKey &&
|
|
134
|
+
lease.pid === process.pid);
|
|
135
|
+
if (activeLeaseForPid) {
|
|
136
|
+
const [identityKey, lease] = activeLeaseForPid;
|
|
137
|
+
lease.updated_at = now;
|
|
138
|
+
claimedKey = identityKey;
|
|
139
|
+
claimedIdentity = identities[identityKey] ?? null;
|
|
140
|
+
state.agent_identity_leases = leases;
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
const slotKeys = new Set();
|
|
144
|
+
for (const identityKey of Object.keys(identities)) {
|
|
145
|
+
if (identityKey.startsWith(namespacePrefix)) {
|
|
146
|
+
slotKeys.add(identityKey);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const identityKey of Object.keys(leases)) {
|
|
150
|
+
if (identityKey.startsWith(namespacePrefix)) {
|
|
151
|
+
slotKeys.add(identityKey);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const sortedSlotKeys = [...slotKeys].sort((left, right) => {
|
|
155
|
+
const leftIndex = parseFallbackSlotIndex(left, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
|
|
156
|
+
const rightIndex = parseFallbackSlotIndex(right, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
|
|
157
|
+
return leftIndex - rightIndex;
|
|
158
|
+
});
|
|
159
|
+
for (const identityKey of sortedSlotKeys) {
|
|
160
|
+
if (leases[identityKey]) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
leases[identityKey] = {
|
|
164
|
+
namespace_key: namespaceKey,
|
|
165
|
+
pid: process.pid,
|
|
166
|
+
acquired_at: now,
|
|
167
|
+
updated_at: now,
|
|
168
|
+
};
|
|
169
|
+
claimedKey = identityKey;
|
|
170
|
+
claimedIdentity = identities[identityKey] ?? null;
|
|
171
|
+
state.agent_identity_leases = leases;
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
174
|
+
let nextSlotIndex = 0;
|
|
175
|
+
while (slotKeys.has(getFallbackSlotKey(namespaceKey, nextSlotIndex))) {
|
|
176
|
+
nextSlotIndex += 1;
|
|
177
|
+
}
|
|
178
|
+
claimedKey = getFallbackSlotKey(namespaceKey, nextSlotIndex);
|
|
179
|
+
leases[claimedKey] = {
|
|
180
|
+
namespace_key: namespaceKey,
|
|
181
|
+
pid: process.pid,
|
|
182
|
+
acquired_at: now,
|
|
183
|
+
updated_at: now,
|
|
184
|
+
};
|
|
185
|
+
claimedIdentity = identities[claimedKey] ?? null;
|
|
186
|
+
state.agent_identity_leases = leases;
|
|
187
|
+
return state;
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
identityKey: claimedKey,
|
|
191
|
+
identity: claimedIdentity,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function ensureAgentIdentityKey(ideLabel) {
|
|
195
|
+
if (EXPLICIT_AGENT_IDENTITY_KEY) {
|
|
196
|
+
currentAgentIdentityKey = EXPLICIT_AGENT_IDENTITY_KEY;
|
|
197
|
+
currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
|
|
198
|
+
return currentAgentIdentityKey;
|
|
199
|
+
}
|
|
200
|
+
// v1: Use per-instance UUID for guaranteed distinct identity.
|
|
201
|
+
// Each MCP process gets a unique UUID — no slot/PID coordination needed.
|
|
202
|
+
// Trade-off: process restart = new identity (accepted for v1).
|
|
203
|
+
const uuidKey = `instance:${AGENT_INSTANCE_UUID}`;
|
|
204
|
+
currentAgentIdentityKey = uuidKey;
|
|
205
|
+
currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
|
|
206
|
+
return currentAgentIdentityKey;
|
|
207
|
+
}
|
|
197
208
|
async function getAuthenticatedAgentDirectory() {
|
|
198
209
|
try {
|
|
199
210
|
const result = await apiCall("/agents/me");
|
|
@@ -213,9 +224,9 @@ async function getAuthenticatedAgentDirectory() {
|
|
|
213
224
|
return null;
|
|
214
225
|
}
|
|
215
226
|
}
|
|
216
|
-
function shouldReuseStoredIdentity(identity) {
|
|
227
|
+
function shouldReuseStoredIdentity(identity, identityKey) {
|
|
217
228
|
return Boolean(identity &&
|
|
218
|
-
identity.runtime_key ===
|
|
229
|
+
identity.runtime_key === identityKey &&
|
|
219
230
|
identity.display_name?.trim() &&
|
|
220
231
|
identity.ide_label?.trim() &&
|
|
221
232
|
identity.owner_attribution?.trim());
|
|
@@ -236,43 +247,36 @@ function resolveExplicitAgentIdentity() {
|
|
|
236
247
|
}
|
|
237
248
|
return null;
|
|
238
249
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const word = AGENT_CODENAMES[index];
|
|
242
|
-
return {
|
|
243
|
-
name: normalizeAgentBaseName(word),
|
|
244
|
-
display_name: toTitleCaseCodename(word),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
async function resolveAgentName(authAvailable) {
|
|
250
|
+
// pickLocalCodename — imported from ./codenames.js
|
|
251
|
+
async function resolveAgentName(authAvailable, identityKey) {
|
|
248
252
|
const explicit = resolveExplicitAgentIdentity();
|
|
249
253
|
if (explicit) {
|
|
250
254
|
return explicit;
|
|
251
255
|
}
|
|
252
|
-
if (shouldReuseStoredIdentity(currentAgentIdentity)) {
|
|
256
|
+
if (shouldReuseStoredIdentity(currentAgentIdentity, identityKey)) {
|
|
253
257
|
return {
|
|
254
258
|
name: currentAgentIdentity.name,
|
|
255
259
|
display_name: currentAgentIdentity.display_name,
|
|
256
260
|
};
|
|
257
261
|
}
|
|
258
262
|
if (!authAvailable) {
|
|
259
|
-
return pickLocalCodename(
|
|
263
|
+
return pickLocalCodename(identityKey);
|
|
260
264
|
}
|
|
261
265
|
const directory = await getAuthenticatedAgentDirectory();
|
|
262
266
|
const existingNames = new Set((directory?.agents ?? [])
|
|
263
267
|
.map((agent) => normalizeAgentBaseName(agent.name || ""))
|
|
264
268
|
.filter(Boolean));
|
|
265
|
-
for (let offset = 0; offset <
|
|
266
|
-
const candidate = pickLocalCodename(
|
|
269
|
+
for (let offset = 0; offset < AGENT_CODENAME_SPACE; offset += 1) {
|
|
270
|
+
const candidate = pickLocalCodename(identityKey, offset);
|
|
267
271
|
if (!existingNames.has(candidate.name)) {
|
|
268
272
|
return candidate;
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
const fallbackHash = createHash("sha256")
|
|
272
|
-
.update(
|
|
276
|
+
.update(identityKey)
|
|
273
277
|
.digest("hex")
|
|
274
278
|
.slice(0, 4);
|
|
275
|
-
const fallback = pickLocalCodename(
|
|
279
|
+
const fallback = pickLocalCodename(identityKey);
|
|
276
280
|
return {
|
|
277
281
|
name: `${fallback.name}-${fallbackHash}`,
|
|
278
282
|
display_name: `${fallback.display_name} ${fallbackHash.toUpperCase()}`,
|
|
@@ -377,6 +381,7 @@ function toPublicAgentIdentity(identity) {
|
|
|
377
381
|
actor_label: identity.actor_label,
|
|
378
382
|
canonical_key: identity.canonical_key ?? null,
|
|
379
383
|
runtime_key: identity.runtime_key ?? null,
|
|
384
|
+
agent_instance_id: AGENT_INSTANCE_UUID,
|
|
380
385
|
source: identity.source,
|
|
381
386
|
};
|
|
382
387
|
}
|
|
@@ -384,8 +389,9 @@ async function ensureAgentIdentity() {
|
|
|
384
389
|
const owner = await resolveOwnerContext();
|
|
385
390
|
const authAvailable = Boolean(getLetagentsToken());
|
|
386
391
|
const ideLabel = detectAgentIdeLabel();
|
|
392
|
+
const identityKey = ensureAgentIdentityKey(ideLabel);
|
|
387
393
|
const ownerAttribution = formatOwnerAttribution(owner.label);
|
|
388
|
-
const { name, display_name: displayName } = await resolveAgentName(authAvailable);
|
|
394
|
+
const { name, display_name: displayName } = await resolveAgentName(authAvailable, identityKey);
|
|
389
395
|
const actorLabel = buildAgentActorLabel({
|
|
390
396
|
display_name: displayName,
|
|
391
397
|
owner_label: owner.label,
|
|
@@ -399,7 +405,7 @@ async function ensureAgentIdentity() {
|
|
|
399
405
|
ide_label: ideLabel,
|
|
400
406
|
actor_label: actorLabel,
|
|
401
407
|
canonical_key: owner.login ? `${owner.login}/${name}` : null,
|
|
402
|
-
runtime_key:
|
|
408
|
+
runtime_key: identityKey,
|
|
403
409
|
source: "local",
|
|
404
410
|
resolved_at: new Date().toISOString(),
|
|
405
411
|
};
|
|
@@ -452,7 +458,7 @@ async function ensureAgentIdentity() {
|
|
|
452
458
|
currentAgentIdentity = setStoredAgentIdentity({
|
|
453
459
|
...resolved,
|
|
454
460
|
resolved_at: new Date().toISOString(),
|
|
455
|
-
},
|
|
461
|
+
}, identityKey);
|
|
456
462
|
}
|
|
457
463
|
return currentAgentIdentity ?? resolved;
|
|
458
464
|
}
|
|
@@ -628,22 +634,32 @@ function toRoomState(input) {
|
|
|
628
634
|
joined_via: input.joined_via,
|
|
629
635
|
};
|
|
630
636
|
}
|
|
637
|
+
function getCanonicalRoomWebUrl(roomId) {
|
|
638
|
+
return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
|
|
639
|
+
}
|
|
640
|
+
function withCanonicalRoomLink(roomId, payload) {
|
|
641
|
+
return {
|
|
642
|
+
...payload,
|
|
643
|
+
room_path: getCanonicalRoomWebPath(roomId),
|
|
644
|
+
room_url: getCanonicalRoomWebUrl(roomId),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
631
647
|
function toPublicRoomState(state) {
|
|
632
648
|
if (!state) {
|
|
633
649
|
return null;
|
|
634
650
|
}
|
|
635
|
-
return {
|
|
651
|
+
return withCanonicalRoomLink(state.room_id, {
|
|
636
652
|
room_id: state.room_id,
|
|
637
653
|
code: state.code ?? null,
|
|
638
654
|
display_name: state.display_name ?? null,
|
|
639
655
|
joined_via: state.joined_via,
|
|
640
|
-
};
|
|
656
|
+
});
|
|
641
657
|
}
|
|
642
658
|
function toPublicStoredRoomSession(session) {
|
|
643
659
|
if (!session) {
|
|
644
660
|
return null;
|
|
645
661
|
}
|
|
646
|
-
return {
|
|
662
|
+
return withCanonicalRoomLink(session.room_id, {
|
|
647
663
|
room_id: session.room_id,
|
|
648
664
|
code: session.code ?? null,
|
|
649
665
|
display_name: session.display_name ?? null,
|
|
@@ -651,12 +667,12 @@ function toPublicStoredRoomSession(session) {
|
|
|
651
667
|
joined_at: session.joined_at,
|
|
652
668
|
last_seen_at: session.last_seen_at,
|
|
653
669
|
last_message_id: session.last_message_id ?? null,
|
|
654
|
-
};
|
|
670
|
+
});
|
|
655
671
|
}
|
|
656
672
|
function toPublicRoomResponse(response, fallbackRoomId) {
|
|
657
673
|
const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
|
|
658
674
|
return {
|
|
659
|
-
...rest,
|
|
675
|
+
...withCanonicalRoomLink(typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId, rest),
|
|
660
676
|
room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
|
|
661
677
|
};
|
|
662
678
|
}
|
|
@@ -868,18 +884,35 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
868
884
|
const normalizedRoomId = String(room_id);
|
|
869
885
|
const storedSession = getStoredRoomSession(normalizedRoomId) ??
|
|
870
886
|
(currentRoom?.room_id === normalizedRoomId ? getStoredCurrentRoom() : null);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
887
|
+
// Paginate through all pages to return full message history
|
|
888
|
+
const allMessages = [];
|
|
889
|
+
let afterCursor;
|
|
890
|
+
for (;;) {
|
|
891
|
+
const query = new URLSearchParams();
|
|
892
|
+
if (afterCursor)
|
|
893
|
+
query.set("after", afterCursor);
|
|
894
|
+
const qs = query.toString();
|
|
895
|
+
const result = await roomScopedApiCall({
|
|
896
|
+
room_id: normalizedRoomId,
|
|
897
|
+
project_id: storedSession?.project_id ?? null,
|
|
898
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
|
|
899
|
+
project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`,
|
|
900
|
+
});
|
|
901
|
+
const msgs = result.messages ?? [];
|
|
902
|
+
allMessages.push(...msgs);
|
|
903
|
+
if (!result.has_more || msgs.length === 0)
|
|
904
|
+
break;
|
|
905
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
906
|
+
if (!lastMsg?.id)
|
|
907
|
+
break;
|
|
908
|
+
afterCursor = lastMsg.id;
|
|
909
|
+
}
|
|
877
910
|
return {
|
|
878
911
|
contents: [
|
|
879
912
|
{
|
|
880
913
|
uri: uri.href,
|
|
881
914
|
mimeType: "application/json",
|
|
882
|
-
text: JSON.stringify(
|
|
915
|
+
text: JSON.stringify({ messages: allMessages }, null, 2),
|
|
883
916
|
},
|
|
884
917
|
],
|
|
885
918
|
};
|
|
@@ -972,7 +1005,7 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
972
1005
|
? {
|
|
973
1006
|
connected: true,
|
|
974
1007
|
...toPublicRoomState(currentRoom),
|
|
975
|
-
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(
|
|
1008
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
976
1009
|
auth: getStoredAuth()
|
|
977
1010
|
? {
|
|
978
1011
|
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
@@ -1163,15 +1196,31 @@ server.tool("get_board", "Get the current task board for the room. By default sh
|
|
|
1163
1196
|
params.set("status", status);
|
|
1164
1197
|
if (open_only !== false)
|
|
1165
1198
|
params.set("open", "true");
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1199
|
+
// Paginate through all pages to return the full board
|
|
1200
|
+
const allTasks = [];
|
|
1201
|
+
let afterCursor;
|
|
1202
|
+
for (;;) {
|
|
1203
|
+
const pageParams = new URLSearchParams(params);
|
|
1204
|
+
if (afterCursor)
|
|
1205
|
+
pageParams.set("after", afterCursor);
|
|
1206
|
+
const qs = pageParams.toString();
|
|
1207
|
+
const result = await roomScopedApiCall({
|
|
1208
|
+
room_id: targetRoomId,
|
|
1209
|
+
project_id: targetProjectId,
|
|
1210
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
|
|
1211
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
|
|
1212
|
+
});
|
|
1213
|
+
const tasks = result.tasks ?? [];
|
|
1214
|
+
allTasks.push(...tasks);
|
|
1215
|
+
if (!result.has_more || tasks.length === 0)
|
|
1216
|
+
break;
|
|
1217
|
+
const lastTask = tasks[tasks.length - 1];
|
|
1218
|
+
if (!lastTask?.id)
|
|
1219
|
+
break;
|
|
1220
|
+
afterCursor = lastTask.id;
|
|
1221
|
+
}
|
|
1173
1222
|
return {
|
|
1174
|
-
content: [{ type: "text", text: JSON.stringify({ success: true,
|
|
1223
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
|
|
1175
1224
|
};
|
|
1176
1225
|
});
|
|
1177
1226
|
server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
|
|
@@ -1497,17 +1546,41 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1497
1546
|
}, async ({ room_id }) => {
|
|
1498
1547
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1499
1548
|
const targetProjectId = getFallbackProjectId();
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1549
|
+
// Paginate through all pages to honor the "read all messages" contract
|
|
1550
|
+
const allMessages = [];
|
|
1551
|
+
let afterCursor;
|
|
1552
|
+
let roomIdFromResponse;
|
|
1553
|
+
for (;;) {
|
|
1554
|
+
const query = new URLSearchParams();
|
|
1555
|
+
if (afterCursor)
|
|
1556
|
+
query.set("after", afterCursor);
|
|
1557
|
+
const qs = query.toString();
|
|
1558
|
+
const result = await roomScopedApiCall({
|
|
1559
|
+
room_id: targetRoomId,
|
|
1560
|
+
project_id: targetProjectId,
|
|
1561
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1562
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`,
|
|
1563
|
+
});
|
|
1564
|
+
roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
|
|
1565
|
+
const msgs = result.messages ?? [];
|
|
1566
|
+
allMessages.push(...msgs);
|
|
1567
|
+
if (!result.has_more || msgs.length === 0)
|
|
1568
|
+
break;
|
|
1569
|
+
// Use the last message ID as the cursor for the next page
|
|
1570
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
1571
|
+
if (!lastMsg?.id)
|
|
1572
|
+
break;
|
|
1573
|
+
afterCursor = lastMsg.id;
|
|
1574
|
+
}
|
|
1575
|
+
const output = { messages: allMessages };
|
|
1576
|
+
if (roomIdFromResponse) {
|
|
1577
|
+
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1578
|
+
}
|
|
1506
1579
|
return {
|
|
1507
1580
|
content: [
|
|
1508
1581
|
{
|
|
1509
1582
|
type: "text",
|
|
1510
|
-
text: JSON.stringify(
|
|
1583
|
+
text: JSON.stringify(output, null, 2),
|
|
1511
1584
|
},
|
|
1512
1585
|
],
|
|
1513
1586
|
};
|
|
@@ -1535,21 +1608,49 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1535
1608
|
params.set("after", after_message_id);
|
|
1536
1609
|
params.set("timeout", String(serverTimeout));
|
|
1537
1610
|
const queryString = params.toString();
|
|
1538
|
-
const
|
|
1611
|
+
const firstResult = await roomScopedApiCall({
|
|
1539
1612
|
room_id: targetRoomId,
|
|
1540
1613
|
project_id: targetProjectId,
|
|
1541
1614
|
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
|
|
1542
1615
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
|
|
1543
1616
|
options: { signal: AbortSignal.timeout(clientTimeout) },
|
|
1544
1617
|
});
|
|
1618
|
+
const allMessages = [...(firstResult.messages ?? [])];
|
|
1619
|
+
const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
|
|
1620
|
+
// If the immediate response has more pages, paginate through them
|
|
1621
|
+
if (firstResult.has_more && allMessages.length > 0) {
|
|
1622
|
+
let afterCursor = allMessages[allMessages.length - 1]?.id;
|
|
1623
|
+
while (afterCursor) {
|
|
1624
|
+
const pageParams = new URLSearchParams();
|
|
1625
|
+
pageParams.set("after", afterCursor);
|
|
1626
|
+
const qs = pageParams.toString();
|
|
1627
|
+
const page = await roomScopedApiCall({
|
|
1628
|
+
room_id: targetRoomId,
|
|
1629
|
+
project_id: targetProjectId,
|
|
1630
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`,
|
|
1631
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`,
|
|
1632
|
+
});
|
|
1633
|
+
const msgs = page.messages ?? [];
|
|
1634
|
+
allMessages.push(...msgs);
|
|
1635
|
+
if (!page.has_more || msgs.length === 0)
|
|
1636
|
+
break;
|
|
1637
|
+
afterCursor = msgs[msgs.length - 1]?.id;
|
|
1638
|
+
if (!afterCursor)
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const output = { messages: allMessages };
|
|
1643
|
+
if (roomIdFromResponse) {
|
|
1644
|
+
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1645
|
+
}
|
|
1545
1646
|
if (targetRoomId) {
|
|
1546
|
-
touchRoomSession(targetRoomId, getLastMessageId(
|
|
1647
|
+
touchRoomSession(targetRoomId, getLastMessageId(output));
|
|
1547
1648
|
}
|
|
1548
1649
|
return {
|
|
1549
1650
|
content: [
|
|
1550
1651
|
{
|
|
1551
1652
|
type: "text",
|
|
1552
|
-
text: JSON.stringify(
|
|
1653
|
+
text: JSON.stringify(output, null, 2),
|
|
1553
1654
|
},
|
|
1554
1655
|
],
|
|
1555
1656
|
};
|
|
@@ -1592,7 +1693,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
|
|
|
1592
1693
|
account: storedAuth?.account ?? null,
|
|
1593
1694
|
token_expires_at: storedAuth?.expires_at ?? null,
|
|
1594
1695
|
pending_device_auth: pendingAuth,
|
|
1595
|
-
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(
|
|
1696
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
1596
1697
|
current_room: toPublicRoomState(currentRoom),
|
|
1597
1698
|
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
1598
1699
|
detected_room_from_context: detectedRoom,
|
|
@@ -1763,7 +1864,11 @@ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On s
|
|
|
1763
1864
|
account: storedAuth.account ?? null,
|
|
1764
1865
|
expires_at: storedAuth.expires_at ?? null,
|
|
1765
1866
|
auto_joined_room: joinedRoom,
|
|
1867
|
+
...(joinedRoom?.room_id
|
|
1868
|
+
? withCanonicalRoomLink(joinedRoom.room_id, {})
|
|
1869
|
+
: {}),
|
|
1766
1870
|
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
1871
|
+
hint: "You can change your agent's display name anytime using the set_agent_name tool.",
|
|
1767
1872
|
}, null, 2),
|
|
1768
1873
|
},
|
|
1769
1874
|
],
|
|
@@ -1787,6 +1892,95 @@ server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token an
|
|
|
1787
1892
|
],
|
|
1788
1893
|
};
|
|
1789
1894
|
});
|
|
1895
|
+
server.tool("set_agent_name", "Set or change the agent's display name. The agent will be known by this name in the room. Use this to pick a custom name instead of the auto-generated codename.", {
|
|
1896
|
+
name: z
|
|
1897
|
+
.string()
|
|
1898
|
+
.min(2)
|
|
1899
|
+
.max(64)
|
|
1900
|
+
.describe("The desired display name for this agent (2-64 characters)."),
|
|
1901
|
+
}, async ({ name: desiredName }) => {
|
|
1902
|
+
const trimmedName = desiredName.trim();
|
|
1903
|
+
if (trimmedName.length < 2 || trimmedName.length > 64) {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [
|
|
1906
|
+
{
|
|
1907
|
+
type: "text",
|
|
1908
|
+
text: JSON.stringify({ success: false, error: "Name must be between 2 and 64 characters." }, null, 2),
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
const authAvailable = Boolean(getLetagentsToken());
|
|
1914
|
+
if (!authAvailable) {
|
|
1915
|
+
return {
|
|
1916
|
+
content: [
|
|
1917
|
+
{
|
|
1918
|
+
type: "text",
|
|
1919
|
+
text: JSON.stringify({ success: false, error: "Authentication required. Run start_device_auth first." }, null, 2),
|
|
1920
|
+
},
|
|
1921
|
+
],
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
const slugName = normalizeAgentBaseName(trimmedName);
|
|
1925
|
+
try {
|
|
1926
|
+
const owner = await resolveOwnerContext();
|
|
1927
|
+
const registered = await apiCall("/agents", {
|
|
1928
|
+
method: "POST",
|
|
1929
|
+
body: JSON.stringify({
|
|
1930
|
+
name: slugName,
|
|
1931
|
+
display_name: trimmedName,
|
|
1932
|
+
owner_label: owner.label,
|
|
1933
|
+
}),
|
|
1934
|
+
});
|
|
1935
|
+
const ideLabel = detectAgentIdeLabel();
|
|
1936
|
+
const ownerAttribution = formatOwnerAttribution(owner.label);
|
|
1937
|
+
const actorLabel = buildAgentActorLabel({
|
|
1938
|
+
display_name: trimmedName,
|
|
1939
|
+
owner_label: owner.label,
|
|
1940
|
+
ide_label: ideLabel,
|
|
1941
|
+
});
|
|
1942
|
+
const updatedIdentity = {
|
|
1943
|
+
name: slugName,
|
|
1944
|
+
display_name: trimmedName,
|
|
1945
|
+
owner_label: owner.label,
|
|
1946
|
+
owner_attribution: ownerAttribution,
|
|
1947
|
+
ide_label: ideLabel,
|
|
1948
|
+
actor_label: actorLabel,
|
|
1949
|
+
canonical_key: typeof registered.canonical_key === "string"
|
|
1950
|
+
? registered.canonical_key
|
|
1951
|
+
: owner.login ? `${owner.login}/${slugName}` : null,
|
|
1952
|
+
runtime_key: currentAgentIdentityKey,
|
|
1953
|
+
source: "api",
|
|
1954
|
+
resolved_at: new Date().toISOString(),
|
|
1955
|
+
};
|
|
1956
|
+
currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
|
|
1957
|
+
return {
|
|
1958
|
+
content: [
|
|
1959
|
+
{
|
|
1960
|
+
type: "text",
|
|
1961
|
+
text: JSON.stringify({
|
|
1962
|
+
success: true,
|
|
1963
|
+
message: `Agent name changed to "${trimmedName}".`,
|
|
1964
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity),
|
|
1965
|
+
}, null, 2),
|
|
1966
|
+
},
|
|
1967
|
+
],
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
catch (error) {
|
|
1971
|
+
return {
|
|
1972
|
+
content: [
|
|
1973
|
+
{
|
|
1974
|
+
type: "text",
|
|
1975
|
+
text: JSON.stringify({
|
|
1976
|
+
success: false,
|
|
1977
|
+
error: `Failed to set name: ${error instanceof Error ? error.message : String(error)}`,
|
|
1978
|
+
}, null, 2),
|
|
1979
|
+
},
|
|
1980
|
+
],
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1790
1984
|
server.tool("resume_room_session", "Rejoin the last locally saved room context, or a specific saved room, after a restart. This recreates participation in the room; it does not preserve a prior server-side session ID.", {
|
|
1791
1985
|
room_id: z
|
|
1792
1986
|
.string()
|
|
@@ -1817,7 +2011,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
1817
2011
|
rejoined_from_local_state: true,
|
|
1818
2012
|
server_session_resumed: false,
|
|
1819
2013
|
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
1820
|
-
room: joined.room,
|
|
2014
|
+
room: toPublicRoomState(joined.room),
|
|
1821
2015
|
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
1822
2016
|
}, null, 2),
|
|
1823
2017
|
},
|