@composer-app/mcp 0.0.4-beta.1 → 0.0.4-beta.2
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-ORB2OJTN.js → chunk-D62LPVSX.js} +77 -27
- package/dist/cli.js +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
|
@@ -4371,22 +4371,22 @@ function buildFlatMap(fragment) {
|
|
|
4371
4371
|
walk(node);
|
|
4372
4372
|
}
|
|
4373
4373
|
if (idx < topLevel.length - 1) {
|
|
4374
|
-
flat += "\n";
|
|
4374
|
+
flat += "\n\n";
|
|
4375
4375
|
}
|
|
4376
4376
|
});
|
|
4377
4377
|
return { flat, map, blockFlatStarts };
|
|
4378
4378
|
}
|
|
4379
|
-
function
|
|
4380
|
-
if (needle.length === 0) return
|
|
4381
|
-
|
|
4382
|
-
let
|
|
4383
|
-
|
|
4384
|
-
const idx =
|
|
4385
|
-
if (idx < 0)
|
|
4386
|
-
|
|
4387
|
-
|
|
4379
|
+
function findAllOccurrences(haystack, needle) {
|
|
4380
|
+
if (needle.length === 0) return [];
|
|
4381
|
+
const out = [];
|
|
4382
|
+
let from2 = 0;
|
|
4383
|
+
while (true) {
|
|
4384
|
+
const idx = haystack.indexOf(needle, from2);
|
|
4385
|
+
if (idx < 0) break;
|
|
4386
|
+
out.push(idx);
|
|
4387
|
+
from2 = idx + 1;
|
|
4388
4388
|
}
|
|
4389
|
-
return
|
|
4389
|
+
return out;
|
|
4390
4390
|
}
|
|
4391
4391
|
function lookupFlatIndex(map, flatIndex) {
|
|
4392
4392
|
for (const entry of map) {
|
|
@@ -4484,13 +4484,11 @@ function resolveServerAnchor(doc, spec) {
|
|
|
4484
4484
|
return {
|
|
4485
4485
|
ok: false,
|
|
4486
4486
|
error: "section_not_found",
|
|
4487
|
-
currentSectionText: ""
|
|
4487
|
+
currentSectionText: "",
|
|
4488
|
+
currentSectionFlat: ""
|
|
4488
4489
|
};
|
|
4489
4490
|
}
|
|
4490
4491
|
const currentSectionText = getSection(doc, spec.headingId);
|
|
4491
|
-
if (!currentSectionText.includes(spec.textToFind)) {
|
|
4492
|
-
return { ok: false, error: "text_not_found", currentSectionText };
|
|
4493
|
-
}
|
|
4494
4492
|
const fragment = doc.getXmlFragment("default");
|
|
4495
4493
|
const { flat, map, blockFlatStarts } = buildFlatMap(fragment);
|
|
4496
4494
|
const range = getSectionBlockRange(doc, spec.headingId);
|
|
@@ -4498,25 +4496,68 @@ function resolveServerAnchor(doc, spec) {
|
|
|
4498
4496
|
return {
|
|
4499
4497
|
ok: false,
|
|
4500
4498
|
error: "section_not_found",
|
|
4501
|
-
currentSectionText
|
|
4499
|
+
currentSectionText,
|
|
4500
|
+
currentSectionFlat: ""
|
|
4502
4501
|
};
|
|
4503
4502
|
}
|
|
4504
4503
|
const sectionFlatStart = blockFlatStarts[range.start] ?? 0;
|
|
4505
4504
|
const sectionFlatEnd = range.end < blockFlatStarts.length ? blockFlatStarts[range.end] : flat.length;
|
|
4506
4505
|
const sectionFlat = flat.slice(sectionFlatStart, sectionFlatEnd);
|
|
4507
|
-
const
|
|
4508
|
-
if (
|
|
4509
|
-
return {
|
|
4506
|
+
const allMatches = findAllOccurrences(sectionFlat, spec.textToFind);
|
|
4507
|
+
if (allMatches.length === 0) {
|
|
4508
|
+
return {
|
|
4509
|
+
ok: false,
|
|
4510
|
+
error: "text_not_found",
|
|
4511
|
+
currentSectionText,
|
|
4512
|
+
currentSectionFlat: sectionFlat
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
const topLevel = fragment.toArray();
|
|
4516
|
+
const isHeadingBlockAt = (absFlatIndex) => {
|
|
4517
|
+
let blockIdx = 0;
|
|
4518
|
+
for (let i = 0; i < blockFlatStarts.length; i++) {
|
|
4519
|
+
if (blockFlatStarts[i] <= absFlatIndex) blockIdx = i;
|
|
4520
|
+
else break;
|
|
4521
|
+
}
|
|
4522
|
+
const block = topLevel[blockIdx];
|
|
4523
|
+
return block instanceof Y4.XmlElement && block.nodeName === "heading";
|
|
4524
|
+
};
|
|
4525
|
+
const ranked = allMatches.map((sectionRelStart) => ({
|
|
4526
|
+
sectionRelStart,
|
|
4527
|
+
isHeading: isHeadingBlockAt(sectionFlatStart + sectionRelStart)
|
|
4528
|
+
}));
|
|
4529
|
+
ranked.sort((a, b) => {
|
|
4530
|
+
if (a.isHeading !== b.isHeading) return a.isHeading ? 1 : -1;
|
|
4531
|
+
return a.sectionRelStart - b.sectionRelStart;
|
|
4532
|
+
});
|
|
4533
|
+
const pick = ranked[occurrence - 1];
|
|
4534
|
+
if (!pick) {
|
|
4535
|
+
return {
|
|
4536
|
+
ok: false,
|
|
4537
|
+
error: "text_not_found",
|
|
4538
|
+
currentSectionText,
|
|
4539
|
+
currentSectionFlat: sectionFlat
|
|
4540
|
+
};
|
|
4510
4541
|
}
|
|
4511
|
-
const flatStart = sectionFlatStart + sectionRelStart;
|
|
4542
|
+
const flatStart = sectionFlatStart + pick.sectionRelStart;
|
|
4512
4543
|
const flatEnd = flatStart + spec.textToFind.length;
|
|
4513
4544
|
const startEntry = lookupFlatIndex(map, flatStart);
|
|
4514
4545
|
if (!startEntry) {
|
|
4515
|
-
return {
|
|
4546
|
+
return {
|
|
4547
|
+
ok: false,
|
|
4548
|
+
error: "text_not_found",
|
|
4549
|
+
currentSectionText,
|
|
4550
|
+
currentSectionFlat: sectionFlat
|
|
4551
|
+
};
|
|
4516
4552
|
}
|
|
4517
4553
|
const lastCharEntry = lookupFlatIndexEnd(map, flatEnd - 1);
|
|
4518
4554
|
if (!lastCharEntry) {
|
|
4519
|
-
return {
|
|
4555
|
+
return {
|
|
4556
|
+
ok: false,
|
|
4557
|
+
error: "text_not_found",
|
|
4558
|
+
currentSectionText,
|
|
4559
|
+
currentSectionFlat: sectionFlat
|
|
4560
|
+
};
|
|
4520
4561
|
}
|
|
4521
4562
|
const fromRelPos = Y4.createRelativePositionFromTypeIndex(
|
|
4522
4563
|
startEntry.xmlText,
|
|
@@ -5576,7 +5617,7 @@ var TOOL_DEFS = [
|
|
|
5576
5617
|
},
|
|
5577
5618
|
{
|
|
5578
5619
|
name: "composer_add_comment",
|
|
5579
|
-
description: "Post a new top-level comment anchored to a text span anywhere in the doc. Anchor is { headingId, textToFind, occurrence? }. Use this to flag something the user didn't ask about \u2014 cross-referencing related sections, raising a concern elsewhere in the doc, or seeding a thread on a new span. Use `composer_reply_comment` instead when continuing an existing thread. Accepts optional `state` (ack-first flow: post with `state: \"thinking\"` to start the live indicator immediately) and `mentions` (array of target userIds \u2014 the invoker's userId from the mention event payload, for the `@invoker` backlink). Returns { id } on success or an isError result if the anchor cannot be resolved.",
|
|
5620
|
+
description: "Post a new top-level comment anchored to a text span anywhere in the doc. Anchor is { headingId, textToFind, occurrence? }. Use this to flag something the user didn't ask about \u2014 cross-referencing related sections, raising a concern elsewhere in the doc, or seeding a thread on a new span. Use `composer_reply_comment` instead when continuing an existing thread. Accepts optional `state` (ack-first flow: post with `state: \"thinking\"` to start the live indicator immediately) and `mentions` (array of target userIds \u2014 the invoker's userId from the mention event payload, for the `@invoker` backlink).\n**`textToFind` matches the doc's stored text, not the markdown source.** Inline marks (`**bold**`, `*italic*`, `` `code` ``, `_emphasis_`, `~~strike~~`) are stored as Y.Marks \u2014 write plain text, no markers. Top-level blocks join with `\\n\\n`; list items have NO separator (three bullets `- a / - b / - c` are stored as `abc`). Copy from `Section as the matcher sees it` in any `text_not_found` error if unsure. Returns { id } on success or an isError result if the anchor cannot be resolved.",
|
|
5580
5621
|
inputSchema: {
|
|
5581
5622
|
type: "object",
|
|
5582
5623
|
properties: {
|
|
@@ -5630,7 +5671,7 @@ var TOOL_DEFS = [
|
|
|
5630
5671
|
},
|
|
5631
5672
|
{
|
|
5632
5673
|
name: "composer_add_suggestion",
|
|
5633
|
-
description: "Post a text replacement suggestion. A suggestion can target ANY span anywhere in the doc \u2014 not just the span of the thread that triggered you. Pick exactly one of:\n - `fromThreadId` \u2014 inherit the source thread's exact stored anchor. Right when the user's request is scoped to what they selected (the common case: 'rewrite this', 'make this shorter').\n - `anchor` \u2014 specify a span yourself via `{ headingId, textToFind, occurrence? }`. Use this when the user's request targets different text ('also update the intro', 'the bullet list in Section 3 is stale') OR for proactive suggestions with no source thread.\n**Anchor = what gets deleted.** Your `textToFind` is literally cut when the user accepts and `replacementText` is inserted in its place. Anchor the whole unit you're changing (full sentence including terminal punctuation; full list item text; full paragraph), match your replacementText's shape (inline for mid-paragraph edits, full markdown block for block replacements), end replacement at the same boundary as the anchor, and include any formatting you want preserved in the replacement itself \u2014 the anchor's bold / link / heading level is gone on accept. A too-narrow or mid-token anchor leaves broken spacing or smashed-together words. See SKILL.md 'Pick the right span' for the full rubric.\n**Ripple coverage is your responsibility.** If the change requires edits nearby or elsewhere (enumeration counts, cross-references, subject/verb agreement, restated facts), call this tool MULTIPLE times in the same turn \u2014 one suggestion per span \u2014 so accepting leaves the doc correct. If you're unsure whether ripples exist elsewhere in the doc, call `composer_get_full_doc` first. Returns { id } on success or an isError result if the anchor cannot be resolved.",
|
|
5674
|
+
description: "Post a text replacement suggestion. A suggestion can target ANY span anywhere in the doc \u2014 not just the span of the thread that triggered you. Pick exactly one of:\n - `fromThreadId` \u2014 inherit the source thread's exact stored anchor. Right when the user's request is scoped to what they selected (the common case: 'rewrite this', 'make this shorter').\n - `anchor` \u2014 specify a span yourself via `{ headingId, textToFind, occurrence? }`. Use this when the user's request targets different text ('also update the intro', 'the bullet list in Section 3 is stale') OR for proactive suggestions with no source thread.\n**Anchor = what gets deleted.** Your `textToFind` is literally cut when the user accepts and `replacementText` is inserted in its place. Anchor the whole unit you're changing (full sentence including terminal punctuation; full list item text; full paragraph), match your replacementText's shape (inline for mid-paragraph edits, full markdown block for block replacements), end replacement at the same boundary as the anchor, and include any formatting you want preserved in the replacement itself \u2014 the anchor's bold / link / heading level is gone on accept. A too-narrow or mid-token anchor leaves broken spacing or smashed-together words. See SKILL.md 'Pick the right span' for the full rubric.\n**`textToFind` matches the doc's stored text, not the markdown source.** Three rules govern the shape:\n 1. **Inline marks are stripped.** `**bold**`, `*italic*`, `` `code` ``, `_emphasis_`, `~~strike~~` are stored as Y.Marks, not as literal characters. Write the plain text \u2014 `the file back` not `` `the file back` ``.\n 2. **Top-level blocks separate with `\\n\\n`.** Heading + body, or two sibling paragraphs, are joined by a blank line. Soft line breaks WITHIN a single paragraph are preserved as single `\\n`.\n 3. **List items have NO separator.** Three bullets `- a / - b / - c` are stored as `abc` (smashed). Same for ordered list items.\n When in doubt, copy from the `Section as the matcher sees it` block in any `text_not_found` error.\n**Ripple coverage is your responsibility.** If the change requires edits nearby or elsewhere (enumeration counts, cross-references, subject/verb agreement, restated facts), call this tool MULTIPLE times in the same turn \u2014 one suggestion per span \u2014 so accepting leaves the doc correct. If you're unsure whether ripples exist elsewhere in the doc, call `composer_get_full_doc` first. Returns { id } on success or an isError result if the anchor cannot be resolved.",
|
|
5634
5675
|
inputSchema: {
|
|
5635
5676
|
type: "object",
|
|
5636
5677
|
properties: {
|
|
@@ -5787,6 +5828,12 @@ function asString(value, field) {
|
|
|
5787
5828
|
}
|
|
5788
5829
|
return value;
|
|
5789
5830
|
}
|
|
5831
|
+
function asStringAllowEmpty(value, field) {
|
|
5832
|
+
if (typeof value !== "string") {
|
|
5833
|
+
throw new Error(`${field} must be a string`);
|
|
5834
|
+
}
|
|
5835
|
+
return value;
|
|
5836
|
+
}
|
|
5790
5837
|
function asOptionalString(value, field) {
|
|
5791
5838
|
if (value === void 0) return void 0;
|
|
5792
5839
|
if (typeof value !== "string") {
|
|
@@ -6139,8 +6186,11 @@ function performAddComment(state, a) {
|
|
|
6139
6186
|
const resolved = resolveServerAnchor(state.doc, anchor);
|
|
6140
6187
|
if (!resolved.ok) {
|
|
6141
6188
|
return errorResult(
|
|
6142
|
-
`anchor ${resolved.error}.
|
|
6143
|
-
${resolved.currentSectionText}
|
|
6189
|
+
`anchor ${resolved.error}. Section as rendered (markdown form, with markers):
|
|
6190
|
+
${resolved.currentSectionText}
|
|
6191
|
+
|
|
6192
|
+
Section as the matcher sees it (use this exact form for textToFind \u2014 inline marks like \`code\`, **bold**, *italic* are stored as marks not characters, so they're absent here; soft line breaks are preserved; list items have no separators):
|
|
6193
|
+
${resolved.currentSectionFlat}`
|
|
6144
6194
|
);
|
|
6145
6195
|
}
|
|
6146
6196
|
const id = nanoid4();
|
|
@@ -6243,7 +6293,7 @@ function handleReplyComment(args) {
|
|
|
6243
6293
|
return performReplyComment(state, a);
|
|
6244
6294
|
}
|
|
6245
6295
|
function performAddSuggestion(state, a) {
|
|
6246
|
-
const replacementText =
|
|
6296
|
+
const replacementText = asStringAllowEmpty(a.replacementText, "replacementText");
|
|
6247
6297
|
const fromThreadId = asOptionalString(a.fromThreadId, "fromThreadId");
|
|
6248
6298
|
const agentState = asOptionalAgentState(a.state, "state");
|
|
6249
6299
|
let anchorFrom;
|
package/dist/cli.js
CHANGED
package/dist/mcp.js
CHANGED