@knowsuchagency/fulcrum 2.17.2 → 3.0.0

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/README.md CHANGED
@@ -179,7 +179,9 @@ Chat with the AI assistant from anywhere via your favorite messaging platform.
179
179
  | **Discord** | Bot token from Developer Portal |
180
180
  | **Telegram** | Bot token from @BotFather |
181
181
  | **Slack** | Bot + App tokens with Socket Mode |
182
+ | **Gmail** | OAuth2, sends emails to your own address |
182
183
 
184
+ - **User-only messaging** — Outbound messages restricted to user's own accounts (no third-party messaging)
183
185
  - **Persistent sessions** — Conversation context maintained across messages
184
186
  - **Email threading** — Each email thread is a separate conversation
185
187
  - **Observe-first** — Email and WhatsApp collect all messages but only respond to authorized senders
@@ -250,14 +252,14 @@ Both plugins include an MCP server with 60+ tools:
250
252
  | **Apps** | Deploy, stop, and monitor Docker Compose applications |
251
253
  | **Filesystem** | Browse directories, read/write files on the Fulcrum server |
252
254
  | **Execution** | Run shell commands with persistent session support |
253
- | **Notifications** | Send notifications to enabled channels |
255
+ | **Notifications** | Send notifications to enabled channels (Slack, Discord, Pushover, WhatsApp, Telegram, Gmail) |
254
256
  | **Backup & Restore** | Snapshot database and settings; auto-safety-backup on restore |
255
257
  | **Settings** | View and update configuration; manage notification channels |
256
258
  | **Search** | Unified full-text search across tasks, projects, messages, events, memories, and conversations |
257
259
  | **Memory** | Read/update master memory file; store ephemeral knowledge with tags |
258
260
  | **Calendar** | Manage CalDAV accounts, sync calendars, configure event copy rules |
259
- | **Gmail** | List Google accounts, manage Gmail drafts (create, update, delete) |
260
- | **Assistant** | Send messages via channels; query sweep history |
261
+ | **Gmail** | List Google accounts, manage Gmail drafts, send emails |
262
+ | **Assistant** | Send messages via channels (WhatsApp, Discord, Telegram, Slack, Gmail); query sweep history |
261
263
 
262
264
  Use `search_tools` to discover available tools by keyword or category.
263
265
 
@@ -317,7 +319,7 @@ Settings are stored in `.fulcrum/settings.json`. The fulcrum directory is resolv
317
319
  | integrations.githubPat | `GITHUB_PAT` | null |
318
320
  | appearance.language | — | null (auto-detect) |
319
321
 
320
- Notification settings (sound, Slack, Discord, Pushover) are configured via Settings UI or CLI.
322
+ Notification settings (sound, Slack, Discord, Pushover, WhatsApp, Telegram, Gmail) are configured via Settings UI or CLI.
321
323
 
322
324
  ### Linear Integration
323
325
 
