letagents 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/mcp/server.js +138 -0
  2. package/package.json +1 -1
@@ -269,6 +269,144 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
269
269
  ],
270
270
  };
271
271
  });
272
+ // -- Task Board Tools -------------------------------------------------------
273
+ const TASK_STATUSES = [
274
+ "proposed", "accepted", "assigned", "in_progress",
275
+ "blocked", "in_review", "merged", "done", "cancelled",
276
+ ];
277
+ server.tool("add_task", "Add a new task to the project board. Tasks start as 'proposed' and must be " +
278
+ "accepted before an agent can claim them. Use this when a human or agent " +
279
+ "identifies work that needs to be done.", {
280
+ title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
281
+ description: z.string().optional().describe("Longer description of what needs to be done"),
282
+ created_by: z.string().describe("Name of the agent or human creating the task"),
283
+ source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
284
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
285
+ }, async ({ title, description, created_by, source_message_id, project_id }) => {
286
+ const targetProjectId = project_id || currentRoom?.project_id;
287
+ if (!targetProjectId) {
288
+ return {
289
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
290
+ };
291
+ }
292
+ const task = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks`, {
293
+ method: "POST",
294
+ body: JSON.stringify({ title, description, created_by, source_message_id }),
295
+ });
296
+ return {
297
+ content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
298
+ };
299
+ });
300
+ server.tool("get_board", "Get the current task board for the project. By default shows only open tasks " +
301
+ "(not done/cancelled). Agents should check this on startup and when idle to " +
302
+ "see if there is unassigned work to claim.", {
303
+ status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
304
+ open_only: z.boolean().optional().describe("If true (default), only show tasks not done/cancelled"),
305
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
306
+ }, async ({ status, open_only, project_id }) => {
307
+ const targetProjectId = project_id || currentRoom?.project_id;
308
+ if (!targetProjectId) {
309
+ return {
310
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
311
+ };
312
+ }
313
+ const params = new URLSearchParams();
314
+ if (status)
315
+ params.set("status", status);
316
+ if (open_only !== false)
317
+ params.set("open", "true");
318
+ const qs = params.toString();
319
+ const result = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`);
320
+ return {
321
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }],
322
+ };
323
+ });
324
+ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
325
+ "status. This sets the assignee to you and moves the status to 'assigned'. " +
326
+ "Do NOT claim proposed tasks — they need to be accepted first.", {
327
+ task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
328
+ assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
329
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
330
+ }, async ({ task_id, assignee, project_id }) => {
331
+ const targetProjectId = project_id || currentRoom?.project_id;
332
+ if (!targetProjectId) {
333
+ return {
334
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
335
+ };
336
+ }
337
+ try {
338
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
339
+ method: "PATCH",
340
+ body: JSON.stringify({ status: "assigned", assignee }),
341
+ });
342
+ return {
343
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
344
+ };
345
+ }
346
+ catch (error) {
347
+ return {
348
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
349
+ };
350
+ }
351
+ });
352
+ server.tool("update_task", "Update a task's status or assignee. Status transitions are validated — " +
353
+ "only valid transitions are allowed (e.g. in_progress → in_review, " +
354
+ "but NOT proposed → in_progress).", {
355
+ task_id: z.string().describe("The task ID to update"),
356
+ status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
357
+ assignee: z.string().optional().describe("New assignee for the task"),
358
+ pr_url: z.string().optional().describe("PR URL to link to the task"),
359
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
360
+ }, async ({ task_id, status, assignee, pr_url, project_id }) => {
361
+ const targetProjectId = project_id || currentRoom?.project_id;
362
+ if (!targetProjectId) {
363
+ return {
364
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
365
+ };
366
+ }
367
+ try {
368
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
369
+ method: "PATCH",
370
+ body: JSON.stringify({ status, assignee, pr_url }),
371
+ });
372
+ return {
373
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
374
+ };
375
+ }
376
+ catch (error) {
377
+ return {
378
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
379
+ };
380
+ }
381
+ });
382
+ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_review' status. " +
383
+ "Optionally attach a PR URL. After this, a reviewer must confirm " +
384
+ "the work is merged before it can be marked done.", {
385
+ task_id: z.string().describe("The task ID to submit for review"),
386
+ pr_url: z.string().optional().describe("GitHub PR URL for the work"),
387
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
388
+ }, async ({ task_id, pr_url, project_id }) => {
389
+ const targetProjectId = project_id || currentRoom?.project_id;
390
+ if (!targetProjectId) {
391
+ return {
392
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
393
+ };
394
+ }
395
+ try {
396
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
397
+ method: "PATCH",
398
+ body: JSON.stringify({ status: "in_review", pr_url }),
399
+ });
400
+ return {
401
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
402
+ };
403
+ }
404
+ catch (error) {
405
+ return {
406
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
407
+ };
408
+ }
409
+ });
272
410
  server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
273
411
  "This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
274
412
  "or accepts a custom room name. Will NOT overwrite an existing .letagents.json. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",