@abraca/mcp 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3757,7 +3757,7 @@ const propertyKeyTypes = new Set([
3757
3757
  "number",
3758
3758
  "symbol"
3759
3759
  ]);
3760
- function escapeRegex(str) {
3760
+ function escapeRegex$1(str) {
3761
3761
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3762
3762
  }
3763
3763
  function clone(inst, def, params) {
@@ -4468,7 +4468,7 @@ const $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (i
4468
4468
  });
4469
4469
  const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
4470
4470
  $ZodCheck.init(inst, def);
4471
- const escapedRegex = escapeRegex(def.includes);
4471
+ const escapedRegex = escapeRegex$1(def.includes);
4472
4472
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
4473
4473
  def.pattern = pattern;
4474
4474
  inst._zod.onattach.push((inst) => {
@@ -4491,7 +4491,7 @@ const $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (ins
4491
4491
  });
4492
4492
  const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
4493
4493
  $ZodCheck.init(inst, def);
4494
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
4494
+ const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
4495
4495
  def.pattern ?? (def.pattern = pattern);
4496
4496
  inst._zod.onattach.push((inst) => {
4497
4497
  const bag = inst._zod.bag;
@@ -4513,7 +4513,7 @@ const $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith",
4513
4513
  });
4514
4514
  const $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
4515
4515
  $ZodCheck.init(inst, def);
4516
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
4516
+ const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
4517
4517
  def.pattern ?? (def.pattern = pattern);
4518
4518
  inst._zod.onattach.push((inst) => {
4519
4519
  const bag = inst._zod.bag;
@@ -5545,7 +5545,7 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
5545
5545
  const values = getEnumValues(def.entries);
5546
5546
  const valuesSet = new Set(values);
5547
5547
  inst._zod.values = valuesSet;
5548
- inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
5548
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
5549
5549
  inst._zod.parse = (payload, _ctx) => {
5550
5550
  const input = payload.value;
5551
5551
  if (valuesSet.has(input)) return payload;
@@ -5563,7 +5563,7 @@ const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
5563
5563
  if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
5564
5564
  const values = new Set(def.values);
5565
5565
  inst._zod.values = values;
5566
- inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
5566
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
5567
5567
  inst._zod.parse = (payload, _ctx) => {
5568
5568
  const input = payload.value;
5569
5569
  if (values.has(input)) return payload;
@@ -19965,6 +19965,47 @@ function signChallenge(challengeB64, privateKey) {
19965
19965
  return toBase64url(sign(challenge, privateKey));
19966
19966
  }
19967
19967
 
19968
+ //#endregion
19969
+ //#region packages/mcp/src/mentions.ts
19970
+ /**
19971
+ * Mention parsing for chat messages.
19972
+ *
19973
+ * Recognizes `@alias` tokens (case-insensitive, word-boundary) so the agent
19974
+ * can decide whether a group-chat message is directed at it.
19975
+ */
19976
+ /** Escape regex metacharacters in an alias string. */
19977
+ function escapeRegex(s) {
19978
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19979
+ }
19980
+ /**
19981
+ * Build a regex that matches `@<alias>` for any of the given aliases.
19982
+ * Requires a non-word char (or start) before `@` and a word boundary after the alias
19983
+ * so `@Claude` matches but `email@claudesomething` does not.
19984
+ */
19985
+ function buildMentionRegex(aliases) {
19986
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
19987
+ if (cleaned.length === 0) return null;
19988
+ const alt = cleaned.map(escapeRegex).join("|");
19989
+ return new RegExp(`(?:^|[^\\w@])@(?:${alt})\\b`, "i");
19990
+ }
19991
+ /** Returns true if `text` contains `@alias` for any alias (case-insensitive). */
19992
+ function containsMention(text, aliases) {
19993
+ const re = buildMentionRegex(aliases);
19994
+ if (!re) return false;
19995
+ return re.test(text);
19996
+ }
19997
+ /**
19998
+ * Remove `@alias` tokens from the text. Leaves surrounding whitespace tidy so
19999
+ * the cleaned prompt reads naturally (e.g. `"@Claude help"` → `"help"`).
20000
+ */
20001
+ function stripMention(text, aliases) {
20002
+ const cleaned = aliases.map((a) => a.trim()).filter((a) => a.length > 0);
20003
+ if (cleaned.length === 0) return text;
20004
+ const alt = cleaned.map(escapeRegex).join("|");
20005
+ const re = new RegExp(`(^|\\s)@(?:${alt})\\b[,:]?\\s*`, "gi");
20006
+ return text.replace(re, (_m, lead) => lead ? " " : "").replace(/\s{2,}/g, " ").trim();
20007
+ }
20008
+
19968
20009
  //#endregion
19969
20010
  //#region packages/mcp/src/server.ts
19970
20011
  /**
@@ -19975,7 +20016,10 @@ function signChallenge(challengeB64, privateKey) {
19975
20016
  * Use switchSpace(docId) to change the active space.
19976
20017
  */
19977
20018
  const IDLE_TIMEOUT_MS = 300 * 1e3;
19978
- var AbracadabraMCPServer = class {
20019
+ var AbracadabraMCPServer = class AbracadabraMCPServer {
20020
+ static {
20021
+ this.TOOL_HISTORY_MAX = 20;
20022
+ }
19979
20023
  constructor(config) {
19980
20024
  this._serverInfo = null;
19981
20025
  this._rootDocId = null;
@@ -19992,6 +20036,7 @@ var AbracadabraMCPServer = class {
19992
20036
  this._typingInterval = null;
19993
20037
  this._lastChatChannel = null;
19994
20038
  this._signFn = null;
20039
+ this._toolHistory = [];
19995
20040
  this.config = config;
19996
20041
  this.client = new AbracadabraClient({
19997
20042
  url: config.url,
@@ -20004,6 +20049,14 @@ var AbracadabraMCPServer = class {
20004
20049
  get agentColor() {
20005
20050
  return this.config.agentColor || "hsl(270, 80%, 60%)";
20006
20051
  }
20052
+ get triggerMode() {
20053
+ return this.config.triggerMode ?? "mention+task";
20054
+ }
20055
+ get mentionAliases() {
20056
+ const explicit = this.config.mentionAliases?.filter((a) => a.trim().length > 0);
20057
+ if (explicit && explicit.length > 0) return explicit;
20058
+ return [this.agentName];
20059
+ }
20007
20060
  get serverInfo() {
20008
20061
  return this._serverInfo;
20009
20062
  }
@@ -20044,7 +20097,7 @@ var AbracadabraMCPServer = class {
20044
20097
  await this.client.loginWithKey(keypair.publicKeyB64, signFn);
20045
20098
  } else throw err;
20046
20099
  }
20047
- console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (${keypair.publicKeyB64.slice(0, 12)}...)`);
20100
+ console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`);
20048
20101
  this._serverInfo = await this.client.serverInfo();
20049
20102
  let initialDocId = this._serverInfo.index_doc_id ?? null;
20050
20103
  try {
@@ -20111,6 +20164,8 @@ var AbracadabraMCPServer = class {
20111
20164
  provider.awareness.setLocalStateField("status", null);
20112
20165
  provider.awareness.setLocalStateField("activeToolCall", null);
20113
20166
  provider.awareness.setLocalStateField("statusContext", null);
20167
+ provider.awareness.setLocalStateField("turnId", null);
20168
+ provider.awareness.setLocalStateField("toolHistory", []);
20114
20169
  const conn = {
20115
20170
  doc,
20116
20171
  provider,
@@ -20234,6 +20289,7 @@ var AbracadabraMCPServer = class {
20234
20289
  _observeRootAwareness(provider) {
20235
20290
  const selfId = provider.awareness.clientID;
20236
20291
  provider.awareness.on("change", () => {
20292
+ if (this.triggerMode === "mention") return;
20237
20293
  const states = provider.awareness.getStates();
20238
20294
  for (const [clientId, state] of states) {
20239
20295
  if (clientId === selfId) continue;
@@ -20246,6 +20302,7 @@ var AbracadabraMCPServer = class {
20246
20302
  const user = state["user"];
20247
20303
  const senderName = user && typeof user === "object" && typeof user.name === "string" ? user.name : "Unknown";
20248
20304
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`);
20305
+ this._beginTurn();
20249
20306
  this.setAutoStatus("thinking");
20250
20307
  this._dispatchAiTask({
20251
20308
  id,
@@ -20308,9 +20365,28 @@ var AbracadabraMCPServer = class {
20308
20365
  if (data.sender_id && data.sender_id === this._userId) return;
20309
20366
  const channel = data.channel;
20310
20367
  const docId = channel?.startsWith("group:") ? channel.slice(6) : "";
20311
- if (channel?.startsWith("dm:")) {
20368
+ const isDM = channel?.startsWith("dm:") ?? false;
20369
+ const isGroup = channel?.startsWith("group:") ?? false;
20370
+ if (isDM) {
20312
20371
  const parts = channel.split(":");
20313
- if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) return;
20372
+ if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) {
20373
+ console.error(`[abracadabra-mcp] Dropping DM: agent _userId=${this._userId} not in channel parts=[${parts[1]}, ${parts[2]}] — channel='${channel}'. The dashboard's awareness likely points at a stale Claude identity.`);
20374
+ return;
20375
+ }
20376
+ }
20377
+ const mode = this.triggerMode;
20378
+ const content = typeof data.content === "string" ? data.content : "";
20379
+ let dispatchContent = content;
20380
+ if (isGroup) {
20381
+ if (mode === "task") return;
20382
+ if (mode === "mention" || mode === "mention+task") {
20383
+ const aliases = this.mentionAliases;
20384
+ if (!containsMention(content, aliases)) {
20385
+ console.error(`[abracadabra-mcp] skipped message on ${channel} — no @mention for ${aliases.join("|")}`);
20386
+ return;
20387
+ }
20388
+ dispatchContent = stripMention(content, aliases) || content;
20389
+ }
20314
20390
  }
20315
20391
  if (channel) {
20316
20392
  const rootProvider = this._activeConnection?.provider;
@@ -20320,14 +20396,13 @@ var AbracadabraMCPServer = class {
20320
20396
  timestamp: Math.floor(Date.now() / 1e3)
20321
20397
  }));
20322
20398
  this._lastChatChannel = channel;
20323
- this.sendTypingIndicator(channel);
20324
- this._startTypingInterval(channel);
20325
20399
  }
20400
+ this._beginTurn();
20326
20401
  this.setAutoStatus("thinking");
20327
20402
  await this._serverRef.notification({
20328
20403
  method: "notifications/claude/channel",
20329
20404
  params: {
20330
- content: data.content ?? "",
20405
+ content: dispatchContent,
20331
20406
  instructions: `You MUST use send_chat_message with channel="${channel ?? ""}" for ALL responses — both progress updates and final answers. The user CANNOT see plain text output; they only see messages sent via send_chat_message. When doing multi-step work, send brief status updates via send_chat_message (e.g. "Looking into that..." or "Found it, writing up results...") so the user knows you're working. Never output plain text as a substitute for send_chat_message.`,
20332
20407
  meta: {
20333
20408
  source: "abracadabra",
@@ -20358,13 +20433,35 @@ var AbracadabraMCPServer = class {
20358
20433
  if (docId !== void 0) provider.awareness.setLocalStateField("docId", docId);
20359
20434
  const context = status ? statusContext !== void 0 ? statusContext : this._lastChatChannel : null;
20360
20435
  provider.awareness.setLocalStateField("statusContext", context ?? null);
20361
- if (!status) this._stopTypingInterval();
20436
+ if (!status) {
20437
+ this._stopTypingInterval();
20438
+ provider.awareness.setLocalStateField("activeToolCall", null);
20439
+ provider.awareness.setLocalStateField("turnId", null);
20440
+ this._toolHistory = [];
20441
+ provider.awareness.setLocalStateField("toolHistory", []);
20442
+ }
20362
20443
  if (status) this._statusClearTimer = setTimeout(() => {
20363
20444
  provider.awareness.setLocalStateField("status", null);
20364
20445
  provider.awareness.setLocalStateField("activeToolCall", null);
20365
20446
  provider.awareness.setLocalStateField("statusContext", null);
20447
+ provider.awareness.setLocalStateField("turnId", null);
20448
+ this._toolHistory = [];
20449
+ provider.awareness.setLocalStateField("toolHistory", []);
20366
20450
  this._stopTypingInterval();
20367
- }, 3e4);
20451
+ }, 1e4);
20452
+ }
20453
+ /**
20454
+ * Start a new agent turn. Mints a fresh UUID and writes it to awareness so
20455
+ * the dashboard can gate the incantation on "there is an active turn",
20456
+ * decoupled from the (racier) status field. Called from chat arrival and
20457
+ * ai:task dispatch right before `setAutoStatus('thinking')`.
20458
+ */
20459
+ _beginTurn() {
20460
+ const provider = this._activeConnection?.provider;
20461
+ if (!provider) return;
20462
+ this._toolHistory = [];
20463
+ provider.awareness.setLocalStateField("toolHistory", []);
20464
+ provider.awareness.setLocalStateField("turnId", crypto.randomUUID());
20368
20465
  }
20369
20466
  /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
20370
20467
  _startTypingInterval(channel) {
@@ -20382,10 +20479,28 @@ var AbracadabraMCPServer = class {
20382
20479
  }
20383
20480
  /**
20384
20481
  * Broadcast which tool the agent is currently executing.
20385
- * Dashboard renders this as a ChatTool indicator.
20482
+ *
20483
+ * Renders as a ChatTool pill on the dashboard. On non-null calls, the tool
20484
+ * is also appended to `toolHistory` (capped at TOOL_HISTORY_MAX) and written
20485
+ * to awareness so the dashboard's inline trace can show the turn's recent
20486
+ * activity. Tools do NOT clear (`setActiveToolCall(null)`) on completion —
20487
+ * the pill stays until the next tool replaces it or `setAutoStatus(null)`
20488
+ * flushes the turn. This keeps pills visible long enough to see.
20386
20489
  */
20387
20490
  setActiveToolCall(toolCall) {
20388
- this._activeConnection?.provider?.awareness.setLocalStateField("activeToolCall", toolCall);
20491
+ const provider = this._activeConnection?.provider;
20492
+ if (!provider) return;
20493
+ provider.awareness.setLocalStateField("activeToolCall", toolCall);
20494
+ if (toolCall) {
20495
+ this._toolHistory.push({
20496
+ tool: toolCall.name,
20497
+ target: toolCall.target,
20498
+ ts: Date.now(),
20499
+ channel: this._lastChatChannel
20500
+ });
20501
+ if (this._toolHistory.length > AbracadabraMCPServer.TOOL_HISTORY_MAX) this._toolHistory.splice(0, this._toolHistory.length - AbracadabraMCPServer.TOOL_HISTORY_MAX);
20502
+ provider.awareness.setLocalStateField("toolHistory", [...this._toolHistory]);
20503
+ }
20389
20504
  }
20390
20505
  /**
20391
20506
  * Send a typing indicator to a chat channel.
@@ -20416,8 +20531,11 @@ var AbracadabraMCPServer = class {
20416
20531
  conn.provider.awareness.setLocalStateField("status", null);
20417
20532
  conn.provider.awareness.setLocalStateField("activeToolCall", null);
20418
20533
  conn.provider.awareness.setLocalStateField("statusContext", null);
20534
+ conn.provider.awareness.setLocalStateField("turnId", null);
20535
+ conn.provider.awareness.setLocalStateField("toolHistory", []);
20419
20536
  conn.provider.destroy();
20420
20537
  }
20538
+ this._toolHistory = [];
20421
20539
  this._spaceConnections.clear();
20422
20540
  this._activeConnection = null;
20423
20541
  console.error("[abracadabra-mcp] Shutdown complete");
@@ -21233,16 +21351,12 @@ function registerTreeTools(mcp, server) {
21233
21351
  server.setAutoStatus("reading");
21234
21352
  server.setActiveToolCall({ name: "list_documents" });
21235
21353
  const treeMap = server.getTreeMap();
21236
- if (!treeMap) {
21237
- server.setActiveToolCall(null);
21238
- return { content: [{
21239
- type: "text",
21240
- text: "Not connected"
21241
- }] };
21242
- }
21354
+ if (!treeMap) return { content: [{
21355
+ type: "text",
21356
+ text: "Not connected"
21357
+ }] };
21243
21358
  const targetId = normalizeRootId(parentId, server);
21244
21359
  const children = childrenOf$1(readEntries$1(treeMap), targetId);
21245
- server.setActiveToolCall(null);
21246
21360
  return { content: [{
21247
21361
  type: "text",
21248
21362
  text: JSON.stringify(children, null, 2)
@@ -21255,17 +21369,13 @@ function registerTreeTools(mcp, server) {
21255
21369
  server.setAutoStatus("reading");
21256
21370
  server.setActiveToolCall({ name: "get_document_tree" });
21257
21371
  const treeMap = server.getTreeMap();
21258
- if (!treeMap) {
21259
- server.setActiveToolCall(null);
21260
- return { content: [{
21261
- type: "text",
21262
- text: "Not connected"
21263
- }] };
21264
- }
21372
+ if (!treeMap) return { content: [{
21373
+ type: "text",
21374
+ text: "Not connected"
21375
+ }] };
21265
21376
  const targetId = normalizeRootId(rootId, server);
21266
21377
  const maxDepth = depth ?? 3;
21267
21378
  const tree = buildTree$1(readEntries$1(treeMap), targetId, maxDepth);
21268
- server.setActiveToolCall(null);
21269
21379
  return { content: [{
21270
21380
  type: "text",
21271
21381
  text: JSON.stringify(tree, null, 2)
@@ -21281,13 +21391,10 @@ function registerTreeTools(mcp, server) {
21281
21391
  target: query
21282
21392
  });
21283
21393
  const treeMap = server.getTreeMap();
21284
- if (!treeMap) {
21285
- server.setActiveToolCall(null);
21286
- return { content: [{
21287
- type: "text",
21288
- text: "Not connected"
21289
- }] };
21290
- }
21394
+ if (!treeMap) return { content: [{
21395
+ type: "text",
21396
+ text: "Not connected"
21397
+ }] };
21291
21398
  const entries = readEntries$1(treeMap);
21292
21399
  const lowerQuery = query.toLowerCase();
21293
21400
  const normalizedRoot = normalizeRootId(rootId, server);
@@ -21312,7 +21419,6 @@ function registerTreeTools(mcp, server) {
21312
21419
  path
21313
21420
  };
21314
21421
  });
21315
- server.setActiveToolCall(null);
21316
21422
  if (results.length === 0) return { content: [{
21317
21423
  type: "text",
21318
21424
  text: `No documents found matching "${query}". Try get_document_tree to see the full hierarchy.`
@@ -21326,7 +21432,7 @@ function registerTreeTools(mcp, server) {
21326
21432
  parentId: z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
21327
21433
  label: z.string().describe("Display name / title for the document."),
21328
21434
  type: z.string().optional().describe("Page type — sets how this document renders. Core types (always available): \"doc\" (rich text), \"kanban\" (columns → cards), \"table\" (columns → rows, positional), \"calendar\" (events with datetimeStart/End), \"timeline\" (epics → tasks with dateStart/End + taskProgress), \"checklist\" (tasks with checked/priority, unlimited nesting), \"outline\" (nested items, unlimited depth), \"gallery\" (visual grid with covers/ratings), \"map\" (markers/lines with geoLat/geoLng), \"graph\" (force-directed knowledge graph), \"dashboard\" (positioned widgets with deskX/deskY/deskMode), \"slides\" (slides → sub-slides with transitions), \"chart\" (bar/stacked bar/line/donut/treemap from data points or aggregation), \"sheets\" (spreadsheet with formulas and formatting), \"overview\" (space home — activity and stats), \"call\" (video call room, no children). Plugin types (require plugin enabled on the server): \"spatial\" (3D scene with spShape/spX/spY/spZ + universal color, plugin: spatial), \"media\" (audio/video player with playlists, plugin: media), \"coder\" (collaborative multi-file coding env with fileType/entry, plugin: coder). Alias: \"desktop\" → \"dashboard\". Omit to inherit parent view. Only set on the parent page, NEVER on child items."),
21329
- meta: z.record(z.unknown()).optional().describe("Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like \"star\"/\"code-2\"/\"users\" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.")
21435
+ meta: z.record(z.string(), z.unknown()).optional().describe("Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like \"star\"/\"code-2\"/\"users\" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.")
21330
21436
  }, async ({ parentId, label, type, meta }) => {
21331
21437
  server.setAutoStatus("creating");
21332
21438
  server.setActiveToolCall({
@@ -21335,13 +21441,10 @@ function registerTreeTools(mcp, server) {
21335
21441
  });
21336
21442
  const treeMap = server.getTreeMap();
21337
21443
  const rootDoc = server.rootDocument;
21338
- if (!treeMap || !rootDoc) {
21339
- server.setActiveToolCall(null);
21340
- return { content: [{
21341
- type: "text",
21342
- text: "Not connected"
21343
- }] };
21344
- }
21444
+ if (!treeMap || !rootDoc) return { content: [{
21445
+ type: "text",
21446
+ text: "Not connected"
21447
+ }] };
21345
21448
  const id = crypto.randomUUID();
21346
21449
  const normalizedParent = normalizeRootId(parentId, server);
21347
21450
  const now = Date.now();
@@ -21357,7 +21460,6 @@ function registerTreeTools(mcp, server) {
21357
21460
  });
21358
21461
  });
21359
21462
  server.setFocusedDoc(id);
21360
- server.setActiveToolCall(null);
21361
21463
  return { content: [{
21362
21464
  type: "text",
21363
21465
  text: JSON.stringify({
@@ -21378,28 +21480,21 @@ function registerTreeTools(mcp, server) {
21378
21480
  target: id
21379
21481
  });
21380
21482
  const treeMap = server.getTreeMap();
21381
- if (!treeMap) {
21382
- server.setActiveToolCall(null);
21383
- return { content: [{
21384
- type: "text",
21385
- text: "Not connected"
21386
- }] };
21387
- }
21483
+ if (!treeMap) return { content: [{
21484
+ type: "text",
21485
+ text: "Not connected"
21486
+ }] };
21388
21487
  const raw = treeMap.get(id);
21389
- if (!raw) {
21390
- server.setActiveToolCall(null);
21391
- return { content: [{
21392
- type: "text",
21393
- text: `Document ${id} not found`
21394
- }] };
21395
- }
21488
+ if (!raw) return { content: [{
21489
+ type: "text",
21490
+ text: `Document ${id} not found`
21491
+ }] };
21396
21492
  const entry = toPlain(raw);
21397
21493
  treeMap.set(id, {
21398
21494
  ...entry,
21399
21495
  label,
21400
21496
  updatedAt: Date.now()
21401
21497
  });
21402
- server.setActiveToolCall(null);
21403
21498
  return { content: [{
21404
21499
  type: "text",
21405
21500
  text: `Renamed to "${label}"`
@@ -21416,21 +21511,15 @@ function registerTreeTools(mcp, server) {
21416
21511
  target: id
21417
21512
  });
21418
21513
  const treeMap = server.getTreeMap();
21419
- if (!treeMap) {
21420
- server.setActiveToolCall(null);
21421
- return { content: [{
21422
- type: "text",
21423
- text: "Not connected"
21424
- }] };
21425
- }
21514
+ if (!treeMap) return { content: [{
21515
+ type: "text",
21516
+ text: "Not connected"
21517
+ }] };
21426
21518
  const raw = treeMap.get(id);
21427
- if (!raw) {
21428
- server.setActiveToolCall(null);
21429
- return { content: [{
21430
- type: "text",
21431
- text: `Document ${id} not found`
21432
- }] };
21433
- }
21519
+ if (!raw) return { content: [{
21520
+ type: "text",
21521
+ text: `Document ${id} not found`
21522
+ }] };
21434
21523
  const entry = toPlain(raw);
21435
21524
  treeMap.set(id, {
21436
21525
  ...entry,
@@ -21438,7 +21527,6 @@ function registerTreeTools(mcp, server) {
21438
21527
  order: order ?? Date.now(),
21439
21528
  updatedAt: Date.now()
21440
21529
  });
21441
- server.setActiveToolCall(null);
21442
21530
  return { content: [{
21443
21531
  type: "text",
21444
21532
  text: `Moved ${id} to parent ${newParentId}`
@@ -21453,13 +21541,10 @@ function registerTreeTools(mcp, server) {
21453
21541
  const treeMap = server.getTreeMap();
21454
21542
  const trashMap = server.getTrashMap();
21455
21543
  const rootDoc = server.rootDocument;
21456
- if (!treeMap || !trashMap || !rootDoc) {
21457
- server.setActiveToolCall(null);
21458
- return { content: [{
21459
- type: "text",
21460
- text: "Not connected"
21461
- }] };
21462
- }
21544
+ if (!treeMap || !trashMap || !rootDoc) return { content: [{
21545
+ type: "text",
21546
+ text: "Not connected"
21547
+ }] };
21463
21548
  const toDelete = [id, ...descendantsOf(readEntries$1(treeMap), id).map((e) => e.id)];
21464
21549
  const now = Date.now();
21465
21550
  rootDoc.transact(() => {
@@ -21478,7 +21563,6 @@ function registerTreeTools(mcp, server) {
21478
21563
  treeMap.delete(nid);
21479
21564
  }
21480
21565
  });
21481
- server.setActiveToolCall(null);
21482
21566
  return { content: [{
21483
21567
  type: "text",
21484
21568
  text: `Deleted ${toDelete.length} document(s)`
@@ -21494,28 +21578,21 @@ function registerTreeTools(mcp, server) {
21494
21578
  target: id
21495
21579
  });
21496
21580
  const treeMap = server.getTreeMap();
21497
- if (!treeMap) {
21498
- server.setActiveToolCall(null);
21499
- return { content: [{
21500
- type: "text",
21501
- text: "Not connected"
21502
- }] };
21503
- }
21581
+ if (!treeMap) return { content: [{
21582
+ type: "text",
21583
+ text: "Not connected"
21584
+ }] };
21504
21585
  const raw = treeMap.get(id);
21505
- if (!raw) {
21506
- server.setActiveToolCall(null);
21507
- return { content: [{
21508
- type: "text",
21509
- text: `Document ${id} not found`
21510
- }] };
21511
- }
21586
+ if (!raw) return { content: [{
21587
+ type: "text",
21588
+ text: `Document ${id} not found`
21589
+ }] };
21512
21590
  const entry = toPlain(raw);
21513
21591
  treeMap.set(id, {
21514
21592
  ...entry,
21515
21593
  type,
21516
21594
  updatedAt: Date.now()
21517
21595
  });
21518
- server.setActiveToolCall(null);
21519
21596
  return { content: [{
21520
21597
  type: "text",
21521
21598
  text: `Changed type to "${type}"`
@@ -22768,7 +22845,6 @@ function registerContentTools(mcp, server) {
22768
22845
  });
22769
22846
  children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
22770
22847
  }
22771
- server.setActiveToolCall(null);
22772
22848
  const result = {
22773
22849
  label,
22774
22850
  type,
@@ -22781,7 +22857,6 @@ function registerContentTools(mcp, server) {
22781
22857
  text: JSON.stringify(result, null, 2)
22782
22858
  }] };
22783
22859
  } catch (error) {
22784
- server.setActiveToolCall(null);
22785
22860
  return {
22786
22861
  content: [{
22787
22862
  type: "text",
@@ -22831,13 +22906,11 @@ function registerContentTools(mcp, server) {
22831
22906
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
22832
22907
  server.setFocusedDoc(docId);
22833
22908
  server.setDocCursor(docId, fragment.length);
22834
- server.setActiveToolCall(null);
22835
22909
  return { content: [{
22836
22910
  type: "text",
22837
22911
  text: `Document ${docId} updated (${writeMode} mode)`
22838
22912
  }] };
22839
22913
  } catch (error) {
22840
- server.setActiveToolCall(null);
22841
22914
  return {
22842
22915
  content: [{
22843
22916
  type: "text",
@@ -22859,22 +22932,15 @@ function registerMetaTools(mcp, server) {
22859
22932
  target: docId
22860
22933
  });
22861
22934
  const treeMap = server.getTreeMap();
22862
- if (!treeMap) {
22863
- server.setActiveToolCall(null);
22864
- return { content: [{
22865
- type: "text",
22866
- text: "Not connected"
22867
- }] };
22868
- }
22935
+ if (!treeMap) return { content: [{
22936
+ type: "text",
22937
+ text: "Not connected"
22938
+ }] };
22869
22939
  const entry = treeMap.get(docId);
22870
- if (!entry) {
22871
- server.setActiveToolCall(null);
22872
- return { content: [{
22873
- type: "text",
22874
- text: `Document ${docId} not found`
22875
- }] };
22876
- }
22877
- server.setActiveToolCall(null);
22940
+ if (!entry) return { content: [{
22941
+ type: "text",
22942
+ text: `Document ${docId} not found`
22943
+ }] };
22878
22944
  return { content: [{
22879
22945
  type: "text",
22880
22946
  text: JSON.stringify({
@@ -22887,7 +22953,7 @@ function registerMetaTools(mcp, server) {
22887
22953
  });
22888
22954
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
22889
22955
  docId: z.string().describe("Document ID."),
22890
- meta: z.record(z.unknown()).describe("Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, timeStart/timeEnd, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId, coverDocId, dateTaken. Geo/Map (children): geoType (\"marker\"|\"line\"|\"measure\"), geoLat, geoLng, geoDescription. Spatial 3D (children, plugin: spatial): spShape (\"box\"|\"sphere\"|\"cylinder\"|\"cone\"|\"plane\"|\"torus\"|\"glb\"), spX/spY/spZ, spRX/spRY/spRZ (deg), spSX/spSY/spSZ (scale), spOpacity (0-100), spModelUploadId, spModelDocId — spatial uses the universal `color` key, NOT spColor. Dashboard (children): deskX, deskY, deskZ, deskMode (\"icon\"|\"widget-sm\"|\"widget-lg\"). Mindmap-layout (children): mmX, mmY. Graph-layout (children): graphX, graphY, graphPinned. Slides (children): slidesTransition (\"none\"|\"fade\"|\"slide\"). Coder (children, plugin: coder): fileType (\"vue\"|\"ts\"|\"js\"|\"css\"|\"json\"|\"folder\"), entry (bool). Cell formatting (sheets cells): bold, italic, textColor, bgColor, align (\"left\"|\"center\"|\"right\"), formula. Renderer config (on the PAGE doc itself, not children): kanbanColumnWidth (\"narrow\"|\"default\"|\"wide\"), galleryColumns (1-6), galleryAspect (\"square\"|\"4:3\"|\"3:2\"|\"16:9\"|\"free\"), galleryCardStyle (\"default\"|\"compact\"|\"detailed\"), galleryShowLabels, gallerySortBy (\"manual\"|\"date\"|\"name\"|\"rating\"), calendarView (\"month\"|\"week\"|\"day\"), calendarWeekStart (\"sun\"|\"mon\"), calendarShowWeekNumbers, tableMode (\"hierarchy\"|\"flat\"), tableSortKey, tableSortDir (\"asc\"|\"desc\"), timelineZoom (\"week\"|\"month\"|\"quarter\"), timelinePixelsPerDay, timelineCenterDate (ISO date), checklistFilter (\"all\"|\"active\"|\"completed\"), checklistSort (\"manual\"|\"priority\"|\"due\"), mapShowLabels, graphSpacing (\"compact\"|\"default\"|\"spacious\"), graphShowLabels, graphEdgeThickness (\"thin\"|\"normal\"|\"thick\"), showRefEdges, mmSpacing, spatialGridVisible, slidesTheme (\"dark\"|\"light\"), chartType (\"bar\"|\"stacked bar\"|\"line\"|\"donut\"|\"treemap\"), chartMetric (\"value\"|\"type\"|\"tag\"|\"status\"|\"priority\"|\"activity\"|\"completion\"), chartColorScheme (\"default\"|\"warm\"|\"cool\"|\"mono\"), chartLimit (3-30), chartShowLegend, chartShowValues, sheetsDefaultColWidth (40-500), sheetsDefaultRowHeight (20-100), sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols, mediaRepeat (\"off\"|\"all\"|\"one\"), mediaShuffle. Set a key to null to clear it.")
22956
+ meta: z.record(z.string(), z.unknown()).describe("Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, timeStart/timeEnd, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId, coverDocId, dateTaken. Geo/Map (children): geoType (\"marker\"|\"line\"|\"measure\"), geoLat, geoLng, geoDescription. Spatial 3D (children, plugin: spatial): spShape (\"box\"|\"sphere\"|\"cylinder\"|\"cone\"|\"plane\"|\"torus\"|\"glb\"), spX/spY/spZ, spRX/spRY/spRZ (deg), spSX/spSY/spSZ (scale), spOpacity (0-100), spModelUploadId, spModelDocId — spatial uses the universal `color` key, NOT spColor. Dashboard (children): deskX, deskY, deskZ, deskMode (\"icon\"|\"widget-sm\"|\"widget-lg\"). Mindmap-layout (children): mmX, mmY. Graph-layout (children): graphX, graphY, graphPinned. Slides (children): slidesTransition (\"none\"|\"fade\"|\"slide\"). Coder (children, plugin: coder): fileType (\"vue\"|\"ts\"|\"js\"|\"css\"|\"json\"|\"folder\"), entry (bool). Cell formatting (sheets cells): bold, italic, textColor, bgColor, align (\"left\"|\"center\"|\"right\"), formula. Renderer config (on the PAGE doc itself, not children): kanbanColumnWidth (\"narrow\"|\"default\"|\"wide\"), galleryColumns (1-6), galleryAspect (\"square\"|\"4:3\"|\"3:2\"|\"16:9\"|\"free\"), galleryCardStyle (\"default\"|\"compact\"|\"detailed\"), galleryShowLabels, gallerySortBy (\"manual\"|\"date\"|\"name\"|\"rating\"), calendarView (\"month\"|\"week\"|\"day\"), calendarWeekStart (\"sun\"|\"mon\"), calendarShowWeekNumbers, tableMode (\"hierarchy\"|\"flat\"), tableSortKey, tableSortDir (\"asc\"|\"desc\"), timelineZoom (\"week\"|\"month\"|\"quarter\"), timelinePixelsPerDay, timelineCenterDate (ISO date), checklistFilter (\"all\"|\"active\"|\"completed\"), checklistSort (\"manual\"|\"priority\"|\"due\"), mapShowLabels, graphSpacing (\"compact\"|\"default\"|\"spacious\"), graphShowLabels, graphEdgeThickness (\"thin\"|\"normal\"|\"thick\"), showRefEdges, mmSpacing, spatialGridVisible, slidesTheme (\"dark\"|\"light\"), chartType (\"bar\"|\"stacked bar\"|\"line\"|\"donut\"|\"treemap\"), chartMetric (\"value\"|\"type\"|\"tag\"|\"status\"|\"priority\"|\"activity\"|\"completion\"), chartColorScheme (\"default\"|\"warm\"|\"cool\"|\"mono\"), chartLimit (3-30), chartShowLegend, chartShowValues, sheetsDefaultColWidth (40-500), sheetsDefaultRowHeight (20-100), sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols, mediaRepeat (\"off\"|\"all\"|\"one\"), mediaShuffle. Set a key to null to clear it.")
22891
22957
  }, async ({ docId, meta }) => {
22892
22958
  server.setAutoStatus("writing", docId);
22893
22959
  server.setActiveToolCall({
@@ -22895,21 +22961,15 @@ function registerMetaTools(mcp, server) {
22895
22961
  target: docId
22896
22962
  });
22897
22963
  const treeMap = server.getTreeMap();
22898
- if (!treeMap) {
22899
- server.setActiveToolCall(null);
22900
- return { content: [{
22901
- type: "text",
22902
- text: "Not connected"
22903
- }] };
22904
- }
22964
+ if (!treeMap) return { content: [{
22965
+ type: "text",
22966
+ text: "Not connected"
22967
+ }] };
22905
22968
  const entry = treeMap.get(docId);
22906
- if (!entry) {
22907
- server.setActiveToolCall(null);
22908
- return { content: [{
22909
- type: "text",
22910
- text: `Document ${docId} not found`
22911
- }] };
22912
- }
22969
+ if (!entry) return { content: [{
22970
+ type: "text",
22971
+ text: `Document ${docId} not found`
22972
+ }] };
22913
22973
  treeMap.set(docId, {
22914
22974
  ...entry,
22915
22975
  meta: {
@@ -22918,7 +22978,6 @@ function registerMetaTools(mcp, server) {
22918
22978
  },
22919
22979
  updatedAt: Date.now()
22920
22980
  });
22921
- server.setActiveToolCall(null);
22922
22981
  return { content: [{
22923
22982
  type: "text",
22924
22983
  text: `Metadata updated for ${docId}`
@@ -22933,6 +22992,11 @@ function registerMetaTools(mcp, server) {
22933
22992
  */
22934
22993
  function registerFileTools(mcp, server) {
22935
22994
  mcp.tool("list_uploads", "List file attachments for a document.", { docId: z.string().describe("Document ID.") }, async ({ docId }) => {
22995
+ server.setAutoStatus("reading", docId);
22996
+ server.setActiveToolCall({
22997
+ name: "list_uploads",
22998
+ target: docId
22999
+ });
22936
23000
  try {
22937
23001
  const uploads = await server.client.listUploads(docId);
22938
23002
  return { content: [{
@@ -22954,6 +23018,11 @@ function registerFileTools(mcp, server) {
22954
23018
  filePath: z.string().describe("Absolute path to the local file to upload."),
22955
23019
  filename: z.string().optional().describe("Override filename (defaults to basename of filePath).")
22956
23020
  }, async ({ docId, filePath, filename }) => {
23021
+ server.setAutoStatus("uploading", docId);
23022
+ server.setActiveToolCall({
23023
+ name: "upload_file",
23024
+ target: path.basename(filePath)
23025
+ });
22957
23026
  try {
22958
23027
  const resolvedPath = path.resolve(filePath);
22959
23028
  const data = fs.readFileSync(resolvedPath);
@@ -22979,6 +23048,11 @@ function registerFileTools(mcp, server) {
22979
23048
  uploadId: z.string().describe("Upload ID to download."),
22980
23049
  saveTo: z.string().describe("Absolute local file path to save the download.")
22981
23050
  }, async ({ docId, uploadId, saveTo }) => {
23051
+ server.setAutoStatus("reading", docId);
23052
+ server.setActiveToolCall({
23053
+ name: "download_file",
23054
+ target: path.basename(saveTo)
23055
+ });
22982
23056
  try {
22983
23057
  const blob = await server.client.getUpload(docId, uploadId);
22984
23058
  const buffer = Buffer.from(await blob.arrayBuffer());
@@ -23002,6 +23076,11 @@ function registerFileTools(mcp, server) {
23002
23076
  docId: z.string().describe("Document ID."),
23003
23077
  uploadId: z.string().describe("Upload ID to delete.")
23004
23078
  }, async ({ docId, uploadId }) => {
23079
+ server.setAutoStatus("writing", docId);
23080
+ server.setActiveToolCall({
23081
+ name: "delete_file",
23082
+ target: uploadId
23083
+ });
23005
23084
  try {
23006
23085
  await server.client.deleteUpload(docId, uploadId);
23007
23086
  return { content: [{
@@ -23043,6 +23122,10 @@ function registerAwarenessTools(mcp, server) {
23043
23122
  docId: z.string().describe("Document ID to set awareness on."),
23044
23123
  fields: z.record(z.string(), z.unknown()).describe("Key-value pairs to set on the child document's awareness state. Use namespaced keys like \"kanban:hovering\", \"table:editing\", \"slides:viewing\", \"outline:editing\", \"calendar:focused\", \"gallery:focused\", \"timeline:focused\", \"graph:focused\", \"map:focused\", \"doc:scroll\". Set a key to null to clear it.")
23045
23124
  }, async ({ docId, fields }) => {
23125
+ server.setActiveToolCall({
23126
+ name: "set_doc_awareness",
23127
+ target: docId
23128
+ });
23046
23129
  try {
23047
23130
  const provider = await server.getChildProvider(docId);
23048
23131
  for (const [key, value] of Object.entries(fields)) provider.awareness.setLocalStateField(key, value ?? null);
@@ -23061,6 +23144,7 @@ function registerAwarenessTools(mcp, server) {
23061
23144
  }
23062
23145
  });
23063
23146
  mcp.tool("poll_inbox", "Check the \"AI Inbox\" document for pending instructions from humans. Returns the inbox content and any pending task sub-documents. Create the inbox as a doc called \"AI Inbox\" under the hub doc if it does not exist yet. Note: channel-based watching via watch_chat is preferred for real-time use.", {}, async () => {
23147
+ server.setActiveToolCall({ name: "poll_inbox" });
23064
23148
  try {
23065
23149
  const treeMap = server.getTreeMap();
23066
23150
  const rootDocId = server.rootDocId;
@@ -23107,6 +23191,10 @@ function registerAwarenessTools(mcp, server) {
23107
23191
  }
23108
23192
  });
23109
23193
  mcp.tool("list_connected_users", "List all connected users and their awareness state. Shows who is online and what they are doing.", { docId: z.string().optional().describe("If provided, list users connected to this specific document. Otherwise lists users from root awareness.") }, async ({ docId }) => {
23194
+ server.setActiveToolCall({
23195
+ name: "list_connected_users",
23196
+ target: docId
23197
+ });
23110
23198
  try {
23111
23199
  let awareness;
23112
23200
  if (docId) awareness = (await server.getChildProvider(docId)).awareness;
@@ -23151,16 +23239,13 @@ function registerChannelTools(mcp, server) {
23151
23239
  });
23152
23240
  const treeMap = server.getTreeMap();
23153
23241
  const rootDoc = server.rootDocument;
23154
- if (!treeMap || !rootDoc) {
23155
- server.setActiveToolCall(null);
23156
- return {
23157
- content: [{
23158
- type: "text",
23159
- text: "Not connected"
23160
- }],
23161
- isError: true
23162
- };
23163
- }
23242
+ if (!treeMap || !rootDoc) return {
23243
+ content: [{
23244
+ type: "text",
23245
+ text: "Not connected"
23246
+ }],
23247
+ isError: true
23248
+ };
23164
23249
  const label = `AI Reply — ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)}: ${text.slice(0, 40).replace(/\n/g, " ")}`;
23165
23250
  const replyId = crypto.randomUUID();
23166
23251
  const now = Date.now();
@@ -23176,7 +23261,6 @@ function registerChannelTools(mcp, server) {
23176
23261
  });
23177
23262
  populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
23178
23263
  if (task_id) server.clearAiTask(task_id);
23179
- server.setActiveToolCall(null);
23180
23264
  return { content: [{
23181
23265
  type: "text",
23182
23266
  text: JSON.stringify({
@@ -23185,7 +23269,6 @@ function registerChannelTools(mcp, server) {
23185
23269
  })
23186
23270
  }] };
23187
23271
  } catch (error) {
23188
- server.setActiveToolCall(null);
23189
23272
  return {
23190
23273
  content: [{
23191
23274
  type: "text",
@@ -23208,14 +23291,15 @@ function registerChannelTools(mcp, server) {
23208
23291
  }],
23209
23292
  isError: true
23210
23293
  };
23294
+ const normalized = text.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\n");
23295
+ server.setAutoStatus(null);
23296
+ server.sendTypingIndicator(channel);
23211
23297
  rootProvider.sendStateless(JSON.stringify({
23212
23298
  type: "chat:send",
23213
23299
  channel,
23214
- content: text,
23300
+ content: normalized,
23215
23301
  sender_name: server.agentName
23216
23302
  }));
23217
- server.setAutoStatus(null);
23218
- server.setActiveToolCall(null);
23219
23303
  return { content: [{
23220
23304
  type: "text",
23221
23305
  text: `Sent to ${channel}`
@@ -23370,16 +23454,13 @@ function registerSvgTools(mcp, server) {
23370
23454
  target: docId
23371
23455
  });
23372
23456
  const cleanSvg = sanitizeSvg(svg);
23373
- if (!cleanSvg) {
23374
- server.setActiveToolCall(null);
23375
- return {
23376
- content: [{
23377
- type: "text",
23378
- text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23379
- }],
23380
- isError: true
23381
- };
23382
- }
23457
+ if (!cleanSvg) return {
23458
+ content: [{
23459
+ type: "text",
23460
+ text: "Error: SVG markup was empty or entirely stripped by sanitizer."
23461
+ }],
23462
+ isError: true
23463
+ };
23383
23464
  const doc = (await server.getChildProvider(docId)).document;
23384
23465
  const fragment = doc.getXmlFragment("default");
23385
23466
  doc.transact(() => {
@@ -23390,13 +23471,11 @@ function registerSvgTools(mcp, server) {
23390
23471
  fragment.insert(insertPos, [el]);
23391
23472
  });
23392
23473
  server.setFocusedDoc(docId);
23393
- server.setActiveToolCall(null);
23394
23474
  return { content: [{
23395
23475
  type: "text",
23396
23476
  text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ""}`
23397
23477
  }] };
23398
23478
  } catch (error) {
23399
- server.setActiveToolCall(null);
23400
23479
  return {
23401
23480
  content: [{
23402
23481
  type: "text",
@@ -24200,6 +24279,9 @@ var HookBridge = class {
24200
24279
  }
24201
24280
  routeEvent(payload) {
24202
24281
  switch (payload.hook_event_name) {
24282
+ case "UserPromptSubmit":
24283
+ this.onUserPromptSubmit();
24284
+ break;
24203
24285
  case "PreToolUse":
24204
24286
  this.onPreToolUse(payload);
24205
24287
  break;
@@ -24210,13 +24292,18 @@ var HookBridge = class {
24210
24292
  this.onSubagentStart(payload);
24211
24293
  break;
24212
24294
  case "SubagentStop":
24213
- this.onSubagentStop(payload);
24295
+ this.onSubagentStop();
24214
24296
  break;
24215
24297
  case "Stop":
24216
24298
  this.onStop();
24217
24299
  break;
24218
24300
  }
24219
24301
  }
24302
+ /** New user turn — reset any lingering status/tool state from the previous turn. */
24303
+ onUserPromptSubmit() {
24304
+ this.server.setAutoStatus(null);
24305
+ this.server.setActiveToolCall(null);
24306
+ }
24220
24307
  onPreToolUse(payload) {
24221
24308
  const toolName = payload.tool_name ?? "";
24222
24309
  if (toolName.startsWith("mcp__abracadabra__")) return;
@@ -24228,7 +24315,6 @@ var HookBridge = class {
24228
24315
  }
24229
24316
  onPostToolUse(payload) {
24230
24317
  if ((payload.tool_name ?? "").startsWith("mcp__abracadabra__")) return;
24231
- this.server.setAutoStatus("thinking");
24232
24318
  }
24233
24319
  onSubagentStart(payload) {
24234
24320
  const agentType = payload.agent_type ?? "agent";
@@ -24238,9 +24324,7 @@ var HookBridge = class {
24238
24324
  });
24239
24325
  this.server.setAutoStatus("thinking");
24240
24326
  }
24241
- onSubagentStop(_payload) {
24242
- this.server.setAutoStatus("thinking");
24243
- }
24327
+ onSubagentStop() {}
24244
24328
  onStop() {
24245
24329
  this.server.setAutoStatus(null);
24246
24330
  this.server.setActiveToolCall(null);
@@ -24253,11 +24337,19 @@ var HookBridge = class {
24253
24337
  * Abracadabra MCP Server — entry point.
24254
24338
  *
24255
24339
  * Environment variables:
24256
- * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24340
+ * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
24257
24341
  * ABRA_AGENT_NAME — Display name (default: "AI Assistant")
24258
24342
  * ABRA_AGENT_COLOR — HSL color for presence (default: "hsl(270, 80%, 60%)")
24259
24343
  * ABRA_INVITE_CODE — Invite code for first-run registration (grants role)
24260
24344
  * ABRA_KEY_FILE — Path to Ed25519 key file (default: ~/.abracadabra/agent.key)
24345
+ * ABRA_AGENT_TRIGGER_MODE — When to respond in group chats:
24346
+ * all → every message (legacy)
24347
+ * mention → only when @<alias> is used
24348
+ * task → only ai:task awareness events
24349
+ * mention+task → mention OR ai:task (default)
24350
+ * DMs always trigger regardless of mode.
24351
+ * ABRA_AGENT_MENTION_ALIASES — Comma-separated aliases for @mentions
24352
+ * (default: [ABRA_AGENT_NAME])
24261
24353
  */
24262
24354
  async function main() {
24263
24355
  const url = process.env.ABRA_URL;
@@ -24265,13 +24357,27 @@ async function main() {
24265
24357
  console.error("Missing required environment variable: ABRA_URL");
24266
24358
  process.exit(1);
24267
24359
  }
24360
+ const rawMode = (process.env.ABRA_AGENT_TRIGGER_MODE ?? "mention+task").trim().toLowerCase();
24361
+ const validModes = [
24362
+ "all",
24363
+ "mention",
24364
+ "task",
24365
+ "mention+task"
24366
+ ];
24367
+ const triggerMode = validModes.includes(rawMode) ? rawMode : "mention+task";
24368
+ if (rawMode && !validModes.includes(rawMode)) console.error(`[abracadabra-mcp] Invalid ABRA_AGENT_TRIGGER_MODE="${rawMode}", falling back to "mention+task"`);
24369
+ const aliasEnv = process.env.ABRA_AGENT_MENTION_ALIASES;
24370
+ const mentionAliases = aliasEnv ? aliasEnv.split(",").map((a) => a.trim()).filter((a) => a.length > 0) : void 0;
24268
24371
  const server = new AbracadabraMCPServer({
24269
24372
  url,
24270
24373
  agentName: process.env.ABRA_AGENT_NAME,
24271
24374
  agentColor: process.env.ABRA_AGENT_COLOR,
24272
24375
  inviteCode: process.env.ABRA_INVITE_CODE,
24273
- keyFile: process.env.ABRA_KEY_FILE
24376
+ keyFile: process.env.ABRA_KEY_FILE,
24377
+ triggerMode,
24378
+ mentionAliases
24274
24379
  });
24380
+ console.error(`[abracadabra-mcp] Trigger mode: ${triggerMode}; aliases: ${server.mentionAliases.join(", ")}`);
24275
24381
  const mcp = new McpServer({
24276
24382
  name: "abracadabra",
24277
24383
  version: "1.0.0"