package/bin/fulcrum.js CHANGED
@@ -1660,6 +1660,12 @@ class FulcrumClient {
1660
1660
  async deleteGmailDraft(accountId, draftId) {
1661
1661
  return this.fetch(`/api/google/accounts/${accountId}/drafts/${draftId}`, { method: "DELETE" });
1662
1662
  }
1663
+ async sendGmailMessage(accountId, body, subject) {
1664
+ return this.fetch(`/api/google/accounts/${accountId}/send`, {
1665
+ method: "POST",
1666
+ body: JSON.stringify({ body, subject })
1667
+ });
1668
+ }
1663
1669
  async readMemoryFile() {
1664
1670
  return this.fetch("/api/memory-file");
1665
1671
  }
@@ -23838,7 +23844,7 @@ var require_formats = __commonJS((exports) => {
23838
23844
  }
23839
23845
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
23840
23846
  function getTime(strictTimeZone) {
23841
- return function time(str) {
23847
+ return function time3(str) {
23842
23848
  const matches = TIME.exec(str);
23843
23849
  if (!matches)
23844
23850
  return false;
@@ -43680,7 +43686,7 @@ var init_registry = __esm(() => {
43680
43686
  name: "send_notification",
43681
43687
  description: "Send a notification to all enabled channels",
43682
43688
  category: "notifications",
43683
- keywords: ["notify", "alert", "message", "slack", "discord"],
43689
+ keywords: ["notify", "alert", "message", "slack", "discord", "gmail"],
43684
43690
  defer_loading: false
43685
43691
  },
43686
43692
  {
@@ -44107,14 +44113,14 @@ var init_registry = __esm(() => {
44107
44113
  name: "get_notification_settings",
44108
44114
  description: "Get notification channel settings",
44109
44115
  category: "settings",
44110
- keywords: ["settings", "notifications", "slack", "discord", "pushover", "sound", "alert"],
44116
+ keywords: ["settings", "notifications", "slack", "discord", "pushover", "gmail", "sound", "alert"],
44111
44117
  defer_loading: false
44112
44118
  },
44113
44119
  {
44114
44120
  name: "update_notification_settings",
44115
44121
  description: "Update notification channel settings",
44116
44122
  category: "settings",
44117
- keywords: ["settings", "notifications", "slack", "discord", "pushover", "sound", "update", "enable", "disable"],
44123
+ keywords: ["settings", "notifications", "slack", "discord", "pushover", "whatsapp", "telegram", "gmail", "sound", "update", "enable", "disable"],
44118
44124
  defer_loading: false
44119
44125
  },
44120
44126
  {
@@ -44203,9 +44209,9 @@ var init_registry = __esm(() => {
44203
44209
  },
44204
44210
  {
44205
44211
  name: "message",
44206
- description: "Send a message to a messaging channel (WhatsApp, Discord, Telegram, Slack)",
44212
+ description: "Send a message to a messaging channel (WhatsApp, Discord, Telegram, Slack, Gmail)",
44207
44213
  category: "assistant",
44208
- keywords: ["message", "send", "reply", "whatsapp", "communicate", "respond"],
44214
+ keywords: ["message", "send", "reply", "whatsapp", "gmail", "email", "communicate", "respond"],
44209
44215
  defer_loading: false
44210
44216
  },
44211
44217
  {
@@ -45596,18 +45602,30 @@ var SENSITIVE_SETTINGS, registerSettingsTools = (server, client) => {
45596
45602
  })).describe("Sound notification settings"),
45597
45603
  slack: exports_external.optional(exports_external.object({
45598
45604
  enabled: exports_external.boolean().describe("Enable or disable Slack notifications"),
45599
- webhookUrl: exports_external.optional(exports_external.string()).describe("Slack webhook URL")
45605
+ webhookUrl: exports_external.optional(exports_external.string()).describe("Slack webhook URL"),
45606
+ useMessagingChannel: exports_external.optional(exports_external.boolean()).describe("Send via messaging channel instead of webhook")
45600
45607
  })).describe("Slack notification settings"),
45601
45608
  discord: exports_external.optional(exports_external.object({
45602
45609
  enabled: exports_external.boolean().describe("Enable or disable Discord notifications"),
45603
- webhookUrl: exports_external.optional(exports_external.string()).describe("Discord webhook URL")
45610
+ webhookUrl: exports_external.optional(exports_external.string()).describe("Discord webhook URL"),
45611
+ useMessagingChannel: exports_external.optional(exports_external.boolean()).describe("Send via messaging channel instead of webhook")
45604
45612
  })).describe("Discord notification settings"),
45605
45613
  pushover: exports_external.optional(exports_external.object({
45606
45614
  enabled: exports_external.boolean().describe("Enable or disable Pushover notifications"),
45607
45615
  appToken: exports_external.optional(exports_external.string()).describe("Pushover app token"),
45608
45616
  userKey: exports_external.optional(exports_external.string()).describe("Pushover user key")
45609
- })).describe("Pushover notification settings")
45610
- }, async ({ enabled, toast, desktop, sound, slack, discord, pushover }) => {
45617
+ })).describe("Pushover notification settings"),
45618
+ whatsapp: exports_external.optional(exports_external.object({
45619
+ enabled: exports_external.boolean().describe("Enable or disable WhatsApp notifications (requires connected messaging channel)")
45620
+ })).describe("WhatsApp notification settings (uses messaging channel)"),
45621
+ telegram: exports_external.optional(exports_external.object({
45622
+ enabled: exports_external.boolean().describe("Enable or disable Telegram notifications (requires connected messaging channel)")
45623
+ })).describe("Telegram notification settings (uses messaging channel)"),
45624
+ gmail: exports_external.optional(exports_external.object({
45625
+ enabled: exports_external.boolean().describe("Enable or disable Gmail notifications (sends email to your own Gmail address)"),
45626
+ googleAccountId: exports_external.optional(exports_external.string()).describe("Google account ID to send notifications from")
45627
+ })).describe("Gmail notification settings (sends email via Gmail API)")
45628
+ }, async ({ enabled, toast, desktop, sound, slack, discord, pushover, whatsapp, telegram, gmail }) => {
45611
45629
  try {
45612
45630
  const updates = {};
45613
45631
  if (enabled !== undefined)
@@ -45624,6 +45642,12 @@ var SENSITIVE_SETTINGS, registerSettingsTools = (server, client) => {
45624
45642
  updates.discord = discord;
45625
45643
  if (pushover !== undefined)
45626
45644
  updates.pushover = pushover;
45645
+ if (whatsapp !== undefined)
45646
+ updates.whatsapp = whatsapp;
45647
+ if (telegram !== undefined)
45648
+ updates.telegram = telegram;
45649
+ if (gmail !== undefined)
45650
+ updates.gmail = gmail;
45627
45651
  const result = await client.updateNotifications(updates);
45628
45652
  return formatSuccess({
45629
45653
  ...result,
@@ -45794,17 +45818,32 @@ var init_email = __esm(() => {
45794
45818
  // cli/src/mcp/tools/assistant.ts
45795
45819
  var ChannelSchema, registerAssistantTools = (server, client) => {
45796
45820
  server.tool("message", "Send a message to a messaging channel (WhatsApp, Discord, Telegram, Slack). Use this to reply to messages or send proactive communications. For email, use create_gmail_draft instead.", {
45797
- channel: ChannelSchema.describe("Target channel: whatsapp, discord, telegram, slack, or all"),
45798
- to: exports_external.optional(exports_external.string()).describe("Recipient identifier. Optional \u2014 if omitted, auto-resolves to the channel's primary user."),
45821
+ channel: ChannelSchema.describe("Target channel: whatsapp, discord, telegram, slack, or gmail"),
45799
45822
  body: exports_external.string().describe("Message content"),
45800
- subject: exports_external.optional(exports_external.string()).describe("Email subject (for email channel only)"),
45823
+ subject: exports_external.optional(exports_external.string()).describe("Email subject (for Gmail channel only)"),
45801
45824
  replyToMessageId: exports_external.optional(exports_external.string()).describe("Message ID to reply to (for threading)"),
45802
- slack_blocks: exports_external.optional(exports_external.array(exports_external.record(exports_external.string(), exports_external.any()))).describe("Slack Block Kit blocks for rich formatting (Slack channel only). Array of block objects.")
45803
- }, async ({ channel, to, body, subject, replyToMessageId, slack_blocks }) => {
45825
+ slack_blocks: exports_external.optional(exports_external.array(exports_external.record(exports_external.string(), exports_external.any()))).describe("Slack Block Kit blocks for rich formatting (Slack channel only). Array of block objects."),
45826
+ googleAccountId: exports_external.optional(exports_external.string()).describe("Google account ID for Gmail channel. If omitted, auto-resolves when exactly one Gmail-enabled account exists.")
45827
+ }, async ({ channel, body, subject, replyToMessageId, slack_blocks, googleAccountId }) => {
45804
45828
  try {
45829
+ if (channel === "gmail") {
45830
+ let accountId = googleAccountId;
45831
+ if (!accountId) {
45832
+ const accounts = await client.listGoogleAccounts();
45833
+ const gmailAccounts = accounts.filter((a2) => a2.gmailEnabled);
45834
+ if (gmailAccounts.length === 0) {
45835
+ return handleToolError(new Error("No Gmail-enabled Google accounts configured"));
45836
+ }
45837
+ if (gmailAccounts.length > 1) {
45838
+ return handleToolError(new Error(`Multiple Gmail-enabled accounts found. Specify googleAccountId. Accounts: ${gmailAccounts.map((a2) => `${a2.id} (${a2.email})`).join(", ")}`));
45839
+ }
45840
+ accountId = gmailAccounts[0].id;
45841
+ }
45842
+ const result2 = await client.sendGmailMessage(accountId, body, subject);
45843
+ return formatSuccess(result2);
45844
+ }
45805
45845
  const result = await client.sendMessage({
45806
45846
  channel,
45807
- to,
45808
45847
  body,
45809
45848
  subject,
45810
45849
  replyToMessageId,
@@ -45829,7 +45868,7 @@ var ChannelSchema, registerAssistantTools = (server, client) => {
45829
45868
  var init_assistant = __esm(() => {
45830
45869
  init_zod2();
45831
45870
  init_utils();
45832
- ChannelSchema = exports_external.enum(["whatsapp", "discord", "telegram", "slack", "all"]);
45871
+ ChannelSchema = exports_external.enum(["whatsapp", "discord", "telegram", "slack", "gmail"]);
45833
45872
  });
45834
45873
 
45835
45874
  // cli/src/mcp/tools/caldav.ts
@@ -46247,7 +46286,7 @@ async function runMcpServer(urlOverride, portOverride) {
46247
46286
  const client = new FulcrumClient(urlOverride, portOverride);
46248
46287
  const server = new McpServer({
46249
46288
  name: "fulcrum",
46250
- version: "2.17.2"
46289
+ version: "3.0.0"
46251
46290
  });
46252
46291
  registerTools(server, client);
46253
46292
  const transport = new StdioServerTransport;
@@ -48596,7 +48635,7 @@ var marketplace_default = `{
48596
48635
  "name": "fulcrum",
48597
48636
  "source": "./",
48598
48637
  "description": "Task orchestration for Claude Code",
48599
- "version": "2.17.2",
48638
+ "version": "3.0.0",
48600
48639
  "skills": [
48601
48640
  "./skills/fulcrum"
48602
48641
  ],
@@ -49224,8 +49263,33 @@ var claudeCommand = defineCommand({
49224
49263
  // cli/src/commands/notifications.ts
49225
49264
  init_client();
49226
49265
  init_errors();
49227
- var VALID_CHANNELS = ["sound", "slack", "discord", "pushover"];
49266
+ var VALID_CHANNELS = ["sound", "slack", "discord", "pushover", "whatsapp", "telegram", "gmail"];
49228
49267
  async function handleNotificationsCommand(action, positional, flags) {
49268
+ if (action === "test") {
49269
+ const [channel] = positional;
49270
+ if (!channel) {
49271
+ throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49272
+ }
49273
+ if (!VALID_CHANNELS.includes(channel)) {
49274
+ throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49275
+ }
49276
+ } else if (action === "set") {
49277
+ const [channel, key, value] = positional;
49278
+ if (!channel) {
49279
+ throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49280
+ }
49281
+ if (!VALID_CHANNELS.includes(channel)) {
49282
+ throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49283
+ }
49284
+ if (!key) {
49285
+ throw new CliError("MISSING_KEY", "Setting key is required", ExitCodes.INVALID_ARGS);
49286
+ }
49287
+ if (value === undefined) {
49288
+ throw new CliError("MISSING_VALUE", "Setting value is required", ExitCodes.INVALID_ARGS);
49289
+ }
49290
+ } else if (action !== "status" && action !== "enable" && action !== "disable" && action !== undefined) {
49291
+ throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, enable, disable, test, set`, ExitCodes.INVALID_ARGS);
49292
+ }
49229
49293
  const client = new FulcrumClient(flags.url, flags.port);
49230
49294
  switch (action) {
49231
49295
  case "status":
@@ -49249,6 +49313,17 @@ Channels:`);
49249
49313
  if (settings.pushover) {
49250
49314
  console.log(` pushover: ${settings.pushover.enabled ? "enabled" : "disabled"}`);
49251
49315
  }
49316
+ if (settings.whatsapp) {
49317
+ console.log(` whatsapp: ${settings.whatsapp.enabled ? "enabled" : "disabled"} (messaging channel)`);
49318
+ }
49319
+ if (settings.telegram) {
49320
+ console.log(` telegram: ${settings.telegram.enabled ? "enabled" : "disabled"} (messaging channel)`);
49321
+ }
49322
+ if (settings.gmail) {
49323
+ const gmail = settings.gmail;
49324
+ const accountInfo = gmail.googleAccountId ? ` (account: ${gmail.googleAccountId})` : " (auto-resolve)";
49325
+ console.log(` gmail: ${gmail.enabled ? "enabled" : "disabled"}${gmail.enabled ? accountInfo : ""}`);
49326
+ }
49252
49327
  }
49253
49328
  break;
49254
49329
  }
@@ -49272,12 +49347,6 @@ Channels:`);
49272
49347
  }
49273
49348
  case "test": {
49274
49349
  const [channel] = positional;
49275
- if (!channel) {
49276
- throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49277
- }
49278
- if (!VALID_CHANNELS.includes(channel)) {
49279
- throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49280
- }
49281
49350
  const result = await client.testNotification(channel);
49282
49351
  if (isJsonOutput()) {
49283
49352
  output(result);
@@ -49292,18 +49361,6 @@ Channels:`);
49292
49361
  }
49293
49362
  case "set": {
49294
49363
  const [channel, key, value] = positional;
49295
- if (!channel) {
49296
- throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49297
- }
49298
- if (!VALID_CHANNELS.includes(channel)) {
49299
- throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
49300
- }
49301
- if (!key) {
49302
- throw new CliError("MISSING_KEY", "Setting key is required", ExitCodes.INVALID_ARGS);
49303
- }
49304
- if (value === undefined) {
49305
- throw new CliError("MISSING_VALUE", "Setting value is required", ExitCodes.INVALID_ARGS);
49306
- }
49307
49364
  const update = buildChannelUpdate(channel, key, value);
49308
49365
  const updated = await client.updateNotifications(update);
49309
49366
  if (isJsonOutput()) {
@@ -49313,8 +49370,6 @@ Channels:`);
49313
49370
  }
49314
49371
  break;
49315
49372
  }
49316
- default:
49317
- throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, enable, disable, test, set`, ExitCodes.INVALID_ARGS);
49318
49373
  }
49319
49374
  }
49320
49375
  function buildChannelUpdate(channel, key, value) {
@@ -49350,7 +49405,7 @@ var notificationsTestCommand = defineCommand({
49350
49405
  meta: { name: "test", description: "Test a notification channel" },
49351
49406
  args: {
49352
49407
  ...globalArgs,
49353
- channel: { type: "positional", description: "Channel to test (sound, slack, discord, pushover)", required: true }
49408
+ channel: { type: "positional", description: "Channel to test (sound, slack, discord, pushover, whatsapp, telegram)", required: true }
49354
49409
  },
49355
49410
  async run({ args }) {
49356
49411
  setupJsonOutput(args);
@@ -49361,7 +49416,7 @@ var notificationsSetCommand = defineCommand({
49361
49416
  meta: { name: "set", description: "Set a notification channel config" },
49362
49417
  args: {
49363
49418
  ...globalArgs,
49364
- channel: { type: "positional", description: "Channel (sound, slack, discord, pushover)", required: true },
49419
+ channel: { type: "positional", description: "Channel (sound, slack, discord, pushover, whatsapp, telegram)", required: true },
49365
49420
  key: { type: "positional", description: "Config key", required: true },
49366
49421
  value: { type: "positional", description: "Config value", required: true }
49367
49422
  },
@@ -49390,12 +49445,12 @@ var notificationsCommand = defineCommand({
49390
49445
  init_client();
49391
49446
  init_errors();
49392
49447
  async function handleNotifyCommand(positional, flags) {
49393
- const client = new FulcrumClient(flags.url, flags.port);
49394
49448
  const title = flags.title || positional[0];
49395
49449
  const message = flags.message || positional.slice(1).join(" ") || positional[0];
49396
49450
  if (!title) {
49397
49451
  throw new CliError("MISSING_TITLE", "Title is required. Usage: fulcrum notify <title> [message] or --title=<title> --message=<message>", ExitCodes.INVALID_ARGS);
49398
49452
  }
49453
+ const client = new FulcrumClient(flags.url, flags.port);
49399
49454
  const result = await client.sendNotification(title, message || title);
49400
49455
  if (isJsonOutput()) {
49401
49456
  output(result);
@@ -49784,7 +49839,7 @@ function compareVersions(v1, v2) {
49784
49839
  var package_default = {
49785
49840
  name: "@knowsuchagency/fulcrum",
49786
49841
  private: true,
49787
- version: "2.17.2",
49842
+ version: "3.0.0",
49788
49843
  description: "Harness Attention. Orchestrate Agents. Ship.",
49789
49844
  license: "PolyForm-Perimeter-1.0.0",
49790
49845
  type: "module",