letagents 0.12.9 → 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 (117) 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 -672
  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 -351
  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 -3157
  105. package/dist/mcp/sse-client.js +6 -14
  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 +4 -3
  109. package/dist/shared/rental/lrt.js +214 -0
  110. package/dist/shared/rental/meter-types.js +14 -0
  111. package/dist/shared/room-agent-prompts.js +14 -1
  112. package/package.json +15 -5
  113. package/dist/mcp/__tests__/config-reader.test.js +0 -93
  114. package/dist/mcp/__tests__/git-remote.test.js +0 -37
  115. package/dist/mcp/__tests__/server-helpers.test.js +0 -121
  116. package/dist/shared/agent-reasoning.js +0 -49
  117. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,113 @@
1
+ import { existsSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { z } from "zod";
4
+ import { getGitRemoteIdentity } from "../../../git-remote.js";
5
+ import { joinRoomIdentifier, } from "../../runtime.js";
6
+ import { findExistingConfig, resolveGitRoot, } from "../../repo-context.js";
7
+ import { jsonToolResponse } from "./response.js";
8
+ export function registerRepoInitializationTool(server) {
9
+ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
10
+ "This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
11
+ "or accepts a custom room name. Will NOT overwrite an existing .letagents.json. " +
12
+ "Always writes to the repo root, not the current working directory.", {
13
+ room: z
14
+ .string()
15
+ .optional()
16
+ .describe("Custom room name. If omitted, auto-derived from git remote (e.g. 'github.com/owner/repo')"),
17
+ cwd: z
18
+ .string()
19
+ .optional()
20
+ .describe("Working directory hint for repo detection. Defaults to the current process directory."),
21
+ }, async ({ room, cwd: targetDir }) => {
22
+ const startDir = targetDir || process.cwd();
23
+ const repoRoot = resolveGitRoot(startDir);
24
+ if (!repoRoot) {
25
+ return jsonToolResponse({
26
+ success: false,
27
+ error: "Not inside a git repository",
28
+ hint: "Run this tool from inside a git repo, or pass a 'cwd' pointing to one.",
29
+ });
30
+ }
31
+ const existingConfigResponse = getExistingConfigResponse(startDir, repoRoot);
32
+ if (existingConfigResponse)
33
+ return jsonToolResponse(existingConfigResponse);
34
+ const roomNameResult = resolveRoomName(room, repoRoot);
35
+ if (!roomNameResult.success)
36
+ return jsonToolResponse(roomNameResult);
37
+ const configPath = join(repoRoot, ".letagents.json");
38
+ try {
39
+ writeFileSync(configPath, JSON.stringify({ room: roomNameResult.roomName }, null, 2) + "\n", "utf-8");
40
+ }
41
+ catch (err) {
42
+ return jsonToolResponse({
43
+ success: false,
44
+ error: `Failed to write config: ${err instanceof Error ? err.message : err}`,
45
+ path: configPath,
46
+ });
47
+ }
48
+ return jsonToolResponse(await joinInitializedRoom({
49
+ configPath,
50
+ roomName: roomNameResult.roomName,
51
+ }));
52
+ });
53
+ }
54
+ function getExistingConfigResponse(startDir, repoRoot) {
55
+ const existingConfigDir = findExistingConfig(startDir);
56
+ if (existingConfigDir) {
57
+ const existingPath = join(existingConfigDir, ".letagents.json");
58
+ return {
59
+ success: false,
60
+ error: ".letagents.json already exists",
61
+ path: existingPath,
62
+ hint: existingConfigDir === repoRoot
63
+ ? "Delete the existing file first if you want to reinitialize."
64
+ : `Found a config in a parent directory (${existingConfigDir}). Delete it or move it to ${repoRoot} to reinitialize.`,
65
+ };
66
+ }
67
+ const configPath = join(repoRoot, ".letagents.json");
68
+ if (existsSync(configPath)) {
69
+ return {
70
+ success: false,
71
+ error: ".letagents.json already exists",
72
+ path: configPath,
73
+ hint: "Delete the existing file first if you want to reinitialize.",
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ function resolveRoomName(room, repoRoot) {
79
+ if (room)
80
+ return { success: true, roomName: room };
81
+ const gitRoom = getGitRemoteIdentity(repoRoot);
82
+ if (!gitRoom) {
83
+ return {
84
+ success: false,
85
+ error: "Cannot derive room name: no git remote found and no custom room name provided",
86
+ hint: "Pass a 'room' parameter or run from inside a git repo with a remote configured.",
87
+ };
88
+ }
89
+ return { success: true, roomName: gitRoom };
90
+ }
91
+ async function joinInitializedRoom(input) {
92
+ try {
93
+ const joined = await joinRoomIdentifier(input.roomName, "config");
94
+ return {
95
+ success: true,
96
+ created: input.configPath,
97
+ room_id: joined.room.room_id,
98
+ code: joined.room.code ?? null,
99
+ joined: true,
100
+ hint: "Consider adding .letagents.json to git so other contributors auto-join the same room.",
101
+ };
102
+ }
103
+ catch (err) {
104
+ return {
105
+ success: true,
106
+ created: input.configPath,
107
+ room_id: input.roomName,
108
+ joined: false,
109
+ error: `Config created but auto-join failed: ${err instanceof Error ? err.message : err}`,
110
+ hint: "The .letagents.json was created. Use join_room to manually connect.",
111
+ };
112
+ }
113
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { autoDetectRepo } from "../../../repo-visibility.js";
3
+ import { jsonToolResponse } from "./response.js";
4
+ export function registerRepoVisibilityTool(server) {
5
+ server.tool("check_repo_visibility", "Auto-detect the current repo's git remote and check whether its Git Room should be public/discoverable or private/auth-gated. Returns the canonical key, provider, visibility, accessMode, and legacy roomType hint.", {
6
+ cwd: z
7
+ .string()
8
+ .optional()
9
+ .describe("Working directory to detect git remote from. Defaults to the MCP server's working directory."),
10
+ }, async ({ cwd }) => {
11
+ const result = await autoDetectRepo(cwd);
12
+ if (!result) {
13
+ return jsonToolResponse({
14
+ error: "Not in a git repository or no remote configured",
15
+ suggestion: "Pass cwd inside a git repo for Git Room detection, or join/create an ad-hoc room manually.",
16
+ });
17
+ }
18
+ return jsonToolResponse(result);
19
+ });
20
+ }
@@ -0,0 +1,10 @@
1
+ export function jsonToolResponse(value) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: JSON.stringify(value, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import { ApiError, RepoRoomAuthRequiredError, ensureAgentIdentity, getStoredCurrentRoom, getStoredRoomSession, parseApiErrorPayload, joinRoomIdentifierWithoutImplicitGitRefCreate, toPublicAgentIdentity, toPublicRoomState, toRepoRoomAuthRequiredResult, withJoinRoomAgentPrompt, } from "../../runtime.js";
3
+ import { jsonToolResponse } from "./response.js";
4
+ export function registerRoomResumeTool(server) {
5
+ 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.", {
6
+ room_id: z
7
+ .string()
8
+ .optional()
9
+ .describe("Optional saved room ID to resume. Defaults to the last current room."),
10
+ }, async ({ room_id }) => {
11
+ const savedRoom = (room_id ? getStoredRoomSession(room_id) : null) ??
12
+ getStoredCurrentRoom();
13
+ if (!savedRoom) {
14
+ return jsonToolResponse({ success: false, error: "No saved room session found." });
15
+ }
16
+ try {
17
+ const joined = await joinRoomIdentifierWithoutImplicitGitRefCreate(savedRoom.room_id, savedRoom.joined_via);
18
+ const agentIdentity = await ensureAgentIdentity();
19
+ return jsonToolResponse(withJoinRoomAgentPrompt({
20
+ success: true,
21
+ rejoined_from_local_state: true,
22
+ server_session_resumed: false,
23
+ last_message_id_before_restart: savedRoom.last_message_id ?? null,
24
+ room: toPublicRoomState(joined.room),
25
+ agent_identity: toPublicAgentIdentity(agentIdentity),
26
+ }));
27
+ }
28
+ catch (error) {
29
+ if (error instanceof RepoRoomAuthRequiredError) {
30
+ return jsonToolResponse(toRepoRoomAuthRequiredResult(error));
31
+ }
32
+ const payload = parseApiErrorPayload(error);
33
+ if (error instanceof ApiError &&
34
+ error.status === 404 &&
35
+ payload?.code === "ROOM_NOT_FOUND") {
36
+ return jsonToolResponse({
37
+ success: false,
38
+ error: "Saved room no longer exists. Join the repository room or ask a human to create the branch room again.",
39
+ code: "ROOM_NOT_FOUND",
40
+ room_id: savedRoom.room_id,
41
+ rejoined_from_local_state: false,
42
+ server_session_resumed: false,
43
+ });
44
+ }
45
+ throw error;
46
+ }
47
+ });
48
+ }
@@ -0,0 +1 @@
1
+ export { registerRepoInitializationTool, registerRepoVisibilityTool, registerRoomInspectionTools, registerRoomJoinTools, registerRoomResumeTool, } from "./rooms/index.js";
@@ -0,0 +1,222 @@
1
+ import { encodeRoomIdPath } from "../../../room-id.js";
2
+ import { addLocalTask, apiCall, claimLocalTaskReviewLease, getLocalTask, isLocalRoomStorageEnabled, listLocalTasks, releaseLocalTaskReviewLease, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, updateLocalTask, } from "../../runtime.js";
3
+ export function taskCollectionRoomPath(roomId) {
4
+ return `/rooms/${encodeRoomIdPath(roomId)}/tasks`;
5
+ }
6
+ export function taskDetailRoomPath(roomId, taskId) {
7
+ return `${taskCollectionRoomPath(roomId)}/${encodeURIComponent(taskId)}`;
8
+ }
9
+ export function boardSettingsRoomPath(roomId) {
10
+ return `/rooms/${encodeRoomIdPath(roomId)}/board-settings`;
11
+ }
12
+ export function boardManagersRoomPath(roomId) {
13
+ return `/rooms/${encodeRoomIdPath(roomId)}/board-managers`;
14
+ }
15
+ export function activeBoardManagerRoomPath(roomId) {
16
+ return `${boardManagersRoomPath(roomId)}/active`;
17
+ }
18
+ export function boardIntentsRoomPath(roomId) {
19
+ return `/rooms/${encodeRoomIdPath(roomId)}/board-intents`;
20
+ }
21
+ export function boardIntentDecisionRoomPath(roomId, intentId, decision) {
22
+ return `${boardIntentsRoomPath(roomId)}/${encodeURIComponent(intentId)}/${decision}`;
23
+ }
24
+ export function taskCollectionProjectPath(projectId) {
25
+ return `/projects/${encodeURIComponent(projectId)}/tasks`;
26
+ }
27
+ export function taskDetailProjectPath(projectId, taskId) {
28
+ return `${taskCollectionProjectPath(projectId)}/${encodeURIComponent(taskId)}`;
29
+ }
30
+ function localRoomIdForTarget(target) {
31
+ return target.effectiveRoomId ?? target.roomId ?? target.projectId ?? null;
32
+ }
33
+ export async function createTask(target, body) {
34
+ const localRoomId = localRoomIdForTarget(target);
35
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
36
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
37
+ return addLocalTask(sqliteRoomId || localRoomId, {
38
+ title: String(body.title || ""),
39
+ description: typeof body.description === "string" ? body.description : null,
40
+ created_by: typeof body.created_by === "string" ? body.created_by : null,
41
+ });
42
+ }
43
+ return roomScopedApiCall({
44
+ room_id: target.roomId,
45
+ project_id: target.projectId,
46
+ room_path: taskCollectionRoomPath,
47
+ project_path: taskCollectionProjectPath,
48
+ options: {
49
+ method: "POST",
50
+ body: JSON.stringify(body),
51
+ },
52
+ });
53
+ }
54
+ export async function listTasks(target, queryString) {
55
+ const localRoomId = localRoomIdForTarget(target);
56
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
57
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
58
+ const params = new URLSearchParams(queryString);
59
+ return listLocalTasks(sqliteRoomId || localRoomId, {
60
+ status: params.get("status"),
61
+ openOnly: params.get("open") !== "false",
62
+ });
63
+ }
64
+ return roomScopedApiCall({
65
+ room_id: target.roomId,
66
+ project_id: target.projectId,
67
+ room_path: (roomId) => `${taskCollectionRoomPath(roomId)}${queryString ? `?${queryString}` : ""}`,
68
+ project_path: (projectId) => `${taskCollectionProjectPath(projectId)}${queryString ? `?${queryString}` : ""}`,
69
+ });
70
+ }
71
+ export async function getCanonicalTask(roomId, taskId) {
72
+ return apiCall(taskDetailRoomPath(roomId, taskId));
73
+ }
74
+ export async function patchTask(target, taskId, body) {
75
+ const localRoomId = localRoomIdForTarget(target);
76
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
77
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
78
+ return updateLocalTask(sqliteRoomId || localRoomId, taskId, body);
79
+ }
80
+ return roomScopedApiCall({
81
+ room_id: target.roomId,
82
+ project_id: target.projectId,
83
+ room_path: (roomId) => taskDetailRoomPath(roomId, taskId),
84
+ project_path: (projectId) => taskDetailProjectPath(projectId, taskId),
85
+ options: {
86
+ method: "PATCH",
87
+ body: JSON.stringify(body),
88
+ },
89
+ });
90
+ }
91
+ export async function postCanonicalTaskAction(roomId, taskId, actionPath, body) {
92
+ if (await isLocalRoomStorageEnabled(roomId)) {
93
+ const { localRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
94
+ const sqliteRoomId = localRoomId || roomId;
95
+ const action = typeof body.action === "string" ? body.action : "noop";
96
+ const existingTask = await getLocalTask(sqliteRoomId, taskId);
97
+ if (!existingTask) {
98
+ throw new Error("Task not found.");
99
+ }
100
+ if (actionPath === "review-lease-action") {
101
+ if (action === "claim") {
102
+ const result = await claimLocalTaskReviewLease(sqliteRoomId, taskId, {
103
+ holder_label: typeof body.actor_label === "string" ? body.actor_label : null,
104
+ agent_key: typeof body.actor_key === "string" ? body.actor_key : null,
105
+ agent_session_id: typeof body.agent_session_id === "string"
106
+ ? body.agent_session_id
107
+ : null,
108
+ });
109
+ return {
110
+ action,
111
+ task: result.task,
112
+ lease: result.lease,
113
+ };
114
+ }
115
+ if (action === "release") {
116
+ const result = await releaseLocalTaskReviewLease(sqliteRoomId, taskId, {
117
+ lease_id: typeof body.lease_id === "string" ? body.lease_id : null,
118
+ });
119
+ return {
120
+ action,
121
+ task: result.task,
122
+ released_lease: result.released_lease,
123
+ };
124
+ }
125
+ return {
126
+ action,
127
+ task: existingTask,
128
+ lease: null,
129
+ released_lease: null,
130
+ };
131
+ }
132
+ if (action === "handoff") {
133
+ const targetActorKey = typeof body.target_actor_key === "string" && body.target_actor_key.trim()
134
+ ? body.target_actor_key.trim()
135
+ : null;
136
+ const targetAgentSessionId = typeof body.target_agent_session_id === "string" && body.target_agent_session_id.trim()
137
+ ? body.target_agent_session_id.trim()
138
+ : null;
139
+ const targetActorInstanceId = typeof body.target_actor_instance_id === "string" && body.target_actor_instance_id.trim()
140
+ ? body.target_actor_instance_id.trim()
141
+ : null;
142
+ const task = targetActorKey
143
+ ? await updateLocalTask(sqliteRoomId, taskId, {
144
+ ...(existingTask.status === "accepted" ? { status: "assigned" } : {}),
145
+ assignee: targetActorKey,
146
+ assignee_agent_key: targetActorKey,
147
+ assignee_agent_instance_id: targetActorInstanceId,
148
+ assignee_agent_session_id: targetAgentSessionId,
149
+ })
150
+ : existingTask;
151
+ return {
152
+ action,
153
+ task,
154
+ released_lease: null,
155
+ new_lease: null,
156
+ };
157
+ }
158
+ if (action === "release") {
159
+ const releasableStatuses = new Set(["assigned", "in_progress", "blocked", "in_review"]);
160
+ const patch = {
161
+ assignee: null,
162
+ assignee_agent_key: null,
163
+ };
164
+ if (releasableStatuses.has(existingTask.status)) {
165
+ patch.status = "accepted";
166
+ patch.skip_transition_validation = true;
167
+ }
168
+ const task = await updateLocalTask(sqliteRoomId, taskId, patch);
169
+ return {
170
+ action,
171
+ task,
172
+ released_lease: null,
173
+ };
174
+ }
175
+ return {
176
+ action,
177
+ task: existingTask,
178
+ released_lease: null,
179
+ };
180
+ }
181
+ const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
182
+ return apiCall(`${taskDetailRoomPath(cloudRoomId || roomId, taskId)}/${actionPath}`, {
183
+ method: "POST",
184
+ body: JSON.stringify(body),
185
+ });
186
+ }
187
+ export async function getBoardSettings(roomId) {
188
+ return apiCall(boardSettingsRoomPath(roomId));
189
+ }
190
+ export async function patchBoardSettings(roomId, body) {
191
+ return apiCall(boardSettingsRoomPath(roomId), {
192
+ method: "PATCH",
193
+ body: JSON.stringify(body),
194
+ });
195
+ }
196
+ export async function postBoardManager(roomId, body) {
197
+ return apiCall(boardManagersRoomPath(roomId), {
198
+ method: "POST",
199
+ body: JSON.stringify(body),
200
+ });
201
+ }
202
+ export async function deleteActiveBoardManager(roomId, body) {
203
+ return apiCall(activeBoardManagerRoomPath(roomId), {
204
+ method: "DELETE",
205
+ body: JSON.stringify(body),
206
+ });
207
+ }
208
+ export async function getBoardIntents(roomId, queryString) {
209
+ return apiCall(`${boardIntentsRoomPath(roomId)}${queryString ? `?${queryString}` : ""}`);
210
+ }
211
+ export async function postBoardIntent(roomId, body) {
212
+ return apiCall(boardIntentsRoomPath(roomId), {
213
+ method: "POST",
214
+ body: JSON.stringify(body),
215
+ });
216
+ }
217
+ export async function postBoardIntentDecision(roomId, intentId, decision, body) {
218
+ return apiCall(boardIntentDecisionRoomPath(roomId, intentId, decision), {
219
+ method: "POST",
220
+ body: JSON.stringify(body),
221
+ });
222
+ }
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { encodeRoomIdPath } from "../../../room-id.js";
3
+ import { roomScopedApiCall } from "../../runtime.js";
4
+ import { resolveTaskToolTarget } from "./context.js";
5
+ import { jsonToolResponse, taskToolError } from "./response.js";
6
+ import { workflowArtifactSchema } from "./schemas.js";
7
+ function buildRoomArtifactsQueryString(input) {
8
+ const params = new URLSearchParams();
9
+ if (input.task_id)
10
+ params.set("task_id", input.task_id);
11
+ if (input.limit)
12
+ params.set("limit", String(input.limit));
13
+ return params.toString();
14
+ }
15
+ export function registerTaskArtifactTools(server) {
16
+ server.tool("get_room_artifacts", "Get shared workflow artifacts for the room. Returns provider-neutral artifacts such as PRs, branches, issues, checks, and their linked task IDs.", {
17
+ task_id: z.string().optional().describe("Optional task ID to filter artifacts linked to that task."),
18
+ limit: z.number().int().min(1).max(250).optional().describe("Max artifacts to return (default 100, max 250)."),
19
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
20
+ }, async ({ task_id, limit, room_id }) => {
21
+ const target = resolveTaskToolTarget(room_id);
22
+ if (!target)
23
+ return taskToolError("Not in a room. Join one first.");
24
+ const qs = buildRoomArtifactsQueryString({ task_id, limit });
25
+ const result = await roomScopedApiCall({
26
+ room_id: target.roomId,
27
+ project_id: target.projectId,
28
+ room_path: (roomId) => `/rooms/${encodeRoomIdPath(roomId)}/artifacts${qs ? `?${qs}` : ""}`,
29
+ project_path: (projectId) => `/rooms/${encodeURIComponent(projectId)}/artifacts${qs ? `?${qs}` : ""}`,
30
+ });
31
+ return jsonToolResponse({
32
+ success: true,
33
+ room_id: result.room_id ?? target.projectId,
34
+ artifacts: result.artifacts ?? [],
35
+ }, 2);
36
+ });
37
+ server.tool("publish_room_artifact", "Publish a provider-neutral Git workflow artifact into the shared room artifact list. Use this for branches, PRs, issues, reviews, checks, or merges that should be visible to the room even if they were not created through a task update or GitHub webhook.", {
38
+ artifact: workflowArtifactSchema.describe("Artifact to publish. It must include provider, kind, and at least one stable identity: url, id, number, or ref."),
39
+ task_id: z.string().optional().describe("Optional task ID to link to this artifact."),
40
+ linked_task_ids: z
41
+ .array(z.string())
42
+ .max(32)
43
+ .optional()
44
+ .describe("Optional task IDs to link to this artifact."),
45
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
46
+ }, async ({ artifact, task_id, linked_task_ids, room_id }) => {
47
+ const target = resolveTaskToolTarget(room_id);
48
+ if (!target)
49
+ return taskToolError("Not in a room. Join one first.");
50
+ try {
51
+ const result = await roomScopedApiCall({
52
+ room_id: target.roomId,
53
+ project_id: target.projectId,
54
+ room_path: (roomId) => `/rooms/${encodeRoomIdPath(roomId)}/artifacts`,
55
+ project_path: (projectId) => `/rooms/${encodeURIComponent(projectId)}/artifacts`,
56
+ options: {
57
+ method: "POST",
58
+ body: JSON.stringify({
59
+ artifact,
60
+ task_id,
61
+ linked_task_ids,
62
+ }),
63
+ },
64
+ });
65
+ return jsonToolResponse({
66
+ success: true,
67
+ room_id: result.room_id ?? target.projectId,
68
+ artifact: result.artifact ?? null,
69
+ }, 2);
70
+ }
71
+ catch (error) {
72
+ return taskToolError(String(error));
73
+ }
74
+ });
75
+ }