@composer-app/mcp 0.0.1-beta.7 → 0.0.3
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-EJUJPBX2.js → chunk-GPFWLOYB.js} +279 -27
- package/dist/cli.js +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/skill/commenting/SKILL.md +159 -0
- package/skill/create/SKILL.md +131 -0
- package/skill/export/SKILL.md +67 -0
- package/skill/join/SKILL.md +117 -0
- package/skill/monitor/SKILL.md +200 -0
- package/skill/suggesting/SKILL.md +177 -0
- package/skill/.claude/settings.local.json +0 -8
- package/skill/SKILL.md +0 -533
|
@@ -4534,6 +4534,24 @@ function resolveServerAnchor(doc, spec) {
|
|
|
4534
4534
|
}
|
|
4535
4535
|
|
|
4536
4536
|
// src/roomState.ts
|
|
4537
|
+
var HAD_UPGRADE_403 = /* @__PURE__ */ Symbol("composer.upgrade403");
|
|
4538
|
+
var TerminalDetectingWS = class extends WebSocket {
|
|
4539
|
+
constructor(...args) {
|
|
4540
|
+
super(...args);
|
|
4541
|
+
this.on("unexpected-response", (_req, res) => {
|
|
4542
|
+
if (res.statusCode === 403) {
|
|
4543
|
+
this[HAD_UPGRADE_403] = true;
|
|
4544
|
+
}
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
};
|
|
4548
|
+
var TERMINAL_CLOSE_CODES = /* @__PURE__ */ new Set([4403, 4410]);
|
|
4549
|
+
var MAX_CONSECUTIVE_FAILURES = 15;
|
|
4550
|
+
function reconnectDelayMs(failuresSoFar) {
|
|
4551
|
+
if (failuresSoFar <= 5) return 1e4;
|
|
4552
|
+
if (failuresSoFar <= 10) return 3e4;
|
|
4553
|
+
return 6e4;
|
|
4554
|
+
}
|
|
4537
4555
|
var RoomState = class _RoomState {
|
|
4538
4556
|
doc;
|
|
4539
4557
|
actingAs;
|
|
@@ -4569,10 +4587,37 @@ var RoomState = class _RoomState {
|
|
|
4569
4587
|
/**
|
|
4570
4588
|
* Set true when `composer_next_event`'s goodbye branch has fired. Tells
|
|
4571
4589
|
* `handleNextEvent` to refuse subsequent calls until the user explicitly
|
|
4572
|
-
* re-engages
|
|
4573
|
-
*
|
|
4590
|
+
* re-engages via `composer_create_room` / `composer_join_room` — both of
|
|
4591
|
+
* which clear the latch (create builds a fresh RoomState; rejoin reuses
|
|
4592
|
+
* the cached one and calls `clearGoodbye()`).
|
|
4574
4593
|
*/
|
|
4575
4594
|
goodbyeIssued = false;
|
|
4595
|
+
/**
|
|
4596
|
+
* Set when the connection enters a terminal state — either the server
|
|
4597
|
+
* explicitly rejected (HTTP 403 / close code 4403 / 4410) or the client
|
|
4598
|
+
* circuit breaker tripped after too many consecutive failures. Once set,
|
|
4599
|
+
* `ensureConnected` / `waitForInitialSync` reject immediately and tool
|
|
4600
|
+
* calls surface a clear error rather than the host silently hanging.
|
|
4601
|
+
*/
|
|
4602
|
+
terminalReject = null;
|
|
4603
|
+
/**
|
|
4604
|
+
* Listeners that fire when terminal state is signalled. Used to unblock
|
|
4605
|
+
* pending `waitForInitialSync` promises so the host doesn't hang for the
|
|
4606
|
+
* full 15s timeout after a kick.
|
|
4607
|
+
*/
|
|
4608
|
+
terminalRejectListeners = /* @__PURE__ */ new Set();
|
|
4609
|
+
/**
|
|
4610
|
+
* Counter for consecutive failed reconnects. Reset to 0 on a successful
|
|
4611
|
+
* `connected` status event. Per the design, does NOT reset on idle
|
|
4612
|
+
* disconnects — only on a real successful open.
|
|
4613
|
+
*/
|
|
4614
|
+
consecutiveFailures = 0;
|
|
4615
|
+
/**
|
|
4616
|
+
* Pending manually-scheduled reconnect timer (we replace y-partyserver's
|
|
4617
|
+
* default exponential backoff with the tiered schedule from the design).
|
|
4618
|
+
* Cleared on destroy and on each new close event.
|
|
4619
|
+
*/
|
|
4620
|
+
scheduledReconnect = null;
|
|
4576
4621
|
constructor(opts) {
|
|
4577
4622
|
this.roomId = opts.roomId;
|
|
4578
4623
|
this.actingAs = opts.actingAs;
|
|
@@ -4590,7 +4635,56 @@ var RoomState = class _RoomState {
|
|
|
4590
4635
|
this.provider = new YProvider(opts.serverHost, opts.roomId, this.doc, {
|
|
4591
4636
|
party: "composer-room",
|
|
4592
4637
|
connect: false,
|
|
4593
|
-
WebSocketPolyfill:
|
|
4638
|
+
WebSocketPolyfill: TerminalDetectingWS
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
if (!opts._providerForTest) {
|
|
4642
|
+
this.provider.on(
|
|
4643
|
+
"connection-close",
|
|
4644
|
+
(...args) => {
|
|
4645
|
+
const event = args[0];
|
|
4646
|
+
if (this.terminalReject) return;
|
|
4647
|
+
if (!this.provider.shouldConnect) {
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
const ws = this.provider.ws;
|
|
4651
|
+
const had403 = ws ? !!ws[HAD_UPGRADE_403] : false;
|
|
4652
|
+
if (had403 || TERMINAL_CLOSE_CODES.has(event.code)) {
|
|
4653
|
+
this.signalTerminalReject({
|
|
4654
|
+
kind: "rejected",
|
|
4655
|
+
code: had403 ? 403 : event.code,
|
|
4656
|
+
reason: had403 ? "server refused upgrade (HTTP 403). composer-mcp may be out of date or this client is blocked. Update composer-mcp or contact the room owner." : event.code === 4410 ? `server has killed this client version. ${event.reason || "Please update composer-mcp."}` : `server kicked this client (close code ${event.code}). ${event.reason || "Please update composer-mcp."}`
|
|
4657
|
+
});
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
this.consecutiveFailures++;
|
|
4661
|
+
if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
4662
|
+
this.signalTerminalReject({
|
|
4663
|
+
kind: "aborted",
|
|
4664
|
+
failures: this.consecutiveFailures
|
|
4665
|
+
});
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
this.provider.shouldConnect = false;
|
|
4669
|
+
if (this.scheduledReconnect) clearTimeout(this.scheduledReconnect);
|
|
4670
|
+
const delay = reconnectDelayMs(this.consecutiveFailures);
|
|
4671
|
+
this.scheduledReconnect = setTimeout(() => {
|
|
4672
|
+
this.scheduledReconnect = null;
|
|
4673
|
+
if (this.terminalReject) return;
|
|
4674
|
+
this.provider.connect?.()?.catch?.((err) => {
|
|
4675
|
+
console.error(
|
|
4676
|
+
`[composer-mcp] reconnect attempt ${this.consecutiveFailures} failed: ${err}`
|
|
4677
|
+
);
|
|
4678
|
+
});
|
|
4679
|
+
}, delay);
|
|
4680
|
+
this.scheduledReconnect.unref?.();
|
|
4681
|
+
}
|
|
4682
|
+
);
|
|
4683
|
+
this.provider.on("status", (...args) => {
|
|
4684
|
+
const status = args[0];
|
|
4685
|
+
if (status.status === "connected") {
|
|
4686
|
+
this.consecutiveFailures = 0;
|
|
4687
|
+
}
|
|
4594
4688
|
});
|
|
4595
4689
|
}
|
|
4596
4690
|
}
|
|
@@ -4618,6 +4712,51 @@ var RoomState = class _RoomState {
|
|
|
4618
4712
|
_docForTest: doc
|
|
4619
4713
|
});
|
|
4620
4714
|
}
|
|
4715
|
+
signalTerminalReject(rej) {
|
|
4716
|
+
if (this.terminalReject) return;
|
|
4717
|
+
this.terminalReject = rej;
|
|
4718
|
+
if (this.scheduledReconnect) {
|
|
4719
|
+
clearTimeout(this.scheduledReconnect);
|
|
4720
|
+
this.scheduledReconnect = null;
|
|
4721
|
+
}
|
|
4722
|
+
const listeners = [...this.terminalRejectListeners];
|
|
4723
|
+
this.terminalRejectListeners.clear();
|
|
4724
|
+
for (const fn of listeners) {
|
|
4725
|
+
try {
|
|
4726
|
+
fn();
|
|
4727
|
+
} catch {
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
this.provider.destroy();
|
|
4731
|
+
}
|
|
4732
|
+
terminalRejectError() {
|
|
4733
|
+
const rej = this.terminalReject;
|
|
4734
|
+
if (!rej) {
|
|
4735
|
+
return new Error(
|
|
4736
|
+
`[composer-mcp] room "${this.roomId}" entered terminal state (unknown).`
|
|
4737
|
+
);
|
|
4738
|
+
}
|
|
4739
|
+
if (rej.kind === "aborted") {
|
|
4740
|
+
return new Error(
|
|
4741
|
+
`[composer-mcp] connection to room "${this.roomId}" aborted after ${rej.failures} failed reconnect attempts. Run composer_join_room to retry.`
|
|
4742
|
+
);
|
|
4743
|
+
}
|
|
4744
|
+
return new Error(
|
|
4745
|
+
`[composer-mcp] room "${this.roomId}" rejected by server: ${rej.reason}`
|
|
4746
|
+
);
|
|
4747
|
+
}
|
|
4748
|
+
/** Test/diagnostic accessor — not part of the stable surface. */
|
|
4749
|
+
get isTerminallyRejected() {
|
|
4750
|
+
return this.terminalReject !== null;
|
|
4751
|
+
}
|
|
4752
|
+
/** Test/diagnostic accessor — not part of the stable surface. */
|
|
4753
|
+
get terminalRejectKind() {
|
|
4754
|
+
return this.terminalReject?.kind ?? null;
|
|
4755
|
+
}
|
|
4756
|
+
/** Test/diagnostic accessor — not part of the stable surface. */
|
|
4757
|
+
get failureCount() {
|
|
4758
|
+
return this.consecutiveFailures;
|
|
4759
|
+
}
|
|
4621
4760
|
/** Test-only accessor for the underlying awareness instance. */
|
|
4622
4761
|
get awarenessForTest() {
|
|
4623
4762
|
return this.provider.awareness;
|
|
@@ -4629,6 +4768,7 @@ var RoomState = class _RoomState {
|
|
|
4629
4768
|
* of hanging the MCP tool call indefinitely.
|
|
4630
4769
|
*/
|
|
4631
4770
|
async waitForInitialSync(timeoutMs = 15e3) {
|
|
4771
|
+
if (this.terminalReject) throw this.terminalRejectError();
|
|
4632
4772
|
if (this.provider.synced) return;
|
|
4633
4773
|
if (!this.provider.on || !this.provider.off) return;
|
|
4634
4774
|
const provider = this.provider;
|
|
@@ -4638,12 +4778,20 @@ var RoomState = class _RoomState {
|
|
|
4638
4778
|
const onSync = (...args) => {
|
|
4639
4779
|
const synced = args[0];
|
|
4640
4780
|
if (!synced) return;
|
|
4781
|
+
cleanup();
|
|
4782
|
+
resolve();
|
|
4783
|
+
};
|
|
4784
|
+
const onTerminal = () => {
|
|
4785
|
+
cleanup();
|
|
4786
|
+
reject(this.terminalRejectError());
|
|
4787
|
+
};
|
|
4788
|
+
const cleanup = () => {
|
|
4641
4789
|
clearTimeout(timer);
|
|
4642
4790
|
offSub("sync", onSync);
|
|
4643
|
-
|
|
4791
|
+
this.terminalRejectListeners.delete(onTerminal);
|
|
4644
4792
|
};
|
|
4645
4793
|
const timer = setTimeout(() => {
|
|
4646
|
-
|
|
4794
|
+
cleanup();
|
|
4647
4795
|
reject(
|
|
4648
4796
|
new Error(
|
|
4649
4797
|
`timed out after ${timeoutMs}ms waiting for sync handshake on room "${this.roomId}"`
|
|
@@ -4651,6 +4799,7 @@ var RoomState = class _RoomState {
|
|
|
4651
4799
|
);
|
|
4652
4800
|
}, timeoutMs);
|
|
4653
4801
|
onSub("sync", onSync);
|
|
4802
|
+
this.terminalRejectListeners.add(onTerminal);
|
|
4654
4803
|
});
|
|
4655
4804
|
}
|
|
4656
4805
|
idleDisconnectTimer = null;
|
|
@@ -4661,6 +4810,7 @@ var RoomState = class _RoomState {
|
|
|
4661
4810
|
* disconnect because we're back in active use.
|
|
4662
4811
|
*/
|
|
4663
4812
|
async ensureConnected(timeoutMs = 15e3) {
|
|
4813
|
+
if (this.terminalReject) throw this.terminalRejectError();
|
|
4664
4814
|
if (this.idleDisconnectTimer) {
|
|
4665
4815
|
clearTimeout(this.idleDisconnectTimer);
|
|
4666
4816
|
this.idleDisconnectTimer = null;
|
|
@@ -4694,7 +4844,7 @@ var RoomState = class _RoomState {
|
|
|
4694
4844
|
this.idleDisconnectTimer = setTimeout(() => {
|
|
4695
4845
|
this.idleDisconnectTimer = null;
|
|
4696
4846
|
this.setMonitoring(false);
|
|
4697
|
-
this.
|
|
4847
|
+
this.disconnect();
|
|
4698
4848
|
}, ms);
|
|
4699
4849
|
this.idleDisconnectTimer.unref?.();
|
|
4700
4850
|
}
|
|
@@ -4702,13 +4852,18 @@ var RoomState = class _RoomState {
|
|
|
4702
4852
|
* Close the socket immediately. Available for explicit teardown
|
|
4703
4853
|
* (e.g., destroy()). Does not touch monitoring state — the socket close
|
|
4704
4854
|
* causes y-partyserver to broadcast the awareness removal, which is the
|
|
4705
|
-
* visual signal peers care about.
|
|
4855
|
+
* visual signal peers care about. Clears any pending circuit-breaker
|
|
4856
|
+
* reconnect so the user's intent to disconnect isn't silently undone.
|
|
4706
4857
|
*/
|
|
4707
4858
|
disconnect() {
|
|
4708
4859
|
if (this.idleDisconnectTimer) {
|
|
4709
4860
|
clearTimeout(this.idleDisconnectTimer);
|
|
4710
4861
|
this.idleDisconnectTimer = null;
|
|
4711
4862
|
}
|
|
4863
|
+
if (this.scheduledReconnect) {
|
|
4864
|
+
clearTimeout(this.scheduledReconnect);
|
|
4865
|
+
this.scheduledReconnect = null;
|
|
4866
|
+
}
|
|
4712
4867
|
this.provider.disconnect?.();
|
|
4713
4868
|
}
|
|
4714
4869
|
/**
|
|
@@ -4717,9 +4872,9 @@ var RoomState = class _RoomState {
|
|
|
4717
4872
|
* socket must already be open (caller should have ensured this).
|
|
4718
4873
|
*
|
|
4719
4874
|
* setMonitoring(true) — called once on first composer_next_event entry.
|
|
4720
|
-
* setMonitoring(false) — called on the
|
|
4721
|
-
* immediately so the avatar disappears
|
|
4722
|
-
*
|
|
4875
|
+
* setMonitoring(false) — called on the silent-exit branch; clears
|
|
4876
|
+
* awareness immediately so the avatar disappears
|
|
4877
|
+
* as the agent leaves.
|
|
4723
4878
|
*
|
|
4724
4879
|
* Subsequent calls with the same value are no-ops.
|
|
4725
4880
|
*/
|
|
@@ -4754,6 +4909,15 @@ var RoomState = class _RoomState {
|
|
|
4754
4909
|
markGoodbyeIssued() {
|
|
4755
4910
|
this.goodbyeIssued = true;
|
|
4756
4911
|
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Reset the goodbye latch on rejoin. `composer_join_room` reuses the
|
|
4914
|
+
* cached RoomState when one is still in the rooms map, so without this
|
|
4915
|
+
* the latch would remain set and the next `composer_next_event` would
|
|
4916
|
+
* be refused — even though the user explicitly asked to re-engage.
|
|
4917
|
+
*/
|
|
4918
|
+
clearGoodbye() {
|
|
4919
|
+
this.goodbyeIssued = false;
|
|
4920
|
+
}
|
|
4757
4921
|
get hasIssuedGoodbye() {
|
|
4758
4922
|
return this.goodbyeIssued;
|
|
4759
4923
|
}
|
|
@@ -4822,6 +4986,11 @@ var RoomState = class _RoomState {
|
|
|
4822
4986
|
clearTimeout(this.idleDisconnectTimer);
|
|
4823
4987
|
this.idleDisconnectTimer = null;
|
|
4824
4988
|
}
|
|
4989
|
+
if (this.scheduledReconnect) {
|
|
4990
|
+
clearTimeout(this.scheduledReconnect);
|
|
4991
|
+
this.scheduledReconnect = null;
|
|
4992
|
+
}
|
|
4993
|
+
this.terminalRejectListeners.clear();
|
|
4825
4994
|
this.provider.destroy();
|
|
4826
4995
|
this.doc.destroy();
|
|
4827
4996
|
}
|
|
@@ -5269,15 +5438,18 @@ async function resolveActingAs(actingAsArg) {
|
|
|
5269
5438
|
if (!actingAsArg) {
|
|
5270
5439
|
throw new Error(
|
|
5271
5440
|
[
|
|
5272
|
-
"First Composer room on this machine \u2014
|
|
5441
|
+
"First Composer room on this machine \u2014 orient the user, then ask what to call you. STOP and output ONE short message before retrying.",
|
|
5273
5442
|
"",
|
|
5274
|
-
"
|
|
5275
|
-
|
|
5276
|
-
|
|
5443
|
+
"The message should cover four things, in order:",
|
|
5444
|
+
' 1. GREET \u2014 open with "Welcome to Composer!"',
|
|
5445
|
+
' 2. WHAT COMPOSER IS \u2014 "Composer is a real-time markdown editor for humans and agents."',
|
|
5446
|
+
' 3. WHAT YOU CAN DO IN IT \u2014 "I can join your docs with you, respond to comments, and post inline suggestions."',
|
|
5447
|
+
" 4. ASK FOR A NAME \u2014 suggest ONE default they can accept in a single word.",
|
|
5448
|
+
` - If you know the user's first name from conversation context, suggest "<FirstName>'s Agent" (e.g. "Josh's Agent").`,
|
|
5449
|
+
" - Otherwise pick something playful from outside the model-family space (e.g. Monty, Gerty, Rosie, Otto, Pip). Do NOT suggest Claude / Gemini / GPT / Sonnet / Opus / Haiku / other model names.",
|
|
5450
|
+
` - Phrase it as a check, e.g. "I'll go by <suggested name> in the doc if that's okay with you?"`,
|
|
5277
5451
|
"",
|
|
5278
|
-
|
|
5279
|
-
"",
|
|
5280
|
-
"Retry with their answer as `actingAs`. It persists to ~/.composer/user.json and is reused for every future room on this machine, so only ask once."
|
|
5452
|
+
"Retry the call with their answer as `actingAs`. The name persists to ~/.composer/user.json and is reused for every future room on this machine, so this orientation only happens once per machine."
|
|
5281
5453
|
].join("\n")
|
|
5282
5454
|
);
|
|
5283
5455
|
}
|
|
@@ -5351,7 +5523,7 @@ var TOOL_DEFS = [
|
|
|
5351
5523
|
},
|
|
5352
5524
|
{
|
|
5353
5525
|
name: "composer_next_event",
|
|
5354
|
-
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
|
|
5526
|
+
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 a final `timeout` (no `requiredNextToolCall`), the return carries `userMessage` \u2014 the agent's goodbye line in its own voice. Print `userMessage` verbatim as your final output before exiting; do NOT post anything in the doc.",
|
|
5355
5527
|
inputSchema: {
|
|
5356
5528
|
type: "object",
|
|
5357
5529
|
properties: {
|
|
@@ -5518,6 +5690,21 @@ var TOOL_DEFS = [
|
|
|
5518
5690
|
required: ["roomId", "threadId"]
|
|
5519
5691
|
}
|
|
5520
5692
|
},
|
|
5693
|
+
{
|
|
5694
|
+
name: "composer_export_to_file",
|
|
5695
|
+
description: "Dump the current Composer doc to a local markdown file. ONE-WAY ONLY: Composer is the source of truth, the file is OVERWRITTEN on every call. The path must be absolute. Use when the user asks to 'save this to a file', 'export to disk', or runs /composer:export. Returns { roomId, path, bytesWritten }.",
|
|
5696
|
+
inputSchema: {
|
|
5697
|
+
type: "object",
|
|
5698
|
+
properties: {
|
|
5699
|
+
roomId: { type: "string" },
|
|
5700
|
+
path: {
|
|
5701
|
+
type: "string",
|
|
5702
|
+
description: "Absolute filesystem path to write to. Existing files are overwritten. Parent directory must already exist."
|
|
5703
|
+
}
|
|
5704
|
+
},
|
|
5705
|
+
required: ["roomId", "path"]
|
|
5706
|
+
}
|
|
5707
|
+
},
|
|
5521
5708
|
{
|
|
5522
5709
|
name: "composer_agent_status",
|
|
5523
5710
|
description: 'Drive the live state indicator on an in-flight agent-authored record (a reply, or the thread-head Comment / Suggestion). Use AFTER posting an ack with `state: "thinking"` via the reply/add tools \u2014 this tool advances state mid-work without creating a new reply, and optionally rewrites text in place at completion. Transitions: thinking \u2192 working (tool use started) \u2192 replying (assembling final answer, optional) \u2192 ready (terminal; text now holds the final content OR a thin pointer to a standalone artifact). When `replyId` is set, updates that reply inside `threadId`; when absent, updates the thread-head record (use `kind` to disambiguate). Returns { replyId } for reply updates or { id } for thread-head updates.',
|
|
@@ -5644,11 +5831,12 @@ function asAnchor(value) {
|
|
|
5644
5831
|
}
|
|
5645
5832
|
function buildMonitorHandoff(roomId, browserUrl, actingAs, opts) {
|
|
5646
5833
|
const mention = `@${actingAs}`;
|
|
5647
|
-
const
|
|
5834
|
+
const opener = opts.mode === "create" ? "I've created a new doc for you" : "I've joined the doc";
|
|
5835
|
+
const step1_sayToUser = opts.isFirstRun ? `${opener} and am launching another agent to watch for comments. Tag me (${mention}) in any Composer comment to bring me in \u2014 and share the link to invite other people (or their agents) to collaborate with us.
|
|
5648
5836
|
|
|
5649
|
-
|
|
5837
|
+
${browserUrl}` : `${opener} and am launching another agent to watch for comments. Tag me (${mention}) in any Composer comment to bring me in.
|
|
5650
5838
|
|
|
5651
|
-
|
|
5839
|
+
${browserUrl}`;
|
|
5652
5840
|
return {
|
|
5653
5841
|
step1_sayToUser,
|
|
5654
5842
|
step2_callTool: {
|
|
@@ -5697,6 +5885,7 @@ async function handleCreateRoom(args) {
|
|
|
5697
5885
|
identity
|
|
5698
5886
|
});
|
|
5699
5887
|
await state.ensureConnected();
|
|
5888
|
+
state.setMonitoring(true);
|
|
5700
5889
|
if (seedMarkdown) {
|
|
5701
5890
|
writeMarkdownToFragment(state.doc.getXmlFragment("default"), seedMarkdown);
|
|
5702
5891
|
}
|
|
@@ -5708,7 +5897,10 @@ async function handleCreateRoom(args) {
|
|
|
5708
5897
|
browserUrl,
|
|
5709
5898
|
actingAs,
|
|
5710
5899
|
snapshot: state.snapshot(),
|
|
5711
|
-
...buildMonitorHandoff(roomId, browserUrl, actingAs, {
|
|
5900
|
+
...buildMonitorHandoff(roomId, browserUrl, actingAs, {
|
|
5901
|
+
isFirstRun,
|
|
5902
|
+
mode: "create"
|
|
5903
|
+
})
|
|
5712
5904
|
});
|
|
5713
5905
|
}
|
|
5714
5906
|
async function handleJoinRoom(args) {
|
|
@@ -5723,6 +5915,9 @@ async function handleJoinRoom(args) {
|
|
|
5723
5915
|
roomId,
|
|
5724
5916
|
actingAs: existing.actingAs
|
|
5725
5917
|
});
|
|
5918
|
+
existing.clearGoodbye();
|
|
5919
|
+
await existing.ensureConnected();
|
|
5920
|
+
existing.setMonitoring(true);
|
|
5726
5921
|
existing.scheduleIdleDisconnect(3e4);
|
|
5727
5922
|
return okResult({
|
|
5728
5923
|
roomId,
|
|
@@ -5733,7 +5928,8 @@ async function handleJoinRoom(args) {
|
|
|
5733
5928
|
// first-run situation — we already wrote a name to disk at least
|
|
5734
5929
|
// once in this process lifetime to have created `existing`.
|
|
5735
5930
|
...buildMonitorHandoff(roomId, browserUrl, existing.actingAs, {
|
|
5736
|
-
isFirstRun: false
|
|
5931
|
+
isFirstRun: false,
|
|
5932
|
+
mode: "join"
|
|
5737
5933
|
})
|
|
5738
5934
|
});
|
|
5739
5935
|
}
|
|
@@ -5746,6 +5942,7 @@ async function handleJoinRoom(args) {
|
|
|
5746
5942
|
identity
|
|
5747
5943
|
});
|
|
5748
5944
|
await state.ensureConnected();
|
|
5945
|
+
state.setMonitoring(true);
|
|
5749
5946
|
rooms.set(roomId, state);
|
|
5750
5947
|
log(`composer room joined \u2192 ${browserUrl}`, { roomId, actingAs });
|
|
5751
5948
|
state.scheduleIdleDisconnect(3e4);
|
|
@@ -5754,7 +5951,10 @@ async function handleJoinRoom(args) {
|
|
|
5754
5951
|
browserUrl,
|
|
5755
5952
|
actingAs,
|
|
5756
5953
|
snapshot: state.snapshot(),
|
|
5757
|
-
...buildMonitorHandoff(roomId, browserUrl, actingAs, {
|
|
5954
|
+
...buildMonitorHandoff(roomId, browserUrl, actingAs, {
|
|
5955
|
+
isFirstRun,
|
|
5956
|
+
mode: "join"
|
|
5957
|
+
})
|
|
5758
5958
|
});
|
|
5759
5959
|
}
|
|
5760
5960
|
function handleAttachRoom(args) {
|
|
@@ -5769,7 +5969,14 @@ var ACTIVITY_WINDOW_MS = (() => {
|
|
|
5769
5969
|
const n = Number(raw);
|
|
5770
5970
|
return Number.isFinite(n) && n > 0 ? n : 15 * 60 * 1e3;
|
|
5771
5971
|
})();
|
|
5772
|
-
var
|
|
5972
|
+
var goodbyeIdleTimeout = (roomId) => `Run \`/composer:join\` when you want me back \u2014 or open the doc again at ${browserUrlFor(roomId)}.`;
|
|
5973
|
+
var GOODBYE_SERVER_KICKED = `I'm having trouble reaching the Composer doc. Try restarting your Claude session, or try again in a bit.`;
|
|
5974
|
+
var GOODBYE_RECONNECT_ABORTED = `I'm having trouble reaching the Composer doc. Make sure you're connected to the internet or try running \`/composer:join\` in a bit to have me join again.`;
|
|
5975
|
+
function terminalRejectGoodbye(state) {
|
|
5976
|
+
if (state.terminalRejectKind === "rejected") return GOODBYE_SERVER_KICKED;
|
|
5977
|
+
if (state.terminalRejectKind === "aborted") return GOODBYE_RECONNECT_ABORTED;
|
|
5978
|
+
return null;
|
|
5979
|
+
}
|
|
5773
5980
|
async function handleNextEvent(args, signal) {
|
|
5774
5981
|
const a = asObject(args);
|
|
5775
5982
|
const roomId = asString(a.roomId, "roomId");
|
|
@@ -5780,6 +5987,16 @@ async function handleNextEvent(args, signal) {
|
|
|
5780
5987
|
"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."
|
|
5781
5988
|
);
|
|
5782
5989
|
}
|
|
5990
|
+
const earlyGoodbye = terminalRejectGoodbye(state);
|
|
5991
|
+
if (earlyGoodbye) {
|
|
5992
|
+
state.markGoodbyeIssued();
|
|
5993
|
+
return okResult({
|
|
5994
|
+
kind: "timeout",
|
|
5995
|
+
recentActivity: false,
|
|
5996
|
+
userMessage: earlyGoodbye,
|
|
5997
|
+
instruction: `Print \`userMessage\` verbatim as your final output, then exit. Do NOT post anything in the doc \u2014 the connection is gone, doc-side awareness is already cleared. Stop calling composer_next_event on this room until the user explicitly rejoins.`
|
|
5998
|
+
});
|
|
5999
|
+
}
|
|
5783
6000
|
state.setMonitoring(true);
|
|
5784
6001
|
const onAbort = () => {
|
|
5785
6002
|
state.setMonitoring(false);
|
|
@@ -5808,6 +6025,16 @@ async function handleNextEvent(args, signal) {
|
|
|
5808
6025
|
}
|
|
5809
6026
|
});
|
|
5810
6027
|
}
|
|
6028
|
+
const lateGoodbye = terminalRejectGoodbye(state);
|
|
6029
|
+
if (lateGoodbye) {
|
|
6030
|
+
state.markGoodbyeIssued();
|
|
6031
|
+
return okResult({
|
|
6032
|
+
kind: "timeout",
|
|
6033
|
+
recentActivity: false,
|
|
6034
|
+
userMessage: lateGoodbye,
|
|
6035
|
+
instruction: `Print \`userMessage\` verbatim as your final output, then exit. Do NOT post anything in the doc \u2014 the connection is gone. Stop calling composer_next_event on this room until the user explicitly rejoins.`
|
|
6036
|
+
});
|
|
6037
|
+
}
|
|
5811
6038
|
const msSinceActivity = Date.now() - state.lastRemoteActivityAt;
|
|
5812
6039
|
const recentActivity = msSinceActivity < ACTIVITY_WINDOW_MS;
|
|
5813
6040
|
const minutesSince = Math.max(1, Math.round(msSinceActivity / 6e4));
|
|
@@ -5830,8 +6057,8 @@ async function handleNextEvent(args, signal) {
|
|
|
5830
6057
|
kind: "timeout",
|
|
5831
6058
|
recentActivity: false,
|
|
5832
6059
|
secondsSinceActivity: Math.round(msSinceActivity / 1e3),
|
|
5833
|
-
userMessage:
|
|
5834
|
-
instruction: `
|
|
6060
|
+
userMessage: goodbyeIdleTimeout(roomId),
|
|
6061
|
+
instruction: `Print \`userMessage\` verbatim as your final output, then exit. Do NOT post anything in the doc \u2014 the avatar has already cleared. Stop calling composer_next_event on this room until the user explicitly rejoins.`
|
|
5835
6062
|
});
|
|
5836
6063
|
}
|
|
5837
6064
|
function handleGetSection(args) {
|
|
@@ -6286,6 +6513,29 @@ function handleDone(args) {
|
|
|
6286
6513
|
const state = getOrError(roomId);
|
|
6287
6514
|
return performDone(state, a);
|
|
6288
6515
|
}
|
|
6516
|
+
async function handleExportToFile(args) {
|
|
6517
|
+
const a = asObject(args);
|
|
6518
|
+
const roomId = asString(a.roomId, "roomId");
|
|
6519
|
+
const filePath = asString(a.path, "path");
|
|
6520
|
+
if (!path3.isAbsolute(filePath)) {
|
|
6521
|
+
return errorResult(
|
|
6522
|
+
`path must be absolute, got: ${filePath}. Resolve to an absolute path before calling.`
|
|
6523
|
+
);
|
|
6524
|
+
}
|
|
6525
|
+
const state = getOrError(roomId);
|
|
6526
|
+
const markdown = serializeDocAsMarkdown(state.doc);
|
|
6527
|
+
try {
|
|
6528
|
+
await fs3.writeFile(filePath, markdown, "utf8");
|
|
6529
|
+
} catch (err) {
|
|
6530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6531
|
+
return errorResult(`failed to write ${filePath}: ${message}`);
|
|
6532
|
+
}
|
|
6533
|
+
return okResult({
|
|
6534
|
+
roomId,
|
|
6535
|
+
path: filePath,
|
|
6536
|
+
bytesWritten: Buffer.byteLength(markdown, "utf8")
|
|
6537
|
+
});
|
|
6538
|
+
}
|
|
6289
6539
|
function _registerRoomForTest(roomId, state) {
|
|
6290
6540
|
rooms.set(roomId, state);
|
|
6291
6541
|
}
|
|
@@ -6326,6 +6576,8 @@ async function dispatchTool(name, args, signal) {
|
|
|
6326
6576
|
return handleReplySuggestion(args);
|
|
6327
6577
|
case "composer_resolve_thread":
|
|
6328
6578
|
return handleResolveThread(args);
|
|
6579
|
+
case "composer_export_to_file":
|
|
6580
|
+
return await handleExportToFile(args);
|
|
6329
6581
|
case "composer_agent_status":
|
|
6330
6582
|
return handleAgentStatus(args);
|
|
6331
6583
|
case "composer_done":
|
package/dist/cli.js
CHANGED
package/dist/mcp.js
CHANGED