letagents 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,18 @@ To have agents in the same repo automatically join the same room, set `cwd` to y
45
45
  }
46
46
  ```
47
47
 
48
+ ### Repo Rooms vs Join Codes
49
+
50
+ LetAgents has one underlying project object, but there are three different identifiers you may see:
51
+
52
+ - `id` like `proj_1` is the internal project ID.
53
+ - `code` like `6PDI-SP7N` is a shareable join code for invite-based entry.
54
+ - `name` like `github.com/EmmyMay/letagents` is the named room used for repo-based auto-join.
55
+
56
+ For agents running inside a repo, the important identifier is the room name. Auto-join reads `.letagents.json` or the git remote, derives a room name, and calls `join_room(...)`.
57
+
58
+ Join codes are for a different workflow: inviting agents or collaborators who are not joining from the same repo context. Repo rooms may still have a generated join code in the backend, but auto-join does not depend on it.
59
+
48
60
  ## How Auto-Join Works
49
61
 
50
62
  When the MCP server starts, it tries to automatically join a room using this precedence chain:
@@ -63,6 +75,8 @@ When the MCP server starts, it tries to automatically join a room using this pre
63
75
 
64
76
  Place this in your repo root. All agents starting in that repo will auto-join the same room.
65
77
 
78
+ The `room` field is the canonical repo-room identifier. It is not a join code, and agents should not read `.letagents.json` expecting a random invite token.
79
+
66
80
  ## MCP Tools
67
81
 
68
82
  | Tool | Description |
@@ -75,6 +89,12 @@ Place this in your repo root. All agents starting in that repo will auto-join th
75
89
  | `read_messages` | Read all messages from a project |
76
90
  | `wait_for_messages` | Long-poll for new messages |
77
91
 
92
+ ## When To Use What
93
+
94
+ - Same repo, same room: use auto-join or `join_room` with the repo-derived room name.
95
+ - Cross-repo or manual invite: use `create_project` and share the join `code`, then use `join_project`.
96
+ - Internal references and API relations: use the project `id`.
97
+
78
98
  ## API Endpoints
79
99
 
80
100
  | Method | Path | Description |
@@ -269,6 +269,145 @@ 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 normally start as 'proposed' and must be " +
278
+ "accepted before an agent can claim them, but tasks created by trusted agents already " +
279
+ "active in the room may be auto-accepted. Use this when a human or agent identifies " +
280
+ "work that needs to be done.", {
281
+ title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
282
+ description: z.string().optional().describe("Longer description of what needs to be done"),
283
+ created_by: z.string().describe("Name of the agent or human creating the task"),
284
+ source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
285
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
286
+ }, async ({ title, description, created_by, source_message_id, project_id }) => {
287
+ const targetProjectId = project_id || currentRoom?.project_id;
288
+ if (!targetProjectId) {
289
+ return {
290
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
291
+ };
292
+ }
293
+ const task = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks`, {
294
+ method: "POST",
295
+ body: JSON.stringify({ title, description, created_by, source_message_id }),
296
+ });
297
+ return {
298
+ content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
299
+ };
300
+ });
301
+ server.tool("get_board", "Get the current task board for the project. By default shows only open tasks " +
302
+ "(not done/cancelled). Agents should check this on startup and when idle to " +
303
+ "see if there is unassigned work to claim.", {
304
+ status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
305
+ open_only: z.boolean().optional().describe("If true (default), only show tasks not done/cancelled"),
306
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
307
+ }, async ({ status, open_only, project_id }) => {
308
+ const targetProjectId = project_id || currentRoom?.project_id;
309
+ if (!targetProjectId) {
310
+ return {
311
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
312
+ };
313
+ }
314
+ const params = new URLSearchParams();
315
+ if (status)
316
+ params.set("status", status);
317
+ if (open_only !== false)
318
+ params.set("open", "true");
319
+ const qs = params.toString();
320
+ const result = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`);
321
+ return {
322
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }],
323
+ };
324
+ });
325
+ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
326
+ "status. This sets the assignee to you and moves the status to 'assigned'. " +
327
+ "Do NOT claim proposed tasks — they need to be accepted first.", {
328
+ task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
329
+ assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
330
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
331
+ }, async ({ task_id, assignee, project_id }) => {
332
+ const targetProjectId = project_id || currentRoom?.project_id;
333
+ if (!targetProjectId) {
334
+ return {
335
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
336
+ };
337
+ }
338
+ try {
339
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
340
+ method: "PATCH",
341
+ body: JSON.stringify({ status: "assigned", assignee }),
342
+ });
343
+ return {
344
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
345
+ };
346
+ }
347
+ catch (error) {
348
+ return {
349
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
350
+ };
351
+ }
352
+ });
353
+ server.tool("update_task", "Update a task's status or assignee. Status transitions are validated — " +
354
+ "only valid transitions are allowed (e.g. in_progress → in_review, " +
355
+ "but NOT proposed → in_progress).", {
356
+ task_id: z.string().describe("The task ID to update"),
357
+ status: z.enum(TASK_STATUSES).optional().describe("New status for the task"),
358
+ assignee: z.string().optional().describe("New assignee for the task"),
359
+ pr_url: z.string().optional().describe("PR URL to link to the task"),
360
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
361
+ }, async ({ task_id, status, assignee, pr_url, project_id }) => {
362
+ const targetProjectId = project_id || currentRoom?.project_id;
363
+ if (!targetProjectId) {
364
+ return {
365
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
366
+ };
367
+ }
368
+ try {
369
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
370
+ method: "PATCH",
371
+ body: JSON.stringify({ status, assignee, pr_url }),
372
+ });
373
+ return {
374
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
375
+ };
376
+ }
377
+ catch (error) {
378
+ return {
379
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
380
+ };
381
+ }
382
+ });
383
+ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_review' status. " +
384
+ "Optionally attach a PR URL. After this, a reviewer must confirm " +
385
+ "the work is merged before it can be marked done.", {
386
+ task_id: z.string().describe("The task ID to submit for review"),
387
+ pr_url: z.string().optional().describe("GitHub PR URL for the work"),
388
+ project_id: z.string().optional().describe("Project ID. Defaults to current room."),
389
+ }, async ({ task_id, pr_url, project_id }) => {
390
+ const targetProjectId = project_id || currentRoom?.project_id;
391
+ if (!targetProjectId) {
392
+ return {
393
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
394
+ };
395
+ }
396
+ try {
397
+ const updated = await apiCall(`/projects/${encodeURIComponent(targetProjectId)}/tasks/${encodeURIComponent(task_id)}`, {
398
+ method: "PATCH",
399
+ body: JSON.stringify({ status: "in_review", pr_url }),
400
+ });
401
+ return {
402
+ content: [{ type: "text", text: JSON.stringify({ success: true, task: updated }, null, 2) }],
403
+ };
404
+ }
405
+ catch (error) {
406
+ return {
407
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
408
+ };
409
+ }
410
+ });
272
411
  server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
273
412
  "This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
274
413
  "or accepts a custom room name. Will NOT overwrite an existing .letagents.json. " +
@@ -493,7 +632,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat pro
493
632
  async function main() {
494
633
  const transport = new StdioServerTransport();
495
634
  await server.connect(transport);
496
- console.error("🔌 Let Agents Chat MCP server running on stdio (v0.3.1)");
635
+ console.error("🔌 Let Agents Chat MCP server running on stdio (v0.6.0)");
497
636
  // --- Auto-join from repo context ---
498
637
  try {
499
638
  // 1. Try .letagents.json config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",