@knowsuchagency/fulcrum 3.2.0 → 3.3.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/server/index.js CHANGED
@@ -1147055,8 +1147055,10 @@ You have access to Fulcrum's MCP tools. Use them proactively to help users.
1147055
1147055
  - Use \`search\` with \`memoryTags: ["actionable"]\` to review tracked items
1147056
1147056
 
1147057
1147057
  **Unified Search:**
1147058
- - \`search\` - Cross-entity FTS5 full-text search across tasks, projects, messages, events, memories, and conversations
1147059
- - Filter by entity type: \`entities: ["tasks", "projects", "messages", "events", "memories", "conversations"]\`
1147058
+ - \`search\` - Cross-entity FTS5 full-text search across tasks, projects, messages, events, memories, conversations, and gmail
1147059
+ - Filter by entity type: \`entities: ["tasks", "projects", "messages", "events", "memories", "conversations", "gmail"]\`
1147060
+ - **Gmail is opt-in** \u2014 not included in default searches to avoid latency/rate-limit impact. Must be explicitly requested via \`entities: ["gmail"]\`
1147061
+ - Gmail-specific filters: \`gmailFrom\`, \`gmailTo\`, \`gmailAfter\`, \`gmailBefore\`
1147060
1147062
  - Entity-specific filters: \`taskStatus\`, \`projectStatus\`, \`messageChannel\`, \`messageDirection\`, \`eventFrom\`, \`eventTo\`, \`memoryTags\`, \`conversationRole\`, \`conversationProvider\`, \`conversationProjectId\`
1147061
1147063
  - Conversations search indexes AI assistant chat messages (excludes system prompts) with session context
1147062
1147064
  - Results sorted by relevance score with BM25 ranking
@@ -1147398,7 +1147400,7 @@ Fulcrum is your digital concierge - a personal command center where you track ev
1147398
1147400
  - list_projects, create_project
1147399
1147401
  - execute_command (run any CLI command)
1147400
1147402
  - send_notification
1147401
- - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations)
1147403
+ - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations; gmail is opt-in via \`entities: ["gmail"]\`)
1147402
1147404
  - memory_file_read, memory_file_update (master memory file - always in prompt)
1147403
1147405
  - memory_store (ephemeral knowledge snippets with tags)
1147404
1147406
  - message (send to WhatsApp/Discord/Telegram/Slack/Gmail - user-only, concierge mode)
@@ -1147597,7 +1147599,7 @@ function addMessage(sessionId, message) {
1147597
1147599
  return db2.select().from(chatMessages).where(eq(chatMessages.id, id)).get();
1147598
1147600
  }
1147599
1147601
  function getMessages(sessionId) {
1147600
- return db2.select().from(chatMessages).where(eq(chatMessages.sessionId, sessionId)).orderBy(chatMessages.createdAt).all();
1147602
+ return db2.select().from(chatMessages).where(eq(chatMessages.sessionId, sessionId)).orderBy(chatMessages.createdAt, sql`rowid`).all();
1147601
1147603
  }
1147602
1147604
  function buildBaselinePrompt(condensed = false) {
1147603
1147605
  const settings = getSettings();
@@ -1148030,6 +1148032,7 @@ ${attachment.data}`);
1148030
1148032
  const resultMsg = message;
1148031
1148033
  if (resultMsg.subtype?.startsWith("error_")) {
1148032
1148034
  const errors2 = resultMsg.errors || ["Unknown error"];
1148035
+ state.claudeSessionId = undefined;
1148033
1148036
  yield { type: "error", data: { message: errors2.join(", ") } };
1148034
1148037
  }
1148035
1148038
  if (resultMsg.structured_output) {
@@ -1148042,18 +1148045,21 @@ ${attachment.data}`);
1148042
1148045
  });
1148043
1148046
  }
1148044
1148047
  }
1148045
- addMessage(sessionId, {
1148046
- role: "assistant",
1148047
- content: currentText,
1148048
- model: MODEL_MAP[effectiveModelId],
1148049
- tokensIn,
1148050
- tokensOut,
1148051
- sessionId
1148052
- });
1148048
+ if (currentText.trim()) {
1148049
+ addMessage(sessionId, {
1148050
+ role: "assistant",
1148051
+ content: currentText,
1148052
+ model: MODEL_MAP[effectiveModelId],
1148053
+ tokensIn,
1148054
+ tokensOut,
1148055
+ sessionId
1148056
+ });
1148057
+ }
1148053
1148058
  yield { type: "done", data: {} };
1148054
1148059
  } catch (err) {
1148055
1148060
  const errorMsg = err instanceof Error ? err.message : String(err);
1148056
1148061
  log2.assistant.error("Assistant stream error", { sessionId, error: errorMsg });
1148062
+ state.claudeSessionId = undefined;
1148057
1148063
  yield { type: "error", data: { message: errorMsg } };
1148058
1148064
  } finally {
1148059
1148065
  for (const tempPath of tempFiles) {
@@ -1150024,6 +1150030,38 @@ var init_opencode_channel_service = __esm(() => {
1150024
1150030
  });
1150025
1150031
 
1150026
1150032
  // server/services/channels/message-handler.ts
1150033
+ function recordObserverFailure() {
1150034
+ OBSERVER_CIRCUIT_BREAKER.failureCount++;
1150035
+ if (OBSERVER_CIRCUIT_BREAKER.failureCount >= OBSERVER_CIRCUIT_BREAKER.failureThreshold) {
1150036
+ if (OBSERVER_CIRCUIT_BREAKER.state !== "open") {
1150037
+ log2.messaging.warn("Observer circuit breaker OPEN \u2014 pausing observe-only processing", {
1150038
+ failures: OBSERVER_CIRCUIT_BREAKER.failureCount,
1150039
+ cooldownMs: OBSERVER_CIRCUIT_BREAKER.cooldownMs
1150040
+ });
1150041
+ }
1150042
+ OBSERVER_CIRCUIT_BREAKER.state = "open";
1150043
+ OBSERVER_CIRCUIT_BREAKER.nextProbeAt = Date.now() + OBSERVER_CIRCUIT_BREAKER.cooldownMs;
1150044
+ OBSERVER_CIRCUIT_BREAKER.cooldownMs = Math.min(OBSERVER_CIRCUIT_BREAKER.cooldownMs * 2, OBSERVER_CIRCUIT_BREAKER.maxCooldownMs);
1150045
+ }
1150046
+ }
1150047
+ function recordObserverSuccess() {
1150048
+ if (OBSERVER_CIRCUIT_BREAKER.state === "open") {
1150049
+ log2.messaging.info("Observer circuit breaker CLOSED \u2014 resuming normal processing", {
1150050
+ previousFailures: OBSERVER_CIRCUIT_BREAKER.failureCount
1150051
+ });
1150052
+ }
1150053
+ OBSERVER_CIRCUIT_BREAKER.failureCount = 0;
1150054
+ OBSERVER_CIRCUIT_BREAKER.state = "closed";
1150055
+ OBSERVER_CIRCUIT_BREAKER.nextProbeAt = 0;
1150056
+ OBSERVER_CIRCUIT_BREAKER.cooldownMs = 60000;
1150057
+ }
1150058
+ function isObserverCircuitOpen() {
1150059
+ if (OBSERVER_CIRCUIT_BREAKER.state !== "open")
1150060
+ return false;
1150061
+ if (Date.now() >= OBSERVER_CIRCUIT_BREAKER.nextProbeAt)
1150062
+ return false;
1150063
+ return true;
1150064
+ }
1150027
1150065
  async function handleIncomingMessage(msg) {
1150028
1150066
  const content = msg.content.trim();
1150029
1150067
  const isObserveOnly = msg.metadata?.observeOnly === true;
@@ -1150271,12 +1150309,16 @@ function splitMessage(content, maxLength) {
1150271
1150309
  return parts;
1150272
1150310
  }
1150273
1150311
  async function processObserveOnlyMessage(msg) {
1150312
+ if (isObserverCircuitOpen()) {
1150313
+ return;
1150314
+ }
1150274
1150315
  const observeSessionKey = `observe-${msg.connectionId}`;
1150275
1150316
  const { session: session3 } = getOrCreateSession(msg.connectionId, observeSessionKey, "Observer", undefined, msg.channelType);
1150276
1150317
  const settings = getSettings();
1150277
1150318
  const observerProvider = settings.assistant.observerProvider ?? settings.assistant.provider;
1150278
1150319
  if (observerProvider === "opencode") {
1150279
1150320
  try {
1150321
+ let hadError = false;
1150280
1150322
  const stream2 = _deps.streamOpencodeObserverMessage(session3.id, msg.content, {
1150281
1150323
  channelType: msg.channelType,
1150282
1150324
  senderId: msg.senderId,
@@ -1150284,15 +1150326,21 @@ async function processObserveOnlyMessage(msg) {
1150284
1150326
  });
1150285
1150327
  for await (const event of stream2) {
1150286
1150328
  if (event.type === "error") {
1150329
+ hadError = true;
1150287
1150330
  const errorMsg = event.data.message;
1150288
1150331
  log2.messaging.error("Error in OpenCode observe-only processing", { error: errorMsg });
1150289
1150332
  }
1150290
1150333
  }
1150334
+ if (hadError)
1150335
+ recordObserverFailure();
1150336
+ else
1150337
+ recordObserverSuccess();
1150291
1150338
  } catch (err) {
1150292
1150339
  log2.messaging.error("Error processing observe-only message via OpenCode", {
1150293
1150340
  connectionId: msg.connectionId,
1150294
1150341
  error: String(err)
1150295
1150342
  });
1150343
+ recordObserverFailure();
1150296
1150344
  }
1150297
1150345
  return;
1150298
1150346
  }
@@ -1150308,6 +1150356,7 @@ async function processObserveOnlyMessage(msg) {
1150308
1150356
  };
1150309
1150357
  const systemPrompt = getObserveOnlySystemPrompt(msg.channelType, context);
1150310
1150358
  try {
1150359
+ let hadError = false;
1150311
1150360
  const observerModelId = settings.assistant.observerModel;
1150312
1150361
  const stream2 = _deps.streamMessage(session3.id, msg.content, {
1150313
1150362
  systemPromptAdditions: systemPrompt,
@@ -1150316,18 +1150365,24 @@ async function processObserveOnlyMessage(msg) {
1150316
1150365
  });
1150317
1150366
  for await (const event of stream2) {
1150318
1150367
  if (event.type === "error") {
1150368
+ hadError = true;
1150319
1150369
  const errorMsg = event.data.message;
1150320
1150370
  log2.messaging.error("Error in observe-only message processing", { error: errorMsg });
1150321
1150371
  }
1150322
1150372
  }
1150373
+ if (hadError)
1150374
+ recordObserverFailure();
1150375
+ else
1150376
+ recordObserverSuccess();
1150323
1150377
  } catch (err) {
1150324
1150378
  log2.messaging.error("Error processing observe-only message", {
1150325
1150379
  connectionId: msg.connectionId,
1150326
1150380
  error: String(err)
1150327
1150381
  });
1150382
+ recordObserverFailure();
1150328
1150383
  }
1150329
1150384
  }
1150330
- var _deps, SLACK_RESPONSE_SCHEMA, COMMANDS;
1150385
+ var _deps, SLACK_RESPONSE_SCHEMA, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150331
1150386
  var init_message_handler = __esm(() => {
1150332
1150387
  init_logger3();
1150333
1150388
  init_channel_manager();
@@ -1150354,6 +1150409,14 @@ var init_message_handler = __esm(() => {
1150354
1150409
  },
1150355
1150410
  required: ["body"]
1150356
1150411
  };
1150412
+ OBSERVER_CIRCUIT_BREAKER = {
1150413
+ failureCount: 0,
1150414
+ failureThreshold: 3,
1150415
+ state: "closed",
1150416
+ nextProbeAt: 0,
1150417
+ cooldownMs: 60000,
1150418
+ maxCooldownMs: 600000
1150419
+ };
1150357
1150420
  COMMANDS = {
1150358
1150421
  RESET: ["/reset", "/new", "/clear"],
1150359
1150422
  HELP: ["/help", "/?"],
@@ -1188324,13 +1188387,13 @@ var require_core = __commonJS((exports) => {
1188324
1188387
  return metaOpts;
1188325
1188388
  }
1188326
1188389
  var noLogs = { log() {}, warn() {}, error() {} };
1188327
- function getLogger(logger6) {
1188328
- if (logger6 === false)
1188390
+ function getLogger(logger7) {
1188391
+ if (logger7 === false)
1188329
1188392
  return noLogs;
1188330
- if (logger6 === undefined)
1188393
+ if (logger7 === undefined)
1188331
1188394
  return console;
1188332
- if (logger6.log && logger6.warn && logger6.error)
1188333
- return logger6;
1188395
+ if (logger7.log && logger7.warn && logger7.error)
1188396
+ return logger7;
1188334
1188397
  throw new Error("logger must implement log, warn and error methods");
1188335
1188398
  }
1188336
1188399
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -1193957,13 +1194020,13 @@ var require_core3 = __commonJS((exports) => {
1193957
1194020
  return metaOpts;
1193958
1194021
  }
1193959
1194022
  var noLogs = { log() {}, warn() {}, error() {} };
1193960
- function getLogger(logger6) {
1193961
- if (logger6 === false)
1194023
+ function getLogger(logger7) {
1194024
+ if (logger7 === false)
1193962
1194025
  return noLogs;
1193963
- if (logger6 === undefined)
1194026
+ if (logger7 === undefined)
1193964
1194027
  return console;
1193965
- if (logger6.log && logger6.warn && logger6.error)
1193966
- return logger6;
1194028
+ if (logger7.log && logger7.warn && logger7.error)
1194029
+ return logger7;
1193967
1194030
  throw new Error("logger must implement log, warn and error methods");
1193968
1194031
  }
1193969
1194032
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -1200355,10 +1200418,10 @@ class CaldavAccountManager {
1200355
1200418
  }
1200356
1200419
  async startAll() {
1200357
1200420
  const accounts = db2.select().from(caldavAccounts).where(eq(caldavAccounts.enabled, true)).all();
1200358
- logger6.info("Starting all CalDAV accounts", { count: accounts.length });
1200421
+ logger7.info("Starting all CalDAV accounts", { count: accounts.length });
1200359
1200422
  for (const account2 of accounts) {
1200360
1200423
  await this.startAccount(account2.id).catch((err) => {
1200361
- logger6.error("Failed to start account", {
1200424
+ logger7.error("Failed to start account", {
1200362
1200425
  accountId: account2.id,
1200363
1200426
  name: account2.name,
1200364
1200427
  error: err instanceof Error ? err.message : String(err)
@@ -1200371,7 +1200434,7 @@ class CaldavAccountManager {
1200371
1200434
  this.stopAccount(accountId);
1200372
1200435
  }
1200373
1200436
  this.connections.clear();
1200374
- logger6.info("All CalDAV accounts stopped");
1200437
+ logger7.info("All CalDAV accounts stopped");
1200375
1200438
  }
1200376
1200439
  async startAccount(accountId) {
1200377
1200440
  const account2 = db2.select().from(caldavAccounts).where(eq(caldavAccounts.id, accountId)).get();
@@ -1200379,7 +1200442,7 @@ class CaldavAccountManager {
1200379
1200442
  throw new Error(`Account not found: ${accountId}`);
1200380
1200443
  }
1200381
1200444
  if (!account2.enabled) {
1200382
- logger6.info("Account is disabled, skipping", { accountId, name: account2.name });
1200445
+ logger7.info("Account is disabled, skipping", { accountId, name: account2.name });
1200383
1200446
  return;
1200384
1200447
  }
1200385
1200448
  this.stopAccount(accountId);
@@ -1200395,11 +1200458,11 @@ class CaldavAccountManager {
1200395
1200458
  try {
1200396
1200459
  conn.client = await this.connect(account2);
1200397
1200460
  this.scheduleSync(accountId, account2.syncIntervalMinutes ?? 15);
1200398
- logger6.info("Started CalDAV account", { accountId, name: account2.name });
1200461
+ logger7.info("Started CalDAV account", { accountId, name: account2.name });
1200399
1200462
  } catch (err) {
1200400
1200463
  conn.lastSyncError = err instanceof Error ? err.message : String(err);
1200401
1200464
  this.updateAccountError(accountId, conn.lastSyncError);
1200402
- logger6.error("Failed to connect account", { accountId, error: conn.lastSyncError });
1200465
+ logger7.error("Failed to connect account", { accountId, error: conn.lastSyncError });
1200403
1200466
  this.scheduleRetry(accountId);
1200404
1200467
  }
1200405
1200468
  }
@@ -1200441,7 +1200504,7 @@ class CaldavAccountManager {
1200441
1200504
  for (const [accountId, conn] of this.connections) {
1200442
1200505
  if (conn.client && !conn.isSyncing) {
1200443
1200506
  promises.push(this.syncAccount(accountId).catch((err) => {
1200444
- logger6.error("Sync failed for account", {
1200507
+ logger7.error("Sync failed for account", {
1200445
1200508
  accountId,
1200446
1200509
  error: err instanceof Error ? err.message : String(err)
1200447
1200510
  });
@@ -1200501,7 +1200564,7 @@ class CaldavAccountManager {
1200501
1200564
  defaultAccountType: "caldav"
1200502
1200565
  });
1200503
1200566
  await client4.login();
1200504
- logger6.info("Connected to CalDAV server (Basic)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200567
+ logger7.info("Connected to CalDAV server (Basic)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200505
1200568
  return client4;
1200506
1200569
  }
1200507
1200570
  async connectOAuth(account2) {
@@ -1200536,9 +1200599,9 @@ class CaldavAccountManager {
1200536
1200599
  credentials.expiration = newTokens.expiration;
1200537
1200600
  try {
1200538
1200601
  db2.update(caldavAccounts).set({ oauthTokens: newTokens, updatedAt: new Date().toISOString() }).where(eq(caldavAccounts.id, accountId)).run();
1200539
- logger6.info("Persisted refreshed OAuth tokens", { accountId });
1200602
+ logger7.info("Persisted refreshed OAuth tokens", { accountId });
1200540
1200603
  } catch (err) {
1200541
- logger6.error("Failed to persist refreshed OAuth tokens", {
1200604
+ logger7.error("Failed to persist refreshed OAuth tokens", {
1200542
1200605
  accountId,
1200543
1200606
  error: err instanceof Error ? err.message : String(err)
1200544
1200607
  });
@@ -1200549,7 +1200612,7 @@ class CaldavAccountManager {
1200549
1200612
  defaultAccountType: "caldav"
1200550
1200613
  });
1200551
1200614
  await client4.login();
1200552
- logger6.info("Connected to CalDAV server (Google OAuth)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200615
+ logger7.info("Connected to CalDAV server (Google OAuth)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200553
1200616
  return client4;
1200554
1200617
  }
1200555
1200618
  scheduleSync(accountId, intervalMinutes) {
@@ -1200561,7 +1200624,7 @@ class CaldavAccountManager {
1200561
1200624
  }
1200562
1200625
  conn.syncInterval = setInterval(() => {
1200563
1200626
  this.syncAccount(accountId).catch((err) => {
1200564
- logger6.error("Periodic sync failed", {
1200627
+ logger7.error("Periodic sync failed", {
1200565
1200628
  accountId,
1200566
1200629
  error: err instanceof Error ? err.message : String(err)
1200567
1200630
  });
@@ -1200574,7 +1200637,7 @@ class CaldavAccountManager {
1200574
1200637
  return;
1200575
1200638
  conn.retryCount++;
1200576
1200639
  const delay2 = Math.min(1000 * Math.pow(2, conn.retryCount), MAX_RETRY_DELAY);
1200577
- logger6.info("Scheduling account retry", { accountId, retryCount: conn.retryCount, delayMs: delay2 });
1200640
+ logger7.info("Scheduling account retry", { accountId, retryCount: conn.retryCount, delayMs: delay2 });
1200578
1200641
  setTimeout(async () => {
1200579
1200642
  const account2 = db2.select().from(caldavAccounts).where(eq(caldavAccounts.id, accountId)).get();
1200580
1200643
  if (!account2 || !account2.enabled)
@@ -1200590,7 +1200653,7 @@ class CaldavAccountManager {
1200590
1200653
  } catch (err) {
1200591
1200654
  conn.lastSyncError = err instanceof Error ? err.message : String(err);
1200592
1200655
  this.updateAccountError(accountId, conn.lastSyncError);
1200593
- logger6.error("Account retry failed", { accountId, error: conn.lastSyncError });
1200656
+ logger7.error("Account retry failed", { accountId, error: conn.lastSyncError });
1200594
1200657
  this.scheduleRetry(accountId);
1200595
1200658
  }
1200596
1200659
  }, delay2);
@@ -1200648,10 +1200711,10 @@ class CaldavAccountManager {
1200648
1200711
  if (!seenUrls.has(local.remoteUrl)) {
1200649
1200712
  db2.delete(caldavEvents2).where(eq(caldavEvents2.calendarId, local.id)).run();
1200650
1200713
  db2.delete(caldavCalendars).where(eq(caldavCalendars.id, local.id)).run();
1200651
- logger6.info("Removed deleted calendar", { displayName: local.displayName, accountId });
1200714
+ logger7.info("Removed deleted calendar", { displayName: local.displayName, accountId });
1200652
1200715
  }
1200653
1200716
  }
1200654
- logger6.info("Account sync complete", { accountId, calendars: remoteCalendars.length });
1200717
+ logger7.info("Account sync complete", { accountId, calendars: remoteCalendars.length });
1200655
1200718
  }
1200656
1200719
  async syncCalendarEvents(calendarId, remoteCal, client4) {
1200657
1200720
  const { caldavEvents: caldavEvents2 } = await Promise.resolve().then(() => (init_db2(), exports_db));
@@ -1200718,13 +1200781,13 @@ class CaldavAccountManager {
1200718
1200781
  }
1200719
1200782
  }
1200720
1200783
  }
1200721
- var logger6, GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", MAX_RETRY_DELAY, accountManager;
1200784
+ var logger7, GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", MAX_RETRY_DELAY, accountManager;
1200722
1200785
  var init_caldav_account_manager = __esm(() => {
1200723
1200786
  init_tsdav_esm();
1200724
1200787
  init_drizzle_orm();
1200725
1200788
  init_db2();
1200726
1200789
  init_logger3();
1200727
- logger6 = createLogger("CalDAV:AccountManager");
1200790
+ logger7 = createLogger("CalDAV:AccountManager");
1200728
1200791
  MAX_RETRY_DELAY = 5 * 60 * 1000;
1200729
1200792
  accountManager = new CaldavAccountManager;
1200730
1200793
  });
@@ -1200747,7 +1200810,7 @@ class GoogleCalendarManager {
1200747
1200810
  try {
1200748
1200811
  await this.syncAccount(accountId);
1200749
1200812
  } catch (err) {
1200750
- logger7.error("Initial Google Calendar sync failed", {
1200813
+ logger8.error("Initial Google Calendar sync failed", {
1200751
1200814
  accountId,
1200752
1200815
  error: err instanceof Error ? err.message : String(err)
1200753
1200816
  });
@@ -1200755,13 +1200818,13 @@ class GoogleCalendarManager {
1200755
1200818
  const intervalMs = (account2.syncIntervalMinutes ?? 15) * 60 * 1000;
1200756
1200819
  conn.syncInterval = setInterval(() => {
1200757
1200820
  this.syncAccount(accountId).catch((err) => {
1200758
- logger7.error("Periodic Google Calendar sync failed", {
1200821
+ logger8.error("Periodic Google Calendar sync failed", {
1200759
1200822
  accountId,
1200760
1200823
  error: err instanceof Error ? err.message : String(err)
1200761
1200824
  });
1200762
1200825
  });
1200763
1200826
  }, intervalMs);
1200764
- logger7.info("Started Google Calendar account", { accountId, name: account2.name });
1200827
+ logger8.info("Started Google Calendar account", { accountId, name: account2.name });
1200765
1200828
  }
1200766
1200829
  stopAccount(accountId) {
1200767
1200830
  const conn = this.connections.get(accountId);
@@ -1200779,10 +1200842,10 @@ class GoogleCalendarManager {
1200779
1200842
  }
1200780
1200843
  async startAll() {
1200781
1200844
  const accounts = db2.select().from(googleAccounts).where(eq(googleAccounts.calendarEnabled, true)).all();
1200782
- logger7.info("Starting Google Calendar accounts", { count: accounts.length });
1200845
+ logger8.info("Starting Google Calendar accounts", { count: accounts.length });
1200783
1200846
  for (const account2 of accounts) {
1200784
1200847
  await this.startAccount(account2.id).catch((err) => {
1200785
- logger7.error("Failed to start Google Calendar account", {
1200848
+ logger8.error("Failed to start Google Calendar account", {
1200786
1200849
  accountId: account2.id,
1200787
1200850
  error: err instanceof Error ? err.message : String(err)
1200788
1200851
  });
@@ -1200844,7 +1200907,7 @@ class GoogleCalendarManager {
1200844
1200907
  if (!seenRemoteUrls.has(local.remoteUrl)) {
1200845
1200908
  db2.delete(caldavEvents).where(eq(caldavEvents.calendarId, local.id)).run();
1200846
1200909
  db2.delete(caldavCalendars).where(eq(caldavCalendars.id, local.id)).run();
1200847
- logger7.info("Removed deleted Google calendar", {
1200910
+ logger8.info("Removed deleted Google calendar", {
1200848
1200911
  displayName: local.displayName,
1200849
1200912
  accountId
1200850
1200913
  });
@@ -1200858,7 +1200921,7 @@ class GoogleCalendarManager {
1200858
1200921
  if (conn) {
1200859
1200922
  conn.lastSyncError = null;
1200860
1200923
  }
1200861
- logger7.info("Google Calendar sync complete", {
1200924
+ logger8.info("Google Calendar sync complete", {
1200862
1200925
  accountId,
1200863
1200926
  calendars: remoteCalendars.length
1200864
1200927
  });
@@ -1201075,14 +1201138,14 @@ class GoogleCalendarManager {
1201075
1201138
  return this.connections.get(accountId)?.lastSyncError ?? null;
1201076
1201139
  }
1201077
1201140
  }
1201078
- var import_googleapis3, logger7, googleCalendarManager;
1201141
+ var import_googleapis3, logger8, googleCalendarManager;
1201079
1201142
  var init_google_calendar_manager = __esm(() => {
1201080
1201143
  init_drizzle_orm();
1201081
1201144
  init_db2();
1201082
1201145
  init_google_oauth();
1201083
1201146
  init_logger3();
1201084
1201147
  import_googleapis3 = __toESM(require_src12(), 1);
1201085
- logger7 = createLogger("Google:Calendar");
1201148
+ logger8 = createLogger("Google:Calendar");
1201086
1201149
  googleCalendarManager = new GoogleCalendarManager;
1201087
1201150
  });
1201088
1201151
 
@@ -1201096,12 +1201159,12 @@ async function executeAllRules() {
1201096
1201159
  const rules = db2.select().from(caldavCopyRules).where(eq(caldavCopyRules.enabled, true)).all();
1201097
1201160
  if (rules.length === 0)
1201098
1201161
  return;
1201099
- logger8.info("Executing copy rules", { count: rules.length });
1201162
+ logger9.info("Executing copy rules", { count: rules.length });
1201100
1201163
  for (const rule of rules) {
1201101
1201164
  try {
1201102
1201165
  await executeSingleRule(rule.id);
1201103
1201166
  } catch (err) {
1201104
- logger8.error("Copy rule failed", {
1201167
+ logger9.error("Copy rule failed", {
1201105
1201168
  ruleId: rule.id,
1201106
1201169
  error: err instanceof Error ? err.message : String(err)
1201107
1201170
  });
@@ -1201115,7 +1201178,7 @@ async function executeSingleRule(ruleId) {
1201115
1201178
  const sourceCal = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, rule.sourceCalendarId)).get();
1201116
1201179
  const destCal = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, rule.destCalendarId)).get();
1201117
1201180
  if (!sourceCal || !destCal) {
1201118
- logger8.warn("Copy rule references missing calendar", {
1201181
+ logger9.warn("Copy rule references missing calendar", {
1201119
1201182
  ruleId,
1201120
1201183
  sourceExists: !!sourceCal,
1201121
1201184
  destExists: !!destCal
@@ -1201125,7 +1201188,7 @@ async function executeSingleRule(ruleId) {
1201125
1201188
  const isGoogleDest = !!destCal.googleAccountId;
1201126
1201189
  const destClient = destCal.accountId ? accountManager.getClient(destCal.accountId) : null;
1201127
1201190
  if (!isGoogleDest && !destClient) {
1201128
- logger8.warn("Destination account not connected", {
1201191
+ logger9.warn("Destination account not connected", {
1201129
1201192
  ruleId,
1201130
1201193
  destCalendarId: destCal.id,
1201131
1201194
  destAccountId: destCal.accountId
@@ -1201211,7 +1201274,7 @@ async function executeSingleRule(ruleId) {
1201211
1201274
  }).run();
1201212
1201275
  created++;
1201213
1201276
  } catch (err) {
1201214
- logger8.error("Failed to copy event", {
1201277
+ logger9.error("Failed to copy event", {
1201215
1201278
  ruleId,
1201216
1201279
  sourceEventId: sourceEvent.id,
1201217
1201280
  error: err instanceof Error ? err.message : String(err)
@@ -1201275,7 +1201338,7 @@ async function executeSingleRule(ruleId) {
1201275
1201338
  db2.update(caldavCopiedEvents).set({ sourceEtag: sourceEvent.etag, updatedAt: now }).where(eq(caldavCopiedEvents.id, existingCopy.id)).run();
1201276
1201339
  updated++;
1201277
1201340
  } catch (err) {
1201278
- logger8.error("Failed to update copied event", {
1201341
+ logger9.error("Failed to update copied event", {
1201279
1201342
  ruleId,
1201280
1201343
  sourceEventId: sourceEvent.id,
1201281
1201344
  destEventId: existingCopy.destEventId,
@@ -1201286,11 +1201349,11 @@ async function executeSingleRule(ruleId) {
1201286
1201349
  }
1201287
1201350
  db2.update(caldavCopyRules).set({ lastExecutedAt: now, updatedAt: now }).where(eq(caldavCopyRules.id, ruleId)).run();
1201288
1201351
  if (created > 0 || updated > 0) {
1201289
- logger8.info("Copy rule executed", { ruleId, created, updated });
1201352
+ logger9.info("Copy rule executed", { ruleId, created, updated });
1201290
1201353
  }
1201291
1201354
  return { created, updated };
1201292
1201355
  }
1201293
- var logger8;
1201356
+ var logger9;
1201294
1201357
  var init_copy_engine = __esm(() => {
1201295
1201358
  init_drizzle_orm();
1201296
1201359
  init_db2();
@@ -1201298,7 +1201361,7 @@ var init_copy_engine = __esm(() => {
1201298
1201361
  init_ical_helpers();
1201299
1201362
  init_caldav_account_manager();
1201300
1201363
  init_google_calendar_manager();
1201301
- logger8 = createLogger("CalDAV:CopyEngine");
1201364
+ logger9 = createLogger("CalDAV:CopyEngine");
1201302
1201365
  });
1201303
1201366
 
1201304
1201367
  // node_modules/@hono/node-server/dist/index.mjs
@@ -1208290,6 +1208353,8 @@ async function updateTaskStatus(taskId, newStatus, newPosition) {
1208290
1208353
  // server/services/search-service.ts
1208291
1208354
  init_db2();
1208292
1208355
  init_drizzle_orm();
1208356
+ init_schema();
1208357
+ init_logger3();
1208293
1208358
  var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories", "conversations"];
1208294
1208359
  async function search(options) {
1208295
1208360
  const entities = options.entities?.length ? options.entities : ALL_ENTITIES;
@@ -1208308,6 +1208373,8 @@ async function search(options) {
1208308
1208373
  return searchMemories2(options.query, { tags: options.memoryTags }, limit);
1208309
1208374
  case "conversations":
1208310
1208375
  return searchConversations(options.query, { role: options.conversationRole, provider: options.conversationProvider, projectId: options.conversationProjectId }, limit);
1208376
+ case "gmail":
1208377
+ return searchGmail(options.query, { from: options.gmailFrom, to: options.gmailTo, after: options.gmailAfter, before: options.gmailBefore }, limit);
1208311
1208378
  }
1208312
1208379
  });
1208313
1208380
  const resultSets = await Promise.all(searches);
@@ -1208511,6 +1208578,55 @@ async function searchConversations(query, filters, limit) {
1208511
1208578
  }
1208512
1208579
  }));
1208513
1208580
  }
1208581
+ var logger6 = createLogger("SearchService");
1208582
+ async function searchGmail(query, filters, limit) {
1208583
+ const accounts = db2.select().from(googleAccounts).where(eq(googleAccounts.gmailEnabled, true)).all();
1208584
+ if (!accounts.length)
1208585
+ return [];
1208586
+ const parts = [query];
1208587
+ if (filters.from)
1208588
+ parts.push(`from:${filters.from}`);
1208589
+ if (filters.to)
1208590
+ parts.push(`to:${filters.to}`);
1208591
+ if (filters.after)
1208592
+ parts.push(`after:${filters.after}`);
1208593
+ if (filters.before)
1208594
+ parts.push(`before:${filters.before}`);
1208595
+ const gmailQuery = parts.join(" ");
1208596
+ const { listMessages: listMessages2 } = await Promise.resolve().then(() => (init_gmail_service(), exports_gmail_service));
1208597
+ const results = await Promise.allSettled(accounts.map(async (account) => {
1208598
+ const messages2 = await listMessages2(account.id, {
1208599
+ query: gmailQuery,
1208600
+ maxResults: limit
1208601
+ });
1208602
+ return messages2.map((msg) => ({
1208603
+ entityType: "gmail",
1208604
+ id: msg.id,
1208605
+ title: msg.subject || "(no subject)",
1208606
+ snippet: msg.snippet || "",
1208607
+ score: 0.9,
1208608
+ metadata: {
1208609
+ threadId: msg.threadId,
1208610
+ from: msg.from,
1208611
+ to: msg.to,
1208612
+ date: msg.date,
1208613
+ labels: msg.labels,
1208614
+ accountId: account.id,
1208615
+ accountEmail: account.email,
1208616
+ gmailLink: `https://mail.google.com/mail/u/0/#inbox/${msg.id}`
1208617
+ }
1208618
+ }));
1208619
+ }));
1208620
+ const allResults = [];
1208621
+ for (const result of results) {
1208622
+ if (result.status === "fulfilled") {
1208623
+ allResults.push(...result.value);
1208624
+ } else {
1208625
+ logger6.warn("Gmail search failed for an account", { error: String(result.reason) });
1208626
+ }
1208627
+ }
1208628
+ return allResults.slice(0, limit);
1208629
+ }
1208514
1208630
  function reindexTaskFTS(taskId) {
1208515
1208631
  db2.run(sql`
1208516
1208632
  UPDATE tasks SET updated_at = updated_at WHERE id = ${taskId}
@@ -1212895,20 +1213011,20 @@ var VERSION4 = "7.0.6";
1212895
1213011
  var noop2 = () => {};
1212896
1213012
  var consoleWarn = console.warn.bind(console);
1212897
1213013
  var consoleError = console.error.bind(console);
1212898
- function createLogger2(logger6 = {}) {
1212899
- if (typeof logger6.debug !== "function") {
1212900
- logger6.debug = noop2;
1213014
+ function createLogger2(logger7 = {}) {
1213015
+ if (typeof logger7.debug !== "function") {
1213016
+ logger7.debug = noop2;
1212901
1213017
  }
1212902
- if (typeof logger6.info !== "function") {
1212903
- logger6.info = noop2;
1213018
+ if (typeof logger7.info !== "function") {
1213019
+ logger7.info = noop2;
1212904
1213020
  }
1212905
- if (typeof logger6.warn !== "function") {
1212906
- logger6.warn = consoleWarn;
1213021
+ if (typeof logger7.warn !== "function") {
1213022
+ logger7.warn = consoleWarn;
1212907
1213023
  }
1212908
- if (typeof logger6.error !== "function") {
1212909
- logger6.error = consoleError;
1213024
+ if (typeof logger7.error !== "function") {
1213025
+ logger7.error = consoleError;
1212910
1213026
  }
1212911
- return logger6;
1213027
+ return logger7;
1212912
1213028
  }
1212913
1213029
  var userAgentTrail = `octokit-core.js/${VERSION4} ${getUserAgent2()}`;
1212914
1213030
 
@@ -1251751,7 +1251867,7 @@ var toolRegistry = [
1251751
1251867
  },
1251752
1251868
  {
1251753
1251869
  name: "create_calendar_event",
1251754
- description: "Create a new calendar event on a CalDAV calendar",
1251870
+ description: "Create a new calendar event on a CalDAV or Google calendar.",
1251755
1251871
  category: "caldav",
1251756
1251872
  keywords: ["calendar", "event", "create", "new", "schedule", "appointment", "meeting"],
1251757
1251873
  defer_loading: true
@@ -1251793,9 +1251909,9 @@ var toolRegistry = [
1251793
1251909
  },
1251794
1251910
  {
1251795
1251911
  name: "search",
1251796
- description: "Search across all Fulcrum entities (tasks, projects, messages, events, memories) using full-text search",
1251912
+ description: "Search across all Fulcrum entities (tasks, projects, messages, events, memories, gmail) using full-text search",
1251797
1251913
  category: "core",
1251798
- keywords: ["search", "find", "query", "fts", "full-text", "task", "project", "message", "event", "memory", "recall", "knowledge"],
1251914
+ keywords: ["search", "find", "query", "fts", "full-text", "task", "project", "message", "event", "memory", "recall", "knowledge", "gmail", "email", "google"],
1251799
1251915
  defer_loading: false
1251800
1251916
  },
1251801
1251917
  {
@@ -1253584,9 +1253700,9 @@ var registerMessagingTools = (server2, client3) => {
1253584
1253700
  };
1253585
1253701
 
1253586
1253702
  // cli/src/mcp/tools/search.ts
1253587
- var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
1253703
+ var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations", "gmail"]);
1253588
1253704
  var registerSearchTools = (server2, client3) => {
1253589
- server2.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.', {
1253705
+ server2.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. Gmail search is opt-in: include "gmail" in entities to search Gmail via API (not included in default searches).', {
1253590
1253706
  query: exports_external.string().describe('FTS5 search query. Supports: AND, OR, NOT operators, "quoted phrases", prefix* matching. Example: "kubernetes deployment" OR k8s'),
1253591
1253707
  entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories, conversations"),
1253592
1253708
  limit: exports_external.optional(exports_external.number()).describe("Maximum results per entity type (default: 10)"),
@@ -1253599,8 +1253715,12 @@ var registerSearchTools = (server2, client3) => {
1253599
1253715
  memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags"),
1253600
1253716
  conversationRole: exports_external.optional(exports_external.string()).describe('Filter conversations by role (e.g., "user", "assistant")'),
1253601
1253717
  conversationProvider: exports_external.optional(exports_external.string()).describe('Filter conversations by provider (e.g., "claude", "opencode")'),
1253602
- conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID")
1253603
- }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId }) => {
1253718
+ conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID"),
1253719
+ gmailFrom: exports_external.optional(exports_external.string()).describe('Filter Gmail results by sender (e.g., "user@example.com")'),
1253720
+ gmailTo: exports_external.optional(exports_external.string()).describe('Filter Gmail results by recipient (e.g., "user@example.com")'),
1253721
+ gmailAfter: exports_external.optional(exports_external.string()).describe("Filter Gmail results after this date (YYYY/MM/DD format)"),
1253722
+ gmailBefore: exports_external.optional(exports_external.string()).describe("Filter Gmail results before this date (YYYY/MM/DD format)")
1253723
+ }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId, gmailFrom, gmailTo, gmailAfter, gmailBefore }) => {
1253604
1253724
  try {
1253605
1253725
  const results = await client3.search({
1253606
1253726
  query,
@@ -1253615,7 +1253735,11 @@ var registerSearchTools = (server2, client3) => {
1253615
1253735
  memoryTags,
1253616
1253736
  conversationRole,
1253617
1253737
  conversationProvider,
1253618
- conversationProjectId
1253738
+ conversationProjectId,
1253739
+ gmailFrom,
1253740
+ gmailTo,
1253741
+ gmailAfter,
1253742
+ gmailBefore
1253619
1253743
  });
1253620
1253744
  return formatSuccess(results);
1253621
1253745
  } catch (err) {
@@ -1254455,6 +1254579,14 @@ class FulcrumClient {
1254455
1254579
  params.set("conversationProvider", input.conversationProvider);
1254456
1254580
  if (input.conversationProjectId)
1254457
1254581
  params.set("conversationProjectId", input.conversationProjectId);
1254582
+ if (input.gmailFrom)
1254583
+ params.set("gmailFrom", input.gmailFrom);
1254584
+ if (input.gmailTo)
1254585
+ params.set("gmailTo", input.gmailTo);
1254586
+ if (input.gmailAfter)
1254587
+ params.set("gmailAfter", input.gmailAfter);
1254588
+ if (input.gmailBefore)
1254589
+ params.set("gmailBefore", input.gmailBefore);
1254458
1254590
  return this.fetch(`/api/search?${params.toString()}`);
1254459
1254591
  }
1254460
1254592
  }
@@ -1254470,7 +1254602,7 @@ mcpRoutes.all("/", async (c) => {
1254470
1254602
  });
1254471
1254603
  const server2 = new McpServer({
1254472
1254604
  name: "fulcrum",
1254473
- version: "3.2.0"
1254605
+ version: "3.3.0"
1254474
1254606
  });
1254475
1254607
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254476
1254608
  registerTools(server2, client3);
@@ -1255793,7 +1255925,8 @@ init_settings();
1255793
1255925
  init_logger3();
1255794
1255926
  init_ical_helpers();
1255795
1255927
  init_caldav_account_manager();
1255796
- var logger9 = createLogger("CalDAV");
1255928
+ init_google_calendar_manager();
1255929
+ var logger10 = createLogger("CalDAV");
1255797
1255930
  function migrateFromSettings() {
1255798
1255931
  const settings = getSettings();
1255799
1255932
  const caldavSettings = settings.caldav;
@@ -1255809,7 +1255942,7 @@ function migrateFromSettings() {
1255809
1255942
  return;
1255810
1255943
  return;
1255811
1255944
  }
1255812
- logger9.info("Migrating CalDAV credentials from settings.json to database", {
1255945
+ logger10.info("Migrating CalDAV credentials from settings.json to database", {
1255813
1255946
  authType: caldavSettings.authType,
1255814
1255947
  orphanedCalendars: orphanedCalendars.length
1255815
1255948
  });
@@ -1255834,28 +1255967,28 @@ function migrateFromSettings() {
1255834
1255967
  for (const cal of orphanedCalendars) {
1255835
1255968
  db2.update(caldavCalendars).set({ accountId, updatedAt: now }).where(eq(caldavCalendars.id, cal.id)).run();
1255836
1255969
  }
1255837
- logger9.info("Migration complete", { accountId, migratedCalendars: orphanedCalendars.length });
1255970
+ logger10.info("Migration complete", { accountId, migratedCalendars: orphanedCalendars.length });
1255838
1255971
  }
1255839
1255972
  async function startCaldavSync() {
1255840
1255973
  const settings = getSettings();
1255841
1255974
  if (!settings.caldav?.enabled) {
1255842
- logger9.info("CalDAV disabled, skipping sync start");
1255975
+ logger10.info("CalDAV disabled, skipping sync start");
1255843
1255976
  return;
1255844
1255977
  }
1255845
1255978
  migrateFromSettings();
1255846
1255979
  await accountManager.startAll();
1255847
1255980
  await accountManager.syncAll().catch((err) => {
1255848
- logger9.error("Initial sync failed", { error: err instanceof Error ? err.message : String(err) });
1255981
+ logger10.error("Initial sync failed", { error: err instanceof Error ? err.message : String(err) });
1255849
1255982
  });
1255850
1255983
  await executeCopyRules().catch((err) => {
1255851
- logger9.error("Copy rules execution failed after sync", {
1255984
+ logger10.error("Copy rules execution failed after sync", {
1255852
1255985
  error: err instanceof Error ? err.message : String(err)
1255853
1255986
  });
1255854
1255987
  });
1255855
1255988
  }
1255856
1255989
  function stopCaldavSync() {
1255857
1255990
  accountManager.stopAll();
1255858
- logger9.info("CalDAV sync stopped");
1255991
+ logger10.info("CalDAV sync stopped");
1255859
1255992
  }
1255860
1255993
  function getCaldavStatus() {
1255861
1255994
  const accounts = accountManager.getStatus();
@@ -1255898,7 +1256031,7 @@ async function createAccount2(input) {
1255898
1256031
  updatedAt: now
1255899
1256032
  };
1255900
1256033
  db2.insert(caldavAccounts).values(account2).run();
1255901
- logger9.info("Created CalDAV account", { id, name: input.name });
1256034
+ logger10.info("Created CalDAV account", { id, name: input.name });
1255902
1256035
  return account2;
1255903
1256036
  }
1255904
1256037
  async function updateAccount(id, updates) {
@@ -1255926,7 +1256059,7 @@ async function deleteAccount(id) {
1255926
1256059
  }
1255927
1256060
  db2.delete(caldavCalendars).where(eq(caldavCalendars.accountId, id)).run();
1255928
1256061
  db2.delete(caldavAccounts).where(eq(caldavAccounts.id, id)).run();
1255929
- logger9.info("Deleted CalDAV account", { id });
1256062
+ logger10.info("Deleted CalDAV account", { id });
1255930
1256063
  }
1255931
1256064
  async function enableAccount(id) {
1255932
1256065
  db2.update(caldavAccounts).set({ enabled: true, updatedAt: new Date().toISOString() }).where(eq(caldavAccounts.id, id)).run();
@@ -1255962,7 +1256095,7 @@ async function testAccountConnection(config3) {
1255962
1256095
  async function syncAccount(id) {
1255963
1256096
  await accountManager.syncAccount(id);
1255964
1256097
  await executeCopyRules().catch((err) => {
1255965
- logger9.error("Copy rules failed after account sync", {
1256098
+ logger10.error("Copy rules failed after account sync", {
1255966
1256099
  error: err instanceof Error ? err.message : String(err)
1255967
1256100
  });
1255968
1256101
  });
@@ -1256068,7 +1256201,7 @@ function listCalendars(accountId) {
1256068
1256201
  async function syncCalendars2() {
1256069
1256202
  await accountManager.syncAll();
1256070
1256203
  await executeCopyRules().catch((err) => {
1256071
- logger9.error("Copy rules failed after sync", {
1256204
+ logger10.error("Copy rules failed after sync", {
1256072
1256205
  error: err instanceof Error ? err.message : String(err)
1256073
1256206
  });
1256074
1256207
  });
@@ -1256098,6 +1256231,20 @@ async function createEvent(input) {
1256098
1256231
  if (!calendar2) {
1256099
1256232
  throw new Error(`Calendar not found: ${input.calendarId}`);
1256100
1256233
  }
1256234
+ if (calendar2.googleAccountId) {
1256235
+ await googleCalendarManager.createEvent(calendar2.id, {
1256236
+ summary: input.summary,
1256237
+ dtstart: input.dtstart,
1256238
+ dtend: input.dtend,
1256239
+ description: input.description,
1256240
+ location: input.location,
1256241
+ allDay: input.allDay
1256242
+ });
1256243
+ const created = db2.select().from(caldavEvents).where(eq(caldavEvents.calendarId, input.calendarId)).orderBy(desc(caldavEvents.createdAt)).limit(1).get();
1256244
+ if (!created)
1256245
+ throw new Error("Failed to retrieve created Google Calendar event");
1256246
+ return created;
1256247
+ }
1256101
1256248
  const client4 = calendar2.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256102
1256249
  if (!client4) {
1256103
1256250
  throw new Error("CalDAV account not connected for this calendar");
@@ -1256145,7 +1256292,7 @@ async function createEvent(input) {
1256145
1256292
  updatedAt: now
1256146
1256293
  };
1256147
1256294
  db2.insert(caldavEvents).values(event).run();
1256148
- logger9.info("Created CalDAV event", { id, summary: input.summary });
1256295
+ logger10.info("Created CalDAV event", { id, summary: input.summary });
1256149
1256296
  return event;
1256150
1256297
  }
1256151
1256298
  async function updateEvent(id, updates) {
@@ -1256154,6 +1256301,10 @@ async function updateEvent(id, updates) {
1256154
1256301
  throw new Error(`Event not found: ${id}`);
1256155
1256302
  }
1256156
1256303
  const calendar2 = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, event.calendarId)).get();
1256304
+ if (calendar2?.googleAccountId) {
1256305
+ await googleCalendarManager.updateEvent(id, updates);
1256306
+ return db2.select().from(caldavEvents).where(eq(caldavEvents.id, id)).get();
1256307
+ }
1256157
1256308
  const client4 = calendar2?.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256158
1256309
  if (!client4) {
1256159
1256310
  throw new Error("CalDAV account not connected for this calendar");
@@ -1256191,7 +1256342,7 @@ async function updateEvent(id, updates) {
1256191
1256342
  rawIcal: updatedIcal,
1256192
1256343
  updatedAt: now
1256193
1256344
  }).where(eq(caldavEvents.id, id)).run();
1256194
- logger9.info("Updated CalDAV event", { id, summary: updates.summary ?? event.summary });
1256345
+ logger10.info("Updated CalDAV event", { id, summary: updates.summary ?? event.summary });
1256195
1256346
  return db2.select().from(caldavEvents).where(eq(caldavEvents.id, id)).get();
1256196
1256347
  }
1256197
1256348
  async function deleteEvent(id) {
@@ -1256200,6 +1256351,10 @@ async function deleteEvent(id) {
1256200
1256351
  throw new Error(`Event not found: ${id}`);
1256201
1256352
  }
1256202
1256353
  const calendar2 = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, event.calendarId)).get();
1256354
+ if (calendar2?.googleAccountId) {
1256355
+ await googleCalendarManager.deleteEvent(id);
1256356
+ return;
1256357
+ }
1256203
1256358
  const client4 = calendar2?.accountId ? accountManager.getClient(calendar2.accountId) : null;
1256204
1256359
  if (!client4) {
1256205
1256360
  throw new Error("CalDAV account not connected for this calendar");
@@ -1256211,7 +1256366,7 @@ async function deleteEvent(id) {
1256211
1256366
  }
1256212
1256367
  });
1256213
1256368
  db2.delete(caldavEvents).where(eq(caldavEvents.id, id)).run();
1256214
- logger9.info("Deleted CalDAV event", { id, summary: event.summary });
1256369
+ logger10.info("Deleted CalDAV event", { id, summary: event.summary });
1256215
1256370
  }
1256216
1256371
  function listCopyRules() {
1256217
1256372
  return db2.select().from(caldavCopyRules).all();
@@ -1256230,7 +1256385,7 @@ function createCopyRule(input) {
1256230
1256385
  updatedAt: now
1256231
1256386
  };
1256232
1256387
  db2.insert(caldavCopyRules).values(rule).run();
1256233
- logger9.info("Created copy rule", { id, source: input.sourceCalendarId, dest: input.destCalendarId });
1256388
+ logger10.info("Created copy rule", { id, source: input.sourceCalendarId, dest: input.destCalendarId });
1256234
1256389
  return rule;
1256235
1256390
  }
1256236
1256391
  function updateCopyRule(id, updates) {
@@ -1256243,7 +1256398,7 @@ function updateCopyRule(id, updates) {
1256243
1256398
  function deleteCopyRule(id) {
1256244
1256399
  db2.delete(caldavCopiedEvents).where(eq(caldavCopiedEvents.ruleId, id)).run();
1256245
1256400
  db2.delete(caldavCopyRules).where(eq(caldavCopyRules.id, id)).run();
1256246
- logger9.info("Deleted copy rule", { id });
1256401
+ logger10.info("Deleted copy rule", { id });
1256247
1256402
  }
1256248
1256403
  async function executeCopyRule(ruleId) {
1256249
1256404
  const { executeSingleRule: executeSingleRule2 } = await Promise.resolve().then(() => (init_copy_engine(), exports_copy_engine));
@@ -1256686,13 +1256841,13 @@ init_db2();
1256686
1256841
  init_settings();
1256687
1256842
  init_google_oauth();
1256688
1256843
  init_logger3();
1256689
- var logger10 = createLogger("GoogleOAuth:Routes");
1256844
+ var logger11 = createLogger("GoogleOAuth:Routes");
1256690
1256845
  var app25 = new Hono2;
1256691
1256846
  app25.get("/authorize", (c) => {
1256692
1256847
  const accountName = c.req.query("accountName");
1256693
1256848
  const accountId = c.req.query("accountId");
1256694
1256849
  const clientOrigin = c.req.query("origin");
1256695
- logger10.info("OAuth authorize request", {
1256850
+ logger11.info("OAuth authorize request", {
1256696
1256851
  accountName,
1256697
1256852
  accountId,
1256698
1256853
  clientOrigin,
@@ -1256716,7 +1256871,7 @@ app25.get("/authorize", (c) => {
1256716
1256871
  accountName: accountName || "Google Account",
1256717
1256872
  redirectUri
1256718
1256873
  });
1256719
- logger10.info("OAuth authorize: generating auth URL", {
1256874
+ logger11.info("OAuth authorize: generating auth URL", {
1256720
1256875
  baseUrl,
1256721
1256876
  redirectUri,
1256722
1256877
  stateJson: state,
@@ -1256725,18 +1256880,18 @@ app25.get("/authorize", (c) => {
1256725
1256880
  const authUrl = generateAuthUrl(client4, redirectUri, state);
1256726
1256881
  try {
1256727
1256882
  const parsed = new URL(authUrl);
1256728
- logger10.info("OAuth authorize: generated auth URL", {
1256883
+ logger11.info("OAuth authorize: generated auth URL", {
1256729
1256884
  authUrl,
1256730
1256885
  redirect_uri_in_url: parsed.searchParams.get("redirect_uri"),
1256731
1256886
  client_id_in_url: parsed.searchParams.get("client_id"),
1256732
1256887
  scope_in_url: parsed.searchParams.get("scope")
1256733
1256888
  });
1256734
1256889
  } catch {
1256735
- logger10.info("OAuth authorize: generated auth URL (unparseable)", { authUrl });
1256890
+ logger11.info("OAuth authorize: generated auth URL (unparseable)", { authUrl });
1256736
1256891
  }
1256737
1256892
  return c.json({ authUrl });
1256738
1256893
  } catch (err) {
1256739
- logger10.error("OAuth authorize failed", {
1256894
+ logger11.error("OAuth authorize failed", {
1256740
1256895
  error: err instanceof Error ? err.message : String(err)
1256741
1256896
  });
1256742
1256897
  return c.json({ error: err instanceof Error ? err.message : "Failed to generate auth URL" }, 400);
@@ -1256747,7 +1256902,7 @@ app25.get("/callback", async (c) => {
1256747
1256902
  const error46 = c.req.query("error");
1256748
1256903
  const stateParam = c.req.query("state");
1256749
1256904
  const fullUrl = c.req.url;
1256750
- logger10.info("OAuth callback received", {
1256905
+ logger11.info("OAuth callback received", {
1256751
1256906
  hasCode: !!code,
1256752
1256907
  codePrefix: code?.slice(0, 20) + "...",
1256753
1256908
  error: error46,
@@ -1256758,11 +1256913,11 @@ app25.get("/callback", async (c) => {
1256758
1256913
  refererHeader: c.req.header("referer")
1256759
1256914
  });
1256760
1256915
  if (error46) {
1256761
- logger10.warn("OAuth callback: Google returned error", { error: error46 });
1256916
+ logger11.warn("OAuth callback: Google returned error", { error: error46 });
1256762
1256917
  return c.html(`<html><body><h2>Authorization Failed</h2><p>${error46}</p><script>setTimeout(()=>window.close(),3000)</script></body></html>`);
1256763
1256918
  }
1256764
1256919
  if (!code) {
1256765
- logger10.warn("OAuth callback: missing authorization code");
1256920
+ logger11.warn("OAuth callback: missing authorization code");
1256766
1256921
  return c.html("<html><body><h2>Missing authorization code</h2><script>setTimeout(()=>window.close(),3000)</script></body></html>", 400);
1256767
1256922
  }
1256768
1256923
  let state = {
@@ -1256774,9 +1256929,9 @@ app25.get("/callback", async (c) => {
1256774
1256929
  state = JSON.parse(stateParam);
1256775
1256930
  }
1256776
1256931
  } catch {
1256777
- logger10.warn("OAuth callback: failed to parse state param", { stateParam });
1256932
+ logger11.warn("OAuth callback: failed to parse state param", { stateParam });
1256778
1256933
  }
1256779
- logger10.info("OAuth callback: parsed state", {
1256934
+ logger11.info("OAuth callback: parsed state", {
1256780
1256935
  accountId: state.accountId,
1256781
1256936
  accountName: state.accountName,
1256782
1256937
  redirectUri: state.redirectUri
@@ -1256787,7 +1256942,7 @@ app25.get("/callback", async (c) => {
1256787
1256942
  throw new Error("Missing redirect URI in state \u2014 cannot match token exchange");
1256788
1256943
  }
1256789
1256944
  const client4 = createOAuth2Client();
1256790
- logger10.info("OAuth callback: exchanging code for tokens", {
1256945
+ logger11.info("OAuth callback: exchanging code for tokens", {
1256791
1256946
  redirectUri,
1256792
1256947
  clientId: client4._clientId,
1256793
1256948
  codePrefix: code.slice(0, 20) + "..."
@@ -1256809,7 +1256964,7 @@ app25.get("/callback", async (c) => {
1256809
1256964
  email: email4 ?? undefined,
1256810
1256965
  updatedAt: now
1256811
1256966
  }).where(eq(googleAccounts.id, state.accountId)).run();
1256812
- logger10.info("Re-authorized Google account", {
1256967
+ logger11.info("Re-authorized Google account", {
1256813
1256968
  accountId: state.accountId,
1256814
1256969
  email: email4
1256815
1256970
  });
@@ -1256829,7 +1256984,7 @@ app25.get("/callback", async (c) => {
1256829
1256984
  createdAt: now,
1256830
1256985
  updatedAt: now
1256831
1256986
  }).run();
1256832
- logger10.info("Created Google account", {
1256987
+ logger11.info("Created Google account", {
1256833
1256988
  accountId,
1256834
1256989
  name: state.accountName,
1256835
1256990
  email: email4
@@ -1256837,7 +1256992,7 @@ app25.get("/callback", async (c) => {
1256837
1256992
  }
1256838
1256993
  return c.html("<html><body><h2>Authorization Successful</h2><p>You can close this window.</p><script>setTimeout(()=>window.close(),2000)</script></body></html>");
1256839
1256994
  } catch (err) {
1256840
- logger10.error("OAuth callback failed", {
1256995
+ logger11.error("OAuth callback failed", {
1256841
1256996
  error: err instanceof Error ? err.message : String(err)
1256842
1256997
  });
1256843
1256998
  return c.html(`<html><body><h2>Authorization Failed</h2><p>${err instanceof Error ? err.message : String(err)}</p><script>setTimeout(()=>window.close(),5000)</script></body></html>`, 500);
@@ -1256852,7 +1257007,7 @@ init_google_calendar_manager();
1256852
1257007
  init_logger3();
1256853
1257008
  init_settings();
1256854
1257009
  init_channel_manager();
1256855
- var logger11 = createLogger("Google:CalendarService");
1257010
+ var logger12 = createLogger("Google:CalendarService");
1256856
1257011
  function listGoogleAccounts() {
1256857
1257012
  return db2.select().from(googleAccounts).all();
1256858
1257013
  }
@@ -1256872,19 +1257027,19 @@ async function deleteGoogleAccount(id) {
1256872
1257027
  }
1256873
1257028
  db2.delete(caldavCalendars).where(eq(caldavCalendars.googleAccountId, id)).run();
1256874
1257029
  db2.delete(googleAccounts).where(eq(googleAccounts.id, id)).run();
1256875
- logger11.info("Deleted Google account", { accountId: id });
1257030
+ logger12.info("Deleted Google account", { accountId: id });
1256876
1257031
  }
1256877
1257032
  async function enableGoogleCalendar(id) {
1256878
1257033
  const now = new Date().toISOString();
1256879
1257034
  db2.update(googleAccounts).set({ calendarEnabled: true, updatedAt: now }).where(eq(googleAccounts.id, id)).run();
1256880
1257035
  await googleCalendarManager.startAccount(id);
1256881
- logger11.info("Enabled Google Calendar for account", { accountId: id });
1257036
+ logger12.info("Enabled Google Calendar for account", { accountId: id });
1256882
1257037
  }
1256883
1257038
  async function disableGoogleCalendar(id) {
1256884
1257039
  const now = new Date().toISOString();
1256885
1257040
  db2.update(googleAccounts).set({ calendarEnabled: false, updatedAt: now }).where(eq(googleAccounts.id, id)).run();
1256886
1257041
  googleCalendarManager.stopAccount(id);
1256887
- logger11.info("Disabled Google Calendar for account", { accountId: id });
1257042
+ logger12.info("Disabled Google Calendar for account", { accountId: id });
1256888
1257043
  }
1256889
1257044
  async function enableGmail(id) {
1256890
1257045
  const now = new Date().toISOString();
@@ -1256894,7 +1257049,7 @@ async function enableGmail(id) {
1256894
1257049
  updateSettingByPath("channels.email.googleAccountId", id);
1256895
1257050
  await stopEmailChannel();
1256896
1257051
  await startEmailChannel();
1256897
- logger11.info("Enabled Gmail for account", { accountId: id });
1257052
+ logger12.info("Enabled Gmail for account", { accountId: id });
1256898
1257053
  }
1256899
1257054
  async function disableGmail(id) {
1256900
1257055
  const now = new Date().toISOString();
@@ -1256902,7 +1257057,7 @@ async function disableGmail(id) {
1256902
1257057
  await stopEmailChannel();
1256903
1257058
  updateSettingByPath("channels.email.enabled", false);
1256904
1257059
  updateSettingByPath("channels.email.googleAccountId", null);
1256905
- logger11.info("Disabled Gmail for account", { accountId: id });
1257060
+ logger12.info("Disabled Gmail for account", { accountId: id });
1256906
1257061
  }
1256907
1257062
  async function syncGoogleCalendar(id) {
1256908
1257063
  await googleCalendarManager.syncAccount(id);
@@ -1257156,6 +1257311,10 @@ app28.get("/", async (c) => {
1257156
1257311
  const conversationRole = c.req.query("conversationRole") || undefined;
1257157
1257312
  const conversationProvider = c.req.query("conversationProvider") || undefined;
1257158
1257313
  const conversationProjectId = c.req.query("conversationProjectId") || undefined;
1257314
+ const gmailFrom = c.req.query("gmailFrom") || undefined;
1257315
+ const gmailTo = c.req.query("gmailTo") || undefined;
1257316
+ const gmailAfter = c.req.query("gmailAfter") || undefined;
1257317
+ const gmailBefore = c.req.query("gmailBefore") || undefined;
1257159
1257318
  try {
1257160
1257319
  const results = await search({
1257161
1257320
  query: query.trim(),
@@ -1257170,7 +1257329,11 @@ app28.get("/", async (c) => {
1257170
1257329
  memoryTags,
1257171
1257330
  conversationRole,
1257172
1257331
  conversationProvider,
1257173
- conversationProjectId
1257332
+ conversationProjectId,
1257333
+ gmailFrom,
1257334
+ gmailTo,
1257335
+ gmailAfter,
1257336
+ gmailBefore
1257174
1257337
  });
1257175
1257338
  return c.json(results);
1257176
1257339
  } catch (err) {