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,73 @@
1
+ import { encodeSessionId, errorMessage, validateSessionId, } from "./shared.js";
2
+ export async function rentalHeartbeat(deps, input) {
3
+ const sessionIdError = validateSessionId(input);
4
+ if (sessionIdError)
5
+ return { success: false, error: sessionIdError };
6
+ try {
7
+ const body = await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/heartbeat`, { method: "POST" });
8
+ return {
9
+ success: true,
10
+ ok: body?.ok ?? true,
11
+ status: typeof body?.status === "string" ? body.status : undefined,
12
+ heartbeat_count: typeof body?.heartbeatCount === "number" ? body.heartbeatCount : undefined,
13
+ transitioned: body?.transitioned ?? false,
14
+ };
15
+ }
16
+ catch (err) {
17
+ return {
18
+ success: false,
19
+ error: errorMessage(err),
20
+ };
21
+ }
22
+ }
23
+ export async function rentalReportUsage(deps, input) {
24
+ const sessionIdError = validateSessionId(input);
25
+ if (sessionIdError)
26
+ return { success: false, error: sessionIdError };
27
+ if (typeof input.report !== "object" ||
28
+ input.report === null ||
29
+ Array.isArray(input.report)) {
30
+ return { success: false, error: "report must be a JSON object" };
31
+ }
32
+ try {
33
+ const meter = await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/usage`, {
34
+ method: "POST",
35
+ headers: { "content-type": "application/json" },
36
+ body: JSON.stringify(input.report),
37
+ });
38
+ return { success: true, meter };
39
+ }
40
+ catch (err) {
41
+ return {
42
+ success: false,
43
+ error: errorMessage(err),
44
+ };
45
+ }
46
+ }
47
+ export async function rentalRefreshQuota(deps, input) {
48
+ const sessionIdError = validateSessionId(input);
49
+ if (sessionIdError)
50
+ return { success: false, error: sessionIdError };
51
+ const bodyPayload = {};
52
+ if (typeof input.provider === "string" && input.provider.trim()) {
53
+ bodyPayload.provider = input.provider.trim();
54
+ }
55
+ try {
56
+ const body = await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/refresh-quota`, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify(bodyPayload),
60
+ });
61
+ return {
62
+ success: true,
63
+ snapshot: body?.snapshot ?? null,
64
+ refreshed: body?.refreshed ?? true,
65
+ };
66
+ }
67
+ catch (err) {
68
+ return {
69
+ success: false,
70
+ error: errorMessage(err),
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,100 @@
1
+ import { encodeSessionId, errorMessage, isPositiveInteger, validateSessionId, } from "./shared.js";
2
+ export async function rentalProposeEdit(deps, input) {
3
+ const sessionIdError = validateSessionId(input);
4
+ if (sessionIdError)
5
+ return { success: false, error: sessionIdError };
6
+ if (typeof input.idempotency_key !== "string" || !input.idempotency_key.trim()) {
7
+ return { success: false, error: "idempotency_key is required" };
8
+ }
9
+ if (typeof input.path !== "string" || !input.path.trim()) {
10
+ return { success: false, error: "path is required" };
11
+ }
12
+ if (typeof input.before_content !== "string" || typeof input.after_content !== "string") {
13
+ return { success: false, error: "before_content and after_content are required" };
14
+ }
15
+ const body = {
16
+ idempotencyKey: input.idempotency_key.trim(),
17
+ path: input.path.trim(),
18
+ beforeContent: input.before_content,
19
+ afterContent: input.after_content,
20
+ };
21
+ if (typeof input.summary === "string" && input.summary.trim()) {
22
+ body.summary = input.summary.trim();
23
+ }
24
+ try {
25
+ return await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/patches/propose-edit`, {
26
+ method: "POST",
27
+ headers: { "content-type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ });
30
+ }
31
+ catch (err) {
32
+ return {
33
+ success: false,
34
+ error: errorMessage(err),
35
+ };
36
+ }
37
+ }
38
+ export async function rentalProposePatch(deps, input) {
39
+ const sessionIdError = validateSessionId(input);
40
+ if (sessionIdError)
41
+ return { success: false, error: sessionIdError };
42
+ if (typeof input.idempotency_key !== "string" || !input.idempotency_key.trim()) {
43
+ return { success: false, error: "idempotency_key is required" };
44
+ }
45
+ if (!Array.isArray(input.files) || input.files.length === 0) {
46
+ return { success: false, error: "files must be a non-empty array" };
47
+ }
48
+ const body = {
49
+ idempotencyKey: input.idempotency_key.trim(),
50
+ files: input.files,
51
+ };
52
+ if (typeof input.summary === "string" && input.summary.trim()) {
53
+ body.summary = input.summary.trim();
54
+ }
55
+ try {
56
+ return await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/patches/propose-patch`, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify(body),
60
+ });
61
+ }
62
+ catch (err) {
63
+ return {
64
+ success: false,
65
+ error: errorMessage(err),
66
+ };
67
+ }
68
+ }
69
+ export async function rentalRunCommand(deps, input) {
70
+ const sessionIdError = validateSessionId(input);
71
+ if (sessionIdError)
72
+ return { success: false, error: sessionIdError };
73
+ if (!Array.isArray(input.argv) || input.argv.length === 0) {
74
+ return { success: false, error: "argv must be a non-empty array" };
75
+ }
76
+ if (input.argv.some((part) => typeof part !== "string" || !part.trim())) {
77
+ return { success: false, error: "argv entries must be non-empty strings" };
78
+ }
79
+ if (input.timeout_ms !== undefined && !isPositiveInteger(input.timeout_ms)) {
80
+ return { success: false, error: "timeout_ms must be a positive integer" };
81
+ }
82
+ const body = {
83
+ argv: input.argv.map((part) => part.trim()),
84
+ };
85
+ if (input.timeout_ms !== undefined)
86
+ body.timeoutMs = input.timeout_ms;
87
+ try {
88
+ return await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/commands/run`, {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify(body),
92
+ });
93
+ }
94
+ catch (err) {
95
+ return {
96
+ success: false,
97
+ error: errorMessage(err),
98
+ };
99
+ }
100
+ }
@@ -0,0 +1,117 @@
1
+ import { encodeSessionId, errorMessage, validateSessionId } from "./shared.js";
2
+ export async function rentalListRequests(deps) {
3
+ try {
4
+ const body = await deps.apiCall("/api/rental/provider/requests", { method: "GET" });
5
+ const requests = Array.isArray(body)
6
+ ? body
7
+ : Array.isArray(body?.requests)
8
+ ? body.requests
9
+ : [];
10
+ return { success: true, requests, count: requests.length };
11
+ }
12
+ catch (err) {
13
+ return {
14
+ success: false,
15
+ requests: [],
16
+ count: 0,
17
+ error: errorMessage(err),
18
+ };
19
+ }
20
+ }
21
+ export async function rentalAccept(deps, input) {
22
+ const sessionIdError = validateSessionId(input);
23
+ if (sessionIdError)
24
+ return { success: false, error: sessionIdError };
25
+ const path = `/api/rental/provider/sessions/${encodeSessionId(input.session_id)}/accept`;
26
+ const idemKey = input.idempotency_key?.trim() || undefined;
27
+ const headers = {};
28
+ const bodyPayload = {};
29
+ if (idemKey) {
30
+ headers["Idempotency-Key"] = idemKey;
31
+ bodyPayload.idempotency_key = idemKey;
32
+ }
33
+ try {
34
+ const session = await deps.apiCall(path, {
35
+ method: "POST",
36
+ headers,
37
+ body: JSON.stringify(bodyPayload),
38
+ });
39
+ return {
40
+ success: true,
41
+ session,
42
+ ...(idemKey ? { idempotency_key: idemKey } : {}),
43
+ };
44
+ }
45
+ catch (err) {
46
+ return {
47
+ success: false,
48
+ error: errorMessage(err),
49
+ ...(idemKey ? { idempotency_key: idemKey } : {}),
50
+ };
51
+ }
52
+ }
53
+ export async function rentalProvision(deps, input) {
54
+ const sessionIdError = validateSessionId(input);
55
+ if (sessionIdError)
56
+ return { success: false, error: sessionIdError };
57
+ const parentRoomId = input.parent_room_id?.trim();
58
+ if (!parentRoomId) {
59
+ return { success: false, error: "parent_room_id is required" };
60
+ }
61
+ const body = { parentRoomId };
62
+ const displayName = input.provider_display_name?.trim();
63
+ if (displayName)
64
+ body.providerDisplayName = displayName;
65
+ try {
66
+ const result = await deps.apiCall(`/api/rental/provider/sessions/${encodeSessionId(input.session_id)}/provision`, {
67
+ method: "POST",
68
+ body: JSON.stringify(body),
69
+ });
70
+ return {
71
+ success: true,
72
+ room_id: typeof result?.roomId === "string" ? result.roomId : undefined,
73
+ participant_id: typeof result?.participantId === "string" ? result.participantId : undefined,
74
+ session: result?.session,
75
+ };
76
+ }
77
+ catch (err) {
78
+ return {
79
+ success: false,
80
+ error: errorMessage(err),
81
+ };
82
+ }
83
+ }
84
+ export async function rentalDecline(deps, input) {
85
+ const sessionIdError = validateSessionId(input);
86
+ if (sessionIdError)
87
+ return { success: false, error: sessionIdError };
88
+ const idemKey = input.idempotency_key?.trim() || undefined;
89
+ const headers = {};
90
+ const payload = {};
91
+ if (idemKey) {
92
+ headers["Idempotency-Key"] = idemKey;
93
+ payload.idempotency_key = idemKey;
94
+ }
95
+ if (typeof input.reason === "string" && input.reason.trim()) {
96
+ payload.reason = input.reason.trim();
97
+ }
98
+ try {
99
+ const session = await deps.apiCall(`/api/rental/provider/sessions/${encodeSessionId(input.session_id)}/decline`, {
100
+ method: "POST",
101
+ headers,
102
+ body: JSON.stringify(payload),
103
+ });
104
+ return {
105
+ success: true,
106
+ session,
107
+ ...(idemKey ? { idempotency_key: idemKey } : {}),
108
+ };
109
+ }
110
+ catch (err) {
111
+ return {
112
+ success: false,
113
+ error: errorMessage(err),
114
+ ...(idemKey ? { idempotency_key: idemKey } : {}),
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,19 @@
1
+ export function validateSessionId(input) {
2
+ const sessionId = input.session_id;
3
+ if (typeof sessionId !== "string" || !sessionId.trim()) {
4
+ return "session_id is required";
5
+ }
6
+ return null;
7
+ }
8
+ export function encodeSessionId(sessionId) {
9
+ return encodeURIComponent(sessionId.trim());
10
+ }
11
+ export function errorMessage(err) {
12
+ return err instanceof Error ? err.message : String(err);
13
+ }
14
+ export function isPositiveInteger(value) {
15
+ return (typeof value === "number" &&
16
+ Number.isFinite(value) &&
17
+ Number.isInteger(value) &&
18
+ value > 0);
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export * from "./rental-tools/activity.js";
2
+ export * from "./rental-tools/budget.js";
3
+ export * from "./rental-tools/context.js";
4
+ export * from "./rental-tools/lifecycle.js";
5
+ export * from "./rental-tools/metering.js";
6
+ export * from "./rental-tools/patches.js";
7
+ export * from "./rental-tools/provider.js";
@@ -6,8 +6,8 @@
6
6
  //
7
7
  // Used in the MCP auto-join flow:
8
8
  // 1. Detect git remote → normalize to canonical form
9
- // 2. Check visibility → public = discoverable room, private = invite room
10
- // 3. Auto-join or prompt for invite code
9
+ // 2. Check visibility → public/private Git Room access mode
10
+ // 3. Auto-join the active Git Room derived from repo + branch
11
11
  //
12
12
  // Rate limits (unauthenticated):
13
13
  // - GitHub: 60 requests/hour
@@ -144,12 +144,13 @@ async function checkBitbucket(owner, repo) {
144
144
  export async function checkRepoVisibility(canonicalKey) {
145
145
  const provider = detectProvider(canonicalKey);
146
146
  const ownerRepo = extractOwnerRepo(canonicalKey);
147
- // Unknown provider — default to invite room (safe fallback)
147
+ // Unknown provider — default to an invite/private-style access hint.
148
148
  if (provider === "unknown" || !ownerRepo) {
149
149
  return {
150
150
  canonicalKey,
151
151
  provider,
152
152
  visibility: "unknown",
153
+ accessMode: "unknown",
153
154
  roomType: "invite",
154
155
  error: provider === "unknown"
155
156
  ? `Unknown git provider for host: ${canonicalKey.split("/")[0]}`
@@ -172,15 +173,16 @@ export async function checkRepoVisibility(canonicalKey) {
172
173
  default:
173
174
  visibility = "unknown";
174
175
  }
175
- // Determine room type from visibility
176
+ // Keep roomType for older callers; accessMode is the Git Room-facing field.
176
177
  const roomType = visibility === "public" ? "discoverable" : "invite";
177
178
  return {
178
179
  canonicalKey,
179
180
  provider,
180
181
  visibility,
182
+ accessMode: visibility,
181
183
  roomType,
182
184
  ...(visibility === "unknown" && {
183
- error: "Could not determine visibility — defaulting to invite room",
185
+ error: "Could not determine visibility — treating Git Room access as private/unknown",
184
186
  }),
185
187
  };
186
188
  }
@@ -189,7 +191,7 @@ export async function checkRepoVisibility(canonicalKey) {
189
191
  // ---------------------------------------------------------------------------
190
192
  /**
191
193
  * Complete auto-detection: read git remote → normalize → check visibility.
192
- * This is the main entry point for the MCP auto-join flow.
194
+ * This is the main entry point for visibility checks before Git Room auto-join.
193
195
  *
194
196
  * @param cwd - Working directory to detect git remote from
195
197
  * @returns VisibilityResult or null if not in a git repo
@@ -0,0 +1,19 @@
1
+ import { registerAgentSessionTools } from "./tools/agent-sessions.js";
2
+ import { registerMessageTools, registerStatusTools } from "./tools/messages.js";
3
+ import { registerOnboardingTools } from "./tools/onboarding.js";
4
+ import { registerRentalTools } from "./tools/rental.js";
5
+ import { registerRepoInitializationTool, registerRepoVisibilityTool, registerRoomInspectionTools, registerRoomJoinTools, registerRoomResumeTool, } from "./tools/rooms.js";
6
+ import { registerTaskTools } from "./tools/tasks.js";
7
+ export function registerTools(server) {
8
+ registerRoomJoinTools(server);
9
+ registerAgentSessionTools(server);
10
+ registerRoomInspectionTools(server);
11
+ registerStatusTools(server);
12
+ registerTaskTools(server);
13
+ registerRepoInitializationTool(server);
14
+ registerMessageTools(server);
15
+ registerOnboardingTools(server);
16
+ registerRoomResumeTool(server);
17
+ registerRentalTools(server);
18
+ registerRepoVisibilityTool(server);
19
+ }
@@ -0,0 +1,28 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ export function resolveGitRoot(dir) {
5
+ try {
6
+ const root = execSync("git rev-parse --show-toplevel", {
7
+ cwd: dir,
8
+ stdio: ["pipe", "pipe", "pipe"],
9
+ encoding: "utf-8",
10
+ }).trim();
11
+ return root || null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function findExistingConfig(startDir) {
18
+ let current = startDir;
19
+ while (true) {
20
+ if (existsSync(join(current, ".letagents.json")))
21
+ return current;
22
+ const parent = dirname(current);
23
+ if (parent === current)
24
+ break;
25
+ current = parent;
26
+ }
27
+ return null;
28
+ }
@@ -0,0 +1,77 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { appendIncludePromptOnly, currentRoom, getLocalChatMessages, getStoredCurrentRoom, getStoredRoomSession, getTargetRoomId, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, toAgentReadableMessages, } from "./runtime.js";
3
+ import { encodeRoomIdPath } from "../room-id.js";
4
+ export function registerRoomResources(server) {
5
+ // ---------------------------------------------------------------------------
6
+ // MCP Resources
7
+ // ---------------------------------------------------------------------------
8
+ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_id}/messages", {
9
+ list: undefined,
10
+ }), async (uri, { room_id }) => {
11
+ const normalizedRoomId = String(room_id);
12
+ const targetRoomId = getTargetRoomId(normalizedRoomId) ?? normalizedRoomId;
13
+ if (targetRoomId && await isLocalRoomStorageEnabled(targetRoomId)) {
14
+ const { localRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
15
+ const effectiveLocalRoomId = localRoomId || targetRoomId;
16
+ const allMessages = [];
17
+ let afterCursor;
18
+ for (;;) {
19
+ const result = await getLocalChatMessages(effectiveLocalRoomId, {
20
+ after: afterCursor,
21
+ include_prompt_only: true,
22
+ });
23
+ const msgs = result.messages ?? [];
24
+ allMessages.push(...msgs);
25
+ if (!result.has_more || msgs.length === 0)
26
+ break;
27
+ const lastMsg = msgs[msgs.length - 1];
28
+ if (!lastMsg?.id)
29
+ break;
30
+ afterCursor = lastMsg.id;
31
+ }
32
+ return {
33
+ contents: [
34
+ {
35
+ uri: uri.href,
36
+ mimeType: "application/json",
37
+ text: JSON.stringify({ messages: toAgentReadableMessages(allMessages) }, null, 2),
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ const storedSession = getStoredRoomSession(normalizedRoomId) ??
43
+ (currentRoom?.room_id === normalizedRoomId ? getStoredCurrentRoom() : null);
44
+ // Paginate through all pages to return full message history
45
+ const allMessages = [];
46
+ let afterCursor;
47
+ for (;;) {
48
+ const query = new URLSearchParams();
49
+ if (afterCursor)
50
+ query.set("after", afterCursor);
51
+ const qs = query.toString();
52
+ const result = await roomScopedApiCall({
53
+ room_id: normalizedRoomId,
54
+ project_id: storedSession?.project_id ?? null,
55
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
56
+ project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
57
+ });
58
+ const msgs = result.messages ?? [];
59
+ allMessages.push(...msgs);
60
+ if (!result.has_more || msgs.length === 0)
61
+ break;
62
+ const lastMsg = msgs[msgs.length - 1];
63
+ if (!lastMsg?.id)
64
+ break;
65
+ afterCursor = lastMsg.id;
66
+ }
67
+ return {
68
+ contents: [
69
+ {
70
+ uri: uri.href,
71
+ mimeType: "application/json",
72
+ text: JSON.stringify({ messages: toAgentReadableMessages(allMessages) }, null, 2),
73
+ },
74
+ ],
75
+ };
76
+ });
77
+ }
@@ -0,0 +1,134 @@
1
+ import { getStoredAgentSession, isLocalRoomStorageEnabled, saveAgentSession, } from "../../local-state.js";
2
+ import { getGitCurrentBranch } from "../../git-remote.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { normalizeAgentBaseName } from "../../../shared/codenames.js";
5
+ import { formatOwnerAttribution } from "../../../shared/agent-identity.js";
6
+ import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, } from "../../../shared/request-headers.js";
7
+ import { AGENT_INSTANCE_UUID, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, } from "./identity.js";
8
+ export function buildAgentDeliveryHeaders(agentSession) {
9
+ if (!agentSession) {
10
+ return {};
11
+ }
12
+ return {
13
+ [LETAGENTS_AGENT_SESSION_ID_HEADER]: agentSession.session_id,
14
+ [LETAGENTS_AGENT_SESSION_TOKEN_HEADER]: agentSession.session_token,
15
+ };
16
+ }
17
+ export function toPublicAgentSession(session) {
18
+ if (!session) {
19
+ return null;
20
+ }
21
+ return {
22
+ session_id: session.session_id,
23
+ room_id: session.room_id,
24
+ session_kind: session.session_kind,
25
+ runtime: session.runtime,
26
+ host_id: session.host_id ?? null,
27
+ host_kind: session.host_kind ?? null,
28
+ host_label: session.host_label ?? null,
29
+ liveness_capability: session.liveness_capability ?? null,
30
+ tool_bridge_id: session.tool_bridge_id ?? null,
31
+ actor_label: session.actor_label,
32
+ agent_key: session.agent_key,
33
+ agent_instance_id: session.agent_instance_id ?? null,
34
+ display_name: session.display_name,
35
+ owner_label: session.owner_label,
36
+ ide_label: session.ide_label,
37
+ repo_branch: session.repo_branch ?? null,
38
+ created_at: session.created_at,
39
+ updated_at: session.updated_at,
40
+ last_seen_at: session.last_seen_at,
41
+ ended_at: session.ended_at ?? null,
42
+ };
43
+ }
44
+ export function resolveAgentSession(roomId, sessionId) {
45
+ if (!sessionId) {
46
+ return null;
47
+ }
48
+ const session = getStoredAgentSession(sessionId);
49
+ if (!session) {
50
+ throw new Error(`Unknown agent_session_id: ${sessionId}`);
51
+ }
52
+ if (session.ended_at) {
53
+ throw new Error(`agent_session_id ${sessionId} ended at ${session.ended_at}`);
54
+ }
55
+ if (roomId && session.room_id !== roomId) {
56
+ throw new Error(`agent_session_id ${sessionId} is registered for ${session.room_id}, not ${roomId}`);
57
+ }
58
+ return session;
59
+ }
60
+ export function identityFromAgentSession(session) {
61
+ return {
62
+ name: normalizeAgentBaseName(session.display_name),
63
+ display_name: session.display_name,
64
+ owner_label: session.owner_label,
65
+ owner_attribution: formatOwnerAttribution(session.owner_label),
66
+ ide_label: session.ide_label,
67
+ actor_label: session.actor_label,
68
+ canonical_key: session.agent_key,
69
+ runtime_key: `agent_session:${session.session_id}`,
70
+ source: "api",
71
+ resolved_at: session.updated_at,
72
+ };
73
+ }
74
+ export function requireWorkerAgentSession(roomId, sessionId) {
75
+ const session = resolveAgentSession(roomId, sessionId);
76
+ if (!session) {
77
+ throw new Error("Registered worker agent_session_id is required for this write action. " +
78
+ "Call register_agent_session for this room first, then pass the returned agent_session_id explicitly.");
79
+ }
80
+ if (session.session_kind !== "worker") {
81
+ throw new Error("Worker agent_session_id is required for this write action.");
82
+ }
83
+ return session;
84
+ }
85
+ export async function resolveWorkerToolIdentity(input) {
86
+ const agentSession = input.agentSessionId
87
+ ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
88
+ : input.roomId && await isLocalRoomStorageEnabled(input.roomId)
89
+ ? await ensureLocalWorkerAgentSession(input.roomId)
90
+ : requireWorkerAgentSession(input.roomId, input.agentSessionId);
91
+ return {
92
+ identity: identityFromAgentSession(agentSession),
93
+ agentSession,
94
+ };
95
+ }
96
+ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
97
+ const identity = await ensureAgentIdentity();
98
+ const now = new Date().toISOString();
99
+ const runtime = input.runtime?.trim() || detectAgentRuntimeLabel();
100
+ const displayName = input.displayName?.trim() || identity.display_name;
101
+ return saveAgentSession({
102
+ session_id: `local_${randomUUID()}`,
103
+ session_token: `local_${randomUUID()}`,
104
+ room_id: roomId,
105
+ session_kind: input.sessionKind ?? "worker",
106
+ runtime,
107
+ host_id: null,
108
+ host_kind: "local",
109
+ host_label: "Local device",
110
+ liveness_capability: null,
111
+ tool_bridge_id: null,
112
+ actor_label: identity.actor_label,
113
+ agent_key: identity.canonical_key || identity.runtime_key || identity.actor_label,
114
+ agent_instance_id: AGENT_INSTANCE_UUID,
115
+ display_name: displayName,
116
+ owner_label: identity.owner_label,
117
+ ide_label: identity.ide_label ?? detectAgentIdeLabel(),
118
+ repo_branch: input.repoBranch ?? null,
119
+ created_at: now,
120
+ updated_at: now,
121
+ last_seen_at: now,
122
+ ended_at: null,
123
+ });
124
+ }
125
+ export function getAgentSessionRepoBranch(cwd) {
126
+ const workingDir = cwd?.trim() || process.cwd();
127
+ return getGitCurrentBranch(workingDir);
128
+ }
129
+ export function agentSessionCredentials(agentSession) {
130
+ return {
131
+ agent_session_id: agentSession.session_id,
132
+ agent_session_token: agentSession.session_token,
133
+ };
134
+ }