@hdwebsoft/hdcode-agent-team 0.1.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/index.js +681 -0
  2. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,681 @@
1
+ // src/teams.ts
2
+ import { readdir as readdir2 } from "node:fs/promises";
3
+ import { join as join3 } from "node:path";
4
+
5
+ // src/utils.ts
6
+ import { readdir, mkdir, rename, unlink } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ var TEAM_ROOT = ".team";
9
+ var SAFE_NAME = /^[a-z0-9-]+$/;
10
+ var LOCK_TIMEOUT_MS = 1e4;
11
+ var LOCK_RETRY_MS = 50;
12
+ var LOCK_MAX_RETRIES = 100;
13
+ function validateName(name, label) {
14
+ if (!SAFE_NAME.test(name)) {
15
+ throw new Error(`Invalid ${label}: "${name}" (must be lowercase alphanumeric + hyphens)`);
16
+ }
17
+ }
18
+ function teamDir(base, teamName) {
19
+ validateName(teamName, "teamName");
20
+ return join(base, TEAM_ROOT, teamName);
21
+ }
22
+ async function exists(path) {
23
+ return Bun.file(path).exists();
24
+ }
25
+ async function writeJsonAtomic(path, data) {
26
+ const tmp = path + ".tmp";
27
+ await Bun.write(tmp, JSON.stringify(data, null, 2));
28
+ await rename(tmp, path);
29
+ }
30
+ async function readJson(path) {
31
+ const file = Bun.file(path);
32
+ if (!await file.exists()) {
33
+ throw new Error(`File not found: ${path}`);
34
+ }
35
+ return file.json();
36
+ }
37
+ async function listJsonFiles(dir) {
38
+ if (!await exists(dir))
39
+ return [];
40
+ const entries = await readdir(dir);
41
+ return entries.filter((e) => e.endsWith(".json")).map((e) => join(dir, e));
42
+ }
43
+ async function ensureDir(path) {
44
+ await mkdir(path, { recursive: true });
45
+ }
46
+ async function withLock(lockPath, fn) {
47
+ let acquired = false;
48
+ for (let i = 0;i < LOCK_MAX_RETRIES; i++) {
49
+ if (await exists(lockPath)) {
50
+ const content = await Bun.file(lockPath).text().catch(() => "0");
51
+ const lockTime = parseInt(content, 10) || 0;
52
+ if (Date.now() - lockTime > LOCK_TIMEOUT_MS) {
53
+ await Bun.write(lockPath, Date.now().toString());
54
+ acquired = true;
55
+ break;
56
+ }
57
+ await Bun.sleep(LOCK_RETRY_MS);
58
+ } else {
59
+ await Bun.write(lockPath, Date.now().toString());
60
+ acquired = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!acquired) {
65
+ throw new Error(`Failed to acquire lock: ${lockPath} (timed out after ${LOCK_MAX_RETRIES * LOCK_RETRY_MS}ms)`);
66
+ }
67
+ try {
68
+ return await fn();
69
+ } finally {
70
+ await unlink(lockPath).catch(() => {});
71
+ }
72
+ }
73
+
74
+ // src/tasks.ts
75
+ import { join as join2 } from "node:path";
76
+ function taskFilePath(base, teamName, taskId) {
77
+ return join2(teamDir(base, teamName), "tasks", `${taskId}.json`);
78
+ }
79
+ async function nextTaskId(tasksDir) {
80
+ const counterPath = join2(tasksDir, ".counter");
81
+ let counter = 0;
82
+ if (await exists(counterPath)) {
83
+ const content = await Bun.file(counterPath).text();
84
+ counter = parseInt(content, 10) || 0;
85
+ }
86
+ counter++;
87
+ await Bun.write(counterPath, counter.toString());
88
+ return counter.toString();
89
+ }
90
+ async function loadAllTasks(base, teamName) {
91
+ const dir = join2(teamDir(base, teamName), "tasks");
92
+ const files = await listJsonFiles(dir);
93
+ const tasks = [];
94
+ for (const f of files) {
95
+ tasks.push(await readJson(f));
96
+ }
97
+ return tasks;
98
+ }
99
+ function hasCycle(taskId, blockedBy, allTasks) {
100
+ const visited = new Set;
101
+ const queue = [...blockedBy];
102
+ while (queue.length > 0) {
103
+ const current = queue.shift();
104
+ if (current === taskId)
105
+ return true;
106
+ if (visited.has(current))
107
+ continue;
108
+ visited.add(current);
109
+ const task = allTasks.get(current);
110
+ if (task)
111
+ queue.push(...task.blockedBy);
112
+ }
113
+ return false;
114
+ }
115
+ async function unblockDependents(base, teamName, completedTaskId) {
116
+ const unblocked = [];
117
+ const allTasks = await loadAllTasks(base, teamName);
118
+ for (const t of allTasks) {
119
+ if (t.id === completedTaskId)
120
+ continue;
121
+ if (!t.blockedBy.includes(completedTaskId))
122
+ continue;
123
+ t.blockedBy = t.blockedBy.filter((bid) => bid !== completedTaskId);
124
+ if (t.blockedBy.length === 0 && t.status === "pending") {
125
+ unblocked.push(t.id);
126
+ }
127
+ await writeJsonAtomic(taskFilePath(base, teamName, t.id), t);
128
+ }
129
+ return unblocked;
130
+ }
131
+
132
+ // src/teams.ts
133
+ async function getActiveTeamsSummary(base) {
134
+ const root = join3(base, TEAM_ROOT);
135
+ if (!await exists(root))
136
+ return null;
137
+ const entries = await readdir2(root, { withFileTypes: true });
138
+ const summaries = [];
139
+ for (const entry of entries) {
140
+ if (!entry.isDirectory())
141
+ continue;
142
+ const configPath = join3(root, entry.name, "config.json");
143
+ if (!await exists(configPath))
144
+ continue;
145
+ try {
146
+ const config = await readJson(configPath);
147
+ const tasks = await loadAllTasks(base, entry.name);
148
+ const pending = tasks.filter((t) => t.status === "pending").length;
149
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
150
+ const completed = tasks.filter((t) => t.status === "completed").length;
151
+ const members = config.members.filter((m) => m.isActive).length;
152
+ if (pending > 0 || inProgress > 0) {
153
+ summaries.push(`- Team "${config.name}" (${config.description}): ${members} active members, ${inProgress} in_progress, ${pending} pending, ${completed} completed`);
154
+ }
155
+ } catch {}
156
+ }
157
+ return summaries.length > 0 ? summaries.join(`
158
+ `) : null;
159
+ }
160
+
161
+ // src/tools/team-tools.ts
162
+ import { readdir as readdir3, rm } from "node:fs/promises";
163
+ import { join as join4 } from "node:path";
164
+ import { tool } from "@opencode-ai/plugin";
165
+
166
+ // src/messages.ts
167
+ async function readInbox(inboxPath) {
168
+ if (!await exists(inboxPath))
169
+ return [];
170
+ return readJson(inboxPath);
171
+ }
172
+ async function appendToInbox(inboxPath, message) {
173
+ const messages = await readInbox(inboxPath);
174
+ messages.push(message);
175
+ await writeJsonAtomic(inboxPath, messages);
176
+ }
177
+
178
+ // src/tools/team-tools.ts
179
+ var teamTools = {
180
+ team_create: tool({
181
+ description: "Create a new agent team with directory structure and CC-aligned config. Lead is automatically added as first member. Returns the team config.",
182
+ args: {
183
+ teamName: tool.schema.string().regex(/^[a-z0-9-]+$/).describe("Team name in kebab-case"),
184
+ description: tool.schema.string().describe("Team description")
185
+ },
186
+ async execute(args, context) {
187
+ const dir = teamDir(context.directory, args.teamName);
188
+ if (await exists(join4(dir, "config.json"))) {
189
+ return `Error: Team "${args.teamName}" already exists`;
190
+ }
191
+ await ensureDir(join4(dir, "tasks"));
192
+ await ensureDir(join4(dir, "inboxes"));
193
+ await ensureDir(join4(dir, "reports"));
194
+ const now = Date.now();
195
+ const leadId = `team-lead@${args.teamName}`;
196
+ const config = {
197
+ name: args.teamName,
198
+ description: args.description,
199
+ createdAt: now,
200
+ leadAgentId: leadId,
201
+ members: [
202
+ {
203
+ agentId: leadId,
204
+ name: "team-lead",
205
+ agentType: "team-lead",
206
+ model: "unknown",
207
+ planModeRequired: false,
208
+ joinedAt: now,
209
+ cwd: context.directory,
210
+ isActive: true
211
+ }
212
+ ]
213
+ };
214
+ await writeJsonAtomic(join4(dir, "config.json"), config);
215
+ await writeJsonAtomic(join4(dir, "inboxes", "team-lead.json"), []);
216
+ return JSON.stringify(config, null, 2);
217
+ }
218
+ }),
219
+ team_delete: tool({
220
+ description: "Delete a team and all its data (tasks, inboxes, reports).",
221
+ args: {
222
+ teamName: tool.schema.string().describe("Team name to delete")
223
+ },
224
+ async execute(args, context) {
225
+ const dir = teamDir(context.directory, args.teamName);
226
+ if (!await exists(join4(dir, "config.json"))) {
227
+ return `Error: Team "${args.teamName}" not found`;
228
+ }
229
+ await rm(dir, { recursive: true, force: true });
230
+ return `Team "${args.teamName}" deleted successfully`;
231
+ }
232
+ }),
233
+ team_status: tool({
234
+ description: "Get team status: config, task summary by status, message counts per agent, reports, completion flag.",
235
+ args: {
236
+ teamName: tool.schema.string().describe("Team name")
237
+ },
238
+ async execute(args, context) {
239
+ const dir = teamDir(context.directory, args.teamName);
240
+ if (!await exists(join4(dir, "config.json"))) {
241
+ return `Error: Team "${args.teamName}" not found`;
242
+ }
243
+ const config = await readJson(join4(dir, "config.json"));
244
+ const tasks = await loadAllTasks(context.directory, args.teamName);
245
+ const summary = { pending: 0, in_progress: 0, completed: 0 };
246
+ for (const t of tasks) {
247
+ if (t.status in summary) {
248
+ summary[t.status]++;
249
+ }
250
+ }
251
+ const inboxesDir = join4(dir, "inboxes");
252
+ const messageCounts = {};
253
+ if (await exists(inboxesDir)) {
254
+ const inboxFiles = await listJsonFiles(inboxesDir);
255
+ for (const f of inboxFiles) {
256
+ const agentName = f.split("/").pop().replace(".json", "");
257
+ const messages = await readInbox(f);
258
+ messageCounts[agentName] = {
259
+ total: messages.length,
260
+ unread: messages.filter((m) => !m.read).length
261
+ };
262
+ }
263
+ }
264
+ const reportDir = join4(dir, "reports");
265
+ const reports = await exists(reportDir) ? (await readdir3(reportDir)).filter((f) => !f.startsWith(".")) : [];
266
+ const total = Object.values(summary).reduce((a, b) => a + b, 0);
267
+ const isComplete = total > 0 && summary.pending === 0 && summary.in_progress === 0;
268
+ return JSON.stringify({ config, taskSummary: summary, messageCounts, reports, isComplete }, null, 2);
269
+ }
270
+ }),
271
+ team_list: tool({
272
+ description: "List all teams with basic info.",
273
+ args: {},
274
+ async execute(_args, context) {
275
+ const root = join4(context.directory, TEAM_ROOT);
276
+ if (!await exists(root))
277
+ return "No teams found";
278
+ const entries = await readdir3(root, { withFileTypes: true });
279
+ const teams = [];
280
+ for (const entry of entries) {
281
+ if (!entry.isDirectory())
282
+ continue;
283
+ const configPath = join4(root, entry.name, "config.json");
284
+ if (!await exists(configPath))
285
+ continue;
286
+ const config = await readJson(configPath);
287
+ const tasks = await loadAllTasks(context.directory, entry.name);
288
+ const pending = tasks.filter((t) => t.status === "pending").length;
289
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
290
+ const completed = tasks.filter((t) => t.status === "completed").length;
291
+ const isComplete = tasks.length > 0 && pending === 0 && inProgress === 0;
292
+ teams.push({
293
+ name: config.name,
294
+ description: config.description,
295
+ createdAt: config.createdAt,
296
+ memberCount: config.members.length,
297
+ taskSummary: { pending, in_progress: inProgress, completed },
298
+ isComplete
299
+ });
300
+ }
301
+ return teams.length > 0 ? JSON.stringify(teams, null, 2) : "No teams found";
302
+ }
303
+ })
304
+ };
305
+
306
+ // src/tools/task-tools.ts
307
+ import { join as join5 } from "node:path";
308
+ import { tool as tool2 } from "@opencode-ai/plugin";
309
+ var taskTools = {
310
+ task_create: tool2({
311
+ description: "Create a task with auto-increment integer ID. Supports optional dependencies (addBlocks/addBlockedBy) at creation. If blockedBy tasks are not completed, status stays pending. Returns created task.",
312
+ args: {
313
+ teamName: tool2.schema.string().describe("Team name"),
314
+ subject: tool2.schema.string().describe("Task subject, <60 chars, imperative"),
315
+ description: tool2.schema.string().describe("Task description"),
316
+ activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
317
+ addBlocks: tool2.schema.string().optional().describe('JSON array of task IDs this task blocks, e.g. ["3","4"]'),
318
+ addBlockedBy: tool2.schema.string().optional().describe('JSON array of task IDs that block this task, e.g. ["1"]'),
319
+ metadata: tool2.schema.string().optional().describe("JSON object of arbitrary metadata")
320
+ },
321
+ async execute(args, context) {
322
+ const dir = teamDir(context.directory, args.teamName);
323
+ if (!await exists(join5(dir, "config.json"))) {
324
+ return `Error: Team "${args.teamName}" not found`;
325
+ }
326
+ const tasksDir = join5(dir, "tasks");
327
+ await ensureDir(tasksDir);
328
+ const tasksLockPath = join5(tasksDir, ".lock");
329
+ return withLock(tasksLockPath, async () => {
330
+ const id = await nextTaskId(tasksDir);
331
+ let addBlocks = [];
332
+ let addBlockedBy = [];
333
+ let metadata;
334
+ if (args.addBlocks) {
335
+ try {
336
+ addBlocks = JSON.parse(args.addBlocks);
337
+ } catch {
338
+ return "Error: Invalid addBlocks JSON";
339
+ }
340
+ }
341
+ if (args.addBlockedBy) {
342
+ try {
343
+ addBlockedBy = JSON.parse(args.addBlockedBy);
344
+ } catch {
345
+ return "Error: Invalid addBlockedBy JSON";
346
+ }
347
+ }
348
+ if (args.metadata) {
349
+ try {
350
+ metadata = JSON.parse(args.metadata);
351
+ } catch {
352
+ return "Error: Invalid metadata JSON";
353
+ }
354
+ }
355
+ const allTasks = await loadAllTasks(context.directory, args.teamName);
356
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
357
+ for (const bid of addBlockedBy) {
358
+ if (!taskMap.has(bid))
359
+ return `Error: Blocker task "${bid}" not found`;
360
+ }
361
+ for (const bid of addBlocks) {
362
+ if (!taskMap.has(bid))
363
+ return `Error: Blocked task "${bid}" not found`;
364
+ }
365
+ if (addBlockedBy.length > 0 && hasCycle(id, addBlockedBy, taskMap)) {
366
+ return `Error: Circular dependency detected for task ${id}`;
367
+ }
368
+ const task = {
369
+ id,
370
+ subject: args.subject,
371
+ description: args.description,
372
+ ...args.activeForm && { activeForm: args.activeForm },
373
+ status: "pending",
374
+ blocks: addBlocks,
375
+ blockedBy: addBlockedBy,
376
+ ...metadata && { metadata }
377
+ };
378
+ await writeJsonAtomic(join5(tasksDir, `${id}.json`), task);
379
+ for (const targetId of addBlockedBy) {
380
+ const targetPath = taskFilePath(context.directory, args.teamName, targetId);
381
+ const target = await readJson(targetPath);
382
+ if (!target.blocks.includes(id)) {
383
+ target.blocks.push(id);
384
+ await writeJsonAtomic(targetPath, target);
385
+ }
386
+ }
387
+ for (const targetId of addBlocks) {
388
+ const targetPath = taskFilePath(context.directory, args.teamName, targetId);
389
+ const target = await readJson(targetPath);
390
+ if (!target.blockedBy.includes(id)) {
391
+ target.blockedBy.push(id);
392
+ await writeJsonAtomic(targetPath, target);
393
+ }
394
+ }
395
+ return JSON.stringify(task, null, 2);
396
+ });
397
+ }
398
+ }),
399
+ task_update: tool2({
400
+ description: "Update task fields, manage dependencies, auto-unblock. Status transitions: pending→in_progress, pending→completed, in_progress→completed. On completion: auto-unblocks dependents. Returns {task, unblocked[]}.",
401
+ args: {
402
+ teamName: tool2.schema.string().describe("Team name"),
403
+ taskId: tool2.schema.string().describe("Task ID (integer string)"),
404
+ status: tool2.schema.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
405
+ subject: tool2.schema.string().optional().describe("New subject"),
406
+ description: tool2.schema.string().optional().describe("New description"),
407
+ activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
408
+ owner: tool2.schema.string().optional().describe("Agent name claiming this task"),
409
+ addBlocks: tool2.schema.string().optional().describe("JSON array of task IDs to add to blocks[]"),
410
+ addBlockedBy: tool2.schema.string().optional().describe("JSON array of task IDs to add to blockedBy[]"),
411
+ metadata: tool2.schema.string().optional().describe("JSON object to merge into metadata")
412
+ },
413
+ async execute(args, context) {
414
+ const dir = teamDir(context.directory, args.teamName);
415
+ if (!await exists(join5(dir, "config.json"))) {
416
+ return `Error: Team "${args.teamName}" not found`;
417
+ }
418
+ const tasksDir = join5(dir, "tasks");
419
+ const tasksLockPath = join5(tasksDir, ".lock");
420
+ return withLock(tasksLockPath, async () => {
421
+ const tPath = taskFilePath(context.directory, args.teamName, args.taskId);
422
+ if (!await exists(tPath)) {
423
+ return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
424
+ }
425
+ const task = await readJson(tPath);
426
+ if (args.status) {
427
+ const validTransitions = {
428
+ pending: ["in_progress", "completed"],
429
+ in_progress: ["completed"],
430
+ completed: []
431
+ };
432
+ const allowed = validTransitions[task.status];
433
+ if (!allowed || !allowed.includes(args.status)) {
434
+ return `Error: Invalid transition ${task.status} → ${args.status}`;
435
+ }
436
+ if (args.status === "in_progress" && task.blockedBy.length > 0) {
437
+ const activeBlockers = [];
438
+ for (const bid of task.blockedBy) {
439
+ const blockerPath = taskFilePath(context.directory, args.teamName, bid);
440
+ if (await exists(blockerPath)) {
441
+ const blocker = await readJson(blockerPath);
442
+ if (blocker.status !== "completed") {
443
+ activeBlockers.push(bid);
444
+ }
445
+ }
446
+ }
447
+ if (activeBlockers.length > 0) {
448
+ return `Error: Cannot start task with unresolved blockers: ${activeBlockers.join(", ")}`;
449
+ }
450
+ }
451
+ task.status = args.status;
452
+ }
453
+ if (args.subject !== undefined)
454
+ task.subject = args.subject;
455
+ if (args.description !== undefined)
456
+ task.description = args.description;
457
+ if (args.activeForm !== undefined)
458
+ task.activeForm = args.activeForm;
459
+ if (args.owner !== undefined)
460
+ task.owner = args.owner;
461
+ if (args.metadata) {
462
+ let newMeta;
463
+ try {
464
+ newMeta = JSON.parse(args.metadata);
465
+ } catch {
466
+ return "Error: Invalid metadata JSON";
467
+ }
468
+ task.metadata = { ...task.metadata, ...newMeta };
469
+ }
470
+ if (args.addBlocks) {
471
+ let newBlocks;
472
+ try {
473
+ newBlocks = JSON.parse(args.addBlocks);
474
+ } catch {
475
+ return "Error: Invalid addBlocks JSON";
476
+ }
477
+ for (const targetId of newBlocks) {
478
+ if (!task.blocks.includes(targetId)) {
479
+ task.blocks.push(targetId);
480
+ }
481
+ const targetPath = taskFilePath(context.directory, args.teamName, targetId);
482
+ if (await exists(targetPath)) {
483
+ const target = await readJson(targetPath);
484
+ if (!target.blockedBy.includes(task.id)) {
485
+ target.blockedBy.push(task.id);
486
+ await writeJsonAtomic(targetPath, target);
487
+ }
488
+ }
489
+ }
490
+ }
491
+ if (args.addBlockedBy) {
492
+ let newBlockedBy;
493
+ try {
494
+ newBlockedBy = JSON.parse(args.addBlockedBy);
495
+ } catch {
496
+ return "Error: Invalid addBlockedBy JSON";
497
+ }
498
+ for (const targetId of newBlockedBy) {
499
+ if (!task.blockedBy.includes(targetId)) {
500
+ task.blockedBy.push(targetId);
501
+ }
502
+ const targetPath = taskFilePath(context.directory, args.teamName, targetId);
503
+ if (await exists(targetPath)) {
504
+ const target = await readJson(targetPath);
505
+ if (!target.blocks.includes(task.id)) {
506
+ target.blocks.push(task.id);
507
+ await writeJsonAtomic(targetPath, target);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ await writeJsonAtomic(tPath, task);
513
+ let unblocked = [];
514
+ if (task.status === "completed") {
515
+ unblocked = await unblockDependents(context.directory, args.teamName, task.id);
516
+ }
517
+ return JSON.stringify({ task, unblocked }, null, 2);
518
+ });
519
+ }
520
+ }),
521
+ task_get: tool2({
522
+ description: "Get a single task by ID.",
523
+ args: {
524
+ teamName: tool2.schema.string().describe("Team name"),
525
+ taskId: tool2.schema.string().describe("Task ID (integer string)")
526
+ },
527
+ async execute(args, context) {
528
+ const tPath = taskFilePath(context.directory, args.teamName, args.taskId);
529
+ if (!await exists(tPath)) {
530
+ return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
531
+ }
532
+ return JSON.stringify(await readJson(tPath), null, 2);
533
+ }
534
+ }),
535
+ task_list: tool2({
536
+ description: "List all tasks in a team with full details, sorted by integer ID.",
537
+ args: {
538
+ teamName: tool2.schema.string().describe("Team name")
539
+ },
540
+ async execute(args, context) {
541
+ const dir = teamDir(context.directory, args.teamName);
542
+ if (!await exists(join5(dir, "config.json"))) {
543
+ return `Error: Team "${args.teamName}" not found`;
544
+ }
545
+ const tasks = await loadAllTasks(context.directory, args.teamName);
546
+ tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
547
+ return tasks.length > 0 ? JSON.stringify(tasks, null, 2) : "No tasks found";
548
+ }
549
+ })
550
+ };
551
+
552
+ // src/tools/message-tools.ts
553
+ import { join as join6 } from "node:path";
554
+ import { tool as tool3 } from "@opencode-ai/plugin";
555
+ var messageTools = {
556
+ message_send: tool3({
557
+ description: "Send a message to a recipient's inbox or broadcast to all agents. Types: message, broadcast, shutdown_request, shutdown_response, plan_approval_response. Returns {sent: true, recipients: [...]}.",
558
+ args: {
559
+ teamName: tool3.schema.string().describe("Team name"),
560
+ type: tool3.schema.enum([
561
+ "message",
562
+ "broadcast",
563
+ "shutdown_request",
564
+ "shutdown_response",
565
+ "plan_approval_response"
566
+ ]).describe("Message type"),
567
+ recipient: tool3.schema.string().optional().describe("Recipient agent name (required for non-broadcast types)"),
568
+ content: tool3.schema.string().describe("Message content (markdown)"),
569
+ summary: tool3.schema.string().optional().describe("5-10 word preview"),
570
+ from: tool3.schema.string().optional().describe("Sender name (defaults to team-lead)")
571
+ },
572
+ async execute(args, context) {
573
+ const dir = teamDir(context.directory, args.teamName);
574
+ const configPath = join6(dir, "config.json");
575
+ if (!await exists(configPath)) {
576
+ return `Error: Team "${args.teamName}" not found`;
577
+ }
578
+ const sender = args.from || "team-lead";
579
+ const inboxesDir = join6(dir, "inboxes");
580
+ await ensureDir(inboxesDir);
581
+ const inboxesLockPath = join6(inboxesDir, ".lock");
582
+ const message = {
583
+ from: sender,
584
+ text: args.content,
585
+ ...args.summary && { summary: args.summary },
586
+ timestamp: new Date().toISOString(),
587
+ read: false
588
+ };
589
+ if (args.type === "broadcast") {
590
+ const config = await readJson(configPath);
591
+ const recipients = [];
592
+ return withLock(inboxesLockPath, async () => {
593
+ for (const member of config.members) {
594
+ if (member.name === sender)
595
+ continue;
596
+ const inboxPath = join6(inboxesDir, `${member.name}.json`);
597
+ await appendToInbox(inboxPath, message);
598
+ recipients.push(member.name);
599
+ }
600
+ return JSON.stringify({ sent: true, recipients }, null, 2);
601
+ });
602
+ } else {
603
+ if (!args.recipient) {
604
+ return "Error: recipient is required for non-broadcast messages";
605
+ }
606
+ return withLock(inboxesLockPath, async () => {
607
+ const inboxPath = join6(inboxesDir, `${args.recipient}.json`);
608
+ await appendToInbox(inboxPath, message);
609
+ return JSON.stringify({ sent: true, recipients: [args.recipient] }, null, 2);
610
+ });
611
+ }
612
+ }
613
+ }),
614
+ message_fetch: tool3({
615
+ description: "Fetch messages from an agent's inbox. Optionally filter by timestamp and mark as read.",
616
+ args: {
617
+ teamName: tool3.schema.string().describe("Team name"),
618
+ agent: tool3.schema.string().describe("Agent name whose inbox to read"),
619
+ since: tool3.schema.string().optional().describe("ISO timestamp — return only messages after this time"),
620
+ markRead: tool3.schema.boolean().optional().describe("Mark fetched messages as read (default false)")
621
+ },
622
+ async execute(args, context) {
623
+ const dir = teamDir(context.directory, args.teamName);
624
+ if (!await exists(join6(dir, "config.json"))) {
625
+ return `Error: Team "${args.teamName}" not found`;
626
+ }
627
+ const inboxPath = join6(dir, "inboxes", `${args.agent}.json`);
628
+ let messages = await readInbox(inboxPath);
629
+ if (args.since) {
630
+ const sinceDate = new Date(args.since);
631
+ messages = messages.filter((m) => new Date(m.timestamp) > sinceDate);
632
+ }
633
+ if (args.markRead && messages.length > 0) {
634
+ const allMessages = await readInbox(inboxPath);
635
+ const sinceDate = args.since ? new Date(args.since) : null;
636
+ let changed = false;
637
+ for (const m of allMessages) {
638
+ if (sinceDate && new Date(m.timestamp) <= sinceDate)
639
+ continue;
640
+ if (!m.read) {
641
+ m.read = true;
642
+ changed = true;
643
+ }
644
+ }
645
+ if (changed) {
646
+ await writeJsonAtomic(inboxPath, allMessages);
647
+ }
648
+ }
649
+ messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
650
+ return messages.length > 0 ? JSON.stringify(messages, null, 2) : "No messages found";
651
+ }
652
+ })
653
+ };
654
+
655
+ // src/index.ts
656
+ var HDTeamPlugin = async ({ directory }) => {
657
+ return {
658
+ "experimental.session.compacting": async (_input, output) => {
659
+ const teamSummary = await getActiveTeamsSummary(directory);
660
+ if (teamSummary) {
661
+ output.context.push(`
662
+ ## Active Agent Teams
663
+
664
+ The following teams are currently active with pending work:
665
+
666
+ ${teamSummary}
667
+
668
+ Use team_status(teamName) to get full details. Continue any in-progress tasks.
669
+ `);
670
+ }
671
+ },
672
+ tool: {
673
+ ...teamTools,
674
+ ...taskTools,
675
+ ...messageTools
676
+ }
677
+ };
678
+ };
679
+ export {
680
+ HDTeamPlugin
681
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@hdwebsoft/hdcode-agent-team",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin for multi-agent team coordination — per-agent inboxes, task management with dependency tracking, and file locking.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun build src/index.ts --outdir dist --target node --external @opencode-ai/plugin",
21
+ "prepublishOnly": "bun run build",
22
+ "test": "bun test"
23
+ },
24
+ "keywords": [
25
+ "opencode",
26
+ "plugin",
27
+ "agent",
28
+ "team",
29
+ "multi-agent",
30
+ "task-management",
31
+ "inbox",
32
+ "coordination"
33
+ ],
34
+ "author": "HDWebSoft",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/hdwebsoft/hdcode-agent-team.git"
39
+ },
40
+ "peerDependencies": {
41
+ "@opencode-ai/plugin": ">=0.15.0"
42
+ },
43
+ "devDependencies": {
44
+ "@opencode-ai/plugin": "^0.15.0",
45
+ "@types/bun": "^1.3.1",
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }