@abraca/mcp 1.0.12 → 1.0.18

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.
@@ -38,8 +38,11 @@ let node_fs_promises = require("node:fs/promises");
38
38
  let node_fs = require("node:fs");
39
39
  node_fs = __toESM(node_fs);
40
40
  let node_os = require("node:os");
41
+ node_os = __toESM(node_os);
41
42
  let node_path = require("node:path");
42
43
  node_path = __toESM(node_path);
44
+ let node_http = require("node:http");
45
+ node_http = __toESM(node_http);
43
46
 
44
47
  //#region node_modules/zod/v3/helpers/util.js
45
48
  var util;
@@ -4064,7 +4067,7 @@ const cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-
4064
4067
  const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
4065
4068
  const base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
4066
4069
  const base64url = /^[A-Za-z0-9_-]*$/;
4067
- const hostname = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
4070
+ const hostname$1 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
4068
4071
  const e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
4069
4072
  const dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`;
4070
4073
  const date$1 = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
@@ -4611,7 +4614,7 @@ const $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
4611
4614
  code: "invalid_format",
4612
4615
  format: "url",
4613
4616
  note: "Invalid hostname",
4614
- pattern: hostname.source,
4617
+ pattern: hostname$1.source,
4615
4618
  input: payload.value,
4616
4619
  inst,
4617
4620
  continue: !def.abort
@@ -19446,6 +19449,9 @@ var AbracadabraMCPServer = class {
19446
19449
  this._serverRef = null;
19447
19450
  this._handledTaskIds = /* @__PURE__ */ new Set();
19448
19451
  this._userId = null;
19452
+ this._statusClearTimer = null;
19453
+ this._typingInterval = null;
19454
+ this._lastChatChannel = null;
19449
19455
  this.config = config;
19450
19456
  this.client = new _abraca_dabra.AbracadabraClient({
19451
19457
  url: config.url,
@@ -19540,8 +19546,12 @@ var AbracadabraMCPServer = class {
19540
19546
  provider.awareness.setLocalStateField("user", {
19541
19547
  name: this.agentName,
19542
19548
  color: this.agentColor,
19543
- publicKey: this._userId
19549
+ publicKey: this._userId,
19550
+ isAgent: true
19544
19551
  });
19552
+ provider.awareness.setLocalStateField("status", null);
19553
+ provider.awareness.setLocalStateField("activeToolCall", null);
19554
+ provider.awareness.setLocalStateField("statusContext", null);
19545
19555
  const conn = {
19546
19556
  doc,
19547
19557
  provider,
@@ -19593,7 +19603,8 @@ var AbracadabraMCPServer = class {
19593
19603
  childProvider.awareness.setLocalStateField("user", {
19594
19604
  name: this.agentName,
19595
19605
  color: this.agentColor,
19596
- publicKey: this._userId
19606
+ publicKey: this._userId,
19607
+ isAgent: true
19597
19608
  });
19598
19609
  this.childCache.set(docId, {
19599
19610
  provider: childProvider,
@@ -19660,6 +19671,7 @@ var AbracadabraMCPServer = class {
19660
19671
  const user = state["user"];
19661
19672
  const senderName = user && typeof user === "object" && typeof user.name === "string" ? user.name : "Unknown";
19662
19673
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`);
19674
+ this.setAutoStatus("thinking");
19663
19675
  this._dispatchAiTask({
19664
19676
  id,
19665
19677
  text,
@@ -19724,6 +19736,18 @@ var AbracadabraMCPServer = class {
19724
19736
  const parts = channel.split(":");
19725
19737
  if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) return;
19726
19738
  }
19739
+ if (channel) {
19740
+ const rootProvider = this._activeConnection?.provider;
19741
+ if (rootProvider) rootProvider.sendStateless(JSON.stringify({
19742
+ type: "chat:mark_read",
19743
+ channel,
19744
+ timestamp: Math.floor(Date.now() / 1e3)
19745
+ }));
19746
+ this._lastChatChannel = channel;
19747
+ this.sendTypingIndicator(channel);
19748
+ this._startTypingInterval(channel);
19749
+ }
19750
+ this.setAutoStatus("thinking");
19727
19751
  await this._serverRef.notification({
19728
19752
  method: "notifications/claude/channel",
19729
19753
  params: {
@@ -19741,15 +19765,82 @@ var AbracadabraMCPServer = class {
19741
19765
  console.error(`[abracadabra-mcp] Chat message from ${data.sender_name ?? "unknown"} on ${channel}`);
19742
19766
  } catch {}
19743
19767
  }
19768
+ /**
19769
+ * Set the agent's status in root awareness with auto-clear after idle.
19770
+ * @param statusContext — scopes the status to a specific channel/context so the
19771
+ * dashboard only shows it in the relevant chat. Defaults to `_lastChatChannel`.
19772
+ */
19773
+ setAutoStatus(status, docId, statusContext) {
19774
+ const provider = this._activeConnection?.provider;
19775
+ if (!provider) return;
19776
+ if (this._statusClearTimer) {
19777
+ clearTimeout(this._statusClearTimer);
19778
+ this._statusClearTimer = null;
19779
+ }
19780
+ provider.awareness.setLocalStateField("status", status);
19781
+ if (docId !== void 0) provider.awareness.setLocalStateField("docId", docId);
19782
+ const context = status ? statusContext !== void 0 ? statusContext : this._lastChatChannel : null;
19783
+ provider.awareness.setLocalStateField("statusContext", context ?? null);
19784
+ if (!status) this._stopTypingInterval();
19785
+ if (status) this._statusClearTimer = setTimeout(() => {
19786
+ provider.awareness.setLocalStateField("status", null);
19787
+ provider.awareness.setLocalStateField("activeToolCall", null);
19788
+ provider.awareness.setLocalStateField("statusContext", null);
19789
+ this._stopTypingInterval();
19790
+ }, 1e4);
19791
+ }
19792
+ /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
19793
+ _startTypingInterval(channel) {
19794
+ this._stopTypingInterval();
19795
+ this._typingInterval = setInterval(() => {
19796
+ this.sendTypingIndicator(channel);
19797
+ }, 2e3);
19798
+ }
19799
+ _stopTypingInterval() {
19800
+ if (this._typingInterval) {
19801
+ clearInterval(this._typingInterval);
19802
+ this._typingInterval = null;
19803
+ }
19804
+ this._lastChatChannel = null;
19805
+ }
19806
+ /**
19807
+ * Broadcast which tool the agent is currently executing.
19808
+ * Dashboard renders this as a ChatTool indicator.
19809
+ */
19810
+ setActiveToolCall(toolCall) {
19811
+ this._activeConnection?.provider?.awareness.setLocalStateField("activeToolCall", toolCall);
19812
+ }
19813
+ /**
19814
+ * Send a typing indicator to a chat channel.
19815
+ */
19816
+ sendTypingIndicator(channel) {
19817
+ const rootProvider = this._activeConnection?.provider;
19818
+ if (!rootProvider) return;
19819
+ rootProvider.sendStateless(JSON.stringify({
19820
+ type: "chat:typing",
19821
+ channel,
19822
+ sender_name: this.agentName
19823
+ }));
19824
+ }
19744
19825
  /** Graceful shutdown. */
19745
19826
  async destroy() {
19827
+ this._stopTypingInterval();
19828
+ if (this._statusClearTimer) {
19829
+ clearTimeout(this._statusClearTimer);
19830
+ this._statusClearTimer = null;
19831
+ }
19746
19832
  if (this.evictionTimer) {
19747
19833
  clearInterval(this.evictionTimer);
19748
19834
  this.evictionTimer = null;
19749
19835
  }
19750
19836
  for (const [, cached] of this.childCache) cached.provider.destroy();
19751
19837
  this.childCache.clear();
19752
- for (const [, conn] of this._spaceConnections) conn.provider.destroy();
19838
+ for (const [, conn] of this._spaceConnections) {
19839
+ conn.provider.awareness.setLocalStateField("status", null);
19840
+ conn.provider.awareness.setLocalStateField("activeToolCall", null);
19841
+ conn.provider.awareness.setLocalStateField("statusContext", null);
19842
+ conn.provider.destroy();
19843
+ }
19753
19844
  this._spaceConnections.clear();
19754
19845
  this._activeConnection = null;
19755
19846
  console.error("[abracadabra-mcp] Shutdown complete");
@@ -19891,15 +19982,23 @@ function registerTreeTools(mcp, server) {
19891
19982
  mcp.tool("create_document", "Create a new document in the tree. Returns the new document ID.", {
19892
19983
  parentId: zod.z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
19893
19984
  label: zod.z.string().describe("Display name / title for the document."),
19894
- type: zod.z.string().optional().describe("Page type: \"doc\", \"kanban\", \"calendar\", \"table\", \"outline\", \"gallery\", \"slides\", \"timeline\", \"whiteboard\", \"map\", \"dashboard\", \"mindmap\", \"graph\". Omit to inherit parent view."),
19985
+ type: zod.z.string().optional().describe("Page type — sets how this document renders. \"doc\" (rich text), \"kanban\" (columns → cards), \"table\" (columns → cells, positional rows), \"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\" (image/media items), \"map\" (markers/lines with geoLat/geoLng), \"graph\" (knowledge graph nodes), \"dashboard\" (positioned widgets with deskX/deskY/deskMode), \"mindmap\" (connected nodes), \"spatial\" (3D objects with spShape/spX/spY/spZ), \"media\" (audio/video tracks), \"slides\" (slide deck), \"whiteboard\" (freeform canvas). Omit to inherit parent view. Only set on the parent page, NEVER on child items."),
19895
19986
  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.")
19896
19987
  }, async ({ parentId, label, type, meta }) => {
19988
+ server.setAutoStatus("creating");
19989
+ server.setActiveToolCall({
19990
+ name: "create_document",
19991
+ target: label
19992
+ });
19897
19993
  const treeMap = server.getTreeMap();
19898
19994
  const rootDoc = server.rootDocument;
19899
- if (!treeMap || !rootDoc) return { content: [{
19900
- type: "text",
19901
- text: "Not connected"
19902
- }] };
19995
+ if (!treeMap || !rootDoc) {
19996
+ server.setActiveToolCall(null);
19997
+ return { content: [{
19998
+ type: "text",
19999
+ text: "Not connected"
20000
+ }] };
20001
+ }
19903
20002
  const id = crypto.randomUUID();
19904
20003
  const normalizedParent = normalizeRootId(parentId, server);
19905
20004
  const now = Date.now();
@@ -19914,6 +20013,8 @@ function registerTreeTools(mcp, server) {
19914
20013
  updatedAt: now
19915
20014
  });
19916
20015
  });
20016
+ server.setFocusedDoc(id);
20017
+ server.setActiveToolCall(null);
19917
20018
  return { content: [{
19918
20019
  type: "text",
19919
20020
  text: JSON.stringify({
@@ -20068,6 +20169,7 @@ function registerTreeTools(mcp, server) {
20068
20169
  label: (entry.label || "Untitled") + " (copy)",
20069
20170
  order: Date.now()
20070
20171
  });
20172
+ server.setFocusedDoc(newId);
20071
20173
  return { content: [{
20072
20174
  type: "text",
20073
20175
  text: JSON.stringify({
@@ -20153,11 +20255,44 @@ function parseFrontmatter(markdown) {
20153
20255
  if (subtitle) meta.subtitle = subtitle;
20154
20256
  const url = getStr(["url"]);
20155
20257
  if (url) meta.url = url;
20258
+ const email = getStr(["email"]);
20259
+ if (email) meta.email = email;
20260
+ const phone = getStr(["phone"]);
20261
+ if (phone) meta.phone = phone;
20156
20262
  const ratingRaw = getStr(["rating"]);
20157
20263
  if (ratingRaw !== void 0) {
20158
20264
  const n = Number(ratingRaw);
20159
20265
  if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
20160
20266
  }
20267
+ const datetimeStart = getStr(["datetimeStart"]);
20268
+ if (datetimeStart) meta.datetimeStart = datetimeStart;
20269
+ const datetimeEnd = getStr(["datetimeEnd"]);
20270
+ if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
20271
+ const allDayRaw = raw["allDay"];
20272
+ if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
20273
+ const geoLatRaw = getStr(["geoLat"]);
20274
+ if (geoLatRaw !== void 0) {
20275
+ const n = Number(geoLatRaw);
20276
+ if (!Number.isNaN(n)) meta.geoLat = n;
20277
+ }
20278
+ const geoLngRaw = getStr(["geoLng"]);
20279
+ if (geoLngRaw !== void 0) {
20280
+ const n = Number(geoLngRaw);
20281
+ if (!Number.isNaN(n)) meta.geoLng = n;
20282
+ }
20283
+ const geoType = getStr(["geoType"]);
20284
+ if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
20285
+ const geoDescription = getStr(["geoDescription"]);
20286
+ if (geoDescription) meta.geoDescription = geoDescription;
20287
+ const numberRaw = getStr(["number"]);
20288
+ if (numberRaw !== void 0) {
20289
+ const n = Number(numberRaw);
20290
+ if (!Number.isNaN(n)) meta.number = n;
20291
+ }
20292
+ const unit = getStr(["unit"]);
20293
+ if (unit) meta.unit = unit;
20294
+ const note = getStr(["note"]);
20295
+ if (note) meta.note = note;
20161
20296
  return {
20162
20297
  title: typeof raw["title"] === "string" ? raw["title"] : void 0,
20163
20298
  meta,
@@ -20195,8 +20330,12 @@ function parseInline(text) {
20195
20330
  attrs: { kbd: { value: kbdProps["value"] || "" } }
20196
20331
  });
20197
20332
  } else if (match[5] !== void 0) {
20198
- const displayText = match[6] ?? match[5];
20199
- tokens.push({ text: displayText });
20333
+ const docId = match[5];
20334
+ const displayText = match[6] ?? docId;
20335
+ tokens.push({
20336
+ text: displayText,
20337
+ attrs: { link: { href: `/doc/${docId}` } }
20338
+ });
20200
20339
  } else if (match[7] !== void 0) tokens.push({
20201
20340
  text: match[7],
20202
20341
  attrs: { strike: true }
@@ -20345,6 +20484,15 @@ function parseBlocks(markdown) {
20345
20484
  i++;
20346
20485
  continue;
20347
20486
  }
20487
+ const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/);
20488
+ if (docEmbedMatch) {
20489
+ blocks.push({
20490
+ type: "docEmbed",
20491
+ docId: docEmbedMatch[1]
20492
+ });
20493
+ i++;
20494
+ continue;
20495
+ }
20348
20496
  const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
20349
20497
  if (imgMatch) {
20350
20498
  const alt = imgMatch[1] ?? "";
@@ -20652,6 +20800,7 @@ function blockElName(b) {
20652
20800
  case "field": return "field";
20653
20801
  case "fieldGroup": return "fieldGroup";
20654
20802
  case "image": return "image";
20803
+ case "docEmbed": return "docEmbed";
20655
20804
  }
20656
20805
  }
20657
20806
  function fillBlock(el, block) {
@@ -20867,6 +21016,9 @@ function fillBlock(el, block) {
20867
21016
  if (block.width) el.setAttribute("width", block.width);
20868
21017
  if (block.height) el.setAttribute("height", block.height);
20869
21018
  break;
21019
+ case "docEmbed":
21020
+ el.setAttribute("docId", block.docId);
21021
+ break;
20870
21022
  }
20871
21023
  }
20872
21024
  function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
@@ -20915,6 +21067,7 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
20915
21067
  case "field": return new yjs.XmlElement("field");
20916
21068
  case "fieldGroup": return new yjs.XmlElement("fieldGroup");
20917
21069
  case "image": return new yjs.XmlElement("image");
21070
+ case "docEmbed": return new yjs.XmlElement("docEmbed");
20918
21071
  }
20919
21072
  });
20920
21073
  fragment.insert(0, [
@@ -20986,6 +21139,10 @@ function serializeElement(el, indent = "") {
20986
21139
  }
20987
21140
  case "horizontalRule": return "---";
20988
21141
  case "table": return serializeTable(el);
21142
+ case "docEmbed": {
21143
+ const docId = el.getAttribute("docId");
21144
+ return docId ? `![[${docId}]]` : "";
21145
+ }
20989
21146
  case "image": {
20990
21147
  const src = el.getAttribute("src") || "";
20991
21148
  const alt = el.getAttribute("alt") || "";
@@ -21151,6 +21308,11 @@ function yjsToMarkdown(fragment) {
21151
21308
  function registerContentTools(mcp, server) {
21152
21309
  mcp.tool("read_document", "Read a document's content as markdown, along with its immediate children. IMPORTANT: A document's full content is its body text PLUS all its children. An empty body does not mean empty content — the children ARE the content (sub-docs, kanban columns, table columns, calendar events, etc.). Always check the returned \"children\" array and read child documents too.", { docId: zod.z.string().describe("Document ID to read.") }, async ({ docId }) => {
21153
21310
  try {
21311
+ server.setAutoStatus("reading", docId);
21312
+ server.setActiveToolCall({
21313
+ name: "read_document",
21314
+ target: docId
21315
+ });
21154
21316
  const { title, markdown } = yjsToMarkdown((await server.getChildProvider(docId)).document.getXmlFragment("default"));
21155
21317
  server.setFocusedDoc(docId);
21156
21318
  server.setDocCursor(docId, 0);
@@ -21176,6 +21338,7 @@ function registerContentTools(mcp, server) {
21176
21338
  });
21177
21339
  children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
21178
21340
  }
21341
+ server.setActiveToolCall(null);
21179
21342
  const result = {
21180
21343
  label,
21181
21344
  type,
@@ -21188,6 +21351,7 @@ function registerContentTools(mcp, server) {
21188
21351
  text: JSON.stringify(result, null, 2)
21189
21352
  }] };
21190
21353
  } catch (error) {
21354
+ server.setActiveToolCall(null);
21191
21355
  return {
21192
21356
  content: [{
21193
21357
  type: "text",
@@ -21197,12 +21361,17 @@ function registerContentTools(mcp, server) {
21197
21361
  };
21198
21362
  }
21199
21363
  });
21200
- mcp.tool("write_document", "Write markdown content to a document. Parses the markdown and writes it to the Y.js CRDT document, which syncs in real-time to all connected clients. Supports optional YAML frontmatter for title and metadata.", {
21364
+ mcp.tool("write_document", "Write markdown content to a document. Parses the markdown and writes it to the Y.js CRDT document, which syncs in real-time to all connected clients. Supports optional YAML frontmatter for title and metadata. Use ![[docId]] to embed another document as a block, or [[docId|label]] for inline doc links.", {
21201
21365
  docId: zod.z.string().describe("Document ID to write to."),
21202
21366
  markdown: zod.z.string().describe("Markdown content to write. Can include YAML frontmatter with title and metadata fields."),
21203
21367
  mode: zod.z.enum(["replace", "append"]).optional().describe("Write mode. \"replace\" clears existing content first (default). \"append\" adds to the end.")
21204
21368
  }, async ({ docId, markdown, mode }) => {
21205
21369
  try {
21370
+ server.setAutoStatus("writing", docId);
21371
+ server.setActiveToolCall({
21372
+ name: "write_document",
21373
+ target: docId
21374
+ });
21206
21375
  const writeMode = mode ?? "replace";
21207
21376
  const doc = (await server.getChildProvider(docId)).document;
21208
21377
  const fragment = doc.getXmlFragment("default");
@@ -21232,11 +21401,13 @@ function registerContentTools(mcp, server) {
21232
21401
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
21233
21402
  server.setFocusedDoc(docId);
21234
21403
  server.setDocCursor(docId, fragment.length);
21404
+ server.setActiveToolCall(null);
21235
21405
  return { content: [{
21236
21406
  type: "text",
21237
21407
  text: `Document ${docId} updated (${writeMode} mode)`
21238
21408
  }] };
21239
21409
  } catch (error) {
21410
+ server.setActiveToolCall(null);
21240
21411
  return {
21241
21412
  content: [{
21242
21413
  type: "text",
@@ -21274,7 +21445,7 @@ function registerMetaTools(mcp, server) {
21274
21445
  });
21275
21446
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
21276
21447
  docId: zod.z.string().describe("Document ID."),
21277
- meta: zod.z.record(zod.z.unknown()).describe("Metadata fields to update (merged with existing). Standard PageMeta keys: color (hex string), icon (Lucide kebab-case name like \"star\"/\"code-2\"/\"users\" — NEVER emoji), dateStart, dateEnd, datetimeStart, datetimeEnd, allDay, tags, checked, priority (0-4), status, rating, url, taskProgress (0-100), subtitle, note. Set a key to null to clear it.")
21448
+ 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, 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. Geo/Map: geoType (\"marker\"|\"line\"|\"measure\"), geoLat, geoLng, geoDescription. Spatial 3D: spShape (\"box\"|\"sphere\"|\"cylinder\"|\"cone\"|\"plane\"|\"torus\"|\"glb\"), spX/spY/spZ, spRX/spRY/spRZ, spSX/spSY/spSZ, spColor, spOpacity (0-100). Dashboard: deskX, deskY, deskZ, deskMode (\"icon\"|\"widget-sm\"|\"widget-lg\"). Renderer config (on the page doc itself): kanbanColumnWidth, galleryColumns, galleryAspect, calendarView, calendarWeekStart, tableMode, showRefEdges. Set a key to null to clear it.")
21278
21449
  }, async ({ docId, meta }) => {
21279
21450
  const treeMap = server.getTreeMap();
21280
21451
  if (!treeMap) return { content: [{
@@ -21519,15 +21690,23 @@ function registerChannelTools(mcp, server) {
21519
21690
  task_id: zod.z.string().optional().describe("If replying to an ai:task, the task ID to clear from awareness.")
21520
21691
  }, async ({ doc_id, text, task_id }) => {
21521
21692
  try {
21693
+ server.setAutoStatus("writing", doc_id);
21694
+ server.setActiveToolCall({
21695
+ name: "reply",
21696
+ target: doc_id
21697
+ });
21522
21698
  const treeMap = server.getTreeMap();
21523
21699
  const rootDoc = server.rootDocument;
21524
- if (!treeMap || !rootDoc) return {
21525
- content: [{
21526
- type: "text",
21527
- text: "Not connected"
21528
- }],
21529
- isError: true
21530
- };
21700
+ if (!treeMap || !rootDoc) {
21701
+ server.setActiveToolCall(null);
21702
+ return {
21703
+ content: [{
21704
+ type: "text",
21705
+ text: "Not connected"
21706
+ }],
21707
+ isError: true
21708
+ };
21709
+ }
21531
21710
  const label = `AI Reply — ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)}: ${text.slice(0, 40).replace(/\n/g, " ")}`;
21532
21711
  const replyId = crypto.randomUUID();
21533
21712
  const now = Date.now();
@@ -21543,6 +21722,7 @@ function registerChannelTools(mcp, server) {
21543
21722
  });
21544
21723
  populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
21545
21724
  if (task_id) server.clearAiTask(task_id);
21725
+ server.setActiveToolCall(null);
21546
21726
  return { content: [{
21547
21727
  type: "text",
21548
21728
  text: JSON.stringify({
@@ -21551,6 +21731,7 @@ function registerChannelTools(mcp, server) {
21551
21731
  })
21552
21732
  }] };
21553
21733
  } catch (error) {
21734
+ server.setActiveToolCall(null);
21554
21735
  return {
21555
21736
  content: [{
21556
21737
  type: "text",
@@ -21579,6 +21760,8 @@ function registerChannelTools(mcp, server) {
21579
21760
  content: text,
21580
21761
  sender_name: server.agentName
21581
21762
  }));
21763
+ server.setAutoStatus(null);
21764
+ server.setActiveToolCall(null);
21582
21765
  return { content: [{
21583
21766
  type: "text",
21584
21767
  text: `Sent to ${channel}`
@@ -21610,6 +21793,18 @@ Every piece of visible data in Abracadabra — a calendar event, a kanban card,
21610
21793
 
21611
21794
  **Page types are views over the same document hierarchy**, not isolated data stores. Switching a page from Kanban to Table to Outline works without migration — the tree doesn't change, only the rendering.
21612
21795
 
21796
+ The same tree viewed as different page types:
21797
+ \`\`\`
21798
+ My Doc
21799
+ ├── "To Do" → Kanban: column / Table: column / Outline: item
21800
+ │ ├── "Fix bug" → Kanban: card / Table: cell / Outline: sub-item
21801
+ │ └── "Add tests"
21802
+ ├── "In Progress"
21803
+ │ └── "Refactor"
21804
+ └── "Done"
21805
+ └── "Deploy"
21806
+ \`\`\`
21807
+
21613
21808
  ---
21614
21809
 
21615
21810
  ## Getting Oriented in a Space
@@ -21629,23 +21824,24 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21629
21824
 
21630
21825
  ## Page Types Reference
21631
21826
 
21632
- | Type | Children Are | Grandchildren Are | Depth Limit |
21633
- |------|-------------|-------------------|-------------|
21634
- | **doc** | Sub-documents | Sub-sub-documents | Unlimited |
21635
- | **kanban** | Columns | Cards | 2 |
21636
- | **table** | Columns | Cells (positional rows) | 2 |
21637
- | **calendar** | Events | — | 1 |
21638
- | **timeline** | Epics | Tasks | 2 |
21639
- | **outline** | Items | Sub-items | Unlimited |
21640
- | **mindmap** | Central nodes | Branches | Unlimited |
21641
- | **graph** | Nodes | | 1 |
21642
- | **gallery** | Items | — | 1 |
21643
- | **slides** | Slides | — | 1 |
21644
- | **whiteboard** | Objects | | 1 |
21645
- | **map** | Markers/Lines | Points (for lines) | 2 |
21646
- | **desktop** | Items | | 1 |
21647
- | **call** | | — | 0 |
21648
- | **game** | | — | 0 |
21827
+ | Type | Children Are | Grandchildren Are | Depth | Key Meta on Children |
21828
+ |------|-------------|-------------------|-------|---------------------|
21829
+ | **doc** | Sub-documents | Sub-sub-documents | | — |
21830
+ | **kanban** | Columns | Cards | 2 | color, icon on cards |
21831
+ | **table** | Columns | Cells (positional rows) | 2 | — |
21832
+ | **calendar** | Events | — | 1 | datetimeStart, datetimeEnd, allDay, color |
21833
+ | **timeline** | Epics | Tasks | 2 | dateStart, dateEnd, taskProgress, color |
21834
+ | **checklist** | Tasks | Sub-tasks | | checked, priority, dateEnd |
21835
+ | **outline** | Items | Sub-items | | |
21836
+ | **mindmap** | Central nodes | Branches | | mmX, mmY |
21837
+ | **graph** | Nodes | — | 1 | graphX, graphY, graphPinned, color |
21838
+ | **gallery** | Items | — | 1 | geoLat, geoLng, datetimeStart, tags |
21839
+ | **map** | Markers/Lines | Points (for lines) | 2 | geoType, geoLat, geoLng, icon, color |
21840
+ | **dashboard** | Items | | 1 | deskX, deskY, deskMode |
21841
+ | **spatial** | Objects | Sub-parts | | spShape, spX/Y/Z, spColor, spOpacity |
21842
+ | **media** | Tracks | — | 1 | tags |
21843
+ | **slides** | Slides | — | 1 | — |
21844
+ | **whiteboard** | Objects | — | 1 | wbX, wbY, wbW, wbH |
21649
21845
 
21650
21846
  ---
21651
21847
 
@@ -21654,26 +21850,110 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21654
21850
  ### Creating Well-Structured Hierarchies
21655
21851
 
21656
21852
  **Kanban board:**
21657
- 1. Create the kanban document: \`create_document(parentId, "Project Board", "kanban")\`
21853
+ 1. \`create_document(parentId, "Project Board", "kanban")\`
21658
21854
  2. Create columns: \`create_document(boardId, "To Do")\`, \`create_document(boardId, "In Progress")\`, \`create_document(boardId, "Done")\`
21659
21855
  3. Create cards under columns: \`create_document(toDoColumnId, "Fix bug #123")\`
21856
+ 4. Optionally set card metadata: \`update_metadata(cardId, { color: "#ef4444", priority: 3, tags: ["urgent"] })\`
21857
+ 5. Optionally configure column width: \`update_metadata(boardId, { kanbanColumnWidth: "wide" })\`
21858
+
21859
+ **Table with data:**
21860
+ 1. \`create_document(parentId, "Contacts", "table")\`
21861
+ 2. Create columns: \`create_document(tableId, "Name")\`, \`create_document(tableId, "Email")\`, \`create_document(tableId, "Phone")\`
21862
+ 3. Create cells (children of columns): \`create_document(nameColId, "Alice")\`, \`create_document(emailColId, "alice@example.com")\`
21863
+ 4. **Rows are positional** — the Nth child of each column forms row N
21864
+ 5. Row count = max children across all columns. Missing cells render as empty.
21865
+ 6. To add a row: create a child under each column at the same index position
21866
+ 7. To delete row N: delete the Nth child from every column
21660
21867
 
21661
21868
  **Calendar with events:**
21662
- 1. Create calendar: \`create_document(parentId, "Team Calendar", "calendar")\`
21869
+ 1. \`create_document(parentId, "Team Calendar", "calendar")\`
21663
21870
  2. Create events: \`create_document(calendarId, "Sprint Planning")\`
21664
21871
  3. Set event times: \`update_metadata(eventId, { datetimeStart: "2026-03-20T10:00:00Z", datetimeEnd: "2026-03-20T11:00:00Z" })\`
21872
+ 4. All-day events: \`update_metadata(eventId, { datetimeStart: "2026-03-20", allDay: true })\`
21873
+ 5. Multi-day events: \`update_metadata(eventId, { datetimeStart: "2026-03-20", datetimeEnd: "2026-03-22", allDay: true })\`
21874
+ 6. Configure view: \`update_metadata(calendarId, { calendarView: "week" })\`
21665
21875
 
21666
- **Timeline with tasks:**
21667
- 1. Create timeline: \`create_document(parentId, "Q1 Roadmap", "timeline")\`
21876
+ **Timeline (Gantt chart):**
21877
+ 1. \`create_document(parentId, "Q1 Roadmap", "timeline")\`
21668
21878
  2. Create epics: \`create_document(timelineId, "Auth Rewrite")\`
21669
- 3. Create tasks: \`create_document(epicId, "Implement JWT refresh")\`
21670
- 4. Set dates/progress: \`update_metadata(taskId, { dateStart: "2026-03-01", dateEnd: "2026-03-15", taskProgress: 50 })\`
21879
+ 3. Create tasks under epics: \`create_document(epicId, "Implement JWT refresh")\`
21880
+ 4. Set dates and progress: \`update_metadata(taskId, { dateStart: "2026-03-01", dateEnd: "2026-03-15", taskProgress: 50, color: "#6366f1" })\`
21671
21881
 
21672
- **Table with data:**
21673
- 1. Create table: \`create_document(parentId, "Contacts", "table")\`
21674
- 2. Create columns: \`create_document(tableId, "Name")\`, \`create_document(tableId, "Email")\`, \`create_document(tableId, "Phone")\`
21675
- 3. Create cells (children of columns): \`create_document(nameColId, "Alice")\`, \`create_document(emailColId, "alice@example.com")\`
21676
- 4. Rows are positional the Nth child of each column forms row N
21882
+ **Checklist (nested task list):**
21883
+ 1. \`create_document(parentId, "Sprint Tasks", "checklist")\`
21884
+ 2. Create tasks: \`create_document(checklistId, "Write tests")\`
21885
+ 3. Set task properties: \`update_metadata(taskId, { checked: false, priority: 3, dateEnd: "2026-04-01" })\`
21886
+ 4. Create sub-tasks (unlimited nesting): \`create_document(taskId, "Unit tests")\`
21887
+
21888
+ **Map with markers and lines:**
21889
+ 1. \`create_document(parentId, "Travel Map", "map")\`
21890
+ 2. Create markers: \`create_document(mapId, "Coffee Shop")\`
21891
+ 3. Set marker position: \`update_metadata(markerId, { geoType: "marker", geoLat: 37.7749, geoLng: -122.4194, icon: "coffee", color: "#f97316" })\`
21892
+ 4. Create a line/route: \`create_document(mapId, "Walking Route")\`
21893
+ 5. Set line properties: \`update_metadata(routeId, { geoType: "line", color: "#3b82f6" })\`
21894
+ 6. Create line points as children: \`create_document(routeId, "Start")\` then \`update_metadata(pointId, { geoLat: 37.7749, geoLng: -122.4194 })\`
21895
+ 7. Create measuring tool: set \`geoType: "measure"\` instead of "line"
21896
+
21897
+ **Gallery:**
21898
+ 1. \`create_document(parentId, "Photo Album", "gallery")\`
21899
+ 2. Create items: \`create_document(galleryId, "Sunset at Beach")\`
21900
+ 3. Set metadata: \`update_metadata(itemId, { geoLat: 37.7749, geoLng: -122.4194, tags: ["travel", "sunset"] })\`
21901
+ 4. Configure layout: \`update_metadata(galleryId, { galleryColumns: 4, galleryAspect: "16:9" })\`
21902
+
21903
+ **Graph (knowledge graph):**
21904
+ 1. \`create_document(parentId, "Tech Stack", "graph")\`
21905
+ 2. Create nodes: \`create_document(graphId, "React")\`, \`create_document(graphId, "Vue")\`
21906
+ 3. Set node properties: \`update_metadata(nodeId, { color: "#6366f1", icon: "file-code" })\`
21907
+ 4. Edges are created by parent-child relationships AND document references (doc embeds/links in content)
21908
+ 5. Enable reference edges: \`update_metadata(graphId, { showRefEdges: true })\`
21909
+
21910
+ **Dashboard:**
21911
+ 1. \`create_document(parentId, "Overview", "dashboard")\`
21912
+ 2. Create items: \`create_document(dashboardId, "Revenue Chart")\`
21913
+ 3. Position items on grid: \`update_metadata(itemId, { deskX: 0, deskY: 0, deskMode: "widget-lg" })\`
21914
+ 4. Modes: \`"icon"\` (small), \`"widget-sm"\` (240×180), \`"widget-lg"\` (400×320)
21915
+ 5. Grid uses 80px cells
21916
+
21917
+ **Spatial (3D scene):**
21918
+ 1. \`create_document(parentId, "3D Scene", "spatial")\`
21919
+ 2. Create objects: \`create_document(sceneId, "Red Cube")\`
21920
+ 3. Set 3D properties: \`update_metadata(objId, { spShape: "box", spColor: "#ef4444", spX: 0, spY: 1, spZ: 0, spSX: 2, spSY: 2, spSZ: 2 })\`
21921
+ 4. Shapes: \`"box"\`, \`"sphere"\`, \`"cylinder"\`, \`"cone"\`, \`"plane"\`, \`"torus"\`, \`"glb"\` (uploaded 3D model)
21922
+ 5. Rotation (degrees): \`spRX\`, \`spRY\`, \`spRZ\`. Scale: \`spSX\`, \`spSY\`, \`spSZ\` (default 1). Opacity: \`spOpacity\` (0–100)
21923
+
21924
+ **Outline (nested items):**
21925
+ 1. \`create_document(parentId, "Meeting Notes", "outline")\`
21926
+ 2. Create items: \`create_document(outlineId, "Agenda Item 1")\`
21927
+ 3. Create sub-items (unlimited depth): \`create_document(itemId, "Sub-point")\`
21928
+
21929
+ ---
21930
+
21931
+ ## Document References
21932
+
21933
+ You can link and embed documents within rich-text content:
21934
+
21935
+ **Block-level doc embed** — renders an embedded document preview:
21936
+ \`\`\`
21937
+ ![[docId]]
21938
+ \`\`\`
21939
+
21940
+ **Inline link to another document:**
21941
+ \`\`\`
21942
+ [Link text](/doc/docId)
21943
+ \`\`\`
21944
+
21945
+ **Inline wikilink** (shorthand — converted to doc link):
21946
+ \`\`\`
21947
+ [[docId]]
21948
+ [[docId|Display Text]]
21949
+ \`\`\`
21950
+
21951
+ Example — write content with references:
21952
+ \`\`\`
21953
+ write_document(docId, "# Project Overview\\n\\nSee the detailed spec:\\n\\n![[specDocId]]\\n\\nCheck the [timeline](/doc/timelineDocId) for dates.")
21954
+ \`\`\`
21955
+
21956
+ In the **graph** page type, document references (embeds and links) create visible edges between nodes when \`showRefEdges\` is enabled.
21677
21957
 
21678
21958
  ---
21679
21959
 
@@ -21684,7 +21964,7 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21684
21964
  | Key | Type | Meaning |
21685
21965
  |-----|------|---------|
21686
21966
  | \`color\` | string | Hex/CSS color (e.g. "#6366f1") |
21687
- | \`icon\` | string | Lucide icon name in kebab-case (e.g. "star", "code-2", "users", "calendar-days", "book-open"). **Never emoji.** Omit to use the page type's default icon. |
21967
+ | \`icon\` | string | Lucide icon name in kebab-case (e.g. "star", "code-2", "users"). **Never emoji.** Omit for default. |
21688
21968
  | \`datetimeStart\` / \`datetimeEnd\` | string | ISO datetime with time |
21689
21969
  | \`allDay\` | boolean | Whether datetime is all-day |
21690
21970
  | \`dateStart\` / \`dateEnd\` | string | ISO date-only (e.g. "2026-03-20") |
@@ -21702,45 +21982,86 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21702
21982
  | \`subtitle\` | string | Secondary text |
21703
21983
  | \`note\` | string | Free-text note |
21704
21984
  | \`taskProgress\` | number | 0–100 progress (timeline tasks) |
21985
+ | \`members\` | { id, label }[] | Assigned users |
21986
+ | \`coverUploadId\` | string | Cover image (upload ID from upload_file) |
21705
21987
 
21706
- ### Type-Specific Meta Schemas
21988
+ ### Geo/Map Keys (for map children)
21989
+
21990
+ | Key | Type | Meaning |
21991
+ |-----|------|---------|
21992
+ | \`geoType\` | string | "marker", "line", or "measure" |
21993
+ | \`geoLat\` | number | Latitude |
21994
+ | \`geoLng\` | number | Longitude |
21995
+ | \`geoDescription\` | string | Location description text |
21707
21996
 
21708
- **Calendar events** require: \`datetimeStart\`, \`datetimeEnd\`, optionally \`allDay\`, \`color\`
21997
+ ### Renderer Config Keys (set on the page doc itself, not its children)
21709
21998
 
21710
- **Timeline tasks** require: \`dateStart\`, \`dateEnd\`, optionally \`taskProgress\` (0–100), \`color\`
21999
+ | Key | Type | Applies to | Values |
22000
+ |-----|------|-----------|--------|
22001
+ | \`kanbanColumnWidth\` | string | kanban | "narrow", "default", "wide" |
22002
+ | \`galleryColumns\` | number | gallery | 1–6 |
22003
+ | \`galleryAspect\` | string | gallery | "square", "4:3", "16:9" |
22004
+ | \`calendarView\` | string | calendar | "month", "week", "day" |
22005
+ | \`calendarWeekStart\` | string | calendar | "sun", "mon" |
22006
+ | \`tableMode\` | string | table | "hierarchy", "flat" |
22007
+ | \`showRefEdges\` | boolean | graph | show doc-reference edges |
21711
22008
 
21712
- **Map markers** require: \`geoLat\`, \`geoLng\`, \`geoType\` ("marker"|"line"|"measure"), optionally \`icon\`, \`color\`
22009
+ ### Spatial 3D Keys (for spatial children)
22010
+
22011
+ | Key | Type | Default | Meaning |
22012
+ |-----|------|---------|---------|
22013
+ | \`spShape\` | string | — | "box", "sphere", "cylinder", "cone", "plane", "torus", "glb" |
22014
+ | \`spColor\` | string | — | CSS color |
22015
+ | \`spOpacity\` | number | 100 | 0–100 |
22016
+ | \`spX\`, \`spY\`, \`spZ\` | number | 0 | Position |
22017
+ | \`spRX\`, \`spRY\`, \`spRZ\` | number | 0 | Rotation (degrees) |
22018
+ | \`spSX\`, \`spSY\`, \`spSZ\` | number | 1 | Scale |
22019
+
22020
+ ### Dashboard Keys (for dashboard children)
22021
+
22022
+ | Key | Type | Meaning |
22023
+ |-----|------|---------|
22024
+ | \`deskX\`, \`deskY\` | number | Grid position (80px cells) |
22025
+ | \`deskZ\` | number | Z-index (layering) |
22026
+ | \`deskMode\` | string | "icon", "widget-sm" (240×180), "widget-lg" (400×320) |
21713
22027
 
21714
22028
  ---
21715
22029
 
21716
22030
  ## Content Structure
21717
22031
 
21718
- Documents use a TipTap editor schema with these top-level elements:
21719
- - **documentHeader** — The document title (H1)
21720
- - **documentMeta** — Metadata display block (skip when reading/writing)
21721
- - **Body blocks** — paragraphs, headings (H2-H6), lists, code blocks, tables, images, and custom components
21722
-
21723
- ### Supported Markdown Elements
22032
+ Documents use a TipTap editor schema. The body supports:
21724
22033
  - Headings (# through ######)
21725
- - Paragraphs with **bold**, *italic*, ~~strikethrough~~, \`code\`, [links](url)
21726
- - Bullet lists, ordered lists, task lists
22034
+ - Paragraphs with **bold**, *italic*, ~~strikethrough~~, \\\`code\\\`, [links](url)
22035
+ - Bullet lists, ordered lists, task lists (- [x] / - [ ])
21727
22036
  - Code blocks with language
21728
22037
  - Blockquotes
21729
22038
  - Tables (pipe syntax)
21730
22039
  - Horizontal rules
21731
- - Images
21732
- - MDC components: callout, collapsible, steps, card, accordion, tabs, code-group, etc.
22040
+ - Images: \`![alt](src){width="..." height="..."}\`
22041
+ - Document embeds: \`![[docId]]\`
22042
+ - Inline doc links: \`[[docId|label]]\` or \`[label](/doc/docId)\`
22043
+ - MDC components: \`::tip\`, \`::note\`, \`::warning\`, \`::collapsible\`, \`::steps\`, \`::card\`, \`::accordion\`, \`::tabs\`, \`::code-group\`, etc.
22044
+ - Badges: \`:badge[Label]{color="..."}\`
22045
+ - Icons: \`:icon{name="star"}\`
21733
22046
 
21734
22047
  ### Writing Content with Frontmatter
21735
22048
 
21736
- You can set document title and metadata via YAML frontmatter:
22049
+ Set document title and metadata via YAML frontmatter:
21737
22050
 
21738
22051
  \`\`\`markdown
21739
22052
  ---
21740
22053
  title: My Document
21741
22054
  tags: [important, review]
21742
22055
  color: "#6366f1"
22056
+ icon: star
21743
22057
  priority: high
22058
+ checked: false
22059
+ datetimeStart: "2026-03-20T10:00:00Z"
22060
+ datetimeEnd: "2026-03-20T11:00:00Z"
22061
+ allDay: false
22062
+ geoLat: 37.7749
22063
+ geoLng: -122.4194
22064
+ geoType: marker
21744
22065
  ---
21745
22066
 
21746
22067
  Content goes here...
@@ -21762,8 +22083,7 @@ You do **not** need to call \`set_presence\` after reading or writing content
21762
22083
  |---|---|
21763
22084
  | \`read_document\` | Root \`docId\` updated; TipTap cursor placed at start of document |
21764
22085
  | \`write_document\` | Root \`docId\` updated; TipTap cursor placed at end of written content |
21765
-
21766
- The TipTap cursor (\`anchor\`/\`head\` as \`Y.RelativePosition\`) makes your colored caret with name label appear inline in the document editor for human collaborators.
22086
+ | \`create_document\` | Root \`docId\` updated to the new document |
21767
22087
 
21768
22088
  ### set_presence — manual overrides only
21769
22089
 
@@ -21785,8 +22105,6 @@ Always clear fields when done by setting them to \`null\`.
21785
22105
  | **Calendar** | \`calendar:focused\` | eventId | Interacting with an event |
21786
22106
  | **Calendar** | \`calendar:viewing\` | "YYYY-MM" | The month/week you're looking at |
21787
22107
  | **Outline** | \`outline:editing\` | nodeId | Editing an outline node |
21788
- | **Outline** | \`outline:selected\` | nodeId | Node selected/hovered |
21789
- | **Slides** | \`slides:viewing\` | slideId | The slide you're currently on |
21790
22108
  | **Gallery** | \`gallery:focused\` | itemId | Item hovered/selected |
21791
22109
  | **Timeline** | \`timeline:focused\` | taskId | Task selected |
21792
22110
  | **Mindmap** | \`mindmap:focused\` | nodeId | Node selected/edited |
@@ -21794,7 +22112,7 @@ Always clear fields when done by setting them to \`null\`.
21794
22112
  | **Map** | \`map:focused\` | markerId | Marker hovered/selected |
21795
22113
  | **Doc** | \`doc:scroll\` | 0–1 number | Scroll position in document |
21796
22114
 
21797
- Example — mark a kanban card as being inspected, then clear on completion:
22115
+ Example — mark a kanban card as being inspected, then clear:
21798
22116
  \`\`\`
21799
22117
  set_doc_awareness(boardDocId, { "kanban:hovering": cardId })
21800
22118
  // ... do work ...
@@ -21805,73 +22123,48 @@ set_doc_awareness(boardDocId, { "kanban:hovering": null })
21805
22123
 
21806
22124
  ## Receiving Instructions from Humans
21807
22125
 
21808
- Three ways humans can send you tasks:
21809
-
21810
22126
  **Channel events (preferred):** When running in channel mode, \`ai:task\` events from
21811
22127
  human users arrive as \`<channel source="abracadabra" ...>\` notifications directly in
21812
- your session. These include the sender name, task ID, and document context. Use the
21813
- \`reply\` tool to send your response back — it creates a reply document and signals
21814
- task completion.
22128
+ your session. Use the \`reply\` tool to send your response it creates a reply document
22129
+ and signals task completion.
21815
22130
 
21816
- **Chat document watching:** Use \`watch_chat\` to observe a document for new messages.
21817
- Changes are pushed as channel notifications in real time. Reply using the \`reply\` tool
21818
- with the chat document's ID.
22131
+ **Chat messages:** Chat messages from the platform arrive as channel notifications. Use
22132
+ \`send_chat_message\` to respond in the same channel.
21819
22133
 
21820
22134
  **Polling (fallback):** Call \`poll_inbox\` to read the "AI Inbox" document.
21821
- Act on any pending tasks you find there. Write your response back with \`write_document\`
21822
- or \`create_document\` under the inbox.
21823
-
21824
- ### Channel Tools
21825
-
21826
- | Tool | Purpose |
21827
- |------|---------|
21828
- | \`reply\` | Send a reply to Abracadabra — creates a child doc with your response. Pass \`task_id\` to signal ai:task completion. |
21829
- | \`watch_chat\` | Start/stop watching a document for chat messages. New content arrives as channel notifications. |
21830
-
21831
- When you complete a task, always use \`reply\` (in channel mode) or write a response
21832
- document (in polling mode) so the human knows the work is done.
21833
22135
 
21834
22136
  ---
21835
22137
 
21836
22138
  ## Common Patterns
21837
22139
 
21838
- ### Populate a kanban board from a list
21839
- \`\`\`
21840
- 1. create_document → kanban parent
21841
- 2. For each column: create_document under kanban
21842
- 3. For each card: create_document under the appropriate column
21843
- 4. Optionally set metadata (color, tags, priority) on cards
21844
- \`\`\`
21845
-
21846
22140
  ### Explore a document fully (the correct way)
21847
22141
  \`\`\`
21848
- User: "Check out the Marketing doc"
21849
-
21850
- 1. read_document(marketingId)
22142
+ 1. read_document(docId)
21851
22143
  → returns { markdown, children: [...] }
21852
22144
  2. The body may be empty — that does NOT mean the doc is empty.
21853
22145
  Children ARE the content. Read every child:
21854
22146
  read_document(childId1), read_document(childId2), ...
21855
- 3. Each child's response also includes ITS children.
21856
- Continue recursively until no children remain.
22147
+ 3. Continue recursively until no children remain.
21857
22148
  \`\`\`
21858
22149
 
21859
22150
  **Never conclude a document is "empty" just because its markdown body is empty.
21860
22151
  Always check and traverse the \`children\` array returned by \`read_document\`.**
21861
22152
 
21862
- ### Write rich content to a document
22153
+ ### Populate a kanban board from a list
21863
22154
  \`\`\`
21864
- 1. read_document to see current content (and children)
21865
- 2. write_document with markdown (mode: "replace" to overwrite, "append" to add)
21866
- 3. Include frontmatter for title/metadata if needed
22155
+ 1. create_document kanban parent
22156
+ 2. For each column: create_document under kanban
22157
+ 3. For each card: create_document under the appropriate column
22158
+ 4. Optionally set metadata (color, tags, priority) on cards
21867
22159
  \`\`\`
21868
22160
 
21869
- ### Organize documents into a hierarchy
22161
+ ### Write rich content with references
21870
22162
  \`\`\`
21871
- 1. get_document_tree to understand current structure
21872
- 2. create_document to add new nodes
21873
- 3. move_document to reorganize
21874
- 4. change_document_type to switch views
22163
+ 1. read_document to see current content (and children)
22164
+ 2. write_document with markdown (mode: "replace" or "append")
22165
+ 3. Use ![[docId]] to embed other documents
22166
+ 4. Use [[docId|label]] for inline doc links
22167
+ 5. Include frontmatter for title/metadata if needed
21875
22168
  \`\`\`
21876
22169
 
21877
22170
  ---
@@ -21897,6 +22190,7 @@ Always check and traverse the \`children\` array returned by \`read_document\`.*
21897
22190
  - Use emoji as \`icon\` values — only lowercase kebab-case Lucide icon names are valid
21898
22191
  - Create top-level documents without first confirming the correct hub doc ID
21899
22192
  - Rename or write content to the space hub document itself
22193
+ - Set \`type\` on child items (cards, cells, events) — only set type on the parent page
21900
22194
  `;
21901
22195
  function registerAgentGuide(mcp) {
21902
22196
  mcp.resource("agent-guide", "abracadabra://agent-guide", {
@@ -21989,6 +22283,221 @@ function registerServerInfoResource(mcp, server) {
21989
22283
  });
21990
22284
  }
21991
22285
 
22286
+ //#endregion
22287
+ //#region packages/mcp/src/tools/hooks.ts
22288
+ function registerHookTools(mcp, hookBridge) {
22289
+ mcp.tool("get_hook_config", "Returns Claude Code hook configuration JSON for bridging activity to the Abracadabra dashboard. Copy the \"hooks\" object into your .claude/settings.local.json to enable real-time activity indicators (tool calls, subagents, etc.) visible to all connected users.", {}, async () => {
22290
+ const port = hookBridge.port;
22291
+ if (!port) return {
22292
+ content: [{
22293
+ type: "text",
22294
+ text: "Hook bridge is not running."
22295
+ }],
22296
+ isError: true
22297
+ };
22298
+ const hookEntry = {
22299
+ type: "http",
22300
+ url: `http://127.0.0.1:${port}/hook`,
22301
+ timeout: 3
22302
+ };
22303
+ const config = { hooks: {
22304
+ PreToolUse: [{ hooks: [hookEntry] }],
22305
+ PostToolUse: [{ hooks: [hookEntry] }],
22306
+ SubagentStart: [{ hooks: [hookEntry] }],
22307
+ SubagentStop: [{ hooks: [hookEntry] }],
22308
+ Stop: [{ hooks: [hookEntry] }]
22309
+ } };
22310
+ return { content: [{
22311
+ type: "text",
22312
+ text: JSON.stringify(config, null, 2)
22313
+ }] };
22314
+ });
22315
+ }
22316
+
22317
+ //#endregion
22318
+ //#region packages/mcp/src/hook-bridge.ts
22319
+ /**
22320
+ * HookBridge — lightweight HTTP server that receives Claude Code hook events
22321
+ * and translates them into Yjs awareness updates via AbracadabraMCPServer.
22322
+ *
22323
+ * Claude Code hooks (PreToolUse, PostToolUse, SubagentStart, SubagentStop, Stop)
22324
+ * POST JSON to http://127.0.0.1:{port}/hook. The bridge maps these to awareness
22325
+ * fields (status, activeToolCall) so the cou-sh dashboard shows real-time activity.
22326
+ */
22327
+ /** Map Claude Code tool names to awareness-friendly names + extract a target string. */
22328
+ function mapToolCall(toolName, toolInput) {
22329
+ switch (toolName) {
22330
+ case "Bash": return {
22331
+ name: "bash",
22332
+ target: truncate(toolInput.command ?? toolInput.description, 60)
22333
+ };
22334
+ case "Read": return {
22335
+ name: "read_file",
22336
+ target: basename(toolInput.file_path)
22337
+ };
22338
+ case "Edit": return {
22339
+ name: "edit_file",
22340
+ target: basename(toolInput.file_path)
22341
+ };
22342
+ case "Write": return {
22343
+ name: "write_file",
22344
+ target: basename(toolInput.file_path)
22345
+ };
22346
+ case "Grep": return {
22347
+ name: "grep",
22348
+ target: truncate(toolInput.pattern, 40)
22349
+ };
22350
+ case "Glob": return {
22351
+ name: "glob",
22352
+ target: truncate(toolInput.pattern, 40)
22353
+ };
22354
+ case "Agent": return {
22355
+ name: "subagent",
22356
+ target: toolInput.description || toolInput.subagent_type || "agent"
22357
+ };
22358
+ case "WebFetch": return {
22359
+ name: "web_fetch",
22360
+ target: hostname(toolInput.url)
22361
+ };
22362
+ case "WebSearch": return {
22363
+ name: "web_search",
22364
+ target: truncate(toolInput.query, 40)
22365
+ };
22366
+ default: return { name: toolName.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() };
22367
+ }
22368
+ }
22369
+ function truncate(str, max) {
22370
+ if (!str) return void 0;
22371
+ return str.length > max ? str.slice(0, max) + "..." : str;
22372
+ }
22373
+ function basename(filePath) {
22374
+ if (!filePath) return void 0;
22375
+ return node_path.basename(filePath);
22376
+ }
22377
+ function hostname(url) {
22378
+ if (!url) return void 0;
22379
+ try {
22380
+ return new URL(url).hostname;
22381
+ } catch {
22382
+ return url.slice(0, 30);
22383
+ }
22384
+ }
22385
+ var HookBridge = class {
22386
+ constructor(server) {
22387
+ this.server = server;
22388
+ this.httpServer = null;
22389
+ this._port = null;
22390
+ this.portFilePath = process.env.ABRA_HOOK_PORT_FILE || node_path.join(node_os.tmpdir(), "abracadabra-mcp-hook.port");
22391
+ }
22392
+ get port() {
22393
+ return this._port;
22394
+ }
22395
+ /** Start the HTTP server on a random port and write the port file. */
22396
+ async start() {
22397
+ return new Promise((resolve, reject) => {
22398
+ const srv = node_http.createServer((req, res) => this.handleRequest(req, res));
22399
+ srv.on("error", reject);
22400
+ srv.listen(0, "127.0.0.1", () => {
22401
+ const addr = srv.address();
22402
+ if (!addr || typeof addr === "string") {
22403
+ reject(/* @__PURE__ */ new Error("Failed to get server address"));
22404
+ return;
22405
+ }
22406
+ this._port = addr.port;
22407
+ this.httpServer = srv;
22408
+ try {
22409
+ node_fs.writeFileSync(this.portFilePath, String(this._port), "utf-8");
22410
+ } catch (err) {
22411
+ console.error(`[hook-bridge] Warning: could not write port file: ${err.message}`);
22412
+ }
22413
+ console.error(`[hook-bridge] Listening on 127.0.0.1:${this._port}`);
22414
+ console.error(`[hook-bridge] Port file: ${this.portFilePath}`);
22415
+ resolve(this._port);
22416
+ });
22417
+ });
22418
+ }
22419
+ /** Shut down the HTTP server and remove the port file. */
22420
+ async destroy() {
22421
+ if (this.httpServer) {
22422
+ await new Promise((resolve) => {
22423
+ this.httpServer.close(() => resolve());
22424
+ });
22425
+ this.httpServer = null;
22426
+ }
22427
+ try {
22428
+ node_fs.unlinkSync(this.portFilePath);
22429
+ } catch {}
22430
+ this._port = null;
22431
+ console.error("[hook-bridge] Shut down");
22432
+ }
22433
+ handleRequest(req, res) {
22434
+ if (req.method !== "POST" || req.url !== "/hook") {
22435
+ res.writeHead(404);
22436
+ res.end();
22437
+ return;
22438
+ }
22439
+ let body = "";
22440
+ req.on("data", (chunk) => {
22441
+ body += chunk.toString();
22442
+ });
22443
+ req.on("end", () => {
22444
+ res.writeHead(200, { "Content-Type": "application/json" });
22445
+ res.end("{}");
22446
+ try {
22447
+ const payload = JSON.parse(body);
22448
+ this.routeEvent(payload);
22449
+ } catch {}
22450
+ });
22451
+ }
22452
+ routeEvent(payload) {
22453
+ switch (payload.hook_event_name) {
22454
+ case "PreToolUse":
22455
+ this.onPreToolUse(payload);
22456
+ break;
22457
+ case "PostToolUse":
22458
+ this.onPostToolUse(payload);
22459
+ break;
22460
+ case "SubagentStart":
22461
+ this.onSubagentStart(payload);
22462
+ break;
22463
+ case "SubagentStop":
22464
+ this.onSubagentStop(payload);
22465
+ break;
22466
+ case "Stop":
22467
+ this.onStop();
22468
+ break;
22469
+ }
22470
+ }
22471
+ onPreToolUse(payload) {
22472
+ const toolName = payload.tool_name ?? "";
22473
+ if (toolName.startsWith("mcp__abracadabra__")) return;
22474
+ const mapped = mapToolCall(toolName, payload.tool_input ?? {});
22475
+ if (mapped) {
22476
+ this.server.setActiveToolCall(mapped);
22477
+ this.server.setAutoStatus("working");
22478
+ }
22479
+ }
22480
+ onPostToolUse(payload) {
22481
+ if ((payload.tool_name ?? "").startsWith("mcp__abracadabra__")) return;
22482
+ this.server.setActiveToolCall(null);
22483
+ }
22484
+ onSubagentStart(payload) {
22485
+ const agentType = payload.agent_type ?? "agent";
22486
+ this.server.setActiveToolCall({
22487
+ name: "subagent",
22488
+ target: agentType
22489
+ });
22490
+ this.server.setAutoStatus("thinking");
22491
+ }
22492
+ onSubagentStop(_payload) {
22493
+ this.server.setActiveToolCall(null);
22494
+ }
22495
+ onStop() {
22496
+ this.server.setAutoStatus(null);
22497
+ this.server.setActiveToolCall(null);
22498
+ }
22499
+ };
22500
+
21992
22501
  //#endregion
21993
22502
  //#region packages/mcp/src/index.ts
21994
22503
  /**
@@ -22030,8 +22539,9 @@ async function main() {
22030
22539
  ## Key Concepts
22031
22540
  - Documents form a tree. A kanban board's columns are child documents; cards are grandchildren.
22032
22541
  - A document's label IS its display name everywhere. Children ARE the content (not just the body text).
22033
- - Page types (doc, kanban, table, calendar, timeline, outline, etc.) are views over the SAME tree — switching types preserves data.
22542
+ - Page types (doc, kanban, table, calendar, timeline, checklist, outline, gallery, map, graph, dashboard, spatial, media, mindmap, etc.) are views over the SAME tree — switching types preserves data.
22034
22543
  - An empty markdown body does NOT mean empty content — always check the children array.
22544
+ - Use ![[docId]] in content to embed another document, or [[docId|label]] for inline links.
22035
22545
 
22036
22546
  ## Finding Documents
22037
22547
  - list_documents only shows ONE level of children. If you don't find what you need, use find_document to search the entire tree by name, or get_document_tree to see the full hierarchy.
@@ -22068,12 +22578,21 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
22068
22578
  console.error(`[abracadabra-mcp] Failed to connect: ${error.message}`);
22069
22579
  process.exit(1);
22070
22580
  }
22581
+ const hookBridge = new HookBridge(server);
22582
+ try {
22583
+ const hookPort = await hookBridge.start();
22584
+ console.error(`[abracadabra-mcp] Hook bridge listening on port ${hookPort}`);
22585
+ } catch (error) {
22586
+ console.error(`[abracadabra-mcp] Hook bridge failed to start: ${error.message}`);
22587
+ }
22588
+ registerHookTools(mcp, hookBridge);
22071
22589
  const transport = new StdioServerTransport();
22072
22590
  await mcp.connect(transport);
22073
22591
  server.startChannelNotifications(mcp);
22074
22592
  console.error("[abracadabra-mcp] MCP server running on stdio");
22075
22593
  const shutdown = async () => {
22076
22594
  console.error("[abracadabra-mcp] Shutting down...");
22595
+ await hookBridge.destroy();
22077
22596
  await server.destroy();
22078
22597
  process.exit(0);
22079
22598
  };