@knowsuchagency/fulcrum 3.1.1 → 3.2.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.
package/bin/fulcrum.js CHANGED
@@ -1482,6 +1482,9 @@ class FulcrumClient {
1482
1482
  async getEmail(id) {
1483
1483
  return this.fetch(`/api/messaging/email/emails/${id}`);
1484
1484
  }
1485
+ async getMessage(id) {
1486
+ return this.fetch(`/api/messaging/messages/${id}`);
1487
+ }
1485
1488
  async searchEmails(criteria) {
1486
1489
  return this.fetch("/api/messaging/email/search", {
1487
1490
  method: "POST",
@@ -44207,6 +44210,13 @@ var init_registry = __esm(() => {
44207
44210
  keywords: ["email", "fetch", "download", "imap", "uid", "message", "mail"],
44208
44211
  defer_loading: false
44209
44212
  },
44213
+ {
44214
+ name: "get_message",
44215
+ description: "Get details of a specific message by ID, including full content and metadata",
44216
+ category: "messaging",
44217
+ keywords: ["message", "get", "read", "channel", "whatsapp", "discord", "telegram", "slack", "chat"],
44218
+ defer_loading: false
44219
+ },
44210
44220
  {
44211
44221
  name: "message",
44212
44222
  description: "Send a message to a messaging channel (WhatsApp, Discord, Telegram, Slack, Gmail)",
@@ -44307,7 +44317,7 @@ var init_registry = __esm(() => {
44307
44317
  },
44308
44318
  {
44309
44319
  name: "create_calendar_event",
44310
- description: "Create a new calendar event on a CalDAV calendar",
44320
+ description: "Create a new calendar event on a CalDAV or Google calendar.",
44311
44321
  category: "caldav",
44312
44322
  keywords: ["calendar", "event", "create", "new", "schedule", "appointment", "meeting"],
44313
44323
  defer_loading: true
@@ -46185,6 +46195,24 @@ var init_memory_file = __esm(() => {
46185
46195
  init_utils();
46186
46196
  });
46187
46197
 
46198
+ // cli/src/mcp/tools/messaging.ts
46199
+ var registerMessagingTools = (server, client) => {
46200
+ server.tool("get_message", "Get details of a specific message by ID, including full content and metadata.", {
46201
+ id: exports_external.string().describe("Message ID")
46202
+ }, async ({ id }) => {
46203
+ try {
46204
+ const message = await client.getMessage(id);
46205
+ return formatSuccess(message);
46206
+ } catch (err) {
46207
+ return handleToolError(err);
46208
+ }
46209
+ });
46210
+ };
46211
+ var init_messaging = __esm(() => {
46212
+ init_zod2();
46213
+ init_utils();
46214
+ });
46215
+
46188
46216
  // cli/src/mcp/tools/search.ts
46189
46217
  var EntityTypeSchema, registerSearchTools = (server, client) => {
46190
46218
  server.tool("search", 'Search across all Fulcrum entities (tasks, projects, messages, calendar events, memories, conversations) using full-text search. Supports boolean operators (AND, OR, NOT), phrase matching ("quoted phrases"), and prefix matching (term*). Returns results ranked by relevance.', {
@@ -46243,6 +46271,7 @@ function registerTools(server, client) {
46243
46271
  registerSettingsTools(server, client);
46244
46272
  registerBackupTools(server, client);
46245
46273
  registerEmailTools(server, client);
46274
+ registerMessagingTools(server, client);
46246
46275
  registerAssistantTools(server, client);
46247
46276
  registerCaldavTools(server, client);
46248
46277
  registerGmailTools(server, client);
@@ -46267,6 +46296,7 @@ var init_tools = __esm(() => {
46267
46296
  init_gmail();
46268
46297
  init_memory();
46269
46298
  init_memory_file();
46299
+ init_messaging();
46270
46300
  init_search();
46271
46301
  init_types4();
46272
46302
  });
@@ -46286,7 +46316,7 @@ async function runMcpServer(urlOverride, portOverride) {
46286
46316
  const client = new FulcrumClient(urlOverride, portOverride);
46287
46317
  const server = new McpServer({
46288
46318
  name: "fulcrum",
46289
- version: "3.1.1"
46319
+ version: "3.2.1"
46290
46320
  });
46291
46321
  registerTools(server, client);
46292
46322
  const transport = new StdioServerTransport;
@@ -48635,7 +48665,7 @@ var marketplace_default = `{
48635
48665
  "name": "fulcrum",
48636
48666
  "source": "./",
48637
48667
  "description": "Task orchestration for Claude Code",
48638
- "version": "3.1.1",
48668
+ "version": "3.2.1",
48639
48669
  "skills": [
48640
48670
  "./skills/fulcrum"
48641
48671
  ],
@@ -49839,7 +49869,7 @@ function compareVersions(v1, v2) {
49839
49869
  var package_default = {
49840
49870
  name: "@knowsuchagency/fulcrum",
49841
49871
  private: true,
49842
- version: "3.1.1",
49872
+ version: "3.2.1",
49843
49873
  description: "Harness Attention. Orchestrate Agents. Ship.",
49844
49874
  license: "PolyForm-Perimeter-1.0.0",
49845
49875
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowsuchagency/fulcrum",
3
- "version": "3.1.1",
3
+ "version": "3.2.1",
4
4
  "description": "Harness Attention. Orchestrate Agents. Ship.",
5
5
  "license": "PolyForm-Perimeter-1.0.0",
6
6
  "repository": {
package/server/index.js CHANGED
@@ -1135470,6 +1135470,9 @@ function getChannelMessages(options = {}) {
1135470
1135470
  const results = db2.select().from(channelMessages).where(whereClause).orderBy(desc(channelMessages.messageTimestamp)).limit(options.limit ?? 50).offset(options.offset ?? 0).all();
1135471
1135471
  return results;
1135472
1135472
  }
1135473
+ function getChannelMessageById(id) {
1135474
+ return db2.select().from(channelMessages).where(eq(channelMessages.id, id)).get();
1135475
+ }
1135473
1135476
  function getChannelMessageCounts() {
1135474
1135477
  const results = db2.select({
1135475
1135478
  channelType: channelMessages.channelType,
@@ -1148027,6 +1148030,7 @@ ${attachment.data}`);
1148027
1148030
  const resultMsg = message;
1148028
1148031
  if (resultMsg.subtype?.startsWith("error_")) {
1148029
1148032
  const errors2 = resultMsg.errors || ["Unknown error"];
1148033
+ state.claudeSessionId = undefined;
1148030
1148034
  yield { type: "error", data: { message: errors2.join(", ") } };
1148031
1148035
  }
1148032
1148036
  if (resultMsg.structured_output) {
@@ -1148039,18 +1148043,21 @@ ${attachment.data}`);
1148039
1148043
  });
1148040
1148044
  }
1148041
1148045
  }
1148042
- addMessage(sessionId, {
1148043
- role: "assistant",
1148044
- content: currentText,
1148045
- model: MODEL_MAP[effectiveModelId],
1148046
- tokensIn,
1148047
- tokensOut,
1148048
- sessionId
1148049
- });
1148046
+ if (currentText.trim()) {
1148047
+ addMessage(sessionId, {
1148048
+ role: "assistant",
1148049
+ content: currentText,
1148050
+ model: MODEL_MAP[effectiveModelId],
1148051
+ tokensIn,
1148052
+ tokensOut,
1148053
+ sessionId
1148054
+ });
1148055
+ }
1148050
1148056
  yield { type: "done", data: {} };
1148051
1148057
  } catch (err) {
1148052
1148058
  const errorMsg = err instanceof Error ? err.message : String(err);
1148053
1148059
  log2.assistant.error("Assistant stream error", { sessionId, error: errorMsg });
1148060
+ state.claudeSessionId = undefined;
1148054
1148061
  yield { type: "error", data: { message: errorMsg } };
1148055
1148062
  } finally {
1148056
1148063
  for (const tempPath of tempFiles) {
@@ -1150021,6 +1150028,38 @@ var init_opencode_channel_service = __esm(() => {
1150021
1150028
  });
1150022
1150029
 
1150023
1150030
  // server/services/channels/message-handler.ts
1150031
+ function recordObserverFailure() {
1150032
+ OBSERVER_CIRCUIT_BREAKER.failureCount++;
1150033
+ if (OBSERVER_CIRCUIT_BREAKER.failureCount >= OBSERVER_CIRCUIT_BREAKER.failureThreshold) {
1150034
+ if (OBSERVER_CIRCUIT_BREAKER.state !== "open") {
1150035
+ log2.messaging.warn("Observer circuit breaker OPEN \u2014 pausing observe-only processing", {
1150036
+ failures: OBSERVER_CIRCUIT_BREAKER.failureCount,
1150037
+ cooldownMs: OBSERVER_CIRCUIT_BREAKER.cooldownMs
1150038
+ });
1150039
+ }
1150040
+ OBSERVER_CIRCUIT_BREAKER.state = "open";
1150041
+ OBSERVER_CIRCUIT_BREAKER.nextProbeAt = Date.now() + OBSERVER_CIRCUIT_BREAKER.cooldownMs;
1150042
+ OBSERVER_CIRCUIT_BREAKER.cooldownMs = Math.min(OBSERVER_CIRCUIT_BREAKER.cooldownMs * 2, OBSERVER_CIRCUIT_BREAKER.maxCooldownMs);
1150043
+ }
1150044
+ }
1150045
+ function recordObserverSuccess() {
1150046
+ if (OBSERVER_CIRCUIT_BREAKER.state === "open") {
1150047
+ log2.messaging.info("Observer circuit breaker CLOSED \u2014 resuming normal processing", {
1150048
+ previousFailures: OBSERVER_CIRCUIT_BREAKER.failureCount
1150049
+ });
1150050
+ }
1150051
+ OBSERVER_CIRCUIT_BREAKER.failureCount = 0;
1150052
+ OBSERVER_CIRCUIT_BREAKER.state = "closed";
1150053
+ OBSERVER_CIRCUIT_BREAKER.nextProbeAt = 0;
1150054
+ OBSERVER_CIRCUIT_BREAKER.cooldownMs = 60000;
1150055
+ }
1150056
+ function isObserverCircuitOpen() {
1150057
+ if (OBSERVER_CIRCUIT_BREAKER.state !== "open")
1150058
+ return false;
1150059
+ if (Date.now() >= OBSERVER_CIRCUIT_BREAKER.nextProbeAt)
1150060
+ return false;
1150061
+ return true;
1150062
+ }
1150024
1150063
  async function handleIncomingMessage(msg) {
1150025
1150064
  const content = msg.content.trim();
1150026
1150065
  const isObserveOnly = msg.metadata?.observeOnly === true;
@@ -1150268,12 +1150307,16 @@ function splitMessage(content, maxLength) {
1150268
1150307
  return parts;
1150269
1150308
  }
1150270
1150309
  async function processObserveOnlyMessage(msg) {
1150310
+ if (isObserverCircuitOpen()) {
1150311
+ return;
1150312
+ }
1150271
1150313
  const observeSessionKey = `observe-${msg.connectionId}`;
1150272
1150314
  const { session: session3 } = getOrCreateSession(msg.connectionId, observeSessionKey, "Observer", undefined, msg.channelType);
1150273
1150315
  const settings = getSettings();
1150274
1150316
  const observerProvider = settings.assistant.observerProvider ?? settings.assistant.provider;
1150275
1150317
  if (observerProvider === "opencode") {
1150276
1150318
  try {
1150319
+ let hadError = false;
1150277
1150320
  const stream2 = _deps.streamOpencodeObserverMessage(session3.id, msg.content, {
1150278
1150321
  channelType: msg.channelType,
1150279
1150322
  senderId: msg.senderId,
@@ -1150281,15 +1150324,21 @@ async function processObserveOnlyMessage(msg) {
1150281
1150324
  });
1150282
1150325
  for await (const event of stream2) {
1150283
1150326
  if (event.type === "error") {
1150327
+ hadError = true;
1150284
1150328
  const errorMsg = event.data.message;
1150285
1150329
  log2.messaging.error("Error in OpenCode observe-only processing", { error: errorMsg });
1150286
1150330
  }
1150287
1150331
  }
1150332
+ if (hadError)
1150333
+ recordObserverFailure();
1150334
+ else
1150335
+ recordObserverSuccess();
1150288
1150336
  } catch (err) {
1150289
1150337
  log2.messaging.error("Error processing observe-only message via OpenCode", {
1150290
1150338
  connectionId: msg.connectionId,
1150291
1150339
  error: String(err)
1150292
1150340
  });
1150341
+ recordObserverFailure();
1150293
1150342
  }
1150294
1150343
  return;
1150295
1150344
  }
@@ -1150305,6 +1150354,7 @@ async function processObserveOnlyMessage(msg) {
1150305
1150354
  };
1150306
1150355
  const systemPrompt = getObserveOnlySystemPrompt(msg.channelType, context);
1150307
1150356
  try {
1150357
+ let hadError = false;
1150308
1150358
  const observerModelId = settings.assistant.observerModel;
1150309
1150359
  const stream2 = _deps.streamMessage(session3.id, msg.content, {
1150310
1150360
  systemPromptAdditions: systemPrompt,
@@ -1150313,18 +1150363,24 @@ async function processObserveOnlyMessage(msg) {
1150313
1150363
  });
1150314
1150364
  for await (const event of stream2) {
1150315
1150365
  if (event.type === "error") {
1150366
+ hadError = true;
1150316
1150367
  const errorMsg = event.data.message;
1150317
1150368
  log2.messaging.error("Error in observe-only message processing", { error: errorMsg });
1150318
1150369
  }
1150319
1150370
  }
1150371
+ if (hadError)
1150372
+ recordObserverFailure();
1150373
+ else
1150374
+ recordObserverSuccess();
1150320
1150375
  } catch (err) {
1150321
1150376
  log2.messaging.error("Error processing observe-only message", {
1150322
1150377
  connectionId: msg.connectionId,
1150323
1150378
  error: String(err)
1150324
1150379
  });
1150380
+ recordObserverFailure();
1150325
1150381
  }
1150326
1150382
  }
1150327
- var _deps, SLACK_RESPONSE_SCHEMA, COMMANDS;
1150383
+ var _deps, SLACK_RESPONSE_SCHEMA, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150328
1150384
  var init_message_handler = __esm(() => {
1150329
1150385
  init_logger3();
1150330
1150386
  init_channel_manager();
@@ -1150351,6 +1150407,14 @@ var init_message_handler = __esm(() => {
1150351
1150407
  },
1150352
1150408
  required: ["body"]
1150353
1150409
  };
1150410
+ OBSERVER_CIRCUIT_BREAKER = {
1150411
+ failureCount: 0,
1150412
+ failureThreshold: 3,
1150413
+ state: "closed",
1150414
+ nextProbeAt: 0,
1150415
+ cooldownMs: 60000,
1150416
+ maxCooldownMs: 600000
1150417
+ };
1150354
1150418
  COMMANDS = {
1150355
1150419
  RESET: ["/reset", "/new", "/clear"],
1150356
1150420
  HELP: ["/help", "/?"],
@@ -1251641,6 +1251705,13 @@ var toolRegistry = [
1251641
1251705
  keywords: ["email", "fetch", "download", "imap", "uid", "message", "mail"],
1251642
1251706
  defer_loading: false
1251643
1251707
  },
1251708
+ {
1251709
+ name: "get_message",
1251710
+ description: "Get details of a specific message by ID, including full content and metadata",
1251711
+ category: "messaging",
1251712
+ keywords: ["message", "get", "read", "channel", "whatsapp", "discord", "telegram", "slack", "chat"],
1251713
+ defer_loading: false
1251714
+ },
1251644
1251715
  {
1251645
1251716
  name: "message",
1251646
1251717
  description: "Send a message to a messaging channel (WhatsApp, Discord, Telegram, Slack, Gmail)",
@@ -1251741,7 +1251812,7 @@ var toolRegistry = [
1251741
1251812
  },
1251742
1251813
  {
1251743
1251814
  name: "create_calendar_event",
1251744
- description: "Create a new calendar event on a CalDAV calendar",
1251815
+ description: "Create a new calendar event on a CalDAV or Google calendar.",
1251745
1251816
  category: "caldav",
1251746
1251817
  keywords: ["calendar", "event", "create", "new", "schedule", "appointment", "meeting"],
1251747
1251818
  defer_loading: true
@@ -1253559,6 +1253630,20 @@ var registerMemoryFileTools = (server2, client3) => {
1253559
1253630
  });
1253560
1253631
  };
1253561
1253632
 
1253633
+ // cli/src/mcp/tools/messaging.ts
1253634
+ var registerMessagingTools = (server2, client3) => {
1253635
+ server2.tool("get_message", "Get details of a specific message by ID, including full content and metadata.", {
1253636
+ id: exports_external.string().describe("Message ID")
1253637
+ }, async ({ id }) => {
1253638
+ try {
1253639
+ const message = await client3.getMessage(id);
1253640
+ return formatSuccess(message);
1253641
+ } catch (err) {
1253642
+ return handleToolError(err);
1253643
+ }
1253644
+ });
1253645
+ };
1253646
+
1253562
1253647
  // cli/src/mcp/tools/search.ts
1253563
1253648
  var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
1253564
1253649
  var registerSearchTools = (server2, client3) => {
@@ -1253613,6 +1253698,7 @@ function registerTools(server2, client3) {
1253613
1253698
  registerSettingsTools(server2, client3);
1253614
1253699
  registerBackupTools(server2, client3);
1253615
1253700
  registerEmailTools(server2, client3);
1253701
+ registerMessagingTools(server2, client3);
1253616
1253702
  registerAssistantTools(server2, client3);
1253617
1253703
  registerCaldavTools(server2, client3);
1253618
1253704
  registerGmailTools(server2, client3);
@@ -1254202,6 +1254288,9 @@ class FulcrumClient {
1254202
1254288
  async getEmail(id) {
1254203
1254289
  return this.fetch(`/api/messaging/email/emails/${id}`);
1254204
1254290
  }
1254291
+ async getMessage(id) {
1254292
+ return this.fetch(`/api/messaging/messages/${id}`);
1254293
+ }
1254205
1254294
  async searchEmails(criteria) {
1254206
1254295
  return this.fetch("/api/messaging/email/search", {
1254207
1254296
  method: "POST",
@@ -1254442,7 +1254531,7 @@ mcpRoutes.all("/", async (c) => {
1254442
1254531
  });
1254443
1254532
  const server2 = new McpServer({
1254444
1254533
  name: "fulcrum",
1254445
- version: "3.1.1"
1254534
+ version: "3.2.1"
1254446
1254535
  });
1254447
1254536
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254448
1254537
  registerTools(server2, client3);
@@ -1255107,6 +1255196,7 @@ var assistant_default = assistantRoutes;
1255107
1255196
  // server/routes/messaging.ts
1255108
1255197
  init_channels();
1255109
1255198
  init_email_storage();
1255199
+ init_message_storage();
1255110
1255200
  init_logger3();
1255111
1255201
  var app23 = new Hono2;
1255112
1255202
  app23.get("/channels", (c) => {
@@ -1255504,6 +1255594,19 @@ app23.post("/email/fetch", async (c) => {
1255504
1255594
  return c.json({ error: String(err) }, 500);
1255505
1255595
  }
1255506
1255596
  });
1255597
+ app23.get("/messages/:id", (c) => {
1255598
+ try {
1255599
+ const id = c.req.param("id");
1255600
+ const message = getChannelMessageById(id);
1255601
+ if (!message) {
1255602
+ return c.json({ error: "Message not found" }, 404);
1255603
+ }
1255604
+ return c.json(message);
1255605
+ } catch (err) {
1255606
+ log2.messaging.error("Failed to get message", { error: String(err) });
1255607
+ return c.json({ error: String(err) }, 500);
1255608
+ }
1255609
+ });
1255507
1255610
  app23.post("/send", async (c) => {
1255508
1255611
  try {
1255509
1255612
  const body = await c.req.json();
@@ -1255751,6 +1255854,7 @@ init_settings();
1255751
1255854
  init_logger3();
1255752
1255855
  init_ical_helpers();
1255753
1255856
  init_caldav_account_manager();
1255857
+ init_google_calendar_manager();
1255754
1255858
  var logger9 = createLogger("CalDAV");
1255755
1255859
  function migrateFromSettings() {
1255756
1255860
  const settings = getSettings();
@@ -1256056,6 +1256160,20 @@ async function createEvent(input) {
1256056
1256160
  if (!calendar2) {
1256057
1256161
  throw new Error(`Calendar not found: ${input.calendarId}`);
1256058
1256162
  }
1256163
+ if (calendar2.googleAccountId) {
1256164
+ await googleCalendarManager.createEvent(calendar2.id, {
1256165
+ summary: input.summary,
1256166
+ dtstart: input.dtstart,
1256167
+ dtend: input.dtend,
1256168
+ description: input.description,
1256169
+ location: input.location,
1256170
+ allDay: input.allDay
1256171
+ });
1256172
+ const created = db2.select().from(caldavEvents).where(eq(caldavEvents.calendarId, input.calendarId)).orderBy(desc(caldavEvents.createdAt)).limit(1).get();
1256173
+ if (!created)
1256174
+ throw new Error("Failed to retrieve created Google Calendar event");
1256175
+ return created;
1256176
+ }
1256059
1256177
  const client4 = calendar2.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256060
1256178
  if (!client4) {
1256061
1256179
  throw new Error("CalDAV account not connected for this calendar");
@@ -1256112,6 +1256230,10 @@ async function updateEvent(id, updates) {
1256112
1256230
  throw new Error(`Event not found: ${id}`);
1256113
1256231
  }
1256114
1256232
  const calendar2 = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, event.calendarId)).get();
1256233
+ if (calendar2?.googleAccountId) {
1256234
+ await googleCalendarManager.updateEvent(id, updates);
1256235
+ return db2.select().from(caldavEvents).where(eq(caldavEvents.id, id)).get();
1256236
+ }
1256115
1256237
  const client4 = calendar2?.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256116
1256238
  if (!client4) {
1256117
1256239
  throw new Error("CalDAV account not connected for this calendar");
@@ -1256158,6 +1256280,10 @@ async function deleteEvent(id) {
1256158
1256280
  throw new Error(`Event not found: ${id}`);
1256159
1256281
  }
1256160
1256282
  const calendar2 = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, event.calendarId)).get();
1256283
+ if (calendar2?.googleAccountId) {
1256284
+ await googleCalendarManager.deleteEvent(id);
1256285
+ return;
1256286
+ }
1256161
1256287
  const client4 = calendar2?.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256162
1256288
  if (!client4) {
1256163
1256289
  throw new Error("CalDAV account not connected for this calendar");