@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.
@@ -6,9 +6,11 @@ import { AbracadabraClient, AbracadabraProvider, awarenessStatesToArray } from "
6
6
  import { mkdir, readFile, writeFile } from "node:fs/promises";
7
7
  import * as fs from "node:fs";
8
8
  import { existsSync } from "node:fs";
9
+ import * as os from "node:os";
9
10
  import { homedir } from "node:os";
10
11
  import * as path from "node:path";
11
12
  import { dirname, join } from "node:path";
13
+ import * as http from "node:http";
12
14
 
13
15
  //#region \0rolldown/runtime.js
14
16
  var __create = Object.create;
@@ -4061,7 +4063,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-
4061
4063
  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])$/;
4062
4064
  const base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
4063
4065
  const base64url = /^[A-Za-z0-9_-]*$/;
4064
- const hostname = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
4066
+ const hostname$1 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
4065
4067
  const e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
4066
4068
  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])))`;
4067
4069
  const date$1 = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
@@ -4608,7 +4610,7 @@ const $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
4608
4610
  code: "invalid_format",
4609
4611
  format: "url",
4610
4612
  note: "Invalid hostname",
4611
- pattern: hostname.source,
4613
+ pattern: hostname$1.source,
4612
4614
  input: payload.value,
4613
4615
  inst,
4614
4616
  continue: !def.abort
@@ -19443,6 +19445,9 @@ var AbracadabraMCPServer = class {
19443
19445
  this._serverRef = null;
19444
19446
  this._handledTaskIds = /* @__PURE__ */ new Set();
19445
19447
  this._userId = null;
19448
+ this._statusClearTimer = null;
19449
+ this._typingInterval = null;
19450
+ this._lastChatChannel = null;
19446
19451
  this.config = config;
19447
19452
  this.client = new AbracadabraClient({
19448
19453
  url: config.url,
@@ -19537,8 +19542,12 @@ var AbracadabraMCPServer = class {
19537
19542
  provider.awareness.setLocalStateField("user", {
19538
19543
  name: this.agentName,
19539
19544
  color: this.agentColor,
19540
- publicKey: this._userId
19545
+ publicKey: this._userId,
19546
+ isAgent: true
19541
19547
  });
19548
+ provider.awareness.setLocalStateField("status", null);
19549
+ provider.awareness.setLocalStateField("activeToolCall", null);
19550
+ provider.awareness.setLocalStateField("statusContext", null);
19542
19551
  const conn = {
19543
19552
  doc,
19544
19553
  provider,
@@ -19590,7 +19599,8 @@ var AbracadabraMCPServer = class {
19590
19599
  childProvider.awareness.setLocalStateField("user", {
19591
19600
  name: this.agentName,
19592
19601
  color: this.agentColor,
19593
- publicKey: this._userId
19602
+ publicKey: this._userId,
19603
+ isAgent: true
19594
19604
  });
19595
19605
  this.childCache.set(docId, {
19596
19606
  provider: childProvider,
@@ -19657,6 +19667,7 @@ var AbracadabraMCPServer = class {
19657
19667
  const user = state["user"];
19658
19668
  const senderName = user && typeof user === "object" && typeof user.name === "string" ? user.name : "Unknown";
19659
19669
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`);
19670
+ this.setAutoStatus("thinking");
19660
19671
  this._dispatchAiTask({
19661
19672
  id,
19662
19673
  text,
@@ -19721,6 +19732,18 @@ var AbracadabraMCPServer = class {
19721
19732
  const parts = channel.split(":");
19722
19733
  if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) return;
19723
19734
  }
19735
+ if (channel) {
19736
+ const rootProvider = this._activeConnection?.provider;
19737
+ if (rootProvider) rootProvider.sendStateless(JSON.stringify({
19738
+ type: "chat:mark_read",
19739
+ channel,
19740
+ timestamp: Math.floor(Date.now() / 1e3)
19741
+ }));
19742
+ this._lastChatChannel = channel;
19743
+ this.sendTypingIndicator(channel);
19744
+ this._startTypingInterval(channel);
19745
+ }
19746
+ this.setAutoStatus("thinking");
19724
19747
  await this._serverRef.notification({
19725
19748
  method: "notifications/claude/channel",
19726
19749
  params: {
@@ -19738,15 +19761,82 @@ var AbracadabraMCPServer = class {
19738
19761
  console.error(`[abracadabra-mcp] Chat message from ${data.sender_name ?? "unknown"} on ${channel}`);
19739
19762
  } catch {}
19740
19763
  }
19764
+ /**
19765
+ * Set the agent's status in root awareness with auto-clear after idle.
19766
+ * @param statusContext — scopes the status to a specific channel/context so the
19767
+ * dashboard only shows it in the relevant chat. Defaults to `_lastChatChannel`.
19768
+ */
19769
+ setAutoStatus(status, docId, statusContext) {
19770
+ const provider = this._activeConnection?.provider;
19771
+ if (!provider) return;
19772
+ if (this._statusClearTimer) {
19773
+ clearTimeout(this._statusClearTimer);
19774
+ this._statusClearTimer = null;
19775
+ }
19776
+ provider.awareness.setLocalStateField("status", status);
19777
+ if (docId !== void 0) provider.awareness.setLocalStateField("docId", docId);
19778
+ const context = status ? statusContext !== void 0 ? statusContext : this._lastChatChannel : null;
19779
+ provider.awareness.setLocalStateField("statusContext", context ?? null);
19780
+ if (!status) this._stopTypingInterval();
19781
+ if (status) this._statusClearTimer = setTimeout(() => {
19782
+ provider.awareness.setLocalStateField("status", null);
19783
+ provider.awareness.setLocalStateField("activeToolCall", null);
19784
+ provider.awareness.setLocalStateField("statusContext", null);
19785
+ this._stopTypingInterval();
19786
+ }, 1e4);
19787
+ }
19788
+ /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
19789
+ _startTypingInterval(channel) {
19790
+ this._stopTypingInterval();
19791
+ this._typingInterval = setInterval(() => {
19792
+ this.sendTypingIndicator(channel);
19793
+ }, 2e3);
19794
+ }
19795
+ _stopTypingInterval() {
19796
+ if (this._typingInterval) {
19797
+ clearInterval(this._typingInterval);
19798
+ this._typingInterval = null;
19799
+ }
19800
+ this._lastChatChannel = null;
19801
+ }
19802
+ /**
19803
+ * Broadcast which tool the agent is currently executing.
19804
+ * Dashboard renders this as a ChatTool indicator.
19805
+ */
19806
+ setActiveToolCall(toolCall) {
19807
+ this._activeConnection?.provider?.awareness.setLocalStateField("activeToolCall", toolCall);
19808
+ }
19809
+ /**
19810
+ * Send a typing indicator to a chat channel.
19811
+ */
19812
+ sendTypingIndicator(channel) {
19813
+ const rootProvider = this._activeConnection?.provider;
19814
+ if (!rootProvider) return;
19815
+ rootProvider.sendStateless(JSON.stringify({
19816
+ type: "chat:typing",
19817
+ channel,
19818
+ sender_name: this.agentName
19819
+ }));
19820
+ }
19741
19821
  /** Graceful shutdown. */
19742
19822
  async destroy() {
19823
+ this._stopTypingInterval();
19824
+ if (this._statusClearTimer) {
19825
+ clearTimeout(this._statusClearTimer);
19826
+ this._statusClearTimer = null;
19827
+ }
19743
19828
  if (this.evictionTimer) {
19744
19829
  clearInterval(this.evictionTimer);
19745
19830
  this.evictionTimer = null;
19746
19831
  }
19747
19832
  for (const [, cached] of this.childCache) cached.provider.destroy();
19748
19833
  this.childCache.clear();
19749
- for (const [, conn] of this._spaceConnections) conn.provider.destroy();
19834
+ for (const [, conn] of this._spaceConnections) {
19835
+ conn.provider.awareness.setLocalStateField("status", null);
19836
+ conn.provider.awareness.setLocalStateField("activeToolCall", null);
19837
+ conn.provider.awareness.setLocalStateField("statusContext", null);
19838
+ conn.provider.destroy();
19839
+ }
19750
19840
  this._spaceConnections.clear();
19751
19841
  this._activeConnection = null;
19752
19842
  console.error("[abracadabra-mcp] Shutdown complete");
@@ -19888,15 +19978,23 @@ function registerTreeTools(mcp, server) {
19888
19978
  mcp.tool("create_document", "Create a new document in the tree. Returns the new document ID.", {
19889
19979
  parentId: z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
19890
19980
  label: z.string().describe("Display name / title for the document."),
19891
- type: z.string().optional().describe("Page type: \"doc\", \"kanban\", \"calendar\", \"table\", \"outline\", \"gallery\", \"slides\", \"timeline\", \"whiteboard\", \"map\", \"dashboard\", \"mindmap\", \"graph\". Omit to inherit parent view."),
19981
+ type: 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."),
19892
19982
  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.")
19893
19983
  }, async ({ parentId, label, type, meta }) => {
19984
+ server.setAutoStatus("creating");
19985
+ server.setActiveToolCall({
19986
+ name: "create_document",
19987
+ target: label
19988
+ });
19894
19989
  const treeMap = server.getTreeMap();
19895
19990
  const rootDoc = server.rootDocument;
19896
- if (!treeMap || !rootDoc) return { content: [{
19897
- type: "text",
19898
- text: "Not connected"
19899
- }] };
19991
+ if (!treeMap || !rootDoc) {
19992
+ server.setActiveToolCall(null);
19993
+ return { content: [{
19994
+ type: "text",
19995
+ text: "Not connected"
19996
+ }] };
19997
+ }
19900
19998
  const id = crypto.randomUUID();
19901
19999
  const normalizedParent = normalizeRootId(parentId, server);
19902
20000
  const now = Date.now();
@@ -19911,6 +20009,8 @@ function registerTreeTools(mcp, server) {
19911
20009
  updatedAt: now
19912
20010
  });
19913
20011
  });
20012
+ server.setFocusedDoc(id);
20013
+ server.setActiveToolCall(null);
19914
20014
  return { content: [{
19915
20015
  type: "text",
19916
20016
  text: JSON.stringify({
@@ -20065,6 +20165,7 @@ function registerTreeTools(mcp, server) {
20065
20165
  label: (entry.label || "Untitled") + " (copy)",
20066
20166
  order: Date.now()
20067
20167
  });
20168
+ server.setFocusedDoc(newId);
20068
20169
  return { content: [{
20069
20170
  type: "text",
20070
20171
  text: JSON.stringify({
@@ -20150,11 +20251,44 @@ function parseFrontmatter(markdown) {
20150
20251
  if (subtitle) meta.subtitle = subtitle;
20151
20252
  const url = getStr(["url"]);
20152
20253
  if (url) meta.url = url;
20254
+ const email = getStr(["email"]);
20255
+ if (email) meta.email = email;
20256
+ const phone = getStr(["phone"]);
20257
+ if (phone) meta.phone = phone;
20153
20258
  const ratingRaw = getStr(["rating"]);
20154
20259
  if (ratingRaw !== void 0) {
20155
20260
  const n = Number(ratingRaw);
20156
20261
  if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
20157
20262
  }
20263
+ const datetimeStart = getStr(["datetimeStart"]);
20264
+ if (datetimeStart) meta.datetimeStart = datetimeStart;
20265
+ const datetimeEnd = getStr(["datetimeEnd"]);
20266
+ if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
20267
+ const allDayRaw = raw["allDay"];
20268
+ if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
20269
+ const geoLatRaw = getStr(["geoLat"]);
20270
+ if (geoLatRaw !== void 0) {
20271
+ const n = Number(geoLatRaw);
20272
+ if (!Number.isNaN(n)) meta.geoLat = n;
20273
+ }
20274
+ const geoLngRaw = getStr(["geoLng"]);
20275
+ if (geoLngRaw !== void 0) {
20276
+ const n = Number(geoLngRaw);
20277
+ if (!Number.isNaN(n)) meta.geoLng = n;
20278
+ }
20279
+ const geoType = getStr(["geoType"]);
20280
+ if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
20281
+ const geoDescription = getStr(["geoDescription"]);
20282
+ if (geoDescription) meta.geoDescription = geoDescription;
20283
+ const numberRaw = getStr(["number"]);
20284
+ if (numberRaw !== void 0) {
20285
+ const n = Number(numberRaw);
20286
+ if (!Number.isNaN(n)) meta.number = n;
20287
+ }
20288
+ const unit = getStr(["unit"]);
20289
+ if (unit) meta.unit = unit;
20290
+ const note = getStr(["note"]);
20291
+ if (note) meta.note = note;
20158
20292
  return {
20159
20293
  title: typeof raw["title"] === "string" ? raw["title"] : void 0,
20160
20294
  meta,
@@ -20192,8 +20326,12 @@ function parseInline(text) {
20192
20326
  attrs: { kbd: { value: kbdProps["value"] || "" } }
20193
20327
  });
20194
20328
  } else if (match[5] !== void 0) {
20195
- const displayText = match[6] ?? match[5];
20196
- tokens.push({ text: displayText });
20329
+ const docId = match[5];
20330
+ const displayText = match[6] ?? docId;
20331
+ tokens.push({
20332
+ text: displayText,
20333
+ attrs: { link: { href: `/doc/${docId}` } }
20334
+ });
20197
20335
  } else if (match[7] !== void 0) tokens.push({
20198
20336
  text: match[7],
20199
20337
  attrs: { strike: true }
@@ -20342,6 +20480,15 @@ function parseBlocks(markdown) {
20342
20480
  i++;
20343
20481
  continue;
20344
20482
  }
20483
+ const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/);
20484
+ if (docEmbedMatch) {
20485
+ blocks.push({
20486
+ type: "docEmbed",
20487
+ docId: docEmbedMatch[1]
20488
+ });
20489
+ i++;
20490
+ continue;
20491
+ }
20345
20492
  const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
20346
20493
  if (imgMatch) {
20347
20494
  const alt = imgMatch[1] ?? "";
@@ -20649,6 +20796,7 @@ function blockElName(b) {
20649
20796
  case "field": return "field";
20650
20797
  case "fieldGroup": return "fieldGroup";
20651
20798
  case "image": return "image";
20799
+ case "docEmbed": return "docEmbed";
20652
20800
  }
20653
20801
  }
20654
20802
  function fillBlock(el, block) {
@@ -20864,6 +21012,9 @@ function fillBlock(el, block) {
20864
21012
  if (block.width) el.setAttribute("width", block.width);
20865
21013
  if (block.height) el.setAttribute("height", block.height);
20866
21014
  break;
21015
+ case "docEmbed":
21016
+ el.setAttribute("docId", block.docId);
21017
+ break;
20867
21018
  }
20868
21019
  }
20869
21020
  function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
@@ -20912,6 +21063,7 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
20912
21063
  case "field": return new Y.XmlElement("field");
20913
21064
  case "fieldGroup": return new Y.XmlElement("fieldGroup");
20914
21065
  case "image": return new Y.XmlElement("image");
21066
+ case "docEmbed": return new Y.XmlElement("docEmbed");
20915
21067
  }
20916
21068
  });
20917
21069
  fragment.insert(0, [
@@ -20983,6 +21135,10 @@ function serializeElement(el, indent = "") {
20983
21135
  }
20984
21136
  case "horizontalRule": return "---";
20985
21137
  case "table": return serializeTable(el);
21138
+ case "docEmbed": {
21139
+ const docId = el.getAttribute("docId");
21140
+ return docId ? `![[${docId}]]` : "";
21141
+ }
20986
21142
  case "image": {
20987
21143
  const src = el.getAttribute("src") || "";
20988
21144
  const alt = el.getAttribute("alt") || "";
@@ -21145,6 +21301,11 @@ function yjsToMarkdown(fragment) {
21145
21301
  function registerContentTools(mcp, server) {
21146
21302
  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: z.string().describe("Document ID to read.") }, async ({ docId }) => {
21147
21303
  try {
21304
+ server.setAutoStatus("reading", docId);
21305
+ server.setActiveToolCall({
21306
+ name: "read_document",
21307
+ target: docId
21308
+ });
21148
21309
  const { title, markdown } = yjsToMarkdown((await server.getChildProvider(docId)).document.getXmlFragment("default"));
21149
21310
  server.setFocusedDoc(docId);
21150
21311
  server.setDocCursor(docId, 0);
@@ -21170,6 +21331,7 @@ function registerContentTools(mcp, server) {
21170
21331
  });
21171
21332
  children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
21172
21333
  }
21334
+ server.setActiveToolCall(null);
21173
21335
  const result = {
21174
21336
  label,
21175
21337
  type,
@@ -21182,6 +21344,7 @@ function registerContentTools(mcp, server) {
21182
21344
  text: JSON.stringify(result, null, 2)
21183
21345
  }] };
21184
21346
  } catch (error) {
21347
+ server.setActiveToolCall(null);
21185
21348
  return {
21186
21349
  content: [{
21187
21350
  type: "text",
@@ -21191,12 +21354,17 @@ function registerContentTools(mcp, server) {
21191
21354
  };
21192
21355
  }
21193
21356
  });
21194
- 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.", {
21357
+ 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.", {
21195
21358
  docId: z.string().describe("Document ID to write to."),
21196
21359
  markdown: z.string().describe("Markdown content to write. Can include YAML frontmatter with title and metadata fields."),
21197
21360
  mode: z.enum(["replace", "append"]).optional().describe("Write mode. \"replace\" clears existing content first (default). \"append\" adds to the end.")
21198
21361
  }, async ({ docId, markdown, mode }) => {
21199
21362
  try {
21363
+ server.setAutoStatus("writing", docId);
21364
+ server.setActiveToolCall({
21365
+ name: "write_document",
21366
+ target: docId
21367
+ });
21200
21368
  const writeMode = mode ?? "replace";
21201
21369
  const doc = (await server.getChildProvider(docId)).document;
21202
21370
  const fragment = doc.getXmlFragment("default");
@@ -21226,11 +21394,13 @@ function registerContentTools(mcp, server) {
21226
21394
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
21227
21395
  server.setFocusedDoc(docId);
21228
21396
  server.setDocCursor(docId, fragment.length);
21397
+ server.setActiveToolCall(null);
21229
21398
  return { content: [{
21230
21399
  type: "text",
21231
21400
  text: `Document ${docId} updated (${writeMode} mode)`
21232
21401
  }] };
21233
21402
  } catch (error) {
21403
+ server.setActiveToolCall(null);
21234
21404
  return {
21235
21405
  content: [{
21236
21406
  type: "text",
@@ -21268,7 +21438,7 @@ function registerMetaTools(mcp, server) {
21268
21438
  });
21269
21439
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
21270
21440
  docId: z.string().describe("Document ID."),
21271
- meta: z.record(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.")
21441
+ 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, 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.")
21272
21442
  }, async ({ docId, meta }) => {
21273
21443
  const treeMap = server.getTreeMap();
21274
21444
  if (!treeMap) return { content: [{
@@ -21513,15 +21683,23 @@ function registerChannelTools(mcp, server) {
21513
21683
  task_id: z.string().optional().describe("If replying to an ai:task, the task ID to clear from awareness.")
21514
21684
  }, async ({ doc_id, text, task_id }) => {
21515
21685
  try {
21686
+ server.setAutoStatus("writing", doc_id);
21687
+ server.setActiveToolCall({
21688
+ name: "reply",
21689
+ target: doc_id
21690
+ });
21516
21691
  const treeMap = server.getTreeMap();
21517
21692
  const rootDoc = server.rootDocument;
21518
- if (!treeMap || !rootDoc) return {
21519
- content: [{
21520
- type: "text",
21521
- text: "Not connected"
21522
- }],
21523
- isError: true
21524
- };
21693
+ if (!treeMap || !rootDoc) {
21694
+ server.setActiveToolCall(null);
21695
+ return {
21696
+ content: [{
21697
+ type: "text",
21698
+ text: "Not connected"
21699
+ }],
21700
+ isError: true
21701
+ };
21702
+ }
21525
21703
  const label = `AI Reply — ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)}: ${text.slice(0, 40).replace(/\n/g, " ")}`;
21526
21704
  const replyId = crypto.randomUUID();
21527
21705
  const now = Date.now();
@@ -21537,6 +21715,7 @@ function registerChannelTools(mcp, server) {
21537
21715
  });
21538
21716
  populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
21539
21717
  if (task_id) server.clearAiTask(task_id);
21718
+ server.setActiveToolCall(null);
21540
21719
  return { content: [{
21541
21720
  type: "text",
21542
21721
  text: JSON.stringify({
@@ -21545,6 +21724,7 @@ function registerChannelTools(mcp, server) {
21545
21724
  })
21546
21725
  }] };
21547
21726
  } catch (error) {
21727
+ server.setActiveToolCall(null);
21548
21728
  return {
21549
21729
  content: [{
21550
21730
  type: "text",
@@ -21573,6 +21753,8 @@ function registerChannelTools(mcp, server) {
21573
21753
  content: text,
21574
21754
  sender_name: server.agentName
21575
21755
  }));
21756
+ server.setAutoStatus(null);
21757
+ server.setActiveToolCall(null);
21576
21758
  return { content: [{
21577
21759
  type: "text",
21578
21760
  text: `Sent to ${channel}`
@@ -21604,6 +21786,18 @@ Every piece of visible data in Abracadabra — a calendar event, a kanban card,
21604
21786
 
21605
21787
  **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.
21606
21788
 
21789
+ The same tree viewed as different page types:
21790
+ \`\`\`
21791
+ My Doc
21792
+ ├── "To Do" → Kanban: column / Table: column / Outline: item
21793
+ │ ├── "Fix bug" → Kanban: card / Table: cell / Outline: sub-item
21794
+ │ └── "Add tests"
21795
+ ├── "In Progress"
21796
+ │ └── "Refactor"
21797
+ └── "Done"
21798
+ └── "Deploy"
21799
+ \`\`\`
21800
+
21607
21801
  ---
21608
21802
 
21609
21803
  ## Getting Oriented in a Space
@@ -21623,23 +21817,24 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21623
21817
 
21624
21818
  ## Page Types Reference
21625
21819
 
21626
- | Type | Children Are | Grandchildren Are | Depth Limit |
21627
- |------|-------------|-------------------|-------------|
21628
- | **doc** | Sub-documents | Sub-sub-documents | Unlimited |
21629
- | **kanban** | Columns | Cards | 2 |
21630
- | **table** | Columns | Cells (positional rows) | 2 |
21631
- | **calendar** | Events | — | 1 |
21632
- | **timeline** | Epics | Tasks | 2 |
21633
- | **outline** | Items | Sub-items | Unlimited |
21634
- | **mindmap** | Central nodes | Branches | Unlimited |
21635
- | **graph** | Nodes | | 1 |
21636
- | **gallery** | Items | — | 1 |
21637
- | **slides** | Slides | — | 1 |
21638
- | **whiteboard** | Objects | | 1 |
21639
- | **map** | Markers/Lines | Points (for lines) | 2 |
21640
- | **desktop** | Items | | 1 |
21641
- | **call** | | — | 0 |
21642
- | **game** | | — | 0 |
21820
+ | Type | Children Are | Grandchildren Are | Depth | Key Meta on Children |
21821
+ |------|-------------|-------------------|-------|---------------------|
21822
+ | **doc** | Sub-documents | Sub-sub-documents | | — |
21823
+ | **kanban** | Columns | Cards | 2 | color, icon on cards |
21824
+ | **table** | Columns | Cells (positional rows) | 2 | — |
21825
+ | **calendar** | Events | — | 1 | datetimeStart, datetimeEnd, allDay, color |
21826
+ | **timeline** | Epics | Tasks | 2 | dateStart, dateEnd, taskProgress, color |
21827
+ | **checklist** | Tasks | Sub-tasks | | checked, priority, dateEnd |
21828
+ | **outline** | Items | Sub-items | | |
21829
+ | **mindmap** | Central nodes | Branches | | mmX, mmY |
21830
+ | **graph** | Nodes | — | 1 | graphX, graphY, graphPinned, color |
21831
+ | **gallery** | Items | — | 1 | geoLat, geoLng, datetimeStart, tags |
21832
+ | **map** | Markers/Lines | Points (for lines) | 2 | geoType, geoLat, geoLng, icon, color |
21833
+ | **dashboard** | Items | | 1 | deskX, deskY, deskMode |
21834
+ | **spatial** | Objects | Sub-parts | | spShape, spX/Y/Z, spColor, spOpacity |
21835
+ | **media** | Tracks | — | 1 | tags |
21836
+ | **slides** | Slides | — | 1 | — |
21837
+ | **whiteboard** | Objects | — | 1 | wbX, wbY, wbW, wbH |
21643
21838
 
21644
21839
  ---
21645
21840
 
@@ -21648,26 +21843,110 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21648
21843
  ### Creating Well-Structured Hierarchies
21649
21844
 
21650
21845
  **Kanban board:**
21651
- 1. Create the kanban document: \`create_document(parentId, "Project Board", "kanban")\`
21846
+ 1. \`create_document(parentId, "Project Board", "kanban")\`
21652
21847
  2. Create columns: \`create_document(boardId, "To Do")\`, \`create_document(boardId, "In Progress")\`, \`create_document(boardId, "Done")\`
21653
21848
  3. Create cards under columns: \`create_document(toDoColumnId, "Fix bug #123")\`
21849
+ 4. Optionally set card metadata: \`update_metadata(cardId, { color: "#ef4444", priority: 3, tags: ["urgent"] })\`
21850
+ 5. Optionally configure column width: \`update_metadata(boardId, { kanbanColumnWidth: "wide" })\`
21851
+
21852
+ **Table with data:**
21853
+ 1. \`create_document(parentId, "Contacts", "table")\`
21854
+ 2. Create columns: \`create_document(tableId, "Name")\`, \`create_document(tableId, "Email")\`, \`create_document(tableId, "Phone")\`
21855
+ 3. Create cells (children of columns): \`create_document(nameColId, "Alice")\`, \`create_document(emailColId, "alice@example.com")\`
21856
+ 4. **Rows are positional** — the Nth child of each column forms row N
21857
+ 5. Row count = max children across all columns. Missing cells render as empty.
21858
+ 6. To add a row: create a child under each column at the same index position
21859
+ 7. To delete row N: delete the Nth child from every column
21654
21860
 
21655
21861
  **Calendar with events:**
21656
- 1. Create calendar: \`create_document(parentId, "Team Calendar", "calendar")\`
21862
+ 1. \`create_document(parentId, "Team Calendar", "calendar")\`
21657
21863
  2. Create events: \`create_document(calendarId, "Sprint Planning")\`
21658
21864
  3. Set event times: \`update_metadata(eventId, { datetimeStart: "2026-03-20T10:00:00Z", datetimeEnd: "2026-03-20T11:00:00Z" })\`
21865
+ 4. All-day events: \`update_metadata(eventId, { datetimeStart: "2026-03-20", allDay: true })\`
21866
+ 5. Multi-day events: \`update_metadata(eventId, { datetimeStart: "2026-03-20", datetimeEnd: "2026-03-22", allDay: true })\`
21867
+ 6. Configure view: \`update_metadata(calendarId, { calendarView: "week" })\`
21659
21868
 
21660
- **Timeline with tasks:**
21661
- 1. Create timeline: \`create_document(parentId, "Q1 Roadmap", "timeline")\`
21869
+ **Timeline (Gantt chart):**
21870
+ 1. \`create_document(parentId, "Q1 Roadmap", "timeline")\`
21662
21871
  2. Create epics: \`create_document(timelineId, "Auth Rewrite")\`
21663
- 3. Create tasks: \`create_document(epicId, "Implement JWT refresh")\`
21664
- 4. Set dates/progress: \`update_metadata(taskId, { dateStart: "2026-03-01", dateEnd: "2026-03-15", taskProgress: 50 })\`
21872
+ 3. Create tasks under epics: \`create_document(epicId, "Implement JWT refresh")\`
21873
+ 4. Set dates and progress: \`update_metadata(taskId, { dateStart: "2026-03-01", dateEnd: "2026-03-15", taskProgress: 50, color: "#6366f1" })\`
21665
21874
 
21666
- **Table with data:**
21667
- 1. Create table: \`create_document(parentId, "Contacts", "table")\`
21668
- 2. Create columns: \`create_document(tableId, "Name")\`, \`create_document(tableId, "Email")\`, \`create_document(tableId, "Phone")\`
21669
- 3. Create cells (children of columns): \`create_document(nameColId, "Alice")\`, \`create_document(emailColId, "alice@example.com")\`
21670
- 4. Rows are positional the Nth child of each column forms row N
21875
+ **Checklist (nested task list):**
21876
+ 1. \`create_document(parentId, "Sprint Tasks", "checklist")\`
21877
+ 2. Create tasks: \`create_document(checklistId, "Write tests")\`
21878
+ 3. Set task properties: \`update_metadata(taskId, { checked: false, priority: 3, dateEnd: "2026-04-01" })\`
21879
+ 4. Create sub-tasks (unlimited nesting): \`create_document(taskId, "Unit tests")\`
21880
+
21881
+ **Map with markers and lines:**
21882
+ 1. \`create_document(parentId, "Travel Map", "map")\`
21883
+ 2. Create markers: \`create_document(mapId, "Coffee Shop")\`
21884
+ 3. Set marker position: \`update_metadata(markerId, { geoType: "marker", geoLat: 37.7749, geoLng: -122.4194, icon: "coffee", color: "#f97316" })\`
21885
+ 4. Create a line/route: \`create_document(mapId, "Walking Route")\`
21886
+ 5. Set line properties: \`update_metadata(routeId, { geoType: "line", color: "#3b82f6" })\`
21887
+ 6. Create line points as children: \`create_document(routeId, "Start")\` then \`update_metadata(pointId, { geoLat: 37.7749, geoLng: -122.4194 })\`
21888
+ 7. Create measuring tool: set \`geoType: "measure"\` instead of "line"
21889
+
21890
+ **Gallery:**
21891
+ 1. \`create_document(parentId, "Photo Album", "gallery")\`
21892
+ 2. Create items: \`create_document(galleryId, "Sunset at Beach")\`
21893
+ 3. Set metadata: \`update_metadata(itemId, { geoLat: 37.7749, geoLng: -122.4194, tags: ["travel", "sunset"] })\`
21894
+ 4. Configure layout: \`update_metadata(galleryId, { galleryColumns: 4, galleryAspect: "16:9" })\`
21895
+
21896
+ **Graph (knowledge graph):**
21897
+ 1. \`create_document(parentId, "Tech Stack", "graph")\`
21898
+ 2. Create nodes: \`create_document(graphId, "React")\`, \`create_document(graphId, "Vue")\`
21899
+ 3. Set node properties: \`update_metadata(nodeId, { color: "#6366f1", icon: "file-code" })\`
21900
+ 4. Edges are created by parent-child relationships AND document references (doc embeds/links in content)
21901
+ 5. Enable reference edges: \`update_metadata(graphId, { showRefEdges: true })\`
21902
+
21903
+ **Dashboard:**
21904
+ 1. \`create_document(parentId, "Overview", "dashboard")\`
21905
+ 2. Create items: \`create_document(dashboardId, "Revenue Chart")\`
21906
+ 3. Position items on grid: \`update_metadata(itemId, { deskX: 0, deskY: 0, deskMode: "widget-lg" })\`
21907
+ 4. Modes: \`"icon"\` (small), \`"widget-sm"\` (240×180), \`"widget-lg"\` (400×320)
21908
+ 5. Grid uses 80px cells
21909
+
21910
+ **Spatial (3D scene):**
21911
+ 1. \`create_document(parentId, "3D Scene", "spatial")\`
21912
+ 2. Create objects: \`create_document(sceneId, "Red Cube")\`
21913
+ 3. Set 3D properties: \`update_metadata(objId, { spShape: "box", spColor: "#ef4444", spX: 0, spY: 1, spZ: 0, spSX: 2, spSY: 2, spSZ: 2 })\`
21914
+ 4. Shapes: \`"box"\`, \`"sphere"\`, \`"cylinder"\`, \`"cone"\`, \`"plane"\`, \`"torus"\`, \`"glb"\` (uploaded 3D model)
21915
+ 5. Rotation (degrees): \`spRX\`, \`spRY\`, \`spRZ\`. Scale: \`spSX\`, \`spSY\`, \`spSZ\` (default 1). Opacity: \`spOpacity\` (0–100)
21916
+
21917
+ **Outline (nested items):**
21918
+ 1. \`create_document(parentId, "Meeting Notes", "outline")\`
21919
+ 2. Create items: \`create_document(outlineId, "Agenda Item 1")\`
21920
+ 3. Create sub-items (unlimited depth): \`create_document(itemId, "Sub-point")\`
21921
+
21922
+ ---
21923
+
21924
+ ## Document References
21925
+
21926
+ You can link and embed documents within rich-text content:
21927
+
21928
+ **Block-level doc embed** — renders an embedded document preview:
21929
+ \`\`\`
21930
+ ![[docId]]
21931
+ \`\`\`
21932
+
21933
+ **Inline link to another document:**
21934
+ \`\`\`
21935
+ [Link text](/doc/docId)
21936
+ \`\`\`
21937
+
21938
+ **Inline wikilink** (shorthand — converted to doc link):
21939
+ \`\`\`
21940
+ [[docId]]
21941
+ [[docId|Display Text]]
21942
+ \`\`\`
21943
+
21944
+ Example — write content with references:
21945
+ \`\`\`
21946
+ write_document(docId, "# Project Overview\\n\\nSee the detailed spec:\\n\\n![[specDocId]]\\n\\nCheck the [timeline](/doc/timelineDocId) for dates.")
21947
+ \`\`\`
21948
+
21949
+ In the **graph** page type, document references (embeds and links) create visible edges between nodes when \`showRefEdges\` is enabled.
21671
21950
 
21672
21951
  ---
21673
21952
 
@@ -21678,7 +21957,7 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21678
21957
  | Key | Type | Meaning |
21679
21958
  |-----|------|---------|
21680
21959
  | \`color\` | string | Hex/CSS color (e.g. "#6366f1") |
21681
- | \`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. |
21960
+ | \`icon\` | string | Lucide icon name in kebab-case (e.g. "star", "code-2", "users"). **Never emoji.** Omit for default. |
21682
21961
  | \`datetimeStart\` / \`datetimeEnd\` | string | ISO datetime with time |
21683
21962
  | \`allDay\` | boolean | Whether datetime is all-day |
21684
21963
  | \`dateStart\` / \`dateEnd\` | string | ISO date-only (e.g. "2026-03-20") |
@@ -21696,45 +21975,86 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
21696
21975
  | \`subtitle\` | string | Secondary text |
21697
21976
  | \`note\` | string | Free-text note |
21698
21977
  | \`taskProgress\` | number | 0–100 progress (timeline tasks) |
21978
+ | \`members\` | { id, label }[] | Assigned users |
21979
+ | \`coverUploadId\` | string | Cover image (upload ID from upload_file) |
21699
21980
 
21700
- ### Type-Specific Meta Schemas
21981
+ ### Geo/Map Keys (for map children)
21982
+
21983
+ | Key | Type | Meaning |
21984
+ |-----|------|---------|
21985
+ | \`geoType\` | string | "marker", "line", or "measure" |
21986
+ | \`geoLat\` | number | Latitude |
21987
+ | \`geoLng\` | number | Longitude |
21988
+ | \`geoDescription\` | string | Location description text |
21701
21989
 
21702
- **Calendar events** require: \`datetimeStart\`, \`datetimeEnd\`, optionally \`allDay\`, \`color\`
21990
+ ### Renderer Config Keys (set on the page doc itself, not its children)
21703
21991
 
21704
- **Timeline tasks** require: \`dateStart\`, \`dateEnd\`, optionally \`taskProgress\` (0–100), \`color\`
21992
+ | Key | Type | Applies to | Values |
21993
+ |-----|------|-----------|--------|
21994
+ | \`kanbanColumnWidth\` | string | kanban | "narrow", "default", "wide" |
21995
+ | \`galleryColumns\` | number | gallery | 1–6 |
21996
+ | \`galleryAspect\` | string | gallery | "square", "4:3", "16:9" |
21997
+ | \`calendarView\` | string | calendar | "month", "week", "day" |
21998
+ | \`calendarWeekStart\` | string | calendar | "sun", "mon" |
21999
+ | \`tableMode\` | string | table | "hierarchy", "flat" |
22000
+ | \`showRefEdges\` | boolean | graph | show doc-reference edges |
21705
22001
 
21706
- **Map markers** require: \`geoLat\`, \`geoLng\`, \`geoType\` ("marker"|"line"|"measure"), optionally \`icon\`, \`color\`
22002
+ ### Spatial 3D Keys (for spatial children)
22003
+
22004
+ | Key | Type | Default | Meaning |
22005
+ |-----|------|---------|---------|
22006
+ | \`spShape\` | string | — | "box", "sphere", "cylinder", "cone", "plane", "torus", "glb" |
22007
+ | \`spColor\` | string | — | CSS color |
22008
+ | \`spOpacity\` | number | 100 | 0–100 |
22009
+ | \`spX\`, \`spY\`, \`spZ\` | number | 0 | Position |
22010
+ | \`spRX\`, \`spRY\`, \`spRZ\` | number | 0 | Rotation (degrees) |
22011
+ | \`spSX\`, \`spSY\`, \`spSZ\` | number | 1 | Scale |
22012
+
22013
+ ### Dashboard Keys (for dashboard children)
22014
+
22015
+ | Key | Type | Meaning |
22016
+ |-----|------|---------|
22017
+ | \`deskX\`, \`deskY\` | number | Grid position (80px cells) |
22018
+ | \`deskZ\` | number | Z-index (layering) |
22019
+ | \`deskMode\` | string | "icon", "widget-sm" (240×180), "widget-lg" (400×320) |
21707
22020
 
21708
22021
  ---
21709
22022
 
21710
22023
  ## Content Structure
21711
22024
 
21712
- Documents use a TipTap editor schema with these top-level elements:
21713
- - **documentHeader** — The document title (H1)
21714
- - **documentMeta** — Metadata display block (skip when reading/writing)
21715
- - **Body blocks** — paragraphs, headings (H2-H6), lists, code blocks, tables, images, and custom components
21716
-
21717
- ### Supported Markdown Elements
22025
+ Documents use a TipTap editor schema. The body supports:
21718
22026
  - Headings (# through ######)
21719
- - Paragraphs with **bold**, *italic*, ~~strikethrough~~, \`code\`, [links](url)
21720
- - Bullet lists, ordered lists, task lists
22027
+ - Paragraphs with **bold**, *italic*, ~~strikethrough~~, \\\`code\\\`, [links](url)
22028
+ - Bullet lists, ordered lists, task lists (- [x] / - [ ])
21721
22029
  - Code blocks with language
21722
22030
  - Blockquotes
21723
22031
  - Tables (pipe syntax)
21724
22032
  - Horizontal rules
21725
- - Images
21726
- - MDC components: callout, collapsible, steps, card, accordion, tabs, code-group, etc.
22033
+ - Images: \`![alt](src){width="..." height="..."}\`
22034
+ - Document embeds: \`![[docId]]\`
22035
+ - Inline doc links: \`[[docId|label]]\` or \`[label](/doc/docId)\`
22036
+ - MDC components: \`::tip\`, \`::note\`, \`::warning\`, \`::collapsible\`, \`::steps\`, \`::card\`, \`::accordion\`, \`::tabs\`, \`::code-group\`, etc.
22037
+ - Badges: \`:badge[Label]{color="..."}\`
22038
+ - Icons: \`:icon{name="star"}\`
21727
22039
 
21728
22040
  ### Writing Content with Frontmatter
21729
22041
 
21730
- You can set document title and metadata via YAML frontmatter:
22042
+ Set document title and metadata via YAML frontmatter:
21731
22043
 
21732
22044
  \`\`\`markdown
21733
22045
  ---
21734
22046
  title: My Document
21735
22047
  tags: [important, review]
21736
22048
  color: "#6366f1"
22049
+ icon: star
21737
22050
  priority: high
22051
+ checked: false
22052
+ datetimeStart: "2026-03-20T10:00:00Z"
22053
+ datetimeEnd: "2026-03-20T11:00:00Z"
22054
+ allDay: false
22055
+ geoLat: 37.7749
22056
+ geoLng: -122.4194
22057
+ geoType: marker
21738
22058
  ---
21739
22059
 
21740
22060
  Content goes here...
@@ -21756,8 +22076,7 @@ You do **not** need to call \`set_presence\` after reading or writing content
21756
22076
  |---|---|
21757
22077
  | \`read_document\` | Root \`docId\` updated; TipTap cursor placed at start of document |
21758
22078
  | \`write_document\` | Root \`docId\` updated; TipTap cursor placed at end of written content |
21759
-
21760
- The TipTap cursor (\`anchor\`/\`head\` as \`Y.RelativePosition\`) makes your colored caret with name label appear inline in the document editor for human collaborators.
22079
+ | \`create_document\` | Root \`docId\` updated to the new document |
21761
22080
 
21762
22081
  ### set_presence — manual overrides only
21763
22082
 
@@ -21779,8 +22098,6 @@ Always clear fields when done by setting them to \`null\`.
21779
22098
  | **Calendar** | \`calendar:focused\` | eventId | Interacting with an event |
21780
22099
  | **Calendar** | \`calendar:viewing\` | "YYYY-MM" | The month/week you're looking at |
21781
22100
  | **Outline** | \`outline:editing\` | nodeId | Editing an outline node |
21782
- | **Outline** | \`outline:selected\` | nodeId | Node selected/hovered |
21783
- | **Slides** | \`slides:viewing\` | slideId | The slide you're currently on |
21784
22101
  | **Gallery** | \`gallery:focused\` | itemId | Item hovered/selected |
21785
22102
  | **Timeline** | \`timeline:focused\` | taskId | Task selected |
21786
22103
  | **Mindmap** | \`mindmap:focused\` | nodeId | Node selected/edited |
@@ -21788,7 +22105,7 @@ Always clear fields when done by setting them to \`null\`.
21788
22105
  | **Map** | \`map:focused\` | markerId | Marker hovered/selected |
21789
22106
  | **Doc** | \`doc:scroll\` | 0–1 number | Scroll position in document |
21790
22107
 
21791
- Example — mark a kanban card as being inspected, then clear on completion:
22108
+ Example — mark a kanban card as being inspected, then clear:
21792
22109
  \`\`\`
21793
22110
  set_doc_awareness(boardDocId, { "kanban:hovering": cardId })
21794
22111
  // ... do work ...
@@ -21799,73 +22116,48 @@ set_doc_awareness(boardDocId, { "kanban:hovering": null })
21799
22116
 
21800
22117
  ## Receiving Instructions from Humans
21801
22118
 
21802
- Three ways humans can send you tasks:
21803
-
21804
22119
  **Channel events (preferred):** When running in channel mode, \`ai:task\` events from
21805
22120
  human users arrive as \`<channel source="abracadabra" ...>\` notifications directly in
21806
- your session. These include the sender name, task ID, and document context. Use the
21807
- \`reply\` tool to send your response back — it creates a reply document and signals
21808
- task completion.
22121
+ your session. Use the \`reply\` tool to send your response it creates a reply document
22122
+ and signals task completion.
21809
22123
 
21810
- **Chat document watching:** Use \`watch_chat\` to observe a document for new messages.
21811
- Changes are pushed as channel notifications in real time. Reply using the \`reply\` tool
21812
- with the chat document's ID.
22124
+ **Chat messages:** Chat messages from the platform arrive as channel notifications. Use
22125
+ \`send_chat_message\` to respond in the same channel.
21813
22126
 
21814
22127
  **Polling (fallback):** Call \`poll_inbox\` to read the "AI Inbox" document.
21815
- Act on any pending tasks you find there. Write your response back with \`write_document\`
21816
- or \`create_document\` under the inbox.
21817
-
21818
- ### Channel Tools
21819
-
21820
- | Tool | Purpose |
21821
- |------|---------|
21822
- | \`reply\` | Send a reply to Abracadabra — creates a child doc with your response. Pass \`task_id\` to signal ai:task completion. |
21823
- | \`watch_chat\` | Start/stop watching a document for chat messages. New content arrives as channel notifications. |
21824
-
21825
- When you complete a task, always use \`reply\` (in channel mode) or write a response
21826
- document (in polling mode) so the human knows the work is done.
21827
22128
 
21828
22129
  ---
21829
22130
 
21830
22131
  ## Common Patterns
21831
22132
 
21832
- ### Populate a kanban board from a list
21833
- \`\`\`
21834
- 1. create_document → kanban parent
21835
- 2. For each column: create_document under kanban
21836
- 3. For each card: create_document under the appropriate column
21837
- 4. Optionally set metadata (color, tags, priority) on cards
21838
- \`\`\`
21839
-
21840
22133
  ### Explore a document fully (the correct way)
21841
22134
  \`\`\`
21842
- User: "Check out the Marketing doc"
21843
-
21844
- 1. read_document(marketingId)
22135
+ 1. read_document(docId)
21845
22136
  → returns { markdown, children: [...] }
21846
22137
  2. The body may be empty — that does NOT mean the doc is empty.
21847
22138
  Children ARE the content. Read every child:
21848
22139
  read_document(childId1), read_document(childId2), ...
21849
- 3. Each child's response also includes ITS children.
21850
- Continue recursively until no children remain.
22140
+ 3. Continue recursively until no children remain.
21851
22141
  \`\`\`
21852
22142
 
21853
22143
  **Never conclude a document is "empty" just because its markdown body is empty.
21854
22144
  Always check and traverse the \`children\` array returned by \`read_document\`.**
21855
22145
 
21856
- ### Write rich content to a document
22146
+ ### Populate a kanban board from a list
21857
22147
  \`\`\`
21858
- 1. read_document to see current content (and children)
21859
- 2. write_document with markdown (mode: "replace" to overwrite, "append" to add)
21860
- 3. Include frontmatter for title/metadata if needed
22148
+ 1. create_document kanban parent
22149
+ 2. For each column: create_document under kanban
22150
+ 3. For each card: create_document under the appropriate column
22151
+ 4. Optionally set metadata (color, tags, priority) on cards
21861
22152
  \`\`\`
21862
22153
 
21863
- ### Organize documents into a hierarchy
22154
+ ### Write rich content with references
21864
22155
  \`\`\`
21865
- 1. get_document_tree to understand current structure
21866
- 2. create_document to add new nodes
21867
- 3. move_document to reorganize
21868
- 4. change_document_type to switch views
22156
+ 1. read_document to see current content (and children)
22157
+ 2. write_document with markdown (mode: "replace" or "append")
22158
+ 3. Use ![[docId]] to embed other documents
22159
+ 4. Use [[docId|label]] for inline doc links
22160
+ 5. Include frontmatter for title/metadata if needed
21869
22161
  \`\`\`
21870
22162
 
21871
22163
  ---
@@ -21891,6 +22183,7 @@ Always check and traverse the \`children\` array returned by \`read_document\`.*
21891
22183
  - Use emoji as \`icon\` values — only lowercase kebab-case Lucide icon names are valid
21892
22184
  - Create top-level documents without first confirming the correct hub doc ID
21893
22185
  - Rename or write content to the space hub document itself
22186
+ - Set \`type\` on child items (cards, cells, events) — only set type on the parent page
21894
22187
  `;
21895
22188
  function registerAgentGuide(mcp) {
21896
22189
  mcp.resource("agent-guide", "abracadabra://agent-guide", {
@@ -21983,6 +22276,221 @@ function registerServerInfoResource(mcp, server) {
21983
22276
  });
21984
22277
  }
21985
22278
 
22279
+ //#endregion
22280
+ //#region packages/mcp/src/tools/hooks.ts
22281
+ function registerHookTools(mcp, hookBridge) {
22282
+ 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 () => {
22283
+ const port = hookBridge.port;
22284
+ if (!port) return {
22285
+ content: [{
22286
+ type: "text",
22287
+ text: "Hook bridge is not running."
22288
+ }],
22289
+ isError: true
22290
+ };
22291
+ const hookEntry = {
22292
+ type: "http",
22293
+ url: `http://127.0.0.1:${port}/hook`,
22294
+ timeout: 3
22295
+ };
22296
+ const config = { hooks: {
22297
+ PreToolUse: [{ hooks: [hookEntry] }],
22298
+ PostToolUse: [{ hooks: [hookEntry] }],
22299
+ SubagentStart: [{ hooks: [hookEntry] }],
22300
+ SubagentStop: [{ hooks: [hookEntry] }],
22301
+ Stop: [{ hooks: [hookEntry] }]
22302
+ } };
22303
+ return { content: [{
22304
+ type: "text",
22305
+ text: JSON.stringify(config, null, 2)
22306
+ }] };
22307
+ });
22308
+ }
22309
+
22310
+ //#endregion
22311
+ //#region packages/mcp/src/hook-bridge.ts
22312
+ /**
22313
+ * HookBridge — lightweight HTTP server that receives Claude Code hook events
22314
+ * and translates them into Yjs awareness updates via AbracadabraMCPServer.
22315
+ *
22316
+ * Claude Code hooks (PreToolUse, PostToolUse, SubagentStart, SubagentStop, Stop)
22317
+ * POST JSON to http://127.0.0.1:{port}/hook. The bridge maps these to awareness
22318
+ * fields (status, activeToolCall) so the cou-sh dashboard shows real-time activity.
22319
+ */
22320
+ /** Map Claude Code tool names to awareness-friendly names + extract a target string. */
22321
+ function mapToolCall(toolName, toolInput) {
22322
+ switch (toolName) {
22323
+ case "Bash": return {
22324
+ name: "bash",
22325
+ target: truncate(toolInput.command ?? toolInput.description, 60)
22326
+ };
22327
+ case "Read": return {
22328
+ name: "read_file",
22329
+ target: basename(toolInput.file_path)
22330
+ };
22331
+ case "Edit": return {
22332
+ name: "edit_file",
22333
+ target: basename(toolInput.file_path)
22334
+ };
22335
+ case "Write": return {
22336
+ name: "write_file",
22337
+ target: basename(toolInput.file_path)
22338
+ };
22339
+ case "Grep": return {
22340
+ name: "grep",
22341
+ target: truncate(toolInput.pattern, 40)
22342
+ };
22343
+ case "Glob": return {
22344
+ name: "glob",
22345
+ target: truncate(toolInput.pattern, 40)
22346
+ };
22347
+ case "Agent": return {
22348
+ name: "subagent",
22349
+ target: toolInput.description || toolInput.subagent_type || "agent"
22350
+ };
22351
+ case "WebFetch": return {
22352
+ name: "web_fetch",
22353
+ target: hostname(toolInput.url)
22354
+ };
22355
+ case "WebSearch": return {
22356
+ name: "web_search",
22357
+ target: truncate(toolInput.query, 40)
22358
+ };
22359
+ default: return { name: toolName.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() };
22360
+ }
22361
+ }
22362
+ function truncate(str, max) {
22363
+ if (!str) return void 0;
22364
+ return str.length > max ? str.slice(0, max) + "..." : str;
22365
+ }
22366
+ function basename(filePath) {
22367
+ if (!filePath) return void 0;
22368
+ return path.basename(filePath);
22369
+ }
22370
+ function hostname(url) {
22371
+ if (!url) return void 0;
22372
+ try {
22373
+ return new URL(url).hostname;
22374
+ } catch {
22375
+ return url.slice(0, 30);
22376
+ }
22377
+ }
22378
+ var HookBridge = class {
22379
+ constructor(server) {
22380
+ this.server = server;
22381
+ this.httpServer = null;
22382
+ this._port = null;
22383
+ this.portFilePath = process.env.ABRA_HOOK_PORT_FILE || path.join(os.tmpdir(), "abracadabra-mcp-hook.port");
22384
+ }
22385
+ get port() {
22386
+ return this._port;
22387
+ }
22388
+ /** Start the HTTP server on a random port and write the port file. */
22389
+ async start() {
22390
+ return new Promise((resolve, reject) => {
22391
+ const srv = http.createServer((req, res) => this.handleRequest(req, res));
22392
+ srv.on("error", reject);
22393
+ srv.listen(0, "127.0.0.1", () => {
22394
+ const addr = srv.address();
22395
+ if (!addr || typeof addr === "string") {
22396
+ reject(/* @__PURE__ */ new Error("Failed to get server address"));
22397
+ return;
22398
+ }
22399
+ this._port = addr.port;
22400
+ this.httpServer = srv;
22401
+ try {
22402
+ fs.writeFileSync(this.portFilePath, String(this._port), "utf-8");
22403
+ } catch (err) {
22404
+ console.error(`[hook-bridge] Warning: could not write port file: ${err.message}`);
22405
+ }
22406
+ console.error(`[hook-bridge] Listening on 127.0.0.1:${this._port}`);
22407
+ console.error(`[hook-bridge] Port file: ${this.portFilePath}`);
22408
+ resolve(this._port);
22409
+ });
22410
+ });
22411
+ }
22412
+ /** Shut down the HTTP server and remove the port file. */
22413
+ async destroy() {
22414
+ if (this.httpServer) {
22415
+ await new Promise((resolve) => {
22416
+ this.httpServer.close(() => resolve());
22417
+ });
22418
+ this.httpServer = null;
22419
+ }
22420
+ try {
22421
+ fs.unlinkSync(this.portFilePath);
22422
+ } catch {}
22423
+ this._port = null;
22424
+ console.error("[hook-bridge] Shut down");
22425
+ }
22426
+ handleRequest(req, res) {
22427
+ if (req.method !== "POST" || req.url !== "/hook") {
22428
+ res.writeHead(404);
22429
+ res.end();
22430
+ return;
22431
+ }
22432
+ let body = "";
22433
+ req.on("data", (chunk) => {
22434
+ body += chunk.toString();
22435
+ });
22436
+ req.on("end", () => {
22437
+ res.writeHead(200, { "Content-Type": "application/json" });
22438
+ res.end("{}");
22439
+ try {
22440
+ const payload = JSON.parse(body);
22441
+ this.routeEvent(payload);
22442
+ } catch {}
22443
+ });
22444
+ }
22445
+ routeEvent(payload) {
22446
+ switch (payload.hook_event_name) {
22447
+ case "PreToolUse":
22448
+ this.onPreToolUse(payload);
22449
+ break;
22450
+ case "PostToolUse":
22451
+ this.onPostToolUse(payload);
22452
+ break;
22453
+ case "SubagentStart":
22454
+ this.onSubagentStart(payload);
22455
+ break;
22456
+ case "SubagentStop":
22457
+ this.onSubagentStop(payload);
22458
+ break;
22459
+ case "Stop":
22460
+ this.onStop();
22461
+ break;
22462
+ }
22463
+ }
22464
+ onPreToolUse(payload) {
22465
+ const toolName = payload.tool_name ?? "";
22466
+ if (toolName.startsWith("mcp__abracadabra__")) return;
22467
+ const mapped = mapToolCall(toolName, payload.tool_input ?? {});
22468
+ if (mapped) {
22469
+ this.server.setActiveToolCall(mapped);
22470
+ this.server.setAutoStatus("working");
22471
+ }
22472
+ }
22473
+ onPostToolUse(payload) {
22474
+ if ((payload.tool_name ?? "").startsWith("mcp__abracadabra__")) return;
22475
+ this.server.setActiveToolCall(null);
22476
+ }
22477
+ onSubagentStart(payload) {
22478
+ const agentType = payload.agent_type ?? "agent";
22479
+ this.server.setActiveToolCall({
22480
+ name: "subagent",
22481
+ target: agentType
22482
+ });
22483
+ this.server.setAutoStatus("thinking");
22484
+ }
22485
+ onSubagentStop(_payload) {
22486
+ this.server.setActiveToolCall(null);
22487
+ }
22488
+ onStop() {
22489
+ this.server.setAutoStatus(null);
22490
+ this.server.setActiveToolCall(null);
22491
+ }
22492
+ };
22493
+
21986
22494
  //#endregion
21987
22495
  //#region packages/mcp/src/index.ts
21988
22496
  /**
@@ -22024,8 +22532,9 @@ async function main() {
22024
22532
  ## Key Concepts
22025
22533
  - Documents form a tree. A kanban board's columns are child documents; cards are grandchildren.
22026
22534
  - A document's label IS its display name everywhere. Children ARE the content (not just the body text).
22027
- - Page types (doc, kanban, table, calendar, timeline, outline, etc.) are views over the SAME tree — switching types preserves data.
22535
+ - 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.
22028
22536
  - An empty markdown body does NOT mean empty content — always check the children array.
22537
+ - Use ![[docId]] in content to embed another document, or [[docId|label]] for inline links.
22029
22538
 
22030
22539
  ## Finding Documents
22031
22540
  - 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.
@@ -22062,12 +22571,21 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
22062
22571
  console.error(`[abracadabra-mcp] Failed to connect: ${error.message}`);
22063
22572
  process.exit(1);
22064
22573
  }
22574
+ const hookBridge = new HookBridge(server);
22575
+ try {
22576
+ const hookPort = await hookBridge.start();
22577
+ console.error(`[abracadabra-mcp] Hook bridge listening on port ${hookPort}`);
22578
+ } catch (error) {
22579
+ console.error(`[abracadabra-mcp] Hook bridge failed to start: ${error.message}`);
22580
+ }
22581
+ registerHookTools(mcp, hookBridge);
22065
22582
  const transport = new StdioServerTransport();
22066
22583
  await mcp.connect(transport);
22067
22584
  server.startChannelNotifications(mcp);
22068
22585
  console.error("[abracadabra-mcp] MCP server running on stdio");
22069
22586
  const shutdown = async () => {
22070
22587
  console.error("[abracadabra-mcp] Shutting down...");
22588
+ await hookBridge.destroy();
22071
22589
  await server.destroy();
22072
22590
  process.exit(0);
22073
22591
  };