@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.
@@ -3757,7 +3757,7 @@ const propertyKeyTypes = new Set([
3757
3757
  "number",
3758
3758
  "symbol"
3759
3759
  ]);
3760
- function escapeRegex(str) {
3760
+ function escapeRegex$1(str) {
3761
3761
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3762
3762
  }
3763
3763
  function clone(inst, def, params) {
@@ -4468,7 +4468,7 @@ const $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (i
4468
4468
  });
4469
4469
  const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
4470
4470
  $ZodCheck.init(inst, def);
4471
- const escapedRegex = escapeRegex(def.includes);
4471
+ const escapedRegex = escapeRegex$1(def.includes);
4472
4472
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
4473
4473
  def.pattern = pattern;
4474
4474
  inst._zod.onattach.push((inst) => {
@@ -4491,7 +4491,7 @@ const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (ins
4491
4491
  });
4492
4492
  const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
4493
4493
  $ZodCheck.init(inst, def);
4494
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
4494
+ const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
4495
4495
  def.pattern ?? (def.pattern = pattern);
4496
4496
  inst._zod.onattach.push((inst) => {
4497
4497
  const bag = inst._zod.bag;
@@ -4513,7 +4513,7 @@ const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith",
4513
4513
  });
4514
4514
  const $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
4515
4515
  $ZodCheck.init(inst, def);
4516
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
4516
+ const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
4517
4517
  def.pattern ?? (def.pattern = pattern);
4518
4518
  inst._zod.onattach.push((inst) => {
4519
4519
  const bag = inst._zod.bag;
@@ -5545,7 +5545,7 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
5545
5545
  const values = getEnumValues(def.entries);
5546
5546
  const valuesSet = new Set(values);
5547
5547
  inst._zod.values = valuesSet;
5548
- inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
5548
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
5549
5549
  inst._zod.parse = (payload, _ctx) => {
5550
5550
  const input = payload.value;
5551
5551
  if (valuesSet.has(input)) return payload;
@@ -5563,7 +5563,7 @@ const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
5563
5563
  if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
5564
5564
  const values = new Set(def.values);
5565
5565
  inst._zod.values = values;
5566
- inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
5566
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
5567
5567
  inst._zod.parse = (payload, _ctx) => {
5568
5568
  const input = payload.value;
5569
5569
  if (values.has(input)) return payload;
@@ -19965,6 +19965,47 @@ function signChallenge(challengeB64, privateKey) {
19965
19965
  return toBase64url(sign(challenge, privateKey));
19966
19966
  }
19967
19967
 
19968
+ //#endregion
19969
+ //#region packages/mcp/src/mentions.ts
19970
+ /**
19971
+ * Mention parsing for chat messages.
19972
+ *
19973
+ * Recognizes `@alias` tokens (case-insensitive, word-boundary) so the agent
19974
+ * can decide whether a group-chat message is directed at it.
19975
+ */
19976
+ /** Escape regex metacharacters in an alias string. */
19977
+ function escapeRegex(s) {
19978
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19979
+ }
19980
+ /**
19981
+ * Build a regex that matches `@<alias>` for any of the given aliases.
19982
+ * Requires a non-word char (or start) before `@` and a word boundary after the alias
19983
+ * so `@Claude` matches but `email@claudesomething` does not.
19984
+ */
19985
+ function buildMentionRegex(aliases) {
19986
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
19987
+ if (cleaned.length === 0) return null;
19988
+ const alt = cleaned.map(escapeRegex).join("|");
19989
+ return new RegExp(`(?:^|[^\\w@])@(?:${alt})\\b`, "i");
19990
+ }
19991
+ /** Returns true if `text` contains `@alias` for any alias (case-insensitive). */
19992
+ function containsMention(text, aliases) {
19993
+ const re = buildMentionRegex(aliases);
19994
+ if (!re) return false;
19995
+ return re.test(text);
19996
+ }
19997
+ /**
19998
+ * Remove `@alias` tokens from the text. Leaves surrounding whitespace tidy so
19999
+ * the cleaned prompt reads naturally (e.g. `"@Claude help"` → `"help"`).
20000
+ */
20001
+ function stripMention(text, aliases) {
20002
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
20003
+ if (cleaned.length === 0) return text;
20004
+ const alt = cleaned.map(escapeRegex).join("|");
20005
+ const re = new RegExp(`(^|\\s)@(?:${alt})\\b[,:]?\\s*`, "gi");
20006
+ return text.replace(re, (_m, lead) => lead ? " " : "").replace(/\s{2,}/g, " ").trim();
20007
+ }
20008
+
19968
20009
  //#endregion
19969
20010
  //#region packages/mcp/src/server.ts
19970
20011
  /**
@@ -19975,7 +20016,10 @@ function signChallenge(challengeB64, privateKey) {
19975
20016
  * Use switchSpace(docId) to change the active space.
19976
20017
  */
19977
20018
  const IDLE_TIMEOUT_MS = 300 * 1e3;
19978
- var AbracadabraMCPServer = class {
20019
+ var AbracadabraMCPServer = class AbracadabraMCPServer {
20020
+ static {
20021
+ this.TOOL_HISTORY_MAX = 20;
20022
+ }
19979
20023
  constructor(config) {
19980
20024
  this._serverInfo = null;
19981
20025
  this._rootDocId = null;
@@ -19992,6 +20036,7 @@ var AbracadabraMCPServer = class {
19992
20036
  this._typingInterval = null;
19993
20037
  this._lastChatChannel = null;
19994
20038
  this._signFn = null;
20039
+ this._toolHistory = [];
19995
20040
  this.config = config;
19996
20041
  this.client = new AbracadabraClient({
19997
20042
  url: config.url,
@@ -20004,6 +20049,14 @@ var AbracadabraMCPServer = class {
20004
20049
  get agentColor() {
20005
20050
  return this.config.agentColor || "hsl(270, 80%, 60%)";
20006
20051
  }
20052
+ get triggerMode() {
20053
+ return this.config.triggerMode ?? "mention+task";
20054
+ }
20055
+ get mentionAliases() {
20056
+ const explicit = this.config.mentionAliases?.filter((a) => a.trim().length > 0);
20057
+ if (explicit && explicit.length > 0) return explicit;
20058
+ return [this.agentName];
20059
+ }
20007
20060
  get serverInfo() {
20008
20061
  return this._serverInfo;
20009
20062
  }
@@ -20044,7 +20097,7 @@ var AbracadabraMCPServer = class {
20044
20097
  await this.client.loginWithKey(keypair.publicKeyB64, signFn);
20045
20098
  } else throw err;
20046
20099
  }
20047
- console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (${keypair.publicKeyB64.slice(0, 12)}...)`);
20100
+ console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`);
20048
20101
  this._serverInfo = await this.client.serverInfo();
20049
20102
  let initialDocId = this._serverInfo.index_doc_id ?? null;
20050
20103
  try {
@@ -20111,6 +20164,8 @@ var AbracadabraMCPServer = class {
20111
20164
  provider.awareness.setLocalStateField("status", null);
20112
20165
  provider.awareness.setLocalStateField("activeToolCall", null);
20113
20166
  provider.awareness.setLocalStateField("statusContext", null);
20167
+ provider.awareness.setLocalStateField("turnId", null);
20168
+ provider.awareness.setLocalStateField("toolHistory", []);
20114
20169
  const conn = {
20115
20170
  doc,
20116
20171
  provider,
@@ -20234,6 +20289,7 @@ var AbracadabraMCPServer = class {
20234
20289
  _observeRootAwareness(provider) {
20235
20290
  const selfId = provider.awareness.clientID;
20236
20291
  provider.awareness.on("change", () => {
20292
+ if (this.triggerMode === "mention") return;
20237
20293
  const states = provider.awareness.getStates();
20238
20294
  for (const [clientId, state] of states) {
20239
20295
  if (clientId === selfId) continue;
@@ -20246,6 +20302,7 @@ var AbracadabraMCPServer = class {
20246
20302
  const user = state["user"];
20247
20303
  const senderName = user && typeof user === "object" && typeof user.name === "string" ? user.name : "Unknown";
20248
20304
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`);
20305
+ this._beginTurn();
20249
20306
  this.setAutoStatus("thinking");
20250
20307
  this._dispatchAiTask({
20251
20308
  id,
@@ -20308,9 +20365,28 @@ var AbracadabraMCPServer = class {
20308
20365
  if (data.sender_id && data.sender_id === this._userId) return;
20309
20366
  const channel = data.channel;
20310
20367
  const docId = channel?.startsWith("group:") ? channel.slice(6) : "";
20311
- if (channel?.startsWith("dm:")) {
20368
+ const isDM = channel?.startsWith("dm:") ?? false;
20369
+ const isGroup = channel?.startsWith("group:") ?? false;
20370
+ if (isDM) {
20312
20371
  const parts = channel.split(":");
20313
- if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) return;
20372
+ if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) {
20373
+ 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.`);
20374
+ return;
20375
+ }
20376
+ }
20377
+ const mode = this.triggerMode;
20378
+ const content = typeof data.content === "string" ? data.content : "";
20379
+ let dispatchContent = content;
20380
+ if (isGroup) {
20381
+ if (mode === "task") return;
20382
+ if (mode === "mention" || mode === "mention+task") {
20383
+ const aliases = this.mentionAliases;
20384
+ if (!containsMention(content, aliases)) {
20385
+ console.error(`[abracadabra-mcp] skipped message on ${channel} — no @mention for ${aliases.join("|")}`);
20386
+ return;
20387
+ }
20388
+ dispatchContent = stripMention(content, aliases) || content;
20389
+ }
20314
20390
  }
20315
20391
  if (channel) {
20316
20392
  const rootProvider = this._activeConnection?.provider;
@@ -20320,14 +20396,13 @@ var AbracadabraMCPServer = class {
20320
20396
  timestamp: Math.floor(Date.now() / 1e3)
20321
20397
  }));
20322
20398
  this._lastChatChannel = channel;
20323
- this.sendTypingIndicator(channel);
20324
- this._startTypingInterval(channel);
20325
20399
  }
20400
+ this._beginTurn();
20326
20401
  this.setAutoStatus("thinking");
20327
20402
  await this._serverRef.notification({
20328
20403
  method: "notifications/claude/channel",
20329
20404
  params: {
20330
- content: data.content ?? "",
20405
+ content: dispatchContent,
20331
20406
  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.`,
20332
20407
  meta: {
20333
20408
  source: "abracadabra",
@@ -20358,13 +20433,35 @@ var AbracadabraMCPServer = class {
20358
20433
  if (docId !== void 0) provider.awareness.setLocalStateField("docId", docId);
20359
20434
  const context = status ? statusContext !== void 0 ? statusContext : this._lastChatChannel : null;
20360
20435
  provider.awareness.setLocalStateField("statusContext", context ?? null);
20361
- if (!status) this._stopTypingInterval();
20436
+ if (!status) {
20437
+ this._stopTypingInterval();
20438
+ provider.awareness.setLocalStateField("activeToolCall", null);
20439
+ provider.awareness.setLocalStateField("turnId", null);
20440
+ this._toolHistory = [];
20441
+ provider.awareness.setLocalStateField("toolHistory", []);
20442
+ }
20362
20443
  if (status) this._statusClearTimer = setTimeout(() => {
20363
20444
  provider.awareness.setLocalStateField("status", null);
20364
20445
  provider.awareness.setLocalStateField("activeToolCall", null);
20365
20446
  provider.awareness.setLocalStateField("statusContext", null);
20447
+ provider.awareness.setLocalStateField("turnId", null);
20448
+ this._toolHistory = [];
20449
+ provider.awareness.setLocalStateField("toolHistory", []);
20366
20450
  this._stopTypingInterval();
20367
- }, 3e4);
20451
+ }, 1e4);
20452
+ }
20453
+ /**
20454
+ * Start a new agent turn. Mints a fresh UUID and writes it to awareness so
20455
+ * the dashboard can gate the incantation on "there is an active turn",
20456
+ * decoupled from the (racier) status field. Called from chat arrival and
20457
+ * ai:task dispatch right before `setAutoStatus('thinking')`.
20458
+ */
20459
+ _beginTurn() {
20460
+ const provider = this._activeConnection?.provider;
20461
+ if (!provider) return;
20462
+ this._toolHistory = [];
20463
+ provider.awareness.setLocalStateField("toolHistory", []);
20464
+ provider.awareness.setLocalStateField("turnId", crypto.randomUUID());
20368
20465
  }
20369
20466
  /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
20370
20467
  _startTypingInterval(channel) {
@@ -20382,10 +20479,28 @@ var AbracadabraMCPServer = class {
20382
20479
  }
20383
20480
  /**
20384
20481
  * Broadcast which tool the agent is currently executing.
20385
- * Dashboard renders this as a ChatTool indicator.
20482
+ *
20483
+ * Renders as a ChatTool pill on the dashboard. On non-null calls, the tool
20484
+ * is also appended to `toolHistory` (capped at TOOL_HISTORY_MAX) and written
20485
+ * to awareness so the dashboard's inline trace can show the turn's recent
20486
+ * activity. Tools do NOT clear (`setActiveToolCall(null)`) on completion —
20487
+ * the pill stays until the next tool replaces it or `setAutoStatus(null)`
20488
+ * flushes the turn. This keeps pills visible long enough to see.
20386
20489
  */
20387
20490
  setActiveToolCall(toolCall) {
20388
- this._activeConnection?.provider?.awareness.setLocalStateField("activeToolCall", toolCall);
20491
+ const provider = this._activeConnection?.provider;
20492
+ if (!provider) return;
20493
+ provider.awareness.setLocalStateField("activeToolCall", toolCall);
20494
+ if (toolCall) {
20495
+ this._toolHistory.push({
20496
+ tool: toolCall.name,
20497
+ target: toolCall.target,
20498
+ ts: Date.now(),
20499
+ channel: this._lastChatChannel
20500
+ });
20501
+ if (this._toolHistory.length > AbracadabraMCPServer.TOOL_HISTORY_MAX) this._toolHistory.splice(0, this._toolHistory.length - AbracadabraMCPServer.TOOL_HISTORY_MAX);
20502
+ provider.awareness.setLocalStateField("toolHistory", [...this._toolHistory]);
20503
+ }
20389
20504
  }
20390
20505
  /**
20391
20506
  * Send a typing indicator to a chat channel.
@@ -20416,8 +20531,11 @@ var AbracadabraMCPServer = class {
20416
20531
  conn.provider.awareness.setLocalStateField("status", null);
20417
20532
  conn.provider.awareness.setLocalStateField("activeToolCall", null);
20418
20533
  conn.provider.awareness.setLocalStateField("statusContext", null);
20534
+ conn.provider.awareness.setLocalStateField("turnId", null);
20535
+ conn.provider.awareness.setLocalStateField("toolHistory", []);
20419
20536
  conn.provider.destroy();
20420
20537
  }
20538
+ this._toolHistory = [];
20421
20539
  this._spaceConnections.clear();
20422
20540
  this._activeConnection = null;
20423
20541
  console.error("[abracadabra-mcp] Shutdown complete");
@@ -21233,16 +21351,12 @@ function registerTreeTools(mcp, server) {
21233
21351
  server.setAutoStatus("reading");
21234
21352
  server.setActiveToolCall({ name: "list_documents" });
21235
21353
  const treeMap = server.getTreeMap();
21236
- if (!treeMap) {
21237
- server.setActiveToolCall(null);
21238
- return { content: [{
21239
- type: "text",
21240
- text: "Not connected"
21241
- }] };
21242
- }
21354
+ if (!treeMap) return { content: [{
21355
+ type: "text",
21356
+ text: "Not connected"
21357
+ }] };
21243
21358
  const targetId = normalizeRootId(parentId, server);
21244
21359
  const children = childrenOf$1(readEntries$1(treeMap), targetId);
21245
- server.setActiveToolCall(null);
21246
21360
  return { content: [{
21247
21361
  type: "text",
21248
21362
  text: JSON.stringify(children, null, 2)
@@ -21255,17 +21369,13 @@ function registerTreeTools(mcp, server) {
21255
21369
  server.setAutoStatus("reading");
21256
21370
  server.setActiveToolCall({ name: "get_document_tree" });
21257
21371
  const treeMap = server.getTreeMap();
21258
- if (!treeMap) {
21259
- server.setActiveToolCall(null);
21260
- return { content: [{
21261
- type: "text",
21262
- text: "Not connected"
21263
- }] };
21264
- }
21372
+ if (!treeMap) return { content: [{
21373
+ type: "text",
21374
+ text: "Not connected"
21375
+ }] };
21265
21376
  const targetId = normalizeRootId(rootId, server);
21266
21377
  const maxDepth = depth ?? 3;
21267
21378
  const tree = buildTree$1(readEntries$1(treeMap), targetId, maxDepth);
21268
- server.setActiveToolCall(null);
21269
21379
  return { content: [{
21270
21380
  type: "text",
21271
21381
  text: JSON.stringify(tree, null, 2)
@@ -21281,13 +21391,10 @@ function registerTreeTools(mcp, server) {
21281
21391
  target: query
21282
21392
  });
21283
21393
  const treeMap = server.getTreeMap();
21284
- if (!treeMap) {
21285
- server.setActiveToolCall(null);
21286
- return { content: [{
21287
- type: "text",
21288
- text: "Not connected"
21289
- }] };
21290
- }
21394
+ if (!treeMap) return { content: [{
21395
+ type: "text",
21396
+ text: "Not connected"
21397
+ }] };
21291
21398
  const entries = readEntries$1(treeMap);
21292
21399
  const lowerQuery = query.toLowerCase();
21293
21400
  const normalizedRoot = normalizeRootId(rootId, server);
@@ -21312,7 +21419,6 @@ function registerTreeTools(mcp, server) {
21312
21419
  path
21313
21420
  };
21314
21421
  });
21315
- server.setActiveToolCall(null);
21316
21422
  if (results.length === 0) return { content: [{
21317
21423
  type: "text",
21318
21424
  text: `No documents found matching "${query}". Try get_document_tree to see the full hierarchy.`
@@ -21326,7 +21432,7 @@ function registerTreeTools(mcp, server) {
21326
21432
  parentId: z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
21327
21433
  label: z.string().describe("Display name / title for the document."),
21328
21434
  type: 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."),
21329
- meta: z.record(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.")
21435
+ meta: z.record(z.string(), 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.")
21330
21436
  }, async ({ parentId, label, type, meta }) => {
21331
21437
  server.setAutoStatus("creating");
21332
21438
  server.setActiveToolCall({
@@ -21335,13 +21441,10 @@ function registerTreeTools(mcp, server) {
21335
21441
  });
21336
21442
  const treeMap = server.getTreeMap();
21337
21443
  const rootDoc = server.rootDocument;
21338
- if (!treeMap || !rootDoc) {
21339
- server.setActiveToolCall(null);
21340
- return { content: [{
21341
- type: "text",
21342
- text: "Not connected"
21343
- }] };
21344
- }
21444
+ if (!treeMap || !rootDoc) return { content: [{
21445
+ type: "text",
21446
+ text: "Not connected"
21447
+ }] };
21345
21448
  const id = crypto.randomUUID();
21346
21449
  const normalizedParent = normalizeRootId(parentId, server);
21347
21450
  const now = Date.now();
@@ -21357,7 +21460,6 @@ function registerTreeTools(mcp, server) {
21357
21460
  });
21358
21461
  });
21359
21462
  server.setFocusedDoc(id);
21360
- server.setActiveToolCall(null);
21361
21463
  return { content: [{
21362
21464
  type: "text",
21363
21465
  text: JSON.stringify({
@@ -21378,28 +21480,21 @@ function registerTreeTools(mcp, server) {
21378
21480
  target: id
21379
21481
  });
21380
21482
  const treeMap = server.getTreeMap();
21381
- if (!treeMap) {
21382
- server.setActiveToolCall(null);
21383
- return { content: [{
21384
- type: "text",
21385
- text: "Not connected"
21386
- }] };
21387
- }
21483
+ if (!treeMap) return { content: [{
21484
+ type: "text",
21485
+ text: "Not connected"
21486
+ }] };
21388
21487
  const raw = treeMap.get(id);
21389
- if (!raw) {
21390
- server.setActiveToolCall(null);
21391
- return { content: [{
21392
- type: "text",
21393
- text: `Document ${id} not found`
21394
- }] };
21395
- }
21488
+ if (!raw) return { content: [{
21489
+ type: "text",
21490
+ text: `Document ${id} not found`
21491
+ }] };
21396
21492
  const entry = toPlain(raw);
21397
21493
  treeMap.set(id, {
21398
21494
  ...entry,
21399
21495
  label,
21400
21496
  updatedAt: Date.now()
21401
21497
  });
21402
- server.setActiveToolCall(null);
21403
21498
  return { content: [{
21404
21499
  type: "text",
21405
21500
  text: `Renamed to "${label}"`
@@ -21416,21 +21511,15 @@ function registerTreeTools(mcp, server) {
21416
21511
  target: id
21417
21512
  });
21418
21513
  const treeMap = server.getTreeMap();
21419
- if (!treeMap) {
21420
- server.setActiveToolCall(null);
21421
- return { content: [{
21422
- type: "text",
21423
- text: "Not connected"
21424
- }] };
21425
- }
21514
+ if (!treeMap) return { content: [{
21515
+ type: "text",
21516
+ text: "Not connected"
21517
+ }] };
21426
21518
  const raw = treeMap.get(id);
21427
- if (!raw) {
21428
- server.setActiveToolCall(null);
21429
- return { content: [{
21430
- type: "text",
21431
- text: `Document ${id} not found`
21432
- }] };
21433
- }
21519
+ if (!raw) return { content: [{
21520
+ type: "text",
21521
+ text: `Document ${id} not found`
21522
+ }] };
21434
21523
  const entry = toPlain(raw);
21435
21524
  treeMap.set(id, {
21436
21525
  ...entry,
@@ -21438,7 +21527,6 @@ function registerTreeTools(mcp, server) {
21438
21527
  order: order ?? Date.now(),
21439
21528
  updatedAt: Date.now()
21440
21529
  });
21441
- server.setActiveToolCall(null);
21442
21530
  return { content: [{
21443
21531
  type: "text",
21444
21532
  text: `Moved ${id} to parent ${newParentId}`
@@ -21453,13 +21541,10 @@ function registerTreeTools(mcp, server) {
21453
21541
  const treeMap = server.getTreeMap();
21454
21542
  const trashMap = server.getTrashMap();
21455
21543
  const rootDoc = server.rootDocument;
21456
- if (!treeMap || !trashMap || !rootDoc) {
21457
- server.setActiveToolCall(null);
21458
- return { content: [{
21459
- type: "text",
21460
- text: "Not connected"
21461
- }] };
21462
- }
21544
+ if (!treeMap || !trashMap || !rootDoc) return { content: [{
21545
+ type: "text",
21546
+ text: "Not connected"
21547
+ }] };
21463
21548
  const toDelete = [id, ...descendantsOf(readEntries$1(treeMap), id).map((e) => e.id)];
21464
21549
  const now = Date.now();
21465
21550
  rootDoc.transact(() => {
@@ -21478,7 +21563,6 @@ function registerTreeTools(mcp, server) {
21478
21563
  treeMap.delete(nid);
21479
21564
  }
21480
21565
  });
21481
- server.setActiveToolCall(null);
21482
21566
  return { content: [{
21483
21567
  type: "text",
21484
21568
  text: `Deleted ${toDelete.length} document(s)`
@@ -21494,28 +21578,21 @@ function registerTreeTools(mcp, server) {
21494
21578
  target: id
21495
21579
  });
21496
21580
  const treeMap = server.getTreeMap();
21497
- if (!treeMap) {
21498
- server.setActiveToolCall(null);
21499
- return { content: [{
21500
- type: "text",
21501
- text: "Not connected"
21502
- }] };
21503
- }
21581
+ if (!treeMap) return { content: [{
21582
+ type: "text",
21583
+ text: "Not connected"
21584
+ }] };
21504
21585
  const raw = treeMap.get(id);
21505
- if (!raw) {
21506
- server.setActiveToolCall(null);
21507
- return { content: [{
21508
- type: "text",
21509
- text: `Document ${id} not found`
21510
- }] };
21511
- }
21586
+ if (!raw) return { content: [{
21587
+ type: "text",
21588
+ text: `Document ${id} not found`
21589
+ }] };
21512
21590
  const entry = toPlain(raw);
21513
21591
  treeMap.set(id, {
21514
21592
  ...entry,
21515
21593
  type,
21516
21594
  updatedAt: Date.now()
21517
21595
  });
21518
- server.setActiveToolCall(null);
21519
21596
  return { content: [{
21520
21597
  type: "text",
21521
21598
  text: `Changed type to "${type}"`
@@ -21743,14 +21820,12 @@ function parseInline(text) {
21743
21820
  text: kbdProps["value"] || "",
21744
21821
  attrs: { kbd: { value: kbdProps["value"] || "" } }
21745
21822
  });
21746
- } else if (match[5] !== void 0) {
21747
- const docId = match[5];
21748
- const displayText = match[6] ?? docId;
21749
- tokens.push({
21750
- text: displayText,
21751
- attrs: { link: { href: `/doc/${docId}` } }
21752
- });
21753
- } else if (match[7] !== void 0) tokens.push({
21823
+ } else if (match[5] !== void 0) tokens.push({
21824
+ text: "",
21825
+ node: "docLink",
21826
+ nodeAttrs: { docId: match[5] }
21827
+ });
21828
+ else if (match[7] !== void 0) tokens.push({
21754
21829
  text: match[7],
21755
21830
  attrs: { strike: true }
21756
21831
  });
@@ -21777,7 +21852,7 @@ function parseInline(text) {
21777
21852
  lastIndex = match.index + match[0].length;
21778
21853
  }
21779
21854
  if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
21780
- return tokens.filter((t) => t.text.length > 0);
21855
+ return tokens.filter((t) => t.node || t.text.length > 0);
21781
21856
  }
21782
21857
  function parseTableRow(line) {
21783
21858
  const parts = line.split("|");
@@ -21905,11 +21980,14 @@ function parseBlocks(markdown) {
21905
21980
  i++;
21906
21981
  continue;
21907
21982
  }
21908
- const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/);
21983
+ const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\](\{[^}]*\})?\s*$/);
21909
21984
  if (docEmbedMatch) {
21985
+ const props = parseMdcProps(docEmbedMatch[2]);
21986
+ const seamless = "seamless" in props || props["seamless"] === "true" || /\{[^}]*\bseamless\b[^}]*\}/.test(docEmbedMatch[2] ?? "");
21910
21987
  blocks.push({
21911
21988
  type: "docEmbed",
21912
- docId: docEmbedMatch[1]
21989
+ docId: docEmbedMatch[1],
21990
+ seamless: seamless || void 0
21913
21991
  });
21914
21992
  i++;
21915
21993
  continue;
@@ -22187,13 +22265,22 @@ function parseBlocks(markdown) {
22187
22265
  return blocks;
22188
22266
  }
22189
22267
  function fillTextInto(el, tokens) {
22190
- const filtered = tokens.filter((t) => t.text.length > 0);
22268
+ const filtered = tokens.filter((t) => t.node || t.text.length > 0);
22191
22269
  if (!filtered.length) return;
22192
- const xtNodes = filtered.map(() => new Y.XmlText());
22193
- el.insert(0, xtNodes);
22270
+ const children = filtered.map((tok) => {
22271
+ if (tok.node) {
22272
+ const xe = new Y.XmlElement(tok.node);
22273
+ if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
22274
+ return xe;
22275
+ }
22276
+ return new Y.XmlText();
22277
+ });
22278
+ el.insert(0, children);
22194
22279
  filtered.forEach((tok, i) => {
22195
- if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
22196
- else xtNodes[i].insert(0, tok.text);
22280
+ if (tok.node) return;
22281
+ const xt = children[i];
22282
+ if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
22283
+ else xt.insert(0, tok.text);
22197
22284
  });
22198
22285
  }
22199
22286
  function blockElName(b) {
@@ -22440,6 +22527,7 @@ function fillBlock(el, block) {
22440
22527
  break;
22441
22528
  case "docEmbed":
22442
22529
  el.setAttribute("docId", block.docId);
22530
+ if (block.seamless) el.setAttribute("seamless", "true");
22443
22531
  break;
22444
22532
  case "svgEmbed":
22445
22533
  el.setAttribute("svg", block.svg);
@@ -22539,7 +22627,14 @@ function elementTextContent(el) {
22539
22627
  for (let i = 0; i < el.length; i++) {
22540
22628
  const child = el.get(i);
22541
22629
  if (child instanceof Y.XmlText) parts.push(xmlTextToMarkdown(child));
22542
- else if (child instanceof Y.XmlElement) parts.push(elementTextContent(child));
22630
+ else if (child instanceof Y.XmlElement) {
22631
+ if (child.nodeName === "docLink") {
22632
+ const docId = child.getAttribute("docId");
22633
+ if (docId) parts.push(`[[${docId}]]`);
22634
+ continue;
22635
+ }
22636
+ parts.push(elementTextContent(child));
22637
+ }
22543
22638
  }
22544
22639
  return parts.join("");
22545
22640
  }
@@ -22568,7 +22663,9 @@ function serializeElement(el, indent = "") {
22568
22663
  case "table": return serializeTable(el);
22569
22664
  case "docEmbed": {
22570
22665
  const docId = el.getAttribute("docId");
22571
- return docId ? `![[${docId}]]` : "";
22666
+ if (!docId) return "";
22667
+ const seamlessAttr = el.getAttribute("seamless");
22668
+ return seamlessAttr === true || seamlessAttr === "true" ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
22572
22669
  }
22573
22670
  case "svgEmbed": {
22574
22671
  const svg = el.getAttribute("svg") || "";
@@ -22768,7 +22865,6 @@ function registerContentTools(mcp, server) {
22768
22865
  });
22769
22866
  children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
22770
22867
  }
22771
- server.setActiveToolCall(null);
22772
22868
  const result = {
22773
22869
  label,
22774
22870
  type,
@@ -22781,7 +22877,6 @@ function registerContentTools(mcp, server) {
22781
22877
  text: JSON.stringify(result, null, 2)
22782
22878
  }] };
22783
22879
  } catch (error) {
22784
- server.setActiveToolCall(null);
22785
22880
  return {
22786
22881
  content: [{
22787
22882
  type: "text",
@@ -22831,13 +22926,11 @@ function registerContentTools(mcp, server) {
22831
22926
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
22832
22927
  server.setFocusedDoc(docId);
22833
22928
  server.setDocCursor(docId, fragment.length);
22834
- server.setActiveToolCall(null);
22835
22929
  return { content: [{
22836
22930
  type: "text",
22837
22931
  text: `Document ${docId} updated (${writeMode} mode)`
22838
22932
  }] };
22839
22933
  } catch (error) {
22840
- server.setActiveToolCall(null);
22841
22934
  return {
22842
22935
  content: [{
22843
22936
  type: "text",
@@ -22859,22 +22952,15 @@ function registerMetaTools(mcp, server) {
22859
22952
  target: docId
22860
22953
  });
22861
22954
  const treeMap = server.getTreeMap();
22862
- if (!treeMap) {
22863
- server.setActiveToolCall(null);
22864
- return { content: [{
22865
- type: "text",
22866
- text: "Not connected"
22867
- }] };
22868
- }
22955
+ if (!treeMap) return { content: [{
22956
+ type: "text",
22957
+ text: "Not connected"
22958
+ }] };
22869
22959
  const entry = treeMap.get(docId);
22870
- if (!entry) {
22871
- server.setActiveToolCall(null);
22872
- return { content: [{
22873
- type: "text",
22874
- text: `Document ${docId} not found`
22875
- }] };
22876
- }
22877
- server.setActiveToolCall(null);
22960
+ if (!entry) return { content: [{
22961
+ type: "text",
22962
+ text: `Document ${docId} not found`
22963
+ }] };
22878
22964
  return { content: [{
22879
22965
  type: "text",
22880
22966
  text: JSON.stringify({
@@ -22887,7 +22973,7 @@ function registerMetaTools(mcp, server) {
22887
22973
  });
22888
22974
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
22889
22975
  docId: z.string().describe("Document ID."),
22890
- meta: z.record(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.")
22976
+ meta: z.record(z.string(), 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.")
22891
22977
  }, async ({ docId, meta }) => {
22892
22978
  server.setAutoStatus("writing", docId);
22893
22979
  server.setActiveToolCall({
@@ -22895,21 +22981,15 @@ function registerMetaTools(mcp, server) {
22895
22981
  target: docId
22896
22982
  });
22897
22983
  const treeMap = server.getTreeMap();
22898
- if (!treeMap) {
22899
- server.setActiveToolCall(null);
22900
- return { content: [{
22901
- type: "text",
22902
- text: "Not connected"
22903
- }] };
22904
- }
22984
+ if (!treeMap) return { content: [{
22985
+ type: "text",
22986
+ text: "Not connected"
22987
+ }] };
22905
22988
  const entry = treeMap.get(docId);
22906
- if (!entry) {
22907
- server.setActiveToolCall(null);
22908
- return { content: [{
22909
- type: "text",
22910
- text: `Document ${docId} not found`
22911
- }] };
22912
- }
22989
+ if (!entry) return { content: [{
22990
+ type: "text",
22991
+ text: `Document ${docId} not found`
22992
+ }] };
22913
22993
  treeMap.set(docId, {
22914
22994
  ...entry,
22915
22995
  meta: {
@@ -22918,7 +22998,6 @@ function registerMetaTools(mcp, server) {
22918
22998
  },
22919
22999
  updatedAt: Date.now()
22920
23000
  });
22921
- server.setActiveToolCall(null);
22922
23001
  return { content: [{
22923
23002
  type: "text",
22924
23003
  text: `Metadata updated for ${docId}`
@@ -22933,6 +23012,11 @@ function registerMetaTools(mcp, server) {
22933
23012
  */
22934
23013
  function registerFileTools(mcp, server) {
22935
23014
  mcp.tool("list_uploads", "List file attachments for a document.", { docId: z.string().describe("Document ID.") }, async ({ docId }) => {
23015
+ server.setAutoStatus("reading", docId);
23016
+ server.setActiveToolCall({
23017
+ name: "list_uploads",
23018
+ target: docId
23019
+ });
22936
23020
  try {
22937
23021
  const uploads = await server.client.listUploads(docId);
22938
23022
  return { content: [{
@@ -22954,6 +23038,11 @@ function registerFileTools(mcp, server) {
22954
23038
  filePath: z.string().describe("Absolute path to the local file to upload."),
22955
23039
  filename: z.string().optional().describe("Override filename (defaults to basename of filePath).")
22956
23040
  }, async ({ docId, filePath, filename }) => {
23041
+ server.setAutoStatus("uploading", docId);
23042
+ server.setActiveToolCall({
23043
+ name: "upload_file",
23044
+ target: path.basename(filePath)
23045
+ });
22957
23046
  try {
22958
23047
  const resolvedPath = path.resolve(filePath);
22959
23048
  const data = fs.readFileSync(resolvedPath);
@@ -22979,6 +23068,11 @@ function registerFileTools(mcp, server) {
22979
23068
  uploadId: z.string().describe("Upload ID to download."),
22980
23069
  saveTo: z.string().describe("Absolute local file path to save the download.")
22981
23070
  }, async ({ docId, uploadId, saveTo }) => {
23071
+ server.setAutoStatus("reading", docId);
23072
+ server.setActiveToolCall({
23073
+ name: "download_file",
23074
+ target: path.basename(saveTo)
23075
+ });
22982
23076
  try {
22983
23077
  const blob = await server.client.getUpload(docId, uploadId);
22984
23078
  const buffer = Buffer.from(await blob.arrayBuffer());
@@ -23002,6 +23096,11 @@ function registerFileTools(mcp, server) {
23002
23096
  docId: z.string().describe("Document ID."),
23003
23097
  uploadId: z.string().describe("Upload ID to delete.")
23004
23098
  }, async ({ docId, uploadId }) => {
23099
+ server.setAutoStatus("writing", docId);
23100
+ server.setActiveToolCall({
23101
+ name: "delete_file",
23102
+ target: uploadId
23103
+ });
23005
23104
  try {
23006
23105
  await server.client.deleteUpload(docId, uploadId);
23007
23106
  return { content: [{
@@ -23043,6 +23142,10 @@ function registerAwarenessTools(mcp, server) {
23043
23142
  docId: z.string().describe("Document ID to set awareness on."),
23044
23143
  fields: z.record(z.string(), 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.")
23045
23144
  }, async ({ docId, fields }) => {
23145
+ server.setActiveToolCall({
23146
+ name: "set_doc_awareness",
23147
+ target: docId
23148
+ });
23046
23149
  try {
23047
23150
  const provider = await server.getChildProvider(docId);
23048
23151
  for (const [key, value] of Object.entries(fields)) provider.awareness.setLocalStateField(key, value ?? null);
@@ -23061,6 +23164,7 @@ function registerAwarenessTools(mcp, server) {
23061
23164
  }
23062
23165
  });
23063
23166
  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 () => {
23167
+ server.setActiveToolCall({ name: "poll_inbox" });
23064
23168
  try {
23065
23169
  const treeMap = server.getTreeMap();
23066
23170
  const rootDocId = server.rootDocId;
@@ -23107,6 +23211,10 @@ function registerAwarenessTools(mcp, server) {
23107
23211
  }
23108
23212
  });
23109
23213
  mcp.tool("list_connected_users", "List all connected users and their awareness state. Shows who is online and what they are doing.", { docId: z.string().optional().describe("If provided, list users connected to this specific document. Otherwise lists users from root awareness.") }, async ({ docId }) => {
23214
+ server.setActiveToolCall({
23215
+ name: "list_connected_users",
23216
+ target: docId
23217
+ });
23110
23218
  try {
23111
23219
  let awareness;
23112
23220
  if (docId) awareness = (await server.getChildProvider(docId)).awareness;
@@ -23151,16 +23259,13 @@ function registerChannelTools(mcp, server) {
23151
23259
  });
23152
23260
  const treeMap = server.getTreeMap();
23153
23261
  const rootDoc = server.rootDocument;
23154
- if (!treeMap || !rootDoc) {
23155
- server.setActiveToolCall(null);
23156
- return {
23157
- content: [{
23158
- type: "text",
23159
- text: "Not connected"
23160
- }],
23161
- isError: true
23162
- };
23163
- }
23262
+ if (!treeMap || !rootDoc) return {
23263
+ content: [{
23264
+ type: "text",
23265
+ text: "Not connected"
23266
+ }],
23267
+ isError: true
23268
+ };
23164
23269
  const label = `AI Reply — ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)}: ${text.slice(0, 40).replace(/\n/g, " ")}`;
23165
23270
  const replyId = crypto.randomUUID();
23166
23271
  const now = Date.now();
@@ -23176,7 +23281,6 @@ function registerChannelTools(mcp, server) {
23176
23281
  });
23177
23282
  populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
23178
23283
  if (task_id) server.clearAiTask(task_id);
23179
- server.setActiveToolCall(null);
23180
23284
  return { content: [{
23181
23285
  type: "text",
23182
23286
  text: JSON.stringify({
@@ -23185,7 +23289,6 @@ function registerChannelTools(mcp, server) {
23185
23289
  })
23186
23290
  }] };
23187
23291
  } catch (error) {
23188
- server.setActiveToolCall(null);
23189
23292
  return {
23190
23293
  content: [{
23191
23294
  type: "text",
@@ -23208,14 +23311,15 @@ function registerChannelTools(mcp, server) {
23208
23311
  }],
23209
23312
  isError: true
23210
23313
  };
23314
+ const normalized = text.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\n");
23315
+ server.setAutoStatus(null);
23316
+ server.sendTypingIndicator(channel);
23211
23317
  rootProvider.sendStateless(JSON.stringify({
23212
23318
  type: "chat:send",
23213
23319
  channel,
23214
- content: text,
23320
+ content: normalized,
23215
23321
  sender_name: server.agentName
23216
23322
  }));
23217
- server.setAutoStatus(null);
23218
- server.setActiveToolCall(null);
23219
23323
  return { content: [{
23220
23324
  type: "text",
23221
23325
  text: `Sent to ${channel}`
@@ -23370,16 +23474,13 @@ function registerSvgTools(mcp, server) {
23370
23474
  target: docId
23371
23475
  });
23372
23476
  const cleanSvg = sanitizeSvg(svg);
23373
- if (!cleanSvg) {
23374
- server.setActiveToolCall(null);
23375
- return {
23376
- content: [{
23377
- type: "text",
23378
- text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23379
- }],
23380
- isError: true
23381
- };
23382
- }
23477
+ if (!cleanSvg) return {
23478
+ content: [{
23479
+ type: "text",
23480
+ text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23481
+ }],
23482
+ isError: true
23483
+ };
23383
23484
  const doc = (await server.getChildProvider(docId)).document;
23384
23485
  const fragment = doc.getXmlFragment("default");
23385
23486
  doc.transact(() => {
@@ -23390,13 +23491,11 @@ function registerSvgTools(mcp, server) {
23390
23491
  fragment.insert(insertPos, [el]);
23391
23492
  });
23392
23493
  server.setFocusedDoc(docId);
23393
- server.setActiveToolCall(null);
23394
23494
  return { content: [{
23395
23495
  type: "text",
23396
23496
  text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ""}`
23397
23497
  }] };
23398
23498
  } catch (error) {
23399
- server.setActiveToolCall(null);
23400
23499
  return {
23401
23500
  content: [{
23402
23501
  type: "text",
@@ -24200,6 +24299,9 @@ var HookBridge = class {
24200
24299
  }
24201
24300
  routeEvent(payload) {
24202
24301
  switch (payload.hook_event_name) {
24302
+ case "UserPromptSubmit":
24303
+ this.onUserPromptSubmit();
24304
+ break;
24203
24305
  case "PreToolUse":
24204
24306
  this.onPreToolUse(payload);
24205
24307
  break;
@@ -24210,13 +24312,18 @@ var HookBridge = class {
24210
24312
  this.onSubagentStart(payload);
24211
24313
  break;
24212
24314
  case "SubagentStop":
24213
- this.onSubagentStop(payload);
24315
+ this.onSubagentStop();
24214
24316
  break;
24215
24317
  case "Stop":
24216
24318
  this.onStop();
24217
24319
  break;
24218
24320
  }
24219
24321
  }
24322
+ /** New user turn — reset any lingering status/tool state from the previous turn. */
24323
+ onUserPromptSubmit() {
24324
+ this.server.setAutoStatus(null);
24325
+ this.server.setActiveToolCall(null);
24326
+ }
24220
24327
  onPreToolUse(payload) {
24221
24328
  const toolName = payload.tool_name ?? "";
24222
24329
  if (toolName.startsWith("mcp__abracadabra__")) return;
@@ -24228,7 +24335,6 @@ var HookBridge = class {
24228
24335
  }
24229
24336
  onPostToolUse(payload) {
24230
24337
  if ((payload.tool_name ?? "").startsWith("mcp__abracadabra__")) return;
24231
- this.server.setAutoStatus("thinking");
24232
24338
  }
24233
24339
  onSubagentStart(payload) {
24234
24340
  const agentType = payload.agent_type ?? "agent";
@@ -24238,9 +24344,7 @@ var HookBridge = class {
24238
24344
  });
24239
24345
  this.server.setAutoStatus("thinking");
24240
24346
  }
24241
- onSubagentStop(_payload) {
24242
- this.server.setAutoStatus("thinking");
24243
- }
24347
+ onSubagentStop() {}
24244
24348
  onStop() {
24245
24349
  this.server.setAutoStatus(null);
24246
24350
  this.server.setActiveToolCall(null);
@@ -24253,11 +24357,19 @@ var HookBridge = class {
24253
24357
  * Abracadabra MCP Server — entry point.
24254
24358
  *
24255
24359
  * Environment variables:
24256
- * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24360
+ * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24257
24361
  * ABRA_AGENT_NAME — Display name (default: "AI Assistant")
24258
24362
  * ABRA_AGENT_COLOR — HSL color for presence (default: "hsl(270, 80%, 60%)")
24259
24363
  * ABRA_INVITE_CODE — Invite code for first-run registration (grants role)
24260
24364
  * ABRA_KEY_FILE — Path to Ed25519 key file (default: ~/.abracadabra/agent.key)
24365
+ * ABRA_AGENT_TRIGGER_MODE — When to respond in group chats:
24366
+ * all → every message (legacy)
24367
+ * mention → only when @<alias> is used
24368
+ * task → only ai:task awareness events
24369
+ * mention+task → mention OR ai:task (default)
24370
+ * DMs always trigger regardless of mode.
24371
+ * ABRA_AGENT_MENTION_ALIASES — Comma-separated aliases for @mentions
24372
+ * (default: [ABRA_AGENT_NAME])
24261
24373
  */
24262
24374
  async function main() {
24263
24375
  const url = process.env.ABRA_URL;
@@ -24265,13 +24377,27 @@ async function main() {
24265
24377
  console.error("Missing required environment variable: ABRA_URL");
24266
24378
  process.exit(1);
24267
24379
  }
24380
+ const rawMode = (process.env.ABRA_AGENT_TRIGGER_MODE ?? "mention+task").trim().toLowerCase();
24381
+ const validModes = [
24382
+ "all",
24383
+ "mention",
24384
+ "task",
24385
+ "mention+task"
24386
+ ];
24387
+ const triggerMode = validModes.includes(rawMode) ? rawMode : "mention+task";
24388
+ if (rawMode && !validModes.includes(rawMode)) console.error(`[abracadabra-mcp] Invalid ABRA_AGENT_TRIGGER_MODE="${rawMode}", falling back to "mention+task"`);
24389
+ const aliasEnv = process.env.ABRA_AGENT_MENTION_ALIASES;
24390
+ const mentionAliases = aliasEnv ? aliasEnv.split(",").map((a) => a.trim()).filter((a) => a.length > 0) : void 0;
24268
24391
  const server = new AbracadabraMCPServer({
24269
24392
  url,
24270
24393
  agentName: process.env.ABRA_AGENT_NAME,
24271
24394
  agentColor: process.env.ABRA_AGENT_COLOR,
24272
24395
  inviteCode: process.env.ABRA_INVITE_CODE,
24273
- keyFile: process.env.ABRA_KEY_FILE
24396
+ keyFile: process.env.ABRA_KEY_FILE,
24397
+ triggerMode,
24398
+ mentionAliases
24274
24399
  });
24400
+ console.error(`[abracadabra-mcp] Trigger mode: ${triggerMode}; aliases: ${server.mentionAliases.join(", ")}`);
24275
24401
  const mcp = new McpServer({
24276
24402
  name: "abracadabra",
24277
24403
  version: "1.0.0"