@abraca/mcp 1.8.0 → 1.9.1

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.
@@ -3761,7 +3761,7 @@ const propertyKeyTypes = new Set([
3761
3761
  "number",
3762
3762
  "symbol"
3763
3763
  ]);
3764
- function escapeRegex(str) {
3764
+ function escapeRegex$1(str) {
3765
3765
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3766
3766
  }
3767
3767
  function clone(inst, def, params) {
@@ -4472,7 +4472,7 @@ const $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (i
4472
4472
  });
4473
4473
  const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
4474
4474
  $ZodCheck.init(inst, def);
4475
- const escapedRegex = escapeRegex(def.includes);
4475
+ const escapedRegex = escapeRegex$1(def.includes);
4476
4476
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
4477
4477
  def.pattern = pattern;
4478
4478
  inst._zod.onattach.push((inst) => {
@@ -4495,7 +4495,7 @@ const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (ins
4495
4495
  });
4496
4496
  const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
4497
4497
  $ZodCheck.init(inst, def);
4498
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
4498
+ const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
4499
4499
  def.pattern ?? (def.pattern = pattern);
4500
4500
  inst._zod.onattach.push((inst) => {
4501
4501
  const bag = inst._zod.bag;
@@ -4517,7 +4517,7 @@ const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith",
4517
4517
  });
4518
4518
  const $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
4519
4519
  $ZodCheck.init(inst, def);
4520
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
4520
+ const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
4521
4521
  def.pattern ?? (def.pattern = pattern);
4522
4522
  inst._zod.onattach.push((inst) => {
4523
4523
  const bag = inst._zod.bag;
@@ -5549,7 +5549,7 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
5549
5549
  const values = getEnumValues(def.entries);
5550
5550
  const valuesSet = new Set(values);
5551
5551
  inst._zod.values = valuesSet;
5552
- inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
5552
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
5553
5553
  inst._zod.parse = (payload, _ctx) => {
5554
5554
  const input = payload.value;
5555
5555
  if (valuesSet.has(input)) return payload;
@@ -5567,7 +5567,7 @@ const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
5567
5567
  if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
5568
5568
  const values = new Set(def.values);
5569
5569
  inst._zod.values = values;
5570
- inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
5570
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
5571
5571
  inst._zod.parse = (payload, _ctx) => {
5572
5572
  const input = payload.value;
5573
5573
  if (values.has(input)) return payload;
@@ -19969,6 +19969,47 @@ function signChallenge(challengeB64, privateKey) {
19969
19969
  return toBase64url(sign(challenge, privateKey));
19970
19970
  }
19971
19971
 
19972
+ //#endregion
19973
+ //#region packages/mcp/src/mentions.ts
19974
+ /**
19975
+ * Mention parsing for chat messages.
19976
+ *
19977
+ * Recognizes `@alias` tokens (case-insensitive, word-boundary) so the agent
19978
+ * can decide whether a group-chat message is directed at it.
19979
+ */
19980
+ /** Escape regex metacharacters in an alias string. */
19981
+ function escapeRegex(s) {
19982
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19983
+ }
19984
+ /**
19985
+ * Build a regex that matches `@<alias>` for any of the given aliases.
19986
+ * Requires a non-word char (or start) before `@` and a word boundary after the alias
19987
+ * so `@Claude` matches but `email@claudesomething` does not.
19988
+ */
19989
+ function buildMentionRegex(aliases) {
19990
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
19991
+ if (cleaned.length === 0) return null;
19992
+ const alt = cleaned.map(escapeRegex).join("|");
19993
+ return new RegExp(`(?:^|[^\\w@])@(?:${alt})\\b`, "i");
19994
+ }
19995
+ /** Returns true if `text` contains `@alias` for any alias (case-insensitive). */
19996
+ function containsMention(text, aliases) {
19997
+ const re = buildMentionRegex(aliases);
19998
+ if (!re) return false;
19999
+ return re.test(text);
20000
+ }
20001
+ /**
20002
+ * Remove `@alias` tokens from the text. Leaves surrounding whitespace tidy so
20003
+ * the cleaned prompt reads naturally (e.g. `"@Claude help"` → `"help"`).
20004
+ */
20005
+ function stripMention(text, aliases) {
20006
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
20007
+ if (cleaned.length === 0) return text;
20008
+ const alt = cleaned.map(escapeRegex).join("|");
20009
+ const re = new RegExp(`(^|\\s)@(?:${alt})\\b[,:]?\\s*`, "gi");
20010
+ return text.replace(re, (_m, lead) => lead ? " " : "").replace(/\s{2,}/g, " ").trim();
20011
+ }
20012
+
19972
20013
  //#endregion
19973
20014
  //#region packages/mcp/src/server.ts
19974
20015
  /**
@@ -19979,7 +20020,10 @@ function signChallenge(challengeB64, privateKey) {
19979
20020
  * Use switchSpace(docId) to change the active space.
19980
20021
  */
19981
20022
  const IDLE_TIMEOUT_MS = 300 * 1e3;
19982
- var AbracadabraMCPServer = class {
20023
+ var AbracadabraMCPServer = class AbracadabraMCPServer {
20024
+ static {
20025
+ this.TOOL_HISTORY_MAX = 20;
20026
+ }
19983
20027
  constructor(config) {
19984
20028
  this._serverInfo = null;
19985
20029
  this._rootDocId = null;
@@ -19996,6 +20040,7 @@ var AbracadabraMCPServer = class {
19996
20040
  this._typingInterval = null;
19997
20041
  this._lastChatChannel = null;
19998
20042
  this._signFn = null;
20043
+ this._toolHistory = [];
19999
20044
  this.config = config;
20000
20045
  this.client = new _abraca_dabra.AbracadabraClient({
20001
20046
  url: config.url,
@@ -20008,6 +20053,14 @@ var AbracadabraMCPServer = class {
20008
20053
  get agentColor() {
20009
20054
  return this.config.agentColor || "hsl(270, 80%, 60%)";
20010
20055
  }
20056
+ get triggerMode() {
20057
+ return this.config.triggerMode ?? "mention+task";
20058
+ }
20059
+ get mentionAliases() {
20060
+ const explicit = this.config.mentionAliases?.filter((a) => a.trim().length > 0);
20061
+ if (explicit && explicit.length > 0) return explicit;
20062
+ return [this.agentName];
20063
+ }
20011
20064
  get serverInfo() {
20012
20065
  return this._serverInfo;
20013
20066
  }
@@ -20048,7 +20101,7 @@ var AbracadabraMCPServer = class {
20048
20101
  await this.client.loginWithKey(keypair.publicKeyB64, signFn);
20049
20102
  } else throw err;
20050
20103
  }
20051
- console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (${keypair.publicKeyB64.slice(0, 12)}...)`);
20104
+ console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`);
20052
20105
  this._serverInfo = await this.client.serverInfo();
20053
20106
  let initialDocId = this._serverInfo.index_doc_id ?? null;
20054
20107
  try {
@@ -20115,6 +20168,8 @@ var AbracadabraMCPServer = class {
20115
20168
  provider.awareness.setLocalStateField("status", null);
20116
20169
  provider.awareness.setLocalStateField("activeToolCall", null);
20117
20170
  provider.awareness.setLocalStateField("statusContext", null);
20171
+ provider.awareness.setLocalStateField("turnId", null);
20172
+ provider.awareness.setLocalStateField("toolHistory", []);
20118
20173
  const conn = {
20119
20174
  doc,
20120
20175
  provider,
@@ -20238,6 +20293,7 @@ var AbracadabraMCPServer = class {
20238
20293
  _observeRootAwareness(provider) {
20239
20294
  const selfId = provider.awareness.clientID;
20240
20295
  provider.awareness.on("change", () => {
20296
+ if (this.triggerMode === "mention") return;
20241
20297
  const states = provider.awareness.getStates();
20242
20298
  for (const [clientId, state] of states) {
20243
20299
  if (clientId === selfId) continue;
@@ -20250,6 +20306,7 @@ var AbracadabraMCPServer = class {
20250
20306
  const user = state["user"];
20251
20307
  const senderName = user && typeof user === "object" && typeof user.name === "string" ? user.name : "Unknown";
20252
20308
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`);
20309
+ this._beginTurn();
20253
20310
  this.setAutoStatus("thinking");
20254
20311
  this._dispatchAiTask({
20255
20312
  id,
@@ -20312,9 +20369,28 @@ var AbracadabraMCPServer = class {
20312
20369
  if (data.sender_id && data.sender_id === this._userId) return;
20313
20370
  const channel = data.channel;
20314
20371
  const docId = channel?.startsWith("group:") ? channel.slice(6) : "";
20315
- if (channel?.startsWith("dm:")) {
20372
+ const isDM = channel?.startsWith("dm:") ?? false;
20373
+ const isGroup = channel?.startsWith("group:") ?? false;
20374
+ if (isDM) {
20316
20375
  const parts = channel.split(":");
20317
- if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) return;
20376
+ if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) {
20377
+ console.error(`[abracadabra-mcp] Dropping DM: agent _userId=${this._userId} not in channel parts=[${parts[1]}, ${parts[2]}] — channel='${channel}'. The dashboard's awareness likely points at a stale Claude identity.`);
20378
+ return;
20379
+ }
20380
+ }
20381
+ const mode = this.triggerMode;
20382
+ const content = typeof data.content === "string" ? data.content : "";
20383
+ let dispatchContent = content;
20384
+ if (isGroup) {
20385
+ if (mode === "task") return;
20386
+ if (mode === "mention" || mode === "mention+task") {
20387
+ const aliases = this.mentionAliases;
20388
+ if (!containsMention(content, aliases)) {
20389
+ console.error(`[abracadabra-mcp] skipped message on ${channel} — no @mention for ${aliases.join("|")}`);
20390
+ return;
20391
+ }
20392
+ dispatchContent = stripMention(content, aliases) || content;
20393
+ }
20318
20394
  }
20319
20395
  if (channel) {
20320
20396
  const rootProvider = this._activeConnection?.provider;
@@ -20324,14 +20400,13 @@ var AbracadabraMCPServer = class {
20324
20400
  timestamp: Math.floor(Date.now() / 1e3)
20325
20401
  }));
20326
20402
  this._lastChatChannel = channel;
20327
- this.sendTypingIndicator(channel);
20328
- this._startTypingInterval(channel);
20329
20403
  }
20404
+ this._beginTurn();
20330
20405
  this.setAutoStatus("thinking");
20331
20406
  await this._serverRef.notification({
20332
20407
  method: "notifications/claude/channel",
20333
20408
  params: {
20334
- content: data.content ?? "",
20409
+ content: dispatchContent,
20335
20410
  instructions: `You MUST use send_chat_message with channel="${channel ?? ""}" for ALL responses — both progress updates and final answers. The user CANNOT see plain text output; they only see messages sent via send_chat_message. When doing multi-step work, send brief status updates via send_chat_message (e.g. "Looking into that..." or "Found it, writing up results...") so the user knows you're working. Never output plain text as a substitute for send_chat_message.`,
20336
20411
  meta: {
20337
20412
  source: "abracadabra",
@@ -20362,13 +20437,35 @@ var AbracadabraMCPServer = class {
20362
20437
  if (docId !== void 0) provider.awareness.setLocalStateField("docId", docId);
20363
20438
  const context = status ? statusContext !== void 0 ? statusContext : this._lastChatChannel : null;
20364
20439
  provider.awareness.setLocalStateField("statusContext", context ?? null);
20365
- if (!status) this._stopTypingInterval();
20440
+ if (!status) {
20441
+ this._stopTypingInterval();
20442
+ provider.awareness.setLocalStateField("activeToolCall", null);
20443
+ provider.awareness.setLocalStateField("turnId", null);
20444
+ this._toolHistory = [];
20445
+ provider.awareness.setLocalStateField("toolHistory", []);
20446
+ }
20366
20447
  if (status) this._statusClearTimer = setTimeout(() => {
20367
20448
  provider.awareness.setLocalStateField("status", null);
20368
20449
  provider.awareness.setLocalStateField("activeToolCall", null);
20369
20450
  provider.awareness.setLocalStateField("statusContext", null);
20451
+ provider.awareness.setLocalStateField("turnId", null);
20452
+ this._toolHistory = [];
20453
+ provider.awareness.setLocalStateField("toolHistory", []);
20370
20454
  this._stopTypingInterval();
20371
- }, 3e4);
20455
+ }, 1e4);
20456
+ }
20457
+ /**
20458
+ * Start a new agent turn. Mints a fresh UUID and writes it to awareness so
20459
+ * the dashboard can gate the incantation on "there is an active turn",
20460
+ * decoupled from the (racier) status field. Called from chat arrival and
20461
+ * ai:task dispatch right before `setAutoStatus('thinking')`.
20462
+ */
20463
+ _beginTurn() {
20464
+ const provider = this._activeConnection?.provider;
20465
+ if (!provider) return;
20466
+ this._toolHistory = [];
20467
+ provider.awareness.setLocalStateField("toolHistory", []);
20468
+ provider.awareness.setLocalStateField("turnId", crypto.randomUUID());
20372
20469
  }
20373
20470
  /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
20374
20471
  _startTypingInterval(channel) {
@@ -20386,10 +20483,28 @@ var AbracadabraMCPServer = class {
20386
20483
  }
20387
20484
  /**
20388
20485
  * Broadcast which tool the agent is currently executing.
20389
- * Dashboard renders this as a ChatTool indicator.
20486
+ *
20487
+ * Renders as a ChatTool pill on the dashboard. On non-null calls, the tool
20488
+ * is also appended to `toolHistory` (capped at TOOL_HISTORY_MAX) and written
20489
+ * to awareness so the dashboard's inline trace can show the turn's recent
20490
+ * activity. Tools do NOT clear (`setActiveToolCall(null)`) on completion —
20491
+ * the pill stays until the next tool replaces it or `setAutoStatus(null)`
20492
+ * flushes the turn. This keeps pills visible long enough to see.
20390
20493
  */
20391
20494
  setActiveToolCall(toolCall) {
20392
- this._activeConnection?.provider?.awareness.setLocalStateField("activeToolCall", toolCall);
20495
+ const provider = this._activeConnection?.provider;
20496
+ if (!provider) return;
20497
+ provider.awareness.setLocalStateField("activeToolCall", toolCall);
20498
+ if (toolCall) {
20499
+ this._toolHistory.push({
20500
+ tool: toolCall.name,
20501
+ target: toolCall.target,
20502
+ ts: Date.now(),
20503
+ channel: this._lastChatChannel
20504
+ });
20505
+ if (this._toolHistory.length > AbracadabraMCPServer.TOOL_HISTORY_MAX) this._toolHistory.splice(0, this._toolHistory.length - AbracadabraMCPServer.TOOL_HISTORY_MAX);
20506
+ provider.awareness.setLocalStateField("toolHistory", [...this._toolHistory]);
20507
+ }
20393
20508
  }
20394
20509
  /**
20395
20510
  * Send a typing indicator to a chat channel.
@@ -20420,8 +20535,11 @@ var AbracadabraMCPServer = class {
20420
20535
  conn.provider.awareness.setLocalStateField("status", null);
20421
20536
  conn.provider.awareness.setLocalStateField("activeToolCall", null);
20422
20537
  conn.provider.awareness.setLocalStateField("statusContext", null);
20538
+ conn.provider.awareness.setLocalStateField("turnId", null);
20539
+ conn.provider.awareness.setLocalStateField("toolHistory", []);
20423
20540
  conn.provider.destroy();
20424
20541
  }
20542
+ this._toolHistory = [];
20425
20543
  this._spaceConnections.clear();
20426
20544
  this._activeConnection = null;
20427
20545
  console.error("[abracadabra-mcp] Shutdown complete");
@@ -21237,16 +21355,12 @@ function registerTreeTools(mcp, server) {
21237
21355
  server.setAutoStatus("reading");
21238
21356
  server.setActiveToolCall({ name: "list_documents" });
21239
21357
  const treeMap = server.getTreeMap();
21240
- if (!treeMap) {
21241
- server.setActiveToolCall(null);
21242
- return { content: [{
21243
- type: "text",
21244
- text: "Not connected"
21245
- }] };
21246
- }
21358
+ if (!treeMap) return { content: [{
21359
+ type: "text",
21360
+ text: "Not connected"
21361
+ }] };
21247
21362
  const targetId = normalizeRootId(parentId, server);
21248
21363
  const children = childrenOf$1(readEntries$1(treeMap), targetId);
21249
- server.setActiveToolCall(null);
21250
21364
  return { content: [{
21251
21365
  type: "text",
21252
21366
  text: JSON.stringify(children, null, 2)
@@ -21259,17 +21373,13 @@ function registerTreeTools(mcp, server) {
21259
21373
  server.setAutoStatus("reading");
21260
21374
  server.setActiveToolCall({ name: "get_document_tree" });
21261
21375
  const treeMap = server.getTreeMap();
21262
- if (!treeMap) {
21263
- server.setActiveToolCall(null);
21264
- return { content: [{
21265
- type: "text",
21266
- text: "Not connected"
21267
- }] };
21268
- }
21376
+ if (!treeMap) return { content: [{
21377
+ type: "text",
21378
+ text: "Not connected"
21379
+ }] };
21269
21380
  const targetId = normalizeRootId(rootId, server);
21270
21381
  const maxDepth = depth ?? 3;
21271
21382
  const tree = buildTree$1(readEntries$1(treeMap), targetId, maxDepth);
21272
- server.setActiveToolCall(null);
21273
21383
  return { content: [{
21274
21384
  type: "text",
21275
21385
  text: JSON.stringify(tree, null, 2)
@@ -21285,13 +21395,10 @@ function registerTreeTools(mcp, server) {
21285
21395
  target: query
21286
21396
  });
21287
21397
  const treeMap = server.getTreeMap();
21288
- if (!treeMap) {
21289
- server.setActiveToolCall(null);
21290
- return { content: [{
21291
- type: "text",
21292
- text: "Not connected"
21293
- }] };
21294
- }
21398
+ if (!treeMap) return { content: [{
21399
+ type: "text",
21400
+ text: "Not connected"
21401
+ }] };
21295
21402
  const entries = readEntries$1(treeMap);
21296
21403
  const lowerQuery = query.toLowerCase();
21297
21404
  const normalizedRoot = normalizeRootId(rootId, server);
@@ -21316,7 +21423,6 @@ function registerTreeTools(mcp, server) {
21316
21423
  path
21317
21424
  };
21318
21425
  });
21319
- server.setActiveToolCall(null);
21320
21426
  if (results.length === 0) return { content: [{
21321
21427
  type: "text",
21322
21428
  text: `No documents found matching "${query}". Try get_document_tree to see the full hierarchy.`
@@ -21330,7 +21436,7 @@ function registerTreeTools(mcp, server) {
21330
21436
  parentId: zod.z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
21331
21437
  label: zod.z.string().describe("Display name / title for the document."),
21332
21438
  type: zod.z.string().optional().describe("Page type — sets how this document renders. Core types (always available): \"doc\" (rich text), \"kanban\" (columns → cards), \"table\" (columns → rows, positional), \"calendar\" (events with datetimeStart/End), \"timeline\" (epics → tasks with dateStart/End + taskProgress), \"checklist\" (tasks with checked/priority, unlimited nesting), \"outline\" (nested items, unlimited depth), \"gallery\" (visual grid with covers/ratings), \"map\" (markers/lines with geoLat/geoLng), \"graph\" (force-directed knowledge graph), \"dashboard\" (positioned widgets with deskX/deskY/deskMode), \"slides\" (slides → sub-slides with transitions), \"chart\" (bar/stacked bar/line/donut/treemap from data points or aggregation), \"sheets\" (spreadsheet with formulas and formatting), \"overview\" (space home — activity and stats), \"call\" (video call room, no children). Plugin types (require plugin enabled on the server): \"spatial\" (3D scene with spShape/spX/spY/spZ + universal color, plugin: spatial), \"media\" (audio/video player with playlists, plugin: media), \"coder\" (collaborative multi-file coding env with fileType/entry, plugin: coder). Alias: \"desktop\" → \"dashboard\". Omit to inherit parent view. Only set on the parent page, NEVER on child items."),
21333
- meta: zod.z.record(zod.z.unknown()).optional().describe("Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like \"star\"/\"code-2\"/\"users\" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.")
21439
+ meta: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe("Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like \"star\"/\"code-2\"/\"users\" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.")
21334
21440
  }, async ({ parentId, label, type, meta }) => {
21335
21441
  server.setAutoStatus("creating");
21336
21442
  server.setActiveToolCall({
@@ -21339,13 +21445,10 @@ function registerTreeTools(mcp, server) {
21339
21445
  });
21340
21446
  const treeMap = server.getTreeMap();
21341
21447
  const rootDoc = server.rootDocument;
21342
- if (!treeMap || !rootDoc) {
21343
- server.setActiveToolCall(null);
21344
- return { content: [{
21345
- type: "text",
21346
- text: "Not connected"
21347
- }] };
21348
- }
21448
+ if (!treeMap || !rootDoc) return { content: [{
21449
+ type: "text",
21450
+ text: "Not connected"
21451
+ }] };
21349
21452
  const id = crypto.randomUUID();
21350
21453
  const normalizedParent = normalizeRootId(parentId, server);
21351
21454
  const now = Date.now();
@@ -21361,7 +21464,6 @@ function registerTreeTools(mcp, server) {
21361
21464
  });
21362
21465
  });
21363
21466
  server.setFocusedDoc(id);
21364
- server.setActiveToolCall(null);
21365
21467
  return { content: [{
21366
21468
  type: "text",
21367
21469
  text: JSON.stringify({
@@ -21382,28 +21484,21 @@ function registerTreeTools(mcp, server) {
21382
21484
  target: id
21383
21485
  });
21384
21486
  const treeMap = server.getTreeMap();
21385
- if (!treeMap) {
21386
- server.setActiveToolCall(null);
21387
- return { content: [{
21388
- type: "text",
21389
- text: "Not connected"
21390
- }] };
21391
- }
21487
+ if (!treeMap) return { content: [{
21488
+ type: "text",
21489
+ text: "Not connected"
21490
+ }] };
21392
21491
  const raw = treeMap.get(id);
21393
- if (!raw) {
21394
- server.setActiveToolCall(null);
21395
- return { content: [{
21396
- type: "text",
21397
- text: `Document ${id} not found`
21398
- }] };
21399
- }
21492
+ if (!raw) return { content: [{
21493
+ type: "text",
21494
+ text: `Document ${id} not found`
21495
+ }] };
21400
21496
  const entry = toPlain(raw);
21401
21497
  treeMap.set(id, {
21402
21498
  ...entry,
21403
21499
  label,
21404
21500
  updatedAt: Date.now()
21405
21501
  });
21406
- server.setActiveToolCall(null);
21407
21502
  return { content: [{
21408
21503
  type: "text",
21409
21504
  text: `Renamed to "${label}"`
@@ -21420,21 +21515,15 @@ function registerTreeTools(mcp, server) {
21420
21515
  target: id
21421
21516
  });
21422
21517
  const treeMap = server.getTreeMap();
21423
- if (!treeMap) {
21424
- server.setActiveToolCall(null);
21425
- return { content: [{
21426
- type: "text",
21427
- text: "Not connected"
21428
- }] };
21429
- }
21518
+ if (!treeMap) return { content: [{
21519
+ type: "text",
21520
+ text: "Not connected"
21521
+ }] };
21430
21522
  const raw = treeMap.get(id);
21431
- if (!raw) {
21432
- server.setActiveToolCall(null);
21433
- return { content: [{
21434
- type: "text",
21435
- text: `Document ${id} not found`
21436
- }] };
21437
- }
21523
+ if (!raw) return { content: [{
21524
+ type: "text",
21525
+ text: `Document ${id} not found`
21526
+ }] };
21438
21527
  const entry = toPlain(raw);
21439
21528
  treeMap.set(id, {
21440
21529
  ...entry,
@@ -21442,7 +21531,6 @@ function registerTreeTools(mcp, server) {
21442
21531
  order: order ?? Date.now(),
21443
21532
  updatedAt: Date.now()
21444
21533
  });
21445
- server.setActiveToolCall(null);
21446
21534
  return { content: [{
21447
21535
  type: "text",
21448
21536
  text: `Moved ${id} to parent ${newParentId}`
@@ -21457,13 +21545,10 @@ function registerTreeTools(mcp, server) {
21457
21545
  const treeMap = server.getTreeMap();
21458
21546
  const trashMap = server.getTrashMap();
21459
21547
  const rootDoc = server.rootDocument;
21460
- if (!treeMap || !trashMap || !rootDoc) {
21461
- server.setActiveToolCall(null);
21462
- return { content: [{
21463
- type: "text",
21464
- text: "Not connected"
21465
- }] };
21466
- }
21548
+ if (!treeMap || !trashMap || !rootDoc) return { content: [{
21549
+ type: "text",
21550
+ text: "Not connected"
21551
+ }] };
21467
21552
  const toDelete = [id, ...descendantsOf(readEntries$1(treeMap), id).map((e) => e.id)];
21468
21553
  const now = Date.now();
21469
21554
  rootDoc.transact(() => {
@@ -21482,7 +21567,6 @@ function registerTreeTools(mcp, server) {
21482
21567
  treeMap.delete(nid);
21483
21568
  }
21484
21569
  });
21485
- server.setActiveToolCall(null);
21486
21570
  return { content: [{
21487
21571
  type: "text",
21488
21572
  text: `Deleted ${toDelete.length} document(s)`
@@ -21498,28 +21582,21 @@ function registerTreeTools(mcp, server) {
21498
21582
  target: id
21499
21583
  });
21500
21584
  const treeMap = server.getTreeMap();
21501
- if (!treeMap) {
21502
- server.setActiveToolCall(null);
21503
- return { content: [{
21504
- type: "text",
21505
- text: "Not connected"
21506
- }] };
21507
- }
21585
+ if (!treeMap) return { content: [{
21586
+ type: "text",
21587
+ text: "Not connected"
21588
+ }] };
21508
21589
  const raw = treeMap.get(id);
21509
- if (!raw) {
21510
- server.setActiveToolCall(null);
21511
- return { content: [{
21512
- type: "text",
21513
- text: `Document ${id} not found`
21514
- }] };
21515
- }
21590
+ if (!raw) return { content: [{
21591
+ type: "text",
21592
+ text: `Document ${id} not found`
21593
+ }] };
21516
21594
  const entry = toPlain(raw);
21517
21595
  treeMap.set(id, {
21518
21596
  ...entry,
21519
21597
  type,
21520
21598
  updatedAt: Date.now()
21521
21599
  });
21522
- server.setActiveToolCall(null);
21523
21600
  return { content: [{
21524
21601
  type: "text",
21525
21602
  text: `Changed type to "${type}"`
@@ -21747,14 +21824,12 @@ function parseInline(text) {
21747
21824
  text: kbdProps["value"] || "",
21748
21825
  attrs: { kbd: { value: kbdProps["value"] || "" } }
21749
21826
  });
21750
- } else if (match[5] !== void 0) {
21751
- const docId = match[5];
21752
- const displayText = match[6] ?? docId;
21753
- tokens.push({
21754
- text: displayText,
21755
- attrs: { link: { href: `/doc/${docId}` } }
21756
- });
21757
- } else if (match[7] !== void 0) tokens.push({
21827
+ } else if (match[5] !== void 0) tokens.push({
21828
+ text: "",
21829
+ node: "docLink",
21830
+ nodeAttrs: { docId: match[5] }
21831
+ });
21832
+ else if (match[7] !== void 0) tokens.push({
21758
21833
  text: match[7],
21759
21834
  attrs: { strike: true }
21760
21835
  });
@@ -21781,7 +21856,7 @@ function parseInline(text) {
21781
21856
  lastIndex = match.index + match[0].length;
21782
21857
  }
21783
21858
  if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
21784
- return tokens.filter((t) => t.text.length > 0);
21859
+ return tokens.filter((t) => t.node || t.text.length > 0);
21785
21860
  }
21786
21861
  function parseTableRow(line) {
21787
21862
  const parts = line.split("|");
@@ -21909,11 +21984,14 @@ function parseBlocks(markdown) {
21909
21984
  i++;
21910
21985
  continue;
21911
21986
  }
21912
- const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/);
21987
+ const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\](\{[^}]*\})?\s*$/);
21913
21988
  if (docEmbedMatch) {
21989
+ const props = parseMdcProps(docEmbedMatch[2]);
21990
+ const seamless = "seamless" in props || props["seamless"] === "true" || /\{[^}]*\bseamless\b[^}]*\}/.test(docEmbedMatch[2] ?? "");
21914
21991
  blocks.push({
21915
21992
  type: "docEmbed",
21916
- docId: docEmbedMatch[1]
21993
+ docId: docEmbedMatch[1],
21994
+ seamless: seamless || void 0
21917
21995
  });
21918
21996
  i++;
21919
21997
  continue;
@@ -22191,13 +22269,22 @@ function parseBlocks(markdown) {
22191
22269
  return blocks;
22192
22270
  }
22193
22271
  function fillTextInto(el, tokens) {
22194
- const filtered = tokens.filter((t) => t.text.length > 0);
22272
+ const filtered = tokens.filter((t) => t.node || t.text.length > 0);
22195
22273
  if (!filtered.length) return;
22196
- const xtNodes = filtered.map(() => new yjs.XmlText());
22197
- el.insert(0, xtNodes);
22274
+ const children = filtered.map((tok) => {
22275
+ if (tok.node) {
22276
+ const xe = new yjs.XmlElement(tok.node);
22277
+ if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
22278
+ return xe;
22279
+ }
22280
+ return new yjs.XmlText();
22281
+ });
22282
+ el.insert(0, children);
22198
22283
  filtered.forEach((tok, i) => {
22199
- if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
22200
- else xtNodes[i].insert(0, tok.text);
22284
+ if (tok.node) return;
22285
+ const xt = children[i];
22286
+ if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
22287
+ else xt.insert(0, tok.text);
22201
22288
  });
22202
22289
  }
22203
22290
  function blockElName(b) {
@@ -22444,6 +22531,7 @@ function fillBlock(el, block) {
22444
22531
  break;
22445
22532
  case "docEmbed":
22446
22533
  el.setAttribute("docId", block.docId);
22534
+ if (block.seamless) el.setAttribute("seamless", "true");
22447
22535
  break;
22448
22536
  case "svgEmbed":
22449
22537
  el.setAttribute("svg", block.svg);
@@ -22543,7 +22631,14 @@ function elementTextContent(el) {
22543
22631
  for (let i = 0; i < el.length; i++) {
22544
22632
  const child = el.get(i);
22545
22633
  if (child instanceof yjs.XmlText) parts.push(xmlTextToMarkdown(child));
22546
- else if (child instanceof yjs.XmlElement) parts.push(elementTextContent(child));
22634
+ else if (child instanceof yjs.XmlElement) {
22635
+ if (child.nodeName === "docLink") {
22636
+ const docId = child.getAttribute("docId");
22637
+ if (docId) parts.push(`[[${docId}]]`);
22638
+ continue;
22639
+ }
22640
+ parts.push(elementTextContent(child));
22641
+ }
22547
22642
  }
22548
22643
  return parts.join("");
22549
22644
  }
@@ -22572,7 +22667,9 @@ function serializeElement(el, indent = "") {
22572
22667
  case "table": return serializeTable(el);
22573
22668
  case "docEmbed": {
22574
22669
  const docId = el.getAttribute("docId");
22575
- return docId ? `![[${docId}]]` : "";
22670
+ if (!docId) return "";
22671
+ const seamlessAttr = el.getAttribute("seamless");
22672
+ return seamlessAttr === true || seamlessAttr === "true" ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
22576
22673
  }
22577
22674
  case "svgEmbed": {
22578
22675
  const svg = el.getAttribute("svg") || "";
@@ -22775,7 +22872,6 @@ function registerContentTools(mcp, server) {
22775
22872
  });
22776
22873
  children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
22777
22874
  }
22778
- server.setActiveToolCall(null);
22779
22875
  const result = {
22780
22876
  label,
22781
22877
  type,
@@ -22788,7 +22884,6 @@ function registerContentTools(mcp, server) {
22788
22884
  text: JSON.stringify(result, null, 2)
22789
22885
  }] };
22790
22886
  } catch (error) {
22791
- server.setActiveToolCall(null);
22792
22887
  return {
22793
22888
  content: [{
22794
22889
  type: "text",
@@ -22838,13 +22933,11 @@ function registerContentTools(mcp, server) {
22838
22933
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
22839
22934
  server.setFocusedDoc(docId);
22840
22935
  server.setDocCursor(docId, fragment.length);
22841
- server.setActiveToolCall(null);
22842
22936
  return { content: [{
22843
22937
  type: "text",
22844
22938
  text: `Document ${docId} updated (${writeMode} mode)`
22845
22939
  }] };
22846
22940
  } catch (error) {
22847
- server.setActiveToolCall(null);
22848
22941
  return {
22849
22942
  content: [{
22850
22943
  type: "text",
@@ -22866,22 +22959,15 @@ function registerMetaTools(mcp, server) {
22866
22959
  target: docId
22867
22960
  });
22868
22961
  const treeMap = server.getTreeMap();
22869
- if (!treeMap) {
22870
- server.setActiveToolCall(null);
22871
- return { content: [{
22872
- type: "text",
22873
- text: "Not connected"
22874
- }] };
22875
- }
22962
+ if (!treeMap) return { content: [{
22963
+ type: "text",
22964
+ text: "Not connected"
22965
+ }] };
22876
22966
  const entry = treeMap.get(docId);
22877
- if (!entry) {
22878
- server.setActiveToolCall(null);
22879
- return { content: [{
22880
- type: "text",
22881
- text: `Document ${docId} not found`
22882
- }] };
22883
- }
22884
- server.setActiveToolCall(null);
22967
+ if (!entry) return { content: [{
22968
+ type: "text",
22969
+ text: `Document ${docId} not found`
22970
+ }] };
22885
22971
  return { content: [{
22886
22972
  type: "text",
22887
22973
  text: JSON.stringify({
@@ -22894,7 +22980,7 @@ function registerMetaTools(mcp, server) {
22894
22980
  });
22895
22981
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
22896
22982
  docId: zod.z.string().describe("Document ID."),
22897
- meta: zod.z.record(zod.z.unknown()).describe("Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, timeStart/timeEnd, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId, coverDocId, dateTaken. Geo/Map (children): geoType (\"marker\"|\"line\"|\"measure\"), geoLat, geoLng, geoDescription. Spatial 3D (children, plugin: spatial): spShape (\"box\"|\"sphere\"|\"cylinder\"|\"cone\"|\"plane\"|\"torus\"|\"glb\"), spX/spY/spZ, spRX/spRY/spRZ (deg), spSX/spSY/spSZ (scale), spOpacity (0-100), spModelUploadId, spModelDocId — spatial uses the universal `color` key, NOT spColor. Dashboard (children): deskX, deskY, deskZ, deskMode (\"icon\"|\"widget-sm\"|\"widget-lg\"). Mindmap-layout (children): mmX, mmY. Graph-layout (children): graphX, graphY, graphPinned. Slides (children): slidesTransition (\"none\"|\"fade\"|\"slide\"). Coder (children, plugin: coder): fileType (\"vue\"|\"ts\"|\"js\"|\"css\"|\"json\"|\"folder\"), entry (bool). Cell formatting (sheets cells): bold, italic, textColor, bgColor, align (\"left\"|\"center\"|\"right\"), formula. Renderer config (on the PAGE doc itself, not children): kanbanColumnWidth (\"narrow\"|\"default\"|\"wide\"), galleryColumns (1-6), galleryAspect (\"square\"|\"4:3\"|\"3:2\"|\"16:9\"|\"free\"), galleryCardStyle (\"default\"|\"compact\"|\"detailed\"), galleryShowLabels, gallerySortBy (\"manual\"|\"date\"|\"name\"|\"rating\"), calendarView (\"month\"|\"week\"|\"day\"), calendarWeekStart (\"sun\"|\"mon\"), calendarShowWeekNumbers, tableMode (\"hierarchy\"|\"flat\"), tableSortKey, tableSortDir (\"asc\"|\"desc\"), timelineZoom (\"week\"|\"month\"|\"quarter\"), timelinePixelsPerDay, timelineCenterDate (ISO date), checklistFilter (\"all\"|\"active\"|\"completed\"), checklistSort (\"manual\"|\"priority\"|\"due\"), mapShowLabels, graphSpacing (\"compact\"|\"default\"|\"spacious\"), graphShowLabels, graphEdgeThickness (\"thin\"|\"normal\"|\"thick\"), showRefEdges, mmSpacing, spatialGridVisible, slidesTheme (\"dark\"|\"light\"), chartType (\"bar\"|\"stacked bar\"|\"line\"|\"donut\"|\"treemap\"), chartMetric (\"value\"|\"type\"|\"tag\"|\"status\"|\"priority\"|\"activity\"|\"completion\"), chartColorScheme (\"default\"|\"warm\"|\"cool\"|\"mono\"), chartLimit (3-30), chartShowLegend, chartShowValues, sheetsDefaultColWidth (40-500), sheetsDefaultRowHeight (20-100), sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols, mediaRepeat (\"off\"|\"all\"|\"one\"), mediaShuffle. Set a key to null to clear it.")
22983
+ meta: zod.z.record(zod.z.string(), zod.z.unknown()).describe("Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, timeStart/timeEnd, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId, coverDocId, dateTaken. Geo/Map (children): geoType (\"marker\"|\"line\"|\"measure\"), geoLat, geoLng, geoDescription. Spatial 3D (children, plugin: spatial): spShape (\"box\"|\"sphere\"|\"cylinder\"|\"cone\"|\"plane\"|\"torus\"|\"glb\"), spX/spY/spZ, spRX/spRY/spRZ (deg), spSX/spSY/spSZ (scale), spOpacity (0-100), spModelUploadId, spModelDocId — spatial uses the universal `color` key, NOT spColor. Dashboard (children): deskX, deskY, deskZ, deskMode (\"icon\"|\"widget-sm\"|\"widget-lg\"). Mindmap-layout (children): mmX, mmY. Graph-layout (children): graphX, graphY, graphPinned. Slides (children): slidesTransition (\"none\"|\"fade\"|\"slide\"). Coder (children, plugin: coder): fileType (\"vue\"|\"ts\"|\"js\"|\"css\"|\"json\"|\"folder\"), entry (bool). Cell formatting (sheets cells): bold, italic, textColor, bgColor, align (\"left\"|\"center\"|\"right\"), formula. Renderer config (on the PAGE doc itself, not children): kanbanColumnWidth (\"narrow\"|\"default\"|\"wide\"), galleryColumns (1-6), galleryAspect (\"square\"|\"4:3\"|\"3:2\"|\"16:9\"|\"free\"), galleryCardStyle (\"default\"|\"compact\"|\"detailed\"), galleryShowLabels, gallerySortBy (\"manual\"|\"date\"|\"name\"|\"rating\"), calendarView (\"month\"|\"week\"|\"day\"), calendarWeekStart (\"sun\"|\"mon\"), calendarShowWeekNumbers, tableMode (\"hierarchy\"|\"flat\"), tableSortKey, tableSortDir (\"asc\"|\"desc\"), timelineZoom (\"week\"|\"month\"|\"quarter\"), timelinePixelsPerDay, timelineCenterDate (ISO date), checklistFilter (\"all\"|\"active\"|\"completed\"), checklistSort (\"manual\"|\"priority\"|\"due\"), mapShowLabels, graphSpacing (\"compact\"|\"default\"|\"spacious\"), graphShowLabels, graphEdgeThickness (\"thin\"|\"normal\"|\"thick\"), showRefEdges, mmSpacing, spatialGridVisible, slidesTheme (\"dark\"|\"light\"), chartType (\"bar\"|\"stacked bar\"|\"line\"|\"donut\"|\"treemap\"), chartMetric (\"value\"|\"type\"|\"tag\"|\"status\"|\"priority\"|\"activity\"|\"completion\"), chartColorScheme (\"default\"|\"warm\"|\"cool\"|\"mono\"), chartLimit (3-30), chartShowLegend, chartShowValues, sheetsDefaultColWidth (40-500), sheetsDefaultRowHeight (20-100), sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols, mediaRepeat (\"off\"|\"all\"|\"one\"), mediaShuffle. Set a key to null to clear it.")
22898
22984
  }, async ({ docId, meta }) => {
22899
22985
  server.setAutoStatus("writing", docId);
22900
22986
  server.setActiveToolCall({
@@ -22902,21 +22988,15 @@ function registerMetaTools(mcp, server) {
22902
22988
  target: docId
22903
22989
  });
22904
22990
  const treeMap = server.getTreeMap();
22905
- if (!treeMap) {
22906
- server.setActiveToolCall(null);
22907
- return { content: [{
22908
- type: "text",
22909
- text: "Not connected"
22910
- }] };
22911
- }
22991
+ if (!treeMap) return { content: [{
22992
+ type: "text",
22993
+ text: "Not connected"
22994
+ }] };
22912
22995
  const entry = treeMap.get(docId);
22913
- if (!entry) {
22914
- server.setActiveToolCall(null);
22915
- return { content: [{
22916
- type: "text",
22917
- text: `Document ${docId} not found`
22918
- }] };
22919
- }
22996
+ if (!entry) return { content: [{
22997
+ type: "text",
22998
+ text: `Document ${docId} not found`
22999
+ }] };
22920
23000
  treeMap.set(docId, {
22921
23001
  ...entry,
22922
23002
  meta: {
@@ -22925,7 +23005,6 @@ function registerMetaTools(mcp, server) {
22925
23005
  },
22926
23006
  updatedAt: Date.now()
22927
23007
  });
22928
- server.setActiveToolCall(null);
22929
23008
  return { content: [{
22930
23009
  type: "text",
22931
23010
  text: `Metadata updated for ${docId}`
@@ -22940,6 +23019,11 @@ function registerMetaTools(mcp, server) {
22940
23019
  */
22941
23020
  function registerFileTools(mcp, server) {
22942
23021
  mcp.tool("list_uploads", "List file attachments for a document.", { docId: zod.z.string().describe("Document ID.") }, async ({ docId }) => {
23022
+ server.setAutoStatus("reading", docId);
23023
+ server.setActiveToolCall({
23024
+ name: "list_uploads",
23025
+ target: docId
23026
+ });
22943
23027
  try {
22944
23028
  const uploads = await server.client.listUploads(docId);
22945
23029
  return { content: [{
@@ -22961,6 +23045,11 @@ function registerFileTools(mcp, server) {
22961
23045
  filePath: zod.z.string().describe("Absolute path to the local file to upload."),
22962
23046
  filename: zod.z.string().optional().describe("Override filename (defaults to basename of filePath).")
22963
23047
  }, async ({ docId, filePath, filename }) => {
23048
+ server.setAutoStatus("uploading", docId);
23049
+ server.setActiveToolCall({
23050
+ name: "upload_file",
23051
+ target: node_path.basename(filePath)
23052
+ });
22964
23053
  try {
22965
23054
  const resolvedPath = node_path.resolve(filePath);
22966
23055
  const data = node_fs.readFileSync(resolvedPath);
@@ -22986,6 +23075,11 @@ function registerFileTools(mcp, server) {
22986
23075
  uploadId: zod.z.string().describe("Upload ID to download."),
22987
23076
  saveTo: zod.z.string().describe("Absolute local file path to save the download.")
22988
23077
  }, async ({ docId, uploadId, saveTo }) => {
23078
+ server.setAutoStatus("reading", docId);
23079
+ server.setActiveToolCall({
23080
+ name: "download_file",
23081
+ target: node_path.basename(saveTo)
23082
+ });
22989
23083
  try {
22990
23084
  const blob = await server.client.getUpload(docId, uploadId);
22991
23085
  const buffer = Buffer.from(await blob.arrayBuffer());
@@ -23009,6 +23103,11 @@ function registerFileTools(mcp, server) {
23009
23103
  docId: zod.z.string().describe("Document ID."),
23010
23104
  uploadId: zod.z.string().describe("Upload ID to delete.")
23011
23105
  }, async ({ docId, uploadId }) => {
23106
+ server.setAutoStatus("writing", docId);
23107
+ server.setActiveToolCall({
23108
+ name: "delete_file",
23109
+ target: uploadId
23110
+ });
23012
23111
  try {
23013
23112
  await server.client.deleteUpload(docId, uploadId);
23014
23113
  return { content: [{
@@ -23050,6 +23149,10 @@ function registerAwarenessTools(mcp, server) {
23050
23149
  docId: zod.z.string().describe("Document ID to set awareness on."),
23051
23150
  fields: zod.z.record(zod.z.string(), zod.z.unknown()).describe("Key-value pairs to set on the child document's awareness state. Use namespaced keys like \"kanban:hovering\", \"table:editing\", \"slides:viewing\", \"outline:editing\", \"calendar:focused\", \"gallery:focused\", \"timeline:focused\", \"graph:focused\", \"map:focused\", \"doc:scroll\". Set a key to null to clear it.")
23052
23151
  }, async ({ docId, fields }) => {
23152
+ server.setActiveToolCall({
23153
+ name: "set_doc_awareness",
23154
+ target: docId
23155
+ });
23053
23156
  try {
23054
23157
  const provider = await server.getChildProvider(docId);
23055
23158
  for (const [key, value] of Object.entries(fields)) provider.awareness.setLocalStateField(key, value ?? null);
@@ -23068,6 +23171,7 @@ function registerAwarenessTools(mcp, server) {
23068
23171
  }
23069
23172
  });
23070
23173
  mcp.tool("poll_inbox", "Check the \"AI Inbox\" document for pending instructions from humans. Returns the inbox content and any pending task sub-documents. Create the inbox as a doc called \"AI Inbox\" under the hub doc if it does not exist yet. Note: channel-based watching via watch_chat is preferred for real-time use.", {}, async () => {
23174
+ server.setActiveToolCall({ name: "poll_inbox" });
23071
23175
  try {
23072
23176
  const treeMap = server.getTreeMap();
23073
23177
  const rootDocId = server.rootDocId;
@@ -23114,6 +23218,10 @@ function registerAwarenessTools(mcp, server) {
23114
23218
  }
23115
23219
  });
23116
23220
  mcp.tool("list_connected_users", "List all connected users and their awareness state. Shows who is online and what they are doing.", { docId: zod.z.string().optional().describe("If provided, list users connected to this specific document. Otherwise lists users from root awareness.") }, async ({ docId }) => {
23221
+ server.setActiveToolCall({
23222
+ name: "list_connected_users",
23223
+ target: docId
23224
+ });
23117
23225
  try {
23118
23226
  let awareness;
23119
23227
  if (docId) awareness = (await server.getChildProvider(docId)).awareness;
@@ -23158,16 +23266,13 @@ function registerChannelTools(mcp, server) {
23158
23266
  });
23159
23267
  const treeMap = server.getTreeMap();
23160
23268
  const rootDoc = server.rootDocument;
23161
- if (!treeMap || !rootDoc) {
23162
- server.setActiveToolCall(null);
23163
- return {
23164
- content: [{
23165
- type: "text",
23166
- text: "Not connected"
23167
- }],
23168
- isError: true
23169
- };
23170
- }
23269
+ if (!treeMap || !rootDoc) return {
23270
+ content: [{
23271
+ type: "text",
23272
+ text: "Not connected"
23273
+ }],
23274
+ isError: true
23275
+ };
23171
23276
  const label = `AI Reply — ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)}: ${text.slice(0, 40).replace(/\n/g, " ")}`;
23172
23277
  const replyId = crypto.randomUUID();
23173
23278
  const now = Date.now();
@@ -23183,7 +23288,6 @@ function registerChannelTools(mcp, server) {
23183
23288
  });
23184
23289
  populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
23185
23290
  if (task_id) server.clearAiTask(task_id);
23186
- server.setActiveToolCall(null);
23187
23291
  return { content: [{
23188
23292
  type: "text",
23189
23293
  text: JSON.stringify({
@@ -23192,7 +23296,6 @@ function registerChannelTools(mcp, server) {
23192
23296
  })
23193
23297
  }] };
23194
23298
  } catch (error) {
23195
- server.setActiveToolCall(null);
23196
23299
  return {
23197
23300
  content: [{
23198
23301
  type: "text",
@@ -23215,14 +23318,15 @@ function registerChannelTools(mcp, server) {
23215
23318
  }],
23216
23319
  isError: true
23217
23320
  };
23321
+ const normalized = text.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\n");
23322
+ server.setAutoStatus(null);
23323
+ server.sendTypingIndicator(channel);
23218
23324
  rootProvider.sendStateless(JSON.stringify({
23219
23325
  type: "chat:send",
23220
23326
  channel,
23221
- content: text,
23327
+ content: normalized,
23222
23328
  sender_name: server.agentName
23223
23329
  }));
23224
- server.setAutoStatus(null);
23225
- server.setActiveToolCall(null);
23226
23330
  return { content: [{
23227
23331
  type: "text",
23228
23332
  text: `Sent to ${channel}`
@@ -23377,16 +23481,13 @@ function registerSvgTools(mcp, server) {
23377
23481
  target: docId
23378
23482
  });
23379
23483
  const cleanSvg = sanitizeSvg(svg);
23380
- if (!cleanSvg) {
23381
- server.setActiveToolCall(null);
23382
- return {
23383
- content: [{
23384
- type: "text",
23385
- text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23386
- }],
23387
- isError: true
23388
- };
23389
- }
23484
+ if (!cleanSvg) return {
23485
+ content: [{
23486
+ type: "text",
23487
+ text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23488
+ }],
23489
+ isError: true
23490
+ };
23390
23491
  const doc = (await server.getChildProvider(docId)).document;
23391
23492
  const fragment = doc.getXmlFragment("default");
23392
23493
  doc.transact(() => {
@@ -23397,13 +23498,11 @@ function registerSvgTools(mcp, server) {
23397
23498
  fragment.insert(insertPos, [el]);
23398
23499
  });
23399
23500
  server.setFocusedDoc(docId);
23400
- server.setActiveToolCall(null);
23401
23501
  return { content: [{
23402
23502
  type: "text",
23403
23503
  text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ""}`
23404
23504
  }] };
23405
23505
  } catch (error) {
23406
- server.setActiveToolCall(null);
23407
23506
  return {
23408
23507
  content: [{
23409
23508
  type: "text",
@@ -24207,6 +24306,9 @@ var HookBridge = class {
24207
24306
  }
24208
24307
  routeEvent(payload) {
24209
24308
  switch (payload.hook_event_name) {
24309
+ case "UserPromptSubmit":
24310
+ this.onUserPromptSubmit();
24311
+ break;
24210
24312
  case "PreToolUse":
24211
24313
  this.onPreToolUse(payload);
24212
24314
  break;
@@ -24217,13 +24319,18 @@ var HookBridge = class {
24217
24319
  this.onSubagentStart(payload);
24218
24320
  break;
24219
24321
  case "SubagentStop":
24220
- this.onSubagentStop(payload);
24322
+ this.onSubagentStop();
24221
24323
  break;
24222
24324
  case "Stop":
24223
24325
  this.onStop();
24224
24326
  break;
24225
24327
  }
24226
24328
  }
24329
+ /** New user turn — reset any lingering status/tool state from the previous turn. */
24330
+ onUserPromptSubmit() {
24331
+ this.server.setAutoStatus(null);
24332
+ this.server.setActiveToolCall(null);
24333
+ }
24227
24334
  onPreToolUse(payload) {
24228
24335
  const toolName = payload.tool_name ?? "";
24229
24336
  if (toolName.startsWith("mcp__abracadabra__")) return;
@@ -24235,7 +24342,6 @@ var HookBridge = class {
24235
24342
  }
24236
24343
  onPostToolUse(payload) {
24237
24344
  if ((payload.tool_name ?? "").startsWith("mcp__abracadabra__")) return;
24238
- this.server.setAutoStatus("thinking");
24239
24345
  }
24240
24346
  onSubagentStart(payload) {
24241
24347
  const agentType = payload.agent_type ?? "agent";
@@ -24245,9 +24351,7 @@ var HookBridge = class {
24245
24351
  });
24246
24352
  this.server.setAutoStatus("thinking");
24247
24353
  }
24248
- onSubagentStop(_payload) {
24249
- this.server.setAutoStatus("thinking");
24250
- }
24354
+ onSubagentStop() {}
24251
24355
  onStop() {
24252
24356
  this.server.setAutoStatus(null);
24253
24357
  this.server.setActiveToolCall(null);
@@ -24260,11 +24364,19 @@ var HookBridge = class {
24260
24364
  * Abracadabra MCP Server — entry point.
24261
24365
  *
24262
24366
  * Environment variables:
24263
- * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24367
+ * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24264
24368
  * ABRA_AGENT_NAME — Display name (default: "AI Assistant")
24265
24369
  * ABRA_AGENT_COLOR — HSL color for presence (default: "hsl(270, 80%, 60%)")
24266
24370
  * ABRA_INVITE_CODE — Invite code for first-run registration (grants role)
24267
24371
  * ABRA_KEY_FILE — Path to Ed25519 key file (default: ~/.abracadabra/agent.key)
24372
+ * ABRA_AGENT_TRIGGER_MODE — When to respond in group chats:
24373
+ * all → every message (legacy)
24374
+ * mention → only when @<alias> is used
24375
+ * task → only ai:task awareness events
24376
+ * mention+task → mention OR ai:task (default)
24377
+ * DMs always trigger regardless of mode.
24378
+ * ABRA_AGENT_MENTION_ALIASES — Comma-separated aliases for @mentions
24379
+ * (default: [ABRA_AGENT_NAME])
24268
24380
  */
24269
24381
  async function main() {
24270
24382
  const url = process.env.ABRA_URL;
@@ -24272,13 +24384,27 @@ async function main() {
24272
24384
  console.error("Missing required environment variable: ABRA_URL");
24273
24385
  process.exit(1);
24274
24386
  }
24387
+ const rawMode = (process.env.ABRA_AGENT_TRIGGER_MODE ?? "mention+task").trim().toLowerCase();
24388
+ const validModes = [
24389
+ "all",
24390
+ "mention",
24391
+ "task",
24392
+ "mention+task"
24393
+ ];
24394
+ const triggerMode = validModes.includes(rawMode) ? rawMode : "mention+task";
24395
+ if (rawMode && !validModes.includes(rawMode)) console.error(`[abracadabra-mcp] Invalid ABRA_AGENT_TRIGGER_MODE="${rawMode}", falling back to "mention+task"`);
24396
+ const aliasEnv = process.env.ABRA_AGENT_MENTION_ALIASES;
24397
+ const mentionAliases = aliasEnv ? aliasEnv.split(",").map((a) => a.trim()).filter((a) => a.length > 0) : void 0;
24275
24398
  const server = new AbracadabraMCPServer({
24276
24399
  url,
24277
24400
  agentName: process.env.ABRA_AGENT_NAME,
24278
24401
  agentColor: process.env.ABRA_AGENT_COLOR,
24279
24402
  inviteCode: process.env.ABRA_INVITE_CODE,
24280
- keyFile: process.env.ABRA_KEY_FILE
24403
+ keyFile: process.env.ABRA_KEY_FILE,
24404
+ triggerMode,
24405
+ mentionAliases
24281
24406
  });
24407
+ console.error(`[abracadabra-mcp] Trigger mode: ${triggerMode}; aliases: ${server.mentionAliases.join(", ")}`);
24282
24408
  const mcp = new McpServer({
24283
24409
  name: "abracadabra",
24284
24410
  version: "1.0.0"