letagents 0.12.8 → 0.12.10

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.
Files changed (116) hide show
  1. package/README.md +16 -15
  2. package/dist/mcp/agent-prompt-delivery.js +20 -0
  3. package/dist/mcp/codex-session/app-server.js +89 -0
  4. package/dist/mcp/codex-session/index.js +10 -0
  5. package/dist/mcp/codex-session/rpc-client.js +85 -0
  6. package/dist/mcp/codex-session/runtime-bridge.js +254 -0
  7. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  8. package/dist/mcp/codex-session/session-inspection.js +100 -0
  9. package/dist/mcp/codex-session/session-mapper.js +51 -0
  10. package/dist/mcp/codex-session/session-start.js +182 -0
  11. package/dist/mcp/codex-session/session-status.js +77 -0
  12. package/dist/mcp/codex-session/session-stop.js +43 -0
  13. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  14. package/dist/mcp/codex-session/start-prompt.js +49 -0
  15. package/dist/mcp/codex-session/types.js +1 -0
  16. package/dist/mcp/codex-session.js +1 -500
  17. package/dist/mcp/config-reader.js +10 -2
  18. package/dist/mcp/git-remote.js +90 -14
  19. package/dist/mcp/local-state/agent-identity.js +26 -0
  20. package/dist/mcp/local-state/agent-sessions.js +66 -0
  21. package/dist/mcp/local-state/auth.js +57 -0
  22. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  23. package/dist/mcp/local-state/index.js +8 -0
  24. package/dist/mcp/local-state/local-chat.js +808 -0
  25. package/dist/mcp/local-state/room-sessions.js +51 -0
  26. package/dist/mcp/local-state/storage.js +95 -0
  27. package/dist/mcp/local-state/types.js +1 -0
  28. package/dist/mcp/local-state.js +1 -286
  29. package/dist/mcp/managed-agent-providers.js +65 -0
  30. package/dist/mcp/rental-tools/activity.js +37 -0
  31. package/dist/mcp/rental-tools/budget.js +36 -0
  32. package/dist/mcp/rental-tools/context.js +62 -0
  33. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  34. package/dist/mcp/rental-tools/metering.js +73 -0
  35. package/dist/mcp/rental-tools/patches.js +100 -0
  36. package/dist/mcp/rental-tools/provider.js +117 -0
  37. package/dist/mcp/rental-tools/shared.js +19 -0
  38. package/dist/mcp/rental-tools/types.js +1 -0
  39. package/dist/mcp/rental-tools.js +7 -0
  40. package/dist/mcp/repo-visibility.js +8 -6
  41. package/dist/mcp/server/register-tools.js +19 -0
  42. package/dist/mcp/server/repo-context.js +28 -0
  43. package/dist/mcp/server/resources.js +77 -0
  44. package/dist/mcp/server/runtime/agent-sessions.js +134 -0
  45. package/dist/mcp/server/runtime/api.js +82 -0
  46. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  47. package/dist/mcp/server/runtime/device-auth.js +48 -0
  48. package/dist/mcp/server/runtime/identity/config.js +58 -0
  49. package/dist/mcp/server/runtime/identity/directory.js +87 -0
  50. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  51. package/dist/mcp/server/runtime/identity/names.js +74 -0
  52. package/dist/mcp/server/runtime/identity/public.js +19 -0
  53. package/dist/mcp/server/runtime/identity/state.js +32 -0
  54. package/dist/mcp/server/runtime/identity.js +96 -0
  55. package/dist/mcp/server/runtime/messages.js +160 -0
  56. package/dist/mcp/server/runtime/presence.js +52 -0
  57. package/dist/mcp/server/runtime/room-api.js +42 -0
  58. package/dist/mcp/server/runtime/room-state.js +122 -0
  59. package/dist/mcp/server/runtime/rooms.js +318 -0
  60. package/dist/mcp/server/runtime.js +14 -0
  61. package/dist/mcp/server/tools/agent-sessions.js +374 -0
  62. package/dist/mcp/server/tools/messages/index.js +14 -0
  63. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  64. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  65. package/dist/mcp/server/tools/messages/response.js +17 -0
  66. package/dist/mcp/server/tools/messages/send-tool.js +203 -0
  67. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  68. package/dist/mcp/server/tools/messages/wait-tool.js +333 -0
  69. package/dist/mcp/server/tools/messages.js +1 -0
  70. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +148 -0
  71. package/dist/mcp/server/tools/onboarding/name-tool.js +89 -0
  72. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  73. package/dist/mcp/server/tools/onboarding/status-tool.js +108 -0
  74. package/dist/mcp/server/tools/onboarding.js +8 -0
  75. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  76. package/dist/mcp/server/tools/rental/context-tools.js +35 -0
  77. package/dist/mcp/server/tools/rental/index.js +17 -0
  78. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  79. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  80. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  81. package/dist/mcp/server/tools/rental/response.js +10 -0
  82. package/dist/mcp/server/tools/rental/types.js +1 -0
  83. package/dist/mcp/server/tools/rental.js +1 -0
  84. package/dist/mcp/server/tools/rooms/index.js +5 -0
  85. package/dist/mcp/server/tools/rooms/inspection-tools.js +124 -0
  86. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  87. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  88. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  89. package/dist/mcp/server/tools/rooms/response.js +10 -0
  90. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  91. package/dist/mcp/server/tools/rooms.js +1 -0
  92. package/dist/mcp/server/tools/tasks/api.js +222 -0
  93. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  94. package/dist/mcp/server/tools/tasks/board-intent-tools.js +321 -0
  95. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  96. package/dist/mcp/server/tools/tasks/context.js +35 -0
  97. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  98. package/dist/mcp/server/tools/tasks/index.js +14 -0
  99. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  100. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  101. package/dist/mcp/server/tools/tasks/response.js +13 -0
  102. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  103. package/dist/mcp/server/tools/tasks.js +1 -0
  104. package/dist/mcp/server.js +10 -2540
  105. package/dist/mcp/sse-client.js +22 -16
  106. package/dist/shared/activation-routing.js +263 -0
  107. package/dist/shared/agent-identity.js +21 -5
  108. package/dist/shared/agent-presence.js +24 -0
  109. package/dist/shared/handoff.js +145 -0
  110. package/dist/shared/rental/lrt.js +214 -0
  111. package/dist/shared/rental/meter-types.js +14 -0
  112. package/dist/shared/request-headers.js +3 -0
  113. package/dist/shared/room-agent-activity.js +41 -0
  114. package/dist/shared/room-agent-prompts.js +14 -1
  115. package/package.json +15 -5
  116. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,333 @@
