@composer-app/mcp 0.0.1-beta.4 → 0.0.1-beta.5
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/dist/{chunk-UVXQZ2TN.js → chunk-A5KBJAJW.js} +272 -86
- package/dist/cli.js +1 -1
- package/dist/mcp.js +11 -3
- package/package.json +1 -1
- package/skill/SKILL.md +69 -18
|
@@ -4185,6 +4185,14 @@ var RoomState = class {
|
|
|
4185
4185
|
* on every remote edit, comment, suggestion, or activity-feed write.
|
|
4186
4186
|
*/
|
|
4187
4187
|
_lastRemoteActivityAt = Date.now();
|
|
4188
|
+
monitoringOn = false;
|
|
4189
|
+
/**
|
|
4190
|
+
* Set true when `composer_next_event`'s goodbye branch has fired. Tells
|
|
4191
|
+
* `handleNextEvent` to refuse subsequent calls until the user explicitly
|
|
4192
|
+
* re-engages (which happens via `composer_create_room` / `composer_join_room`,
|
|
4193
|
+
* both of which build a fresh `RoomState` so this flag resets naturally).
|
|
4194
|
+
*/
|
|
4195
|
+
goodbyeIssued = false;
|
|
4188
4196
|
constructor(opts) {
|
|
4189
4197
|
this.roomId = opts.roomId;
|
|
4190
4198
|
this.actingAs = opts.actingAs;
|
|
@@ -4197,46 +4205,9 @@ var RoomState = class {
|
|
|
4197
4205
|
});
|
|
4198
4206
|
this.provider = new YProvider(opts.serverHost, opts.roomId, this.doc, {
|
|
4199
4207
|
party: "composer-room",
|
|
4200
|
-
connect:
|
|
4208
|
+
connect: false,
|
|
4201
4209
|
WebSocketPolyfill: WebSocket
|
|
4202
4210
|
});
|
|
4203
|
-
this.provider.awareness.setLocalStateField("user", {
|
|
4204
|
-
name: opts.actingAs,
|
|
4205
|
-
color: opts.identity.color,
|
|
4206
|
-
userId: opts.identity.userId,
|
|
4207
|
-
isAgent: true
|
|
4208
|
-
});
|
|
4209
|
-
this.installAwarenessHeartbeat();
|
|
4210
|
-
}
|
|
4211
|
-
/**
|
|
4212
|
-
* Re-broadcast the MCP's awareness every 15s.
|
|
4213
|
-
*
|
|
4214
|
-
* y-partyserver's provider disables the y-protocols awareness
|
|
4215
|
-
* `_checkInterval` (see `clearInterval(awareness._checkInterval)` in
|
|
4216
|
-
* `y-partyserver/dist/provider/index.js`), so the MCP sends its awareness
|
|
4217
|
-
* exactly once — on connect — and never heartbeats after that. Combined
|
|
4218
|
-
* with Cloudflare Durable Object hibernation (which evicts the server's
|
|
4219
|
-
* in-memory `document.awareness` Map on wake), this means a browser that
|
|
4220
|
-
* connects more than ~60s after the MCP sees an empty awareness dump in
|
|
4221
|
-
* `onConnect` and never learns the agent is there. The user's own
|
|
4222
|
-
* awareness flows the other direction fine (they send on connect, server
|
|
4223
|
-
* broadcasts to the MCP), which is why the failure is asymmetric.
|
|
4224
|
-
*
|
|
4225
|
-
* y-partyserver's provider listens to `awareness.on("change", ...)`, and
|
|
4226
|
-
* y-protocols only fires `change` when the new state is deep-unequal to
|
|
4227
|
-
* the previous one. Re-setting an identical state emits `update` but NOT
|
|
4228
|
-
* `change`, so the provider never sends a wire frame. We bump a throwaway
|
|
4229
|
-
* `_hb` field each tick to guarantee deep-inequality, forcing the change
|
|
4230
|
-
* event and a broadcast. 15s is well under any realistic hibernation gap.
|
|
4231
|
-
*/
|
|
4232
|
-
installAwarenessHeartbeat() {
|
|
4233
|
-
const heartbeat = setInterval(() => {
|
|
4234
|
-
const local = this.provider.awareness.getLocalState();
|
|
4235
|
-
if (local !== null) {
|
|
4236
|
-
this.provider.awareness.setLocalState({ ...local, _hb: Date.now() });
|
|
4237
|
-
}
|
|
4238
|
-
}, 15e3);
|
|
4239
|
-
heartbeat.unref?.();
|
|
4240
4211
|
}
|
|
4241
4212
|
/**
|
|
4242
4213
|
* Resolves when the provider has completed its first sync handshake.
|
|
@@ -4264,6 +4235,117 @@ var RoomState = class {
|
|
|
4264
4235
|
this.provider.on("sync", onSync);
|
|
4265
4236
|
});
|
|
4266
4237
|
}
|
|
4238
|
+
idleDisconnectTimer = null;
|
|
4239
|
+
/**
|
|
4240
|
+
* Open the WebSocket (if not already) and await the first sync handshake.
|
|
4241
|
+
* Idempotent — no-op if already connected and synced. Call at the top of
|
|
4242
|
+
* every tool handler that touches the doc. Cancels any pending idle
|
|
4243
|
+
* disconnect because we're back in active use.
|
|
4244
|
+
*/
|
|
4245
|
+
async ensureConnected(timeoutMs = 15e3) {
|
|
4246
|
+
if (this.idleDisconnectTimer) {
|
|
4247
|
+
clearTimeout(this.idleDisconnectTimer);
|
|
4248
|
+
this.idleDisconnectTimer = null;
|
|
4249
|
+
}
|
|
4250
|
+
if (this.provider.wsconnected && this.provider.synced) return;
|
|
4251
|
+
await this.provider.connect();
|
|
4252
|
+
await this.waitForInitialSync(timeoutMs);
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* Arm a timer that turns monitoring off and closes the socket after `ms`
|
|
4256
|
+
* of no further tool-handler activity. Cancelled by `ensureConnected`.
|
|
4257
|
+
* Call at the end of every tool handler. Defaults to 30s — long enough
|
|
4258
|
+
* to bridge a normal multi-step monitor turn (mention → reply →
|
|
4259
|
+
* `composer_next_event` again), short enough that an abandoned monitor
|
|
4260
|
+
* loop fades the avatar within the "within a minute" window the user
|
|
4261
|
+
* requested.
|
|
4262
|
+
*
|
|
4263
|
+
* Both presence cleanup paths converge here: setMonitoring(false) clears
|
|
4264
|
+
* our local awareness state, then disconnect() closes the socket which
|
|
4265
|
+
* triggers y-partyserver's onClose → removeAwarenessStates. Either alone
|
|
4266
|
+
* would suffice; doing both keeps local and server state consistent.
|
|
4267
|
+
*/
|
|
4268
|
+
scheduleIdleDisconnect(ms = 3e4) {
|
|
4269
|
+
if (this.idleDisconnectTimer) {
|
|
4270
|
+
clearTimeout(this.idleDisconnectTimer);
|
|
4271
|
+
this.idleDisconnectTimer = null;
|
|
4272
|
+
}
|
|
4273
|
+
if (!this.provider.wsconnected) return;
|
|
4274
|
+
this.idleDisconnectTimer = setTimeout(() => {
|
|
4275
|
+
this.idleDisconnectTimer = null;
|
|
4276
|
+
this.setMonitoring(false);
|
|
4277
|
+
this.provider.disconnect();
|
|
4278
|
+
}, ms);
|
|
4279
|
+
this.idleDisconnectTimer.unref?.();
|
|
4280
|
+
}
|
|
4281
|
+
/**
|
|
4282
|
+
* Close the socket immediately. Available for explicit teardown
|
|
4283
|
+
* (e.g., destroy()). Does not touch monitoring state — the socket close
|
|
4284
|
+
* causes y-partyserver to broadcast the awareness removal, which is the
|
|
4285
|
+
* visual signal peers care about.
|
|
4286
|
+
*/
|
|
4287
|
+
disconnect() {
|
|
4288
|
+
if (this.idleDisconnectTimer) {
|
|
4289
|
+
clearTimeout(this.idleDisconnectTimer);
|
|
4290
|
+
this.idleDisconnectTimer = null;
|
|
4291
|
+
}
|
|
4292
|
+
this.provider.disconnect();
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Toggle whether the agent is visibly monitoring the room. Broadcast via
|
|
4296
|
+
* the `user` awareness field — present ↔ avatar visible to peers. The
|
|
4297
|
+
* socket must already be open (caller should have ensured this).
|
|
4298
|
+
*
|
|
4299
|
+
* setMonitoring(true) — called once on first composer_next_event entry.
|
|
4300
|
+
* setMonitoring(false) — called on the goodbye branch; clears awareness
|
|
4301
|
+
* immediately so the avatar disappears as the
|
|
4302
|
+
* farewell lands.
|
|
4303
|
+
*
|
|
4304
|
+
* Subsequent calls with the same value are no-ops.
|
|
4305
|
+
*/
|
|
4306
|
+
setMonitoring(on) {
|
|
4307
|
+
if (this.monitoringOn === on) return;
|
|
4308
|
+
this.monitoringOn = on;
|
|
4309
|
+
if (on) {
|
|
4310
|
+
const current = this.provider.awareness.getLocalState() ?? {};
|
|
4311
|
+
this.provider.awareness.setLocalState({
|
|
4312
|
+
...current,
|
|
4313
|
+
user: {
|
|
4314
|
+
name: this.actingAs,
|
|
4315
|
+
color: this.identity.color,
|
|
4316
|
+
userId: this.identity.userId,
|
|
4317
|
+
isAgent: true
|
|
4318
|
+
}
|
|
4319
|
+
});
|
|
4320
|
+
} else {
|
|
4321
|
+
this.provider.awareness.setLocalState(null);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
get isMonitoring() {
|
|
4325
|
+
return this.monitoringOn;
|
|
4326
|
+
}
|
|
4327
|
+
/**
|
|
4328
|
+
* Latch flipped by `composer_next_event`'s goodbye branch. Once set, the
|
|
4329
|
+
* model is expected to stop calling `composer_next_event` until the user
|
|
4330
|
+
* explicitly asks to rejoin — which builds a fresh RoomState, resetting
|
|
4331
|
+
* the latch. The MCP refuses further next_event calls on this RoomState
|
|
4332
|
+
* instead of silently re-entering the monitor loop.
|
|
4333
|
+
*/
|
|
4334
|
+
markGoodbyeIssued() {
|
|
4335
|
+
this.goodbyeIssued = true;
|
|
4336
|
+
}
|
|
4337
|
+
get hasIssuedGoodbye() {
|
|
4338
|
+
return this.goodbyeIssued;
|
|
4339
|
+
}
|
|
4340
|
+
/**
|
|
4341
|
+
* Read-only view of this client's local awareness state. Exposed for
|
|
4342
|
+
* tests/diagnostics only — production code should not depend on this.
|
|
4343
|
+
* Returns `null` when monitoring is off (local state cleared) or when
|
|
4344
|
+
* the provider hasn't written any fields yet.
|
|
4345
|
+
*/
|
|
4346
|
+
getLocalAwareness() {
|
|
4347
|
+
return this.provider.awareness.getLocalState();
|
|
4348
|
+
}
|
|
4267
4349
|
snapshot() {
|
|
4268
4350
|
return {
|
|
4269
4351
|
fullDoc: serializeDocAsMarkdown(this.doc),
|
|
@@ -4272,23 +4354,38 @@ var RoomState = class {
|
|
|
4272
4354
|
threadsBacklog: []
|
|
4273
4355
|
};
|
|
4274
4356
|
}
|
|
4275
|
-
async nextEvent(timeoutMs) {
|
|
4357
|
+
async nextEvent(timeoutMs, signal) {
|
|
4276
4358
|
const pending = this.queue.shift();
|
|
4277
4359
|
if (pending) return pending;
|
|
4360
|
+
if (signal?.aborted) return { kind: "timeout" };
|
|
4278
4361
|
return new Promise((resolve) => {
|
|
4279
|
-
const
|
|
4362
|
+
const cleanup = () => {
|
|
4363
|
+
clearTimeout(timer);
|
|
4364
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4280
4365
|
const idx = this.waiters.indexOf(waiter);
|
|
4281
4366
|
if (idx >= 0) this.waiters.splice(idx, 1);
|
|
4367
|
+
};
|
|
4368
|
+
const timer = setTimeout(() => {
|
|
4369
|
+
cleanup();
|
|
4282
4370
|
resolve({ kind: "timeout" });
|
|
4283
4371
|
}, timeoutMs);
|
|
4372
|
+
const onAbort = () => {
|
|
4373
|
+
cleanup();
|
|
4374
|
+
resolve({ kind: "timeout" });
|
|
4375
|
+
};
|
|
4376
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4284
4377
|
const waiter = (ev) => {
|
|
4285
|
-
|
|
4378
|
+
cleanup();
|
|
4286
4379
|
resolve(ev);
|
|
4287
4380
|
};
|
|
4288
4381
|
this.waiters.push(waiter);
|
|
4289
4382
|
});
|
|
4290
4383
|
}
|
|
4291
4384
|
destroy() {
|
|
4385
|
+
if (this.idleDisconnectTimer) {
|
|
4386
|
+
clearTimeout(this.idleDisconnectTimer);
|
|
4387
|
+
this.idleDisconnectTimer = null;
|
|
4388
|
+
}
|
|
4292
4389
|
this.provider.destroy();
|
|
4293
4390
|
this.doc.destroy();
|
|
4294
4391
|
}
|
|
@@ -4605,6 +4702,18 @@ var SERVER_HOST = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
|
|
|
4605
4702
|
var APP_BASE = process.env.COMPOSER_APP_BASE ?? "https://usecomposer.app";
|
|
4606
4703
|
var rooms = /* @__PURE__ */ new Map();
|
|
4607
4704
|
var identityCache = null;
|
|
4705
|
+
function teardownAllRooms() {
|
|
4706
|
+
for (const [roomId, state] of rooms) {
|
|
4707
|
+
try {
|
|
4708
|
+
state.setMonitoring(false);
|
|
4709
|
+
state.disconnect();
|
|
4710
|
+
state.destroy();
|
|
4711
|
+
} catch (err) {
|
|
4712
|
+
logError(`teardown error for room ${roomId}`, err);
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
rooms.clear();
|
|
4716
|
+
}
|
|
4608
4717
|
async function getIdentity() {
|
|
4609
4718
|
if (!identityCache) {
|
|
4610
4719
|
identityCache = await loadOrCreateIdentity(COMPOSER_DIR2);
|
|
@@ -4699,12 +4808,12 @@ var TOOL_DEFS = [
|
|
|
4699
4808
|
},
|
|
4700
4809
|
{
|
|
4701
4810
|
name: "composer_next_event",
|
|
4702
|
-
description: "Block for up to `timeoutSec` (default
|
|
4811
|
+
description: "Block for up to `timeoutSec` (default 30s) waiting for a remote event. Returns `{ kind: 'mention' | 'timeout', ... }`. **The monitor loop is always-on** \u2014 every return carries a directive you must follow without waiting for user input. On `mention`, the `reason` is one of: `direct_mention` (sidecar or text named you \u2014 always reply), `active_thread` (plain reply on a thread you're already in \u2014 reply if the content invites one), or `solo_room` (you're alone with one human who didn't explicitly tag anyone \u2014 default to a helpful reply, but skip if the text reads like a note-to-self, acknowledgement, or aside). Handle the event, then execute the return's `requiredNextToolCall`. On `timeout`, `recentActivity` tells you whether to keep monitoring or exit with the goodbye line from `userMessage`.",
|
|
4703
4812
|
inputSchema: {
|
|
4704
4813
|
type: "object",
|
|
4705
4814
|
properties: {
|
|
4706
4815
|
roomId: { type: "string" },
|
|
4707
|
-
timeoutSec: { type: "number", default:
|
|
4816
|
+
timeoutSec: { type: "number", default: 30 }
|
|
4708
4817
|
},
|
|
4709
4818
|
required: ["roomId"]
|
|
4710
4819
|
}
|
|
@@ -4927,12 +5036,13 @@ async function handleCreateRoom(args) {
|
|
|
4927
5036
|
actingAs,
|
|
4928
5037
|
identity
|
|
4929
5038
|
});
|
|
4930
|
-
await state.
|
|
5039
|
+
await state.ensureConnected();
|
|
4931
5040
|
if (seedMarkdown) {
|
|
4932
5041
|
writeMarkdownToFragment(state.doc.getXmlFragment("default"), seedMarkdown);
|
|
4933
5042
|
}
|
|
4934
5043
|
rooms.set(roomId, state);
|
|
4935
5044
|
log(`composer room created \u2192 ${browserUrl}`, { roomId, actingAs });
|
|
5045
|
+
state.scheduleIdleDisconnect(3e4);
|
|
4936
5046
|
return okResult({
|
|
4937
5047
|
roomId,
|
|
4938
5048
|
browserUrl,
|
|
@@ -4953,6 +5063,7 @@ async function handleJoinRoom(args) {
|
|
|
4953
5063
|
roomId,
|
|
4954
5064
|
actingAs: existing.actingAs
|
|
4955
5065
|
});
|
|
5066
|
+
existing.scheduleIdleDisconnect(3e4);
|
|
4956
5067
|
return okResult({
|
|
4957
5068
|
roomId,
|
|
4958
5069
|
browserUrl,
|
|
@@ -4974,9 +5085,10 @@ async function handleJoinRoom(args) {
|
|
|
4974
5085
|
actingAs,
|
|
4975
5086
|
identity
|
|
4976
5087
|
});
|
|
4977
|
-
await state.
|
|
5088
|
+
await state.ensureConnected();
|
|
4978
5089
|
rooms.set(roomId, state);
|
|
4979
5090
|
log(`composer room joined \u2192 ${browserUrl}`, { roomId, actingAs });
|
|
5091
|
+
state.scheduleIdleDisconnect(3e4);
|
|
4980
5092
|
return okResult({
|
|
4981
5093
|
roomId,
|
|
4982
5094
|
browserUrl,
|
|
@@ -4991,14 +5103,41 @@ function handleAttachRoom(args) {
|
|
|
4991
5103
|
const state = getOrError(roomId);
|
|
4992
5104
|
return okResult({ roomId, snapshot: state.snapshot() });
|
|
4993
5105
|
}
|
|
4994
|
-
var ACTIVITY_WINDOW_MS =
|
|
5106
|
+
var ACTIVITY_WINDOW_MS = (() => {
|
|
5107
|
+
const raw = process.env.COMPOSER_ACTIVITY_WINDOW_MS;
|
|
5108
|
+
if (!raw) return 15 * 60 * 1e3;
|
|
5109
|
+
const n = Number(raw);
|
|
5110
|
+
return Number.isFinite(n) && n > 0 ? n : 15 * 60 * 1e3;
|
|
5111
|
+
})();
|
|
4995
5112
|
var LEAVE_MESSAGE = "I've left the document. You can ask me to rejoin anytime and I'll continue replying.";
|
|
4996
|
-
async function handleNextEvent(args) {
|
|
5113
|
+
async function handleNextEvent(args, signal) {
|
|
4997
5114
|
const a = asObject(args);
|
|
4998
5115
|
const roomId = asString(a.roomId, "roomId");
|
|
4999
|
-
const timeoutSec = typeof a.timeoutSec === "number" && Number.isFinite(a.timeoutSec) ? a.timeoutSec :
|
|
5116
|
+
const timeoutSec = typeof a.timeoutSec === "number" && Number.isFinite(a.timeoutSec) ? a.timeoutSec : 30;
|
|
5000
5117
|
const state = getOrError(roomId);
|
|
5001
|
-
|
|
5118
|
+
if (state.hasIssuedGoodbye) {
|
|
5119
|
+
return errorResult(
|
|
5120
|
+
"Already said goodbye for this room. The user must explicitly ask you to rejoin (which will trigger a fresh `composer_join_room`). Do not call `composer_next_event` again on this roomId."
|
|
5121
|
+
);
|
|
5122
|
+
}
|
|
5123
|
+
state.setMonitoring(true);
|
|
5124
|
+
const onAbort = () => {
|
|
5125
|
+
state.setMonitoring(false);
|
|
5126
|
+
state.disconnect();
|
|
5127
|
+
};
|
|
5128
|
+
if (signal) {
|
|
5129
|
+
if (signal.aborted) onAbort();
|
|
5130
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
5131
|
+
}
|
|
5132
|
+
let event;
|
|
5133
|
+
try {
|
|
5134
|
+
event = await state.nextEvent(timeoutSec * 1e3, signal);
|
|
5135
|
+
} finally {
|
|
5136
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5137
|
+
}
|
|
5138
|
+
if (signal?.aborted) {
|
|
5139
|
+
return okResult({ kind: "aborted" });
|
|
5140
|
+
}
|
|
5002
5141
|
if (event.kind === "mention") {
|
|
5003
5142
|
return okResult({
|
|
5004
5143
|
...event,
|
|
@@ -5024,6 +5163,9 @@ async function handleNextEvent(args) {
|
|
|
5024
5163
|
}
|
|
5025
5164
|
});
|
|
5026
5165
|
}
|
|
5166
|
+
state.setMonitoring(false);
|
|
5167
|
+
state.disconnect();
|
|
5168
|
+
state.markGoodbyeIssued();
|
|
5027
5169
|
return okResult({
|
|
5028
5170
|
kind: "timeout",
|
|
5029
5171
|
recentActivity: false,
|
|
@@ -5256,34 +5398,44 @@ function handleResolveThread(args) {
|
|
|
5256
5398
|
comments.set(threadId, { ...existing, resolved: true });
|
|
5257
5399
|
return okResult({ threadId, resolved: true });
|
|
5258
5400
|
}
|
|
5259
|
-
async function dispatchTool(name, args) {
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5401
|
+
async function dispatchTool(name, args, signal) {
|
|
5402
|
+
const roomId = args && typeof args === "object" && "roomId" in args ? args.roomId : void 0;
|
|
5403
|
+
const needsExistingRoom = name !== "composer_create_room" && name !== "composer_join_room";
|
|
5404
|
+
const state = needsExistingRoom && typeof roomId === "string" ? rooms.get(roomId) : void 0;
|
|
5405
|
+
if (state) {
|
|
5406
|
+
await state.ensureConnected();
|
|
5407
|
+
}
|
|
5408
|
+
try {
|
|
5409
|
+
switch (name) {
|
|
5410
|
+
case "composer_create_room":
|
|
5411
|
+
return await handleCreateRoom(args);
|
|
5412
|
+
case "composer_join_room":
|
|
5413
|
+
return await handleJoinRoom(args);
|
|
5414
|
+
case "composer_attach_room":
|
|
5415
|
+
return handleAttachRoom(args);
|
|
5416
|
+
case "composer_next_event":
|
|
5417
|
+
return await handleNextEvent(args, signal);
|
|
5418
|
+
case "composer_get_section":
|
|
5419
|
+
return handleGetSection(args);
|
|
5420
|
+
case "composer_get_full_doc":
|
|
5421
|
+
return handleGetFullDoc(args);
|
|
5422
|
+
case "composer_get_thread":
|
|
5423
|
+
return handleGetThread(args);
|
|
5424
|
+
case "composer_add_comment":
|
|
5425
|
+
return handleAddComment(args);
|
|
5426
|
+
case "composer_reply_comment":
|
|
5427
|
+
return handleReplyComment(args);
|
|
5428
|
+
case "composer_add_suggestion":
|
|
5429
|
+
return handleAddSuggestion(args);
|
|
5430
|
+
case "composer_reply_suggestion":
|
|
5431
|
+
return handleReplySuggestion(args);
|
|
5432
|
+
case "composer_resolve_thread":
|
|
5433
|
+
return handleResolveThread(args);
|
|
5434
|
+
default:
|
|
5435
|
+
return errorResult(`unknown tool: ${name}`);
|
|
5436
|
+
}
|
|
5437
|
+
} finally {
|
|
5438
|
+
state?.scheduleIdleDisconnect(3e4);
|
|
5287
5439
|
}
|
|
5288
5440
|
}
|
|
5289
5441
|
function buildServer() {
|
|
@@ -5294,13 +5446,13 @@ function buildServer() {
|
|
|
5294
5446
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
5295
5447
|
tools: TOOL_DEFS
|
|
5296
5448
|
}));
|
|
5297
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
5449
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
5298
5450
|
const { name, arguments: args } = req.params;
|
|
5299
5451
|
const roomId = args && typeof args === "object" && "roomId" in args ? args.roomId : void 0;
|
|
5300
5452
|
const started = Date.now();
|
|
5301
5453
|
log(`tool ${name} call`, { roomId });
|
|
5302
5454
|
try {
|
|
5303
|
-
const result = await dispatchTool(name, args);
|
|
5455
|
+
const result = await dispatchTool(name, args, extra.signal);
|
|
5304
5456
|
const elapsedMs = Date.now() - started;
|
|
5305
5457
|
if (result.isError) {
|
|
5306
5458
|
const detail = result.content[0]?.text ?? "";
|
|
@@ -5327,7 +5479,7 @@ async function startMcpServer() {
|
|
|
5327
5479
|
log("mcp server starting", {
|
|
5328
5480
|
pid: process.pid,
|
|
5329
5481
|
node: process.version,
|
|
5330
|
-
build: "
|
|
5482
|
+
build: "socket-driven-v1"
|
|
5331
5483
|
});
|
|
5332
5484
|
const server = buildServer();
|
|
5333
5485
|
const transport = new StdioServerTransport();
|
|
@@ -5339,6 +5491,28 @@ async function startMcpServer() {
|
|
|
5339
5491
|
logFile: LOG_FILE_PATH,
|
|
5340
5492
|
pid: process.pid
|
|
5341
5493
|
});
|
|
5494
|
+
installShutdownHandlers("stdio");
|
|
5495
|
+
}
|
|
5496
|
+
function installShutdownHandlers(transport) {
|
|
5497
|
+
let shuttingDown = false;
|
|
5498
|
+
const shutdown = (reason) => {
|
|
5499
|
+
if (shuttingDown) return;
|
|
5500
|
+
shuttingDown = true;
|
|
5501
|
+
log(`mcp shutdown \u2014 ${reason}`, { transport });
|
|
5502
|
+
try {
|
|
5503
|
+
teardownAllRooms();
|
|
5504
|
+
} catch (err) {
|
|
5505
|
+
logError("teardownAllRooms failed", err);
|
|
5506
|
+
}
|
|
5507
|
+
setTimeout(() => process.exit(0), 200).unref?.();
|
|
5508
|
+
};
|
|
5509
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5510
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5511
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
5512
|
+
if (transport === "stdio") {
|
|
5513
|
+
process.stdin.on("end", () => shutdown("stdin-end"));
|
|
5514
|
+
process.stdin.on("close", () => shutdown("stdin-close"));
|
|
5515
|
+
}
|
|
5342
5516
|
}
|
|
5343
5517
|
async function startMcpHttpServer(opts) {
|
|
5344
5518
|
installCrashHandlers();
|
|
@@ -5346,7 +5520,7 @@ async function startMcpHttpServer(opts) {
|
|
|
5346
5520
|
port: opts.port,
|
|
5347
5521
|
pid: process.pid,
|
|
5348
5522
|
node: process.version,
|
|
5349
|
-
build: "
|
|
5523
|
+
build: "socket-driven-v1"
|
|
5350
5524
|
});
|
|
5351
5525
|
const server = buildServer();
|
|
5352
5526
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -5392,18 +5566,30 @@ async function startMcpHttpServer(opts) {
|
|
|
5392
5566
|
appBase=${APP_BASE}`
|
|
5393
5567
|
);
|
|
5394
5568
|
});
|
|
5395
|
-
|
|
5396
|
-
|
|
5569
|
+
installShutdownHandlers("http");
|
|
5570
|
+
const closeServer = () => {
|
|
5397
5571
|
httpServer.close(() => process.exit(0));
|
|
5398
5572
|
setTimeout(() => process.exit(0), 500).unref();
|
|
5399
5573
|
};
|
|
5400
|
-
process.on("SIGTERM",
|
|
5401
|
-
process.on("SIGINT",
|
|
5574
|
+
process.on("SIGTERM", closeServer);
|
|
5575
|
+
process.on("SIGINT", closeServer);
|
|
5576
|
+
process.on("SIGHUP", closeServer);
|
|
5402
5577
|
}
|
|
5578
|
+
var __test_dispatch = dispatchTool;
|
|
5579
|
+
var __test_setRoom = (id, state) => {
|
|
5580
|
+
rooms.set(id, state);
|
|
5581
|
+
};
|
|
5582
|
+
var __test_clearRooms = () => {
|
|
5583
|
+
rooms.clear();
|
|
5584
|
+
};
|
|
5403
5585
|
|
|
5404
5586
|
export {
|
|
5405
5587
|
loadOrCreateIdentity,
|
|
5406
5588
|
logError,
|
|
5589
|
+
teardownAllRooms,
|
|
5407
5590
|
startMcpServer,
|
|
5408
|
-
startMcpHttpServer
|
|
5591
|
+
startMcpHttpServer,
|
|
5592
|
+
__test_dispatch,
|
|
5593
|
+
__test_setRoom,
|
|
5594
|
+
__test_clearRooms
|
|
5409
5595
|
};
|
package/dist/cli.js
CHANGED
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
__test_clearRooms,
|
|
3
|
+
__test_dispatch,
|
|
4
|
+
__test_setRoom,
|
|
2
5
|
startMcpHttpServer,
|
|
3
|
-
startMcpServer
|
|
4
|
-
|
|
6
|
+
startMcpServer,
|
|
7
|
+
teardownAllRooms
|
|
8
|
+
} from "./chunk-A5KBJAJW.js";
|
|
5
9
|
export {
|
|
10
|
+
__test_clearRooms,
|
|
11
|
+
__test_dispatch,
|
|
12
|
+
__test_setRoom,
|
|
6
13
|
startMcpHttpServer,
|
|
7
|
-
startMcpServer
|
|
14
|
+
startMcpServer,
|
|
15
|
+
teardownAllRooms
|
|
8
16
|
};
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -36,13 +36,14 @@ two ordered steps — the field names encode the order:
|
|
|
36
36
|
also carries the `@<your-name>` tagging hint. Relay it; you can
|
|
37
37
|
paraphrase lightly but do not drop the URL or the mention syntax.
|
|
38
38
|
2. `step2_callTool` — a structured `{ tool, args, why }` directive for
|
|
39
|
-
the
|
|
40
|
-
|
|
39
|
+
the `composer_next_event` loop. **Do not run it inline.** Hand the
|
|
40
|
+
loop to a background subagent (see "Monitor — runs in a subagent"
|
|
41
|
+
below). End your turn once the subagent is spawned.
|
|
41
42
|
|
|
42
43
|
Skipping step 1 leaves the user without the URL — they have no way into
|
|
43
|
-
the doc. Skipping
|
|
44
|
-
"I'm monitoring" without
|
|
45
|
-
every mention.
|
|
44
|
+
the doc. Skipping the subagent spawn leaves the room attached but
|
|
45
|
+
silent; saying "I'm monitoring" without spawning the loop is a lie,
|
|
46
|
+
every mention gets missed.
|
|
46
47
|
|
|
47
48
|
**Seeding — prefer a file path when one exists.** Pick exactly one:
|
|
48
49
|
|
|
@@ -64,16 +65,63 @@ Triggers: a share prompt with a Composer URL, "/composer join <url>".
|
|
|
64
65
|
Action: extract the URL from the prompt and call `composer_join_room({ url })`.
|
|
65
66
|
Same first-run rule as Create. On success, the return carries the same
|
|
66
67
|
ordered pair: output `step1_sayToUser` first (confirms the URL the user
|
|
67
|
-
just joined), then
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
just joined), then spawn the monitor subagent — same flow as Create
|
|
69
|
+
(see "Monitor — runs in a subagent" below).
|
|
70
|
+
|
|
71
|
+
### 3. Monitor — runs in a subagent
|
|
72
|
+
|
|
73
|
+
Triggers: "watch this doc", or automatically after create/join.
|
|
74
|
+
|
|
75
|
+
The monitor loop is delegated to a background subagent. Polling
|
|
76
|
+
`composer_next_event` from the main thread fills the conversation
|
|
77
|
+
context with idle-tick chatter and mention-handling that belongs in
|
|
78
|
+
the doc, not the terminal. Spawn the subagent, end your turn, let the
|
|
79
|
+
doc be the conversation.
|
|
80
|
+
|
|
81
|
+
**How to spawn (Claude Code).** Use the `Agent` tool with:
|
|
82
|
+
|
|
83
|
+
- `subagent_type: "general-purpose"`
|
|
84
|
+
- `run_in_background: true`
|
|
85
|
+
- `description: "Composer monitor: <roomId>"` — short, identifies the room
|
|
86
|
+
- `prompt`: the template below, with `{roomId}` and `{actingAs}` filled in
|
|
87
|
+
|
|
88
|
+
Prompt template:
|
|
89
|
+
|
|
90
|
+
> Invoke the composer skill, then run the monitor loop for room
|
|
91
|
+
> `{roomId}` as `{actingAs}`. Your only job is the in-doc conversation:
|
|
92
|
+
> call `composer_next_event({ roomId: "{roomId}" })` and follow each
|
|
93
|
+
> return's `requiredNextToolCall` directive verbatim, looping until the
|
|
94
|
+
> goodbye branch fires.
|
|
95
|
+
>
|
|
96
|
+
> All write tools (`composer_add_comment`, `composer_add_suggestion`,
|
|
97
|
+
> `composer_reply_comment`, `composer_reply_suggestion`,
|
|
98
|
+
> `composer_resolve_thread`) are yours to use as the skill describes.
|
|
99
|
+
> The doc IS your conversation — do not narrate to the parent between
|
|
100
|
+
> ticks.
|
|
101
|
+
>
|
|
102
|
+
> Exit and return when ANY of these happen:
|
|
103
|
+
> 1. `composer_next_event` returns `kind: "timeout"` with
|
|
104
|
+
> `recentActivity: false` — say `userMessage` EXACTLY in the doc
|
|
105
|
+
> (it's the goodbye line), then exit.
|
|
106
|
+
> 2. A request inside the doc clearly needs the parent terminal
|
|
107
|
+
> (a code change, a shell command, an external action the parent
|
|
108
|
+
> would do). Post a short reply: "I'll get on this in the terminal,"
|
|
109
|
+
> then exit with a one-sentence summary of the ask.
|
|
110
|
+
> 3. An unrecoverable error (auth lost, room destroyed). Exit with
|
|
111
|
+
> the error.
|
|
112
|
+
|
|
113
|
+
**Main thread after spawning.** Turn ends. Do **not** also call
|
|
114
|
+
`composer_next_event` — two listeners on the same room means duplicated
|
|
115
|
+
replies. If the user asks "what's happening in Composer?", check the
|
|
116
|
+
subagent's status (`TaskList` / `TaskOutput`) rather than re-entering
|
|
117
|
+
the loop yourself.
|
|
118
|
+
|
|
119
|
+
**Inside the loop (what the subagent does).** Default timeout is 30
|
|
120
|
+
seconds. Every return carries a structured directive — follow it
|
|
121
|
+
without waiting for user input.
|
|
74
122
|
|
|
75
123
|
On `mention`: handle the event (reply / suggestion / resolve as needed),
|
|
76
|
-
output any
|
|
124
|
+
output any in-doc action, then execute `requiredNextToolCall` — which
|
|
77
125
|
is another `composer_next_event` call. Do not pause for the user to
|
|
78
126
|
acknowledge. The doc is the conversation.
|
|
79
127
|
|
|
@@ -82,9 +130,8 @@ On `timeout`: check `recentActivity`.
|
|
|
82
130
|
Execute it — the user is still working, just not tagging you.
|
|
83
131
|
- `recentActivity: false` → the return includes `userMessage` and
|
|
84
132
|
`instruction` but NO `requiredNextToolCall`. Say `userMessage`
|
|
85
|
-
EXACTLY ("I've left the document…") and
|
|
86
|
-
|
|
87
|
-
paraphrase — users recognize the line across sessions.
|
|
133
|
+
EXACTLY ("I've left the document…") and exit (per exit rule 1
|
|
134
|
+
above). Do not paraphrase — users recognize the line across sessions.
|
|
88
135
|
|
|
89
136
|
On `mention`, the event contains everything you need to act in one turn:
|
|
90
137
|
|
|
@@ -134,8 +181,12 @@ need to catch up on what's already been said.
|
|
|
134
181
|
When in doubt, reply — the user can always ignore you.
|
|
135
182
|
|
|
136
183
|
### 4. Act
|
|
137
|
-
Triggers: direct requests like "add a summary to section 2".
|
|
138
|
-
Action:
|
|
184
|
+
Triggers: direct requests in the terminal like "add a summary to section 2".
|
|
185
|
+
Action: the main thread is also attached to the room — call the write
|
|
186
|
+
tools from here and report back concisely. Don't hand terminal directives
|
|
187
|
+
to the monitor subagent; the subagent handles in-doc mentions, the main
|
|
188
|
+
thread handles in-terminal asks. They share the same MCP, so writes from
|
|
189
|
+
either show up in the doc.
|
|
139
190
|
|
|
140
191
|
## Tools
|
|
141
192
|
|