agentmash 0.3.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/LICENSE +21 -0
- package/README.md +72 -0
- package/agentmash.mjs +1798 -0
- package/clients/cursor/after_file_edit.mjs +115 -0
- package/clients/git/post_commit.mjs +205 -0
- package/hooks/lib.mjs +1957 -0
- package/hooks/mcp_launcher.mjs +386 -0
- package/hooks/post_tool_use.mjs +78 -0
- package/hooks/pre_tool_use.mjs +191 -0
- package/hooks/session_end.mjs +76 -0
- package/hooks/stop.mjs +88 -0
- package/hooks/user_prompt_submit.mjs +65 -0
- package/mcp/coordination.mjs +126 -0
- package/mcp/render.mjs +358 -0
- package/mcp/server.mjs +452 -0
- package/package.json +52 -0
package/mcp/server.mjs
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AgentMash MCP server — "who is working on what?", answerable mid-plan.
|
|
3
|
+
//
|
|
4
|
+
// The hooks tell an agent about a collision at the moment it edits a file,
|
|
5
|
+
// which is the last moment the answer can still be useful. This exposes the
|
|
6
|
+
// same room over MCP so an agent can ask while it is still deciding what to
|
|
7
|
+
// do.
|
|
8
|
+
//
|
|
9
|
+
// It is a stdio server that reuses the coordination server's existing HTTP
|
|
10
|
+
// API and the hooks' config resolution, so there is no new endpoint, no new
|
|
11
|
+
// credential, and nothing to keep in sync but the room id that already lives
|
|
12
|
+
// in the repo.
|
|
13
|
+
//
|
|
14
|
+
// Nothing is ever written to stdout except JSON-RPC: that channel is the
|
|
15
|
+
// protocol. Diagnostics go to stderr.
|
|
16
|
+
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
21
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
22
|
+
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
import { buildReconciliationBrief, toRepoRelative } from '../hooks/lib.mjs';
|
|
26
|
+
import { fetchJson, normalizePath, postJson, resolveCoordination } from './coordination.mjs';
|
|
27
|
+
import {
|
|
28
|
+
renderClaimResult,
|
|
29
|
+
renderClaims,
|
|
30
|
+
renderCollisions,
|
|
31
|
+
renderContestedFiles,
|
|
32
|
+
renderPathActivity,
|
|
33
|
+
renderRelease,
|
|
34
|
+
renderResolved,
|
|
35
|
+
renderUnavailable,
|
|
36
|
+
renderWhoIsWorking,
|
|
37
|
+
} from './render.mjs';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Claims are held by a session, and this process is one. Claude Code does not
|
|
41
|
+
* hand its own session id to MCP servers, so the process mints its own: the
|
|
42
|
+
* hooks' session_end cannot release these, which is why the transport closing
|
|
43
|
+
* does (see start) and why every claim also carries a TTL.
|
|
44
|
+
*/
|
|
45
|
+
const SESSION_ID = `mcp-${crypto.randomBytes(8).toString('base64url')}`;
|
|
46
|
+
/** Only a process that actually claimed something has anything to release on exit. */
|
|
47
|
+
let claimedSomething = false;
|
|
48
|
+
|
|
49
|
+
const NAME = 'agentmash';
|
|
50
|
+
const VERSION = '0.1.0';
|
|
51
|
+
|
|
52
|
+
/** One hour of context by default; a working day when asking about a path. */
|
|
53
|
+
const DEFAULT_WINDOW_MIN = 60;
|
|
54
|
+
const DEFAULT_PATH_WINDOW_MIN = 480;
|
|
55
|
+
const MAX_WINDOW_MIN = 7 * 24 * 60; // the server prunes events after 7 days
|
|
56
|
+
|
|
57
|
+
const INSTRUCTIONS = `AgentMash coordinates the coding agents of everyone working in this same repository. It
|
|
58
|
+
reports who is active right now, what they said they are working on, which branch they are on, and which files
|
|
59
|
+
they have edited — and it lets you claim the scope you are about to work on so their agents route around you.
|
|
60
|
+
|
|
61
|
+
Use it while you are still planning, not after you have started editing. The point is to change what you do,
|
|
62
|
+
not to explain a collision afterwards.
|
|
63
|
+
|
|
64
|
+
The one habit that matters: before starting any task that touches more than a file or two, call claim_work with
|
|
65
|
+
the paths you expect to change and a one-sentence description. If it reports an overlap, split the work — take
|
|
66
|
+
what they have not claimed, or build on top of what they will land — instead of editing the same lines. When you
|
|
67
|
+
finish, call release_claim. Claims expire on their own and end with your session, so forgetting is safe; claiming
|
|
68
|
+
too broadly ("src") is not, because it tells your teammates nothing.
|
|
69
|
+
|
|
70
|
+
When a collision has already happened — you and a teammate both changed the same file — AgentMash will stop you
|
|
71
|
+
before you finish and hand you a reconciliation brief. You can also ask for it any time with get_collisions. Do
|
|
72
|
+
the merge yourself, on this machine, preserving both intents; then call mark_resolved so the room knows it is
|
|
73
|
+
settled and stops asking.
|
|
74
|
+
|
|
75
|
+
who_is_working_on_what, has_anyone_touched, list_claims, list_contested_files and get_collisions only read. When
|
|
76
|
+
the coordination server cannot be reached, every tool says so in as many words — never read a failure as "nobody
|
|
77
|
+
is working on anything".`;
|
|
78
|
+
|
|
79
|
+
const minutesField = (fallback, what) =>
|
|
80
|
+
z
|
|
81
|
+
.number()
|
|
82
|
+
.int()
|
|
83
|
+
.min(1)
|
|
84
|
+
.max(MAX_WINDOW_MIN)
|
|
85
|
+
.default(fallback)
|
|
86
|
+
.describe(
|
|
87
|
+
`How far back to look, in minutes (default ${fallback}${
|
|
88
|
+
fallback === 480 ? ', about a working day' : ''
|
|
89
|
+
}). ${what} Events older than 7 days are pruned by the server, so larger values stop helping.`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const includeSelfField = z
|
|
93
|
+
.boolean()
|
|
94
|
+
.default(false)
|
|
95
|
+
.describe(
|
|
96
|
+
"Include this agent's own developer identity in the answer. Off by default, because you rarely need to be told what you yourself have been editing."
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const text = (body) => ({ content: [{ type: 'text', text: body }] });
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolved per call rather than once at startup: `agentmash init` may connect
|
|
103
|
+
* the repo while this process is already running, and a long-lived server that
|
|
104
|
+
* cached "not connected" would keep saying so for the rest of the session.
|
|
105
|
+
*/
|
|
106
|
+
function coordinationNow() {
|
|
107
|
+
const index = process.argv.indexOf('--repo');
|
|
108
|
+
const fromFlag = index !== -1 ? process.argv[index + 1] : null;
|
|
109
|
+
return resolveCoordination(process.env.CLAUDE_PROJECT_DIR || fromFlag || process.cwd());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Ask /activity once and hand the payload to a renderer, or explain why we could not. */
|
|
113
|
+
async function activity(coordination, minutes, subject, render) {
|
|
114
|
+
const answer = await fetchJson(coordination, `/activity?minutes=${encodeURIComponent(minutes)}`);
|
|
115
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, subject));
|
|
116
|
+
return text(render(answer.data, coordination));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createServer() {
|
|
120
|
+
const server = new McpServer({ name: NAME, version: VERSION }, { instructions: INSTRUCTIONS });
|
|
121
|
+
const annotations = { readOnlyHint: true, openWorldHint: true };
|
|
122
|
+
|
|
123
|
+
server.registerTool(
|
|
124
|
+
'who_is_working_on_what',
|
|
125
|
+
{
|
|
126
|
+
title: 'Who is working on what',
|
|
127
|
+
description: `Ask this repository's AgentMash room who else is working here right now: their name, what
|
|
128
|
+
they told their agent they are doing, the branch they are on, and the files they have touched. Call it while
|
|
129
|
+
you are still planning — before you decide which files to change, and especially when the request is broad
|
|
130
|
+
enough to overlap with someone else ("refactor the auth layer", "add a migration", "clean up the API"). The
|
|
131
|
+
answer is meant to change your plan: if a teammate is already reshaping a module, read their changes first or
|
|
132
|
+
pick work that does not collide. If the coordination server cannot be reached, this says so explicitly; an
|
|
133
|
+
empty room and an unreachable server are worded differently, so do not treat one as the other.`,
|
|
134
|
+
inputSchema: {
|
|
135
|
+
minutes: minutesField(DEFAULT_WINDOW_MIN, 'Someone idle for longer than this will not appear.'),
|
|
136
|
+
include_self: includeSelfField,
|
|
137
|
+
},
|
|
138
|
+
annotations,
|
|
139
|
+
},
|
|
140
|
+
async ({ minutes, include_self }) => {
|
|
141
|
+
const coordination = coordinationNow();
|
|
142
|
+
return activity(coordination, minutes, 'who else is working in this repo', (data) =>
|
|
143
|
+
renderWhoIsWorking(data, coordination, { minutes, includeSelf: include_self })
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
server.registerTool(
|
|
149
|
+
'has_anyone_touched',
|
|
150
|
+
{
|
|
151
|
+
title: 'Has anyone touched this path',
|
|
152
|
+
description: `Ask whether anyone on the team has recently edited a particular file, or anything inside a
|
|
153
|
+
particular directory. Use it the moment you know which part of the repo your task lives in and while you can
|
|
154
|
+
still change course — "has anyone touched this module today?" is exactly the question it answers. Takes a
|
|
155
|
+
repo-relative path, either a file ("server/src/app.ts") or a directory ("server/src", "dashboard"), and
|
|
156
|
+
reports who touched what, when, what their edit did, and the task they were on. A genuinely quiet path and an
|
|
157
|
+
unreachable coordination server are worded differently, so do not read one as the other.`,
|
|
158
|
+
inputSchema: {
|
|
159
|
+
path: z
|
|
160
|
+
.string()
|
|
161
|
+
.min(1)
|
|
162
|
+
.max(1024)
|
|
163
|
+
.describe(
|
|
164
|
+
'Repo-relative path to a file or a directory, with forward slashes — for example "server/src/app.ts" or "server/src". A directory matches everything beneath it.'
|
|
165
|
+
),
|
|
166
|
+
minutes: minutesField(
|
|
167
|
+
DEFAULT_PATH_WINDOW_MIN,
|
|
168
|
+
'Edits older than this will not appear.'
|
|
169
|
+
),
|
|
170
|
+
include_self: includeSelfField,
|
|
171
|
+
},
|
|
172
|
+
annotations,
|
|
173
|
+
},
|
|
174
|
+
async ({ path, minutes, include_self }) => {
|
|
175
|
+
const coordination = coordinationNow();
|
|
176
|
+
// Agents hand over absolute paths as readily as relative ones; the room
|
|
177
|
+
// only ever stores repo-relative ones.
|
|
178
|
+
const target = normalizePath(toRepoRelative(coordination.projectDir, path));
|
|
179
|
+
if (!target) {
|
|
180
|
+
return text('`path` must be a repo-relative file or directory path, for example "server/src/app.ts".');
|
|
181
|
+
}
|
|
182
|
+
return activity(coordination, minutes, `recent activity under ${target}`, (data) =>
|
|
183
|
+
renderPathActivity(data, coordination, { target, minutes, includeSelf: include_self })
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
server.registerTool(
|
|
189
|
+
'list_contested_files',
|
|
190
|
+
{
|
|
191
|
+
title: 'List contested files',
|
|
192
|
+
description: `List the files in this repository that more than one person's agent has edited recently —
|
|
193
|
+
the places where a collision has already begun. Use it when you are choosing where to work, before a refactor
|
|
194
|
+
that spans several files, or whenever you want to know which parts of the codebase are currently crowded. An
|
|
195
|
+
empty list and an unreachable coordination server are worded differently.`,
|
|
196
|
+
inputSchema: {
|
|
197
|
+
minutes: minutesField(
|
|
198
|
+
DEFAULT_WINDOW_MIN,
|
|
199
|
+
'A file last contested longer ago than this will not appear.'
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
annotations,
|
|
203
|
+
},
|
|
204
|
+
async ({ minutes }) => {
|
|
205
|
+
const coordination = coordinationNow();
|
|
206
|
+
return activity(coordination, minutes, 'which files are contested', (data) =>
|
|
207
|
+
renderContestedFiles(data, coordination, { minutes })
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ── claims: say what you are taking, before you take it ──────────────────
|
|
213
|
+
const writeAnnotations = {
|
|
214
|
+
readOnlyHint: false,
|
|
215
|
+
destructiveHint: false,
|
|
216
|
+
idempotentHint: false,
|
|
217
|
+
openWorldHint: true,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
server.registerTool(
|
|
221
|
+
'claim_work',
|
|
222
|
+
{
|
|
223
|
+
title: 'Claim work',
|
|
224
|
+
description: `Tell the room what you are about to work on, so other agents can route around you — and find
|
|
225
|
+
out, before you write anything, whether someone has already claimed part of it. Call this at the start of any
|
|
226
|
+
task that will touch more than a file or two, with the repo-relative paths you expect to change and a
|
|
227
|
+
one-sentence description of the task. If the answer reports an OVERLAP, do not just carry on: split the work —
|
|
228
|
+
take the files and symbols they have not claimed, or build on what they will land rather than changing it. A
|
|
229
|
+
claim is stated intent, not a lock; it expires on its own, ends with your session, and is released early by
|
|
230
|
+
release_claim. Claim what you will actually touch: a claim on "src" tells your teammates nothing.`,
|
|
231
|
+
inputSchema: {
|
|
232
|
+
task: z
|
|
233
|
+
.string()
|
|
234
|
+
.min(3)
|
|
235
|
+
.max(300)
|
|
236
|
+
.describe(
|
|
237
|
+
'What you are about to do, in one sentence, the way you would tell a teammate. Shown to every other agent that comes near this scope.'
|
|
238
|
+
),
|
|
239
|
+
paths: z
|
|
240
|
+
.array(z.string().min(1).max(1024))
|
|
241
|
+
.min(1)
|
|
242
|
+
.max(50)
|
|
243
|
+
.describe(
|
|
244
|
+
'Repo-relative files or directories you expect to change, forward slashes. A directory claims everything beneath it.'
|
|
245
|
+
),
|
|
246
|
+
symbols: z
|
|
247
|
+
.array(z.string().min(1).max(200))
|
|
248
|
+
.max(100)
|
|
249
|
+
.optional()
|
|
250
|
+
.describe(
|
|
251
|
+
'Exported names you expect to change or rename, if you know them. Lets a teammate three files away be warned when you reshape an interface they import.'
|
|
252
|
+
),
|
|
253
|
+
ttl_minutes: z
|
|
254
|
+
.number()
|
|
255
|
+
.int()
|
|
256
|
+
.min(5)
|
|
257
|
+
.max(480)
|
|
258
|
+
.default(120)
|
|
259
|
+
.describe(
|
|
260
|
+
'How long the claim stands if you never release it (default 120). It is released early by release_claim and when your session ends.'
|
|
261
|
+
),
|
|
262
|
+
},
|
|
263
|
+
annotations: writeAnnotations,
|
|
264
|
+
},
|
|
265
|
+
async ({ task, paths, symbols, ttl_minutes }) => {
|
|
266
|
+
const coordination = coordinationNow();
|
|
267
|
+
const rel = paths.map((p) => normalizePath(toRepoRelative(coordination.projectDir, p))).filter(Boolean);
|
|
268
|
+
if (rel.length === 0) {
|
|
269
|
+
return text('`paths` must contain at least one repo-relative file or directory, for example "server/src/app.ts".');
|
|
270
|
+
}
|
|
271
|
+
const answer = await postJson(coordination, '/claims', {
|
|
272
|
+
developer: coordination.developer,
|
|
273
|
+
session_id: SESSION_ID,
|
|
274
|
+
task,
|
|
275
|
+
paths: rel,
|
|
276
|
+
symbols,
|
|
277
|
+
ttl_minutes,
|
|
278
|
+
});
|
|
279
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, 'whether this scope is already claimed'));
|
|
280
|
+
claimedSomething = true;
|
|
281
|
+
return text(renderClaimResult(answer.data, coordination, { paths: rel, task }));
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
server.registerTool(
|
|
286
|
+
'release_claim',
|
|
287
|
+
{
|
|
288
|
+
title: 'Release a claim',
|
|
289
|
+
description: `Release a claim you made with claim_work, once the work is done or you have decided not to do
|
|
290
|
+
it. Pass the claim id to release one, or nothing to release every claim this session holds. Not releasing is
|
|
291
|
+
safe — claims expire and end with your session — but releasing promptly lets a waiting teammate start sooner.`,
|
|
292
|
+
inputSchema: {
|
|
293
|
+
claim_id: z
|
|
294
|
+
.string()
|
|
295
|
+
.regex(/^claim_[A-Za-z0-9_-]+$/)
|
|
296
|
+
.optional()
|
|
297
|
+
.describe('The id claim_work returned. Omit to release every claim this session holds.'),
|
|
298
|
+
},
|
|
299
|
+
annotations: writeAnnotations,
|
|
300
|
+
},
|
|
301
|
+
async ({ claim_id }) => {
|
|
302
|
+
const coordination = coordinationNow();
|
|
303
|
+
const answer = await postJson(coordination, '/claims/release', {
|
|
304
|
+
developer: coordination.developer,
|
|
305
|
+
session_id: SESSION_ID,
|
|
306
|
+
...(claim_id ? { claim_id } : {}),
|
|
307
|
+
});
|
|
308
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, 'whether the claim was released'));
|
|
309
|
+
return text(renderRelease(answer.data, { claimId: claim_id }));
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
server.registerTool(
|
|
314
|
+
'list_claims',
|
|
315
|
+
{
|
|
316
|
+
title: 'List claims',
|
|
317
|
+
description: `List what other people's agents have claimed in this repository right now — the scopes they
|
|
318
|
+
have said they are taking, and for what. Cheaper than reading their edits, because a claim exists before any
|
|
319
|
+
edit does. Use it when choosing where to work. Nothing claimed and an unreachable server are worded differently.`,
|
|
320
|
+
inputSchema: { include_self: includeSelfField },
|
|
321
|
+
annotations,
|
|
322
|
+
},
|
|
323
|
+
async ({ include_self }) => {
|
|
324
|
+
const coordination = coordinationNow();
|
|
325
|
+
const answer = await fetchJson(coordination, '/claims');
|
|
326
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, 'what is claimed right now'));
|
|
327
|
+
return text(renderClaims(answer.data, coordination, { includeSelf: include_self }));
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// ── collisions: settle what the claim did not prevent ────────────────────
|
|
332
|
+
server.registerTool(
|
|
333
|
+
'get_collisions',
|
|
334
|
+
{
|
|
335
|
+
title: 'Get collisions',
|
|
336
|
+
description: `Which files you changed did a teammate also change? For each, this says who, when, on which
|
|
337
|
+
branch, what they were working on, what their edit did, and which symbols you both touched — then how to
|
|
338
|
+
reconcile: see their version, merge so both intents survive, and either open a pull request or commit
|
|
339
|
+
directly, depending on how this room is configured. Call it before you finish a task, or whenever a merge is
|
|
340
|
+
coming. The Stop hook asks the same question automatically as you finish; this lets you ask earlier. No
|
|
341
|
+
collisions and an unreachable server are worded differently.`,
|
|
342
|
+
inputSchema: {
|
|
343
|
+
minutes: minutesField(DEFAULT_WINDOW_MIN, 'Edits older than this are not considered collisions.'),
|
|
344
|
+
},
|
|
345
|
+
annotations,
|
|
346
|
+
},
|
|
347
|
+
async ({ minutes }) => {
|
|
348
|
+
const coordination = coordinationNow();
|
|
349
|
+
const answer = await fetchJson(
|
|
350
|
+
coordination,
|
|
351
|
+
`/collisions?developer=${encodeURIComponent(coordination.developer)}&minutes=${encodeURIComponent(minutes)}`
|
|
352
|
+
);
|
|
353
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, 'whether you have collided with anyone'));
|
|
354
|
+
return text(renderCollisions(answer.data, coordination, { minutes, brief: buildReconciliationBrief }));
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
server.registerTool(
|
|
359
|
+
'mark_resolved',
|
|
360
|
+
{
|
|
361
|
+
title: 'Mark a collision resolved',
|
|
362
|
+
description: `Tell the room a collision on a file is settled, so it stops prompting about it. Call it after
|
|
363
|
+
you have reconciled: outcome "pr" with the pull request URL when this room needs a human to approve, "merged"
|
|
364
|
+
when you committed the reconciled result directly, "no_conflict" when you checked and the two changes did not
|
|
365
|
+
actually overlap, or "dismissed" when the team decided to leave it. The room will prompt again only if someone
|
|
366
|
+
edits the file after this.`,
|
|
367
|
+
inputSchema: {
|
|
368
|
+
file_path: z
|
|
369
|
+
.string()
|
|
370
|
+
.min(1)
|
|
371
|
+
.max(1024)
|
|
372
|
+
.describe('Repo-relative path of the file whose collision you reconciled.'),
|
|
373
|
+
outcome: z
|
|
374
|
+
.enum(['pr', 'merged', 'no_conflict', 'dismissed'])
|
|
375
|
+
.describe('How it was settled. "pr" needs `url`.'),
|
|
376
|
+
url: z.string().url().optional().describe('The pull request URL, for outcome "pr".'),
|
|
377
|
+
note: z
|
|
378
|
+
.string()
|
|
379
|
+
.max(500)
|
|
380
|
+
.optional()
|
|
381
|
+
.describe('One sentence on what you kept from each side, for the people who will read the board.'),
|
|
382
|
+
},
|
|
383
|
+
annotations: writeAnnotations,
|
|
384
|
+
},
|
|
385
|
+
async ({ file_path, outcome, url, note }) => {
|
|
386
|
+
const coordination = coordinationNow();
|
|
387
|
+
const rel = normalizePath(toRepoRelative(coordination.projectDir, file_path));
|
|
388
|
+
if (!rel) return text('`file_path` must be a repo-relative path, for example "src/models/user.ts".');
|
|
389
|
+
const answer = await postJson(coordination, '/resolutions', {
|
|
390
|
+
developer: coordination.developer,
|
|
391
|
+
session_id: SESSION_ID,
|
|
392
|
+
file_path: rel,
|
|
393
|
+
outcome,
|
|
394
|
+
url,
|
|
395
|
+
note,
|
|
396
|
+
});
|
|
397
|
+
if (!answer.ok) return text(renderUnavailable(coordination, answer, 'whether the resolution was recorded'));
|
|
398
|
+
return text(renderResolved(answer.data, { filePath: rel, outcome, url }));
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return server;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Best effort, bounded, and only if this process ever claimed anything: a
|
|
407
|
+
* session that is going away should not leave its name on a scope for the
|
|
408
|
+
* rest of a TTL.
|
|
409
|
+
*/
|
|
410
|
+
async function releaseOwnClaims() {
|
|
411
|
+
if (!claimedSomething) return;
|
|
412
|
+
claimedSomething = false;
|
|
413
|
+
const coordination = { ...coordinationNow(), timeoutMs: 1500 };
|
|
414
|
+
await postJson(coordination, '/claims/release', {
|
|
415
|
+
developer: coordination.developer,
|
|
416
|
+
session_id: SESSION_ID,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Serve on stdio. Exported because this process is not always the entry point:
|
|
422
|
+
* `agentmash mcp` and the committed launcher in .claude/agentmash/ both start
|
|
423
|
+
* the server by importing it, so that the way it is reached can change without
|
|
424
|
+
* the server itself knowing.
|
|
425
|
+
*/
|
|
426
|
+
export async function start() {
|
|
427
|
+
const server = createServer();
|
|
428
|
+
// The host ending the session is our session_end, and it arrives in one of
|
|
429
|
+
// three shapes: the transport closing, stdin ending, or a signal. All three
|
|
430
|
+
// are best effort — a host that calls TerminateProcess (Windows) delivers
|
|
431
|
+
// none of them — which is why every claim also carries a TTL.
|
|
432
|
+
server.server.onclose = () => {
|
|
433
|
+
void releaseOwnClaims();
|
|
434
|
+
};
|
|
435
|
+
process.stdin.once('end', () => {
|
|
436
|
+
void releaseOwnClaims();
|
|
437
|
+
});
|
|
438
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
439
|
+
process.on(signal, () => {
|
|
440
|
+
releaseOwnClaims().finally(() => process.exit(0));
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
await server.connect(new StdioServerTransport());
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Only run when executed directly, so tests can import createServer().
|
|
447
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
448
|
+
start().catch((err) => {
|
|
449
|
+
console.error('[agentmash-mcp] fatal:', err);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
});
|
|
452
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentmash",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Let your team's Claude Code agents see each other's work",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude",
|
|
8
|
+
"hooks",
|
|
9
|
+
"coordination",
|
|
10
|
+
"agents",
|
|
11
|
+
"multi-agent",
|
|
12
|
+
"team"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/dauletbekalim/agentmash#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/dauletbekalim/agentmash/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/dauletbekalim/agentmash.git",
|
|
21
|
+
"directory": "cli"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"agentmash": "./agentmash.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"agentmash.mjs",
|
|
30
|
+
"hooks/",
|
|
31
|
+
"mcp/",
|
|
32
|
+
"clients/"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"sync-hooks": "node sync-hooks.mjs",
|
|
42
|
+
"prepare": "node sync-hooks.mjs",
|
|
43
|
+
"test": "node --import tsx --test test/*.test.mjs"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
47
|
+
"zod": "4.5.4"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"tsx": "^4.19.2"
|
|
51
|
+
}
|
|
52
|
+
}
|