1
+ import { z } from "zod";
2
+ import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
3
+ import { encodeRoomIdPath } from "../../../room-id.js";
4
+ import { appendIncludePromptOnly, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, waitForLocalChatMessages, } from "../../runtime.js";
5
+ import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
6
+ import { jsonToolResponse } from "./response.js";
7
+ const DEFAULT_POLL_TIMEOUT_MS = 30000;
8
+ const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
9
+ export function buildWaitForMessagesRequestOptions(input) {
10
+ return {
11
+ ...(input.signal ? { signal: input.signal } : {}),
12
+ headers: input.deliveryHeaders,
13
+ };
14
+ }
15
+ export function buildWaitForMessagesHistoryPageRequest(input) {
16
+ const querySuffix = input.queryString ? `?${input.queryString}` : "";
17
+ return {
18
+ room_id: input.targetRoomId,
19
+ project_id: input.targetProjectId,
20
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${querySuffix}`),
21
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${querySuffix}`),
22
+ options: buildWaitForMessagesRequestOptions({ deliveryHeaders: input.deliveryHeaders }),
23
+ };
24
+ }
25
+ function isRecord(value) {
26
+ return Boolean(value && typeof value === "object");
27
+ }
28
+ function messageId(message) {
29
+ return typeof message.id === "string" && message.id.trim() ? message.id : null;
30
+ }
31
+ export function resolveEffectiveAfterMessageId(input) {
32
+ const requested = input.requestedAfterMessageId?.trim();
33
+ return requested || undefined;
34
+ }
35
+ function replyReferenceId(message) {
36
+ const reply = message.reply_to ?? message.replyTo ?? null;
37
+ if (typeof reply === "string" && reply.trim()) {
38
+ return reply;
39
+ }
40
+ if (isRecord(reply) && typeof reply.id === "string" && reply.id.trim()) {
41
+ return reply.id;
42
+ }
43
+ return null;
44
+ }
45
+ function activationDecision(message) {
46
+ if (!isRecord(message))
47
+ return null;
48
+ const activation = message.activation;
49
+ if (!isRecord(activation))
50
+ return null;
51
+ const forCurrentAgent = activation.for_current_agent;
52
+ if (!isRecord(forCurrentAgent))
53
+ return null;
54
+ return typeof forCurrentAgent.decision === "string" ? forCurrentAgent.decision : null;
55
+ }
56
+ export function filterSilentActivationMessages(messages) {
57
+ const visibleMessages = [];
58
+ const skippedMessageIds = [];
59
+ let lastObservedMessageId = null;
60
+ for (const message of messages) {
61
+ if (isRecord(message)) {
62
+ lastObservedMessageId = messageId(message) ?? lastObservedMessageId;
63
+ }
64
+ if (activationDecision(message) === "silent") {
65
+ if (isRecord(message)) {
66
+ const id = messageId(message);
67
+ if (id)
68
+ skippedMessageIds.push(id);
69
+ }
70
+ continue;
71
+ }
72
+ visibleMessages.push(message);
73
+ }
74
+ return {
75
+ messages: visibleMessages,
76
+ skipped_message_ids: skippedMessageIds,
77
+ last_observed_message_id: lastObservedMessageId,
78
+ };
79
+ }
80
+ function addActivationRoutingTelemetry(output, routing) {
81
+ if (routing.last_observed_message_id) {
82
+ output.last_observed_message_id = routing.last_observed_message_id;
83
+ }
84
+ if (routing.skipped_message_ids.length > 0) {
85
+ output.skipped_message_ids = routing.skipped_message_ids;
86
+ output.skipped_message_count = routing.skipped_message_ids.length;
87
+ }
88
+ return output;
89
+ }
90
+ export function resolveWaitAgentSession(roomId, agentSessionId) {
91
+ if (!agentSessionId?.trim()) {
92
+ return null;
93
+ }
94
+ return resolveAgentSession(roomId, agentSessionId);
95
+ }
96
+ export async function localActivationContext(roomId) {
97
+ const result = await listLocalTasks(roomId, { openOnly: true });
98
+ return {
99
+ activeTaskLeases: result.tasks
100
+ .flatMap((task) => {
101
+ const agentKey = task.assignee_agent_key?.trim();
102
+ if (!LOCAL_TASK_OWNER_STATUSES.has(task.status) || !agentKey) {
103
+ return [];
104
+ }
105
+ return [{
106
+ kind: "work",
107
+ status: "active",
108
+ actor_label: task.assignee || agentKey,
109
+ agent_key: agentKey,
110
+ agent_instance_id: task.assignee_agent_instance_id,
111
+ agent_session_id: task.assignee_agent_session_id,
112
+ }];
113
+ }),
114
+ };
115
+ }
116
+ async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
117
+ if (!agentSession || agentSession.session_kind !== "worker") {
118
+ return messages;
119
+ }
120
+ const records = messages.filter(isRecord);
121
+ const activationContext = options.includeTaskOwnerLeases === false
122
+ ? undefined
123
+ : await localActivationContext(roomId);
124
+ return attachAgentMessageActivations(records, {
125
+ actor_label: agentSession.actor_label,
126
+ agent_key: agentSession.agent_key,
127
+ agent_instance_id: agentSession.agent_instance_id ?? null,
128
+ agent_session_id: agentSession.session_id,
129
+ display_name: agentSession.display_name,
130
+ session_kind: agentSession.session_kind,
131
+ }, activationContext);
132
+ }
133
+ async function findLocalMessageById(roomId, targetMessageId) {
134
+ let afterCursor;
135
+ for (;;) {
136
+ const result = await getLocalChatMessages(roomId, {
137
+ after: afterCursor,
138
+ include_prompt_only: true,
139
+ });
140
+ const messages = (result.messages ?? []).filter(isRecord);
141
+ const match = messages.find((message) => message.id === targetMessageId);
142
+ if (match)
143
+ return match;
144
+ if (!result.has_more || messages.length === 0)
145
+ return null;
146
+ const lastMessage = messages[messages.length - 1];
147
+ afterCursor = messageId(lastMessage) ?? undefined;
148
+ if (!afterCursor)
149
+ return null;
150
+ }
151
+ }
152
+ async function findRemoteMessageById(input) {
153
+ let afterCursor;
154
+ for (;;) {
155
+ const params = new URLSearchParams();
156
+ if (afterCursor)
157
+ params.set("after", afterCursor);
158
+ const queryString = params.toString();
159
+ const result = await roomScopedApiCall({
160
+ room_id: input.roomId,
161
+ project_id: input.projectId,
162
+ room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${queryString ? `?${queryString}` : ""}`),
163
+ project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${queryString ? `?${queryString}` : ""}`),
164
+ });
165
+ const messages = (result.messages ?? []).filter(isRecord);
166
+ const match = messages.find((message) => message.id === input.targetMessageId);
167
+ if (match)
168
+ return match;
169
+ if (!result.has_more || messages.length === 0)
170
+ return null;
171
+ const lastMessage = messages[messages.length - 1];
172
+ afterCursor = messageId(lastMessage) ?? undefined;
173
+ if (!afterCursor)
174
+ return null;
175
+ }
176
+ }
177
+ async function collectThreadContextMessages(input) {
178
+ const records = input.messages.filter(isRecord);
179
+ const knownIds = new Set(records.map(messageId).filter((id) => Boolean(id)));
180
+ const seenIds = new Set(knownIds);
181
+ const pendingIds = records
182
+ .map(replyReferenceId)
183
+ .filter((id) => Boolean(id && !knownIds.has(id)));
184
+ const contextMessages = [];
185
+ const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
186
+ const localIdentifiers = useLocalStorage
187
+ ? await resolveLocalRoomStorageIdentifiers(input.localRoomId)
188
+ : { localRoomId: input.localRoomId };
189
+ const sqliteRoomId = localIdentifiers.localRoomId || input.localRoomId;
190
+ while (pendingIds.length > 0) {
191
+ const nextId = pendingIds.shift();
192
+ if (!nextId || seenIds.has(nextId))
193
+ continue;
194
+ seenIds.add(nextId);
195
+ const message = useLocalStorage && input.localRoomId
196
+ ? await findLocalMessageById(sqliteRoomId || input.localRoomId, nextId)
197
+ : await findRemoteMessageById({
198
+ roomId: input.roomId,
199
+ projectId: input.projectId,
200
+ targetMessageId: nextId,
201
+ });
202
+ if (!message)
203
+ continue;
204
+ contextMessages.push(message);
205
+ const parentId = replyReferenceId(message);
206
+ if (parentId && !seenIds.has(parentId)) {
207
+ pendingIds.push(parentId);
208
+ }
209
+ }
210
+ return contextMessages;
211
+ }
212
+ export function registerWaitForMessagesTool(server) {
213
+ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room (HTTP long-poll). Messages labeled activation.for_current_agent.decision=\"silent\" are skipped for the current worker; when last_observed_message_id is present, use it as cursor progress even if messages is empty. Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to keep focused side discussion out of the main room. For multi-hour runs, call in a loop: always pass after_message_id from the last message you processed or last_observed_message_id so an empty result means 'nothing new yet', not 'stop working'. If someone posted a premature 'I will wait' closing line, use send_message with a brief continue instruction. Per-call wait is capped (default max 180s unless LETAGENTS_POLL_MAX_MS is set on API and MCP).", {
214
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
215
+ after_message_id: z
216
+ .string()
217
+ .optional()
218
+ .describe("Only return messages after this message ID (e.g. 'msg_3'). If omitted, returns all existing messages immediately."),
219
+ timeout: z
220
+ .number()
221
+ .optional()
222
+ .describe("Maximum wait time in milliseconds (min 1000, capped by LETAGENTS_POLL_MAX_MS / server). If set to 0, the default timeout will be used."),
223
+ agent_session_id: z
224
+ .string()
225
+ .optional()
226
+ .describe("Registered agent session to use. Without this, the MCP transport is treated as controller traffic and is hidden from connected-agent activity."),
227
+ }, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
228
+ const targetRoomId = getTargetRoomId(room_id);
229
+ const targetProjectId = getFallbackProjectId();
230
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
231
+ const identity = await ensureAgentIdentity();
232
+ const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
233
+ const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
234
+ const maxPollMs = getPollTimeoutCapMs();
235
+ const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
236
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
237
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
238
+ const effectiveLocalRoomId = sqliteRoomId || localRoomId;
239
+ const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
240
+ requestedAfterMessageId: after_message_id,
241
+ });
242
+ const existing = await getLocalChatMessages(effectiveLocalRoomId, {
243
+ after: effectiveAfterMessageId,
244
+ include_prompt_only: true,
245
+ });
246
+ const replayingExistingMessages = existing.messages.length > 0;
247
+ const result = replayingExistingMessages
248
+ ? existing
249
+ : await waitForLocalChatMessages(effectiveLocalRoomId, {
250
+ after: effectiveAfterMessageId,
251
+ timeoutMs: serverTimeout,
252
+ include_prompt_only: true,
253
+ });
254
+ const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
255
+ includeTaskOwnerLeases: !replayingExistingMessages,
256
+ });
257
+ const routing = filterSilentActivationMessages(messages);
258
+ touchRoomSession(effectiveLocalRoomId, routing.last_observed_message_id ?? getLastMessageId(result));
259
+ const threadContext = await collectThreadContextMessages({
260
+ messages: routing.messages,
261
+ localRoomId: effectiveLocalRoomId,
262
+ roomId: targetRoomId,
263
+ projectId: targetProjectId,
264
+ });
265
+ return jsonToolResponse({
266
+ room_id: effectiveLocalRoomId,
267
+ ...addActivationRoutingTelemetry({
268
+ messages: toAgentReadableMessages(routing.messages, threadContext),
269
+ }, routing),
270
+ });
271
+ }
272
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
273
+ const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
274
+ const params = new URLSearchParams();
275
+ const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
276
+ requestedAfterMessageId: after_message_id,
277
+ });
278
+ if (effectiveAfterMessageId)
279
+ params.set("after", effectiveAfterMessageId);
280
+ params.set("timeout", String(serverTimeout));
281
+ const queryString = params.toString();
282
+ const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
283
+ const firstResult = await roomScopedApiCall({
284
+ room_id: targetRoomId,
285
+ project_id: targetProjectId,
286
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
287
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
288
+ options: buildWaitForMessagesRequestOptions({
289
+ deliveryHeaders,
290
+ signal: AbortSignal.timeout(clientTimeout),
291
+ }),
292
+ });
293
+ const allMessages = [...(firstResult.messages ?? [])];
294
+ const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
295
+ if (firstResult.has_more && allMessages.length > 0) {
296
+ let afterCursor = allMessages[allMessages.length - 1]?.id;
297
+ while (afterCursor) {
298
+ const pageParams = new URLSearchParams();
299
+ pageParams.set("after", afterCursor);
300
+ const qs = pageParams.toString();
301
+ const page = await roomScopedApiCall(buildWaitForMessagesHistoryPageRequest({
302
+ targetRoomId,
303
+ targetProjectId,
304
+ queryString: qs,
305
+ deliveryHeaders,
306
+ }));
307
+ const msgs = page.messages ?? [];
308
+ allMessages.push(...msgs);
309
+ if (!page.has_more || msgs.length === 0)
310
+ break;
311
+ afterCursor = msgs[msgs.length - 1]?.id;
312
+ }
313
+ }
314
+ const routing = filterSilentActivationMessages(allMessages);
315
+ const threadContext = await collectThreadContextMessages({
316
+ messages: routing.messages,
317
+ localRoomId,
318
+ roomId: targetRoomId,
319
+ projectId: targetProjectId,
320
+ });
321
+ const output = addActivationRoutingTelemetry({
322
+ messages: toAgentReadableMessages(routing.messages, threadContext),
323
+ }, routing);
324
+ if (roomIdFromResponse) {
325
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
326
+ }
327
+ if (targetRoomId) {
328
+ touchRoomSession(targetRoomId, routing.last_observed_message_id ?? getLastMessageId(output));
329
+ }
330
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
331
+ return jsonToolResponse(output);
332
+ });
333
+ }
@@ -0,0 +1 @@
1
+ export { registerMessageTools, registerStatusTools } from "./messages/index.js";
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { getRoomFromConfig } from "../../../config-reader.js";
3
+ import { looksLikeInviteCode } from "../../../room-id.js";
4
+ import { apiCall, clearAuthenticatedAccountCache, clearPendingDeviceAuth, clearStoredAuth, currentRoom, ensureAgentIdentity, getPendingDeviceAuth, joinRoomIdentifierWithoutImplicitGitRefCreate, setAuthenticatedAccountCache, setPendingDeviceAuth, setStoredAuth, toPublicAgentIdentity, withCanonicalRoomLink, } from "../../runtime.js";
5
+ import { jsonTextResponse } from "./responses.js";
6
+ export function registerDeviceAuthTools(server) {
7
+ registerStartDeviceAuthTool(server);
8
+ registerPollDeviceAuthTool(server);
9
+ registerClearSavedAuthTool(server);
10
+ }
11
+ function registerStartDeviceAuthTool(server) {
12
+ server.tool("start_device_auth", "Start GitHub Device Flow for Let Agents Chat and persist the pending request locally. Use this when private repo access or explicit LetAgents auth is needed.", {
13
+ room_id: z
14
+ .string()
15
+ .optional()
16
+ .describe("Optional room to associate with this auth request for later auto-join."),
17
+ force: z
18
+ .boolean()
19
+ .optional()
20
+ .describe("If true, replaces any existing pending device auth request."),
21
+ }, async ({ room_id, force }) => {
22
+ const existing = getPendingDeviceAuth();
23
+ if (existing && !force) {
24
+ return jsonTextResponse({
25
+ success: true,
26
+ reused_existing_request: true,
27
+ ...existing,
28
+ });
29
+ }
30
+ const response = await apiCall("/auth/device/start", {
31
+ method: "POST",
32
+ });
33
+ const pendingAuth = setPendingDeviceAuth({
34
+ request_id: response.request_id,
35
+ user_code: response.user_code,
36
+ verification_uri: response.verification_uri,
37
+ interval_seconds: response.interval,
38
+ expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
39
+ started_at: new Date().toISOString(),
40
+ suggested_room_id: room_id ?? currentRoom?.room_id ?? getRoomFromConfig() ?? undefined,
41
+ });
42
+ return jsonTextResponse({
43
+ success: true,
44
+ ...pendingAuth,
45
+ });
46
+ });
47
+ }
48
+ function registerPollDeviceAuthTool(server) {
49
+ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On success this stores the LetAgents token locally and can optionally join a room immediately.", {
50
+ request_id: z
51
+ .string()
52
+ .optional()
53
+ .describe("The device auth request to poll. Defaults to the locally saved pending request."),
54
+ room_id: z
55
+ .string()
56
+ .optional()
57
+ .describe("Optional room to auto-join after authorization succeeds."),
58
+ auto_join: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("If true, tries to join the room immediately after the auth succeeds."),
62
+ }, async ({ request_id, room_id, auto_join }) => {
63
+ const pendingAuth = request_id
64
+ ? getPendingDeviceAuth()?.request_id === request_id
65
+ ? getPendingDeviceAuth()
66
+ : null
67
+ : getPendingDeviceAuth();
68
+ if (!pendingAuth && !request_id) {
69
+ return jsonTextResponse({
70
+ success: false,
71
+ error: "No pending device auth request found.",
72
+ });
73
+ }
74
+ const requestId = request_id || pendingAuth?.request_id;
75
+ if (!requestId) {
76
+ return jsonTextResponse({
77
+ success: false,
78
+ error: "A request_id is required when nothing is saved locally.",
79
+ });
80
+ }
81
+ const result = await apiCall(`/auth/device/poll/${encodeURIComponent(requestId)}`);
82
+ if (result.status === "pending" || result.status === "slow_down") {
83
+ if (pendingAuth) {
84
+ setPendingDeviceAuth({
85
+ ...pendingAuth,
86
+ interval_seconds: result.interval ?? pendingAuth.interval_seconds,
87
+ expires_at: result.expires_in !== undefined
88
+ ? new Date(Date.now() + result.expires_in * 1000).toISOString()
89
+ : pendingAuth.expires_at,
90
+ });
91
+ }
92
+ return jsonTextResponse({ success: true, ...result });
93
+ }
94
+ if (result.status === "denied" || result.status === "expired") {
95
+ clearPendingDeviceAuth();
96
+ clearStoredAuth();
97
+ clearAuthenticatedAccountCache();
98
+ return jsonTextResponse({ success: false, ...result });
99
+ }
100
+ if (!result.letagents_token) {
101
+ throw new Error("Device auth completed without a LetAgents token.");
102
+ }
103
+ clearPendingDeviceAuth();
104
+ const storedAuth = setStoredAuth({
105
+ token: result.letagents_token,
106
+ expires_at: result.expires_at,
107
+ account: result.account,
108
+ stored_at: new Date().toISOString(),
109
+ source: "device_flow",
110
+ });
111
+ setAuthenticatedAccountCache(storedAuth.account ?? undefined, storedAuth.account ? "stored" : null, null);
112
+ let joinedRoom = null;
113
+ const roomToJoin = room_id ||
114
+ pendingAuth?.suggested_room_id ||
115
+ currentRoom?.room_id ||
116
+ getRoomFromConfig() ||
117
+ undefined;
118
+ if (auto_join && roomToJoin) {
119
+ const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
120
+ const joined = await joinRoomIdentifierWithoutImplicitGitRefCreate(roomToJoin, joinedVia, { fallbackToRepo: true });
121
+ joinedRoom = joined.room;
122
+ }
123
+ const agentIdentity = await ensureAgentIdentity();
124
+ return jsonTextResponse({
125
+ success: true,
126
+ status: "authorized",
127
+ account: storedAuth.account ?? null,
128
+ expires_at: storedAuth.expires_at ?? null,
129
+ auto_joined_room: joinedRoom,
130
+ ...(joinedRoom?.room_id
131
+ ? withCanonicalRoomLink(joinedRoom.room_id, {})
132
+ : {}),
133
+ agent_identity: toPublicAgentIdentity(agentIdentity),
134
+ hint: "You can change your agent's display name anytime using the set_agent_name tool.",
135
+ });
136
+ });
137
+ }
138
+ function registerClearSavedAuthTool(server) {
139
+ server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
140
+ clearPendingDeviceAuth();
141
+ clearStoredAuth();
142
+ clearAuthenticatedAccountCache();
143
+ return jsonTextResponse({
144
+ success: true,
145
+ env_token_still_present: Boolean(process.env.LETAGENTS_TOKEN),
146
+ });
147
+ });
148
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { buildAgentActorLabel, formatOwnerAttribution } from "../../../../shared/agent-identity.js";
3
+ import { normalizeAgentBaseName } from "../../../../shared/codenames.js";
4
+ import { apiCall, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, getConversationIdentity, getLetagentsToken, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, } from "../../runtime.js";
5
+ import { jsonTextResponse } from "./responses.js";
6
+ export function registerSetAgentNameTool(server) {
7
+ 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.", {
8
+ name: z
9
+ .string()
10
+ .min(2)
11
+ .max(64)
12
+ .describe("The desired display name for this agent (2-64 characters)."),
13
+ conversation_id: z
14
+ .string()
15
+ .optional()
16
+ .describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
17
+ }, async ({ name: desiredName, conversation_id }) => {
18
+ const trimmedName = desiredName.trim();
19
+ if (trimmedName.length < 2 || trimmedName.length > 64) {
20
+ return jsonTextResponse({
21
+ success: false,
22
+ error: "Name must be between 2 and 64 characters.",
23
+ });
24
+ }
25
+ const authAvailable = Boolean(getLetagentsToken());
26
+ if (!authAvailable) {
27
+ return jsonTextResponse({
28
+ success: false,
29
+ error: "Authentication required. Run start_device_auth first.",
30
+ });
31
+ }
32
+ const slugName = normalizeAgentBaseName(trimmedName);
33
+ try {
34
+ const owner = await resolveOwnerContext();
35
+ const ideLabel = detectAgentIdeLabel();
36
+ const ownerAttribution = formatOwnerAttribution(owner.label);
37
+ const actorLabel = buildAgentActorLabel({
38
+ display_name: trimmedName,
39
+ owner_label: owner.label,
40
+ ide_label: ideLabel,
41
+ });
42
+ let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
43
+ if (!conversation_id) {
44
+ const registered = await apiCall("/agents", {
45
+ method: "POST",
46
+ body: JSON.stringify({
47
+ name: slugName,
48
+ display_name: trimmedName,
49
+ owner_label: owner.label,
50
+ }),
51
+ });
52
+ if (typeof registered.canonical_key === "string") {
53
+ canonicalKey = registered.canonical_key;
54
+ }
55
+ }
56
+ const updatedIdentity = {
57
+ name: slugName,
58
+ display_name: trimmedName,
59
+ owner_label: owner.label,
60
+ owner_attribution: ownerAttribution,
61
+ ide_label: ideLabel,
62
+ actor_label: actorLabel,
63
+ canonical_key: canonicalKey,
64
+ runtime_key: currentAgentIdentityKey,
65
+ source: conversation_id ? "local" : "api",
66
+ resolved_at: new Date().toISOString(),
67
+ };
68
+ if (conversation_id) {
69
+ setConversationIdentity(conversation_id, updatedIdentity);
70
+ }
71
+ else {
72
+ storeCurrentAgentIdentity(updatedIdentity, currentAgentIdentityKey);
73
+ }
74
+ return jsonTextResponse({
75
+ success: true,
76
+ message: `Agent name changed to "${trimmedName}".`,
77
+ agent_identity: toPublicAgentIdentity(conversation_id
78
+ ? getConversationIdentity(conversation_id)
79
+ : currentAgentIdentity),
80
+ });
81
+ }
82
+ catch (error) {
83
+ return jsonTextResponse({
84
+ success: false,
85
+ error: `Failed to set name: ${error instanceof Error ? error.message : String(error)}`,
86
+ });
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,10 @@
1
+ export function jsonTextResponse(value) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: JSON.stringify(value, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }