@knowsuchagency/fulcrum 3.2.1 → 3.4.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
@@ -4611,6 +4611,7 @@ var init_schema = __esm(() => {
4611
4611
  isFavorite: integer("is_favorite", { mode: "boolean" }).default(false),
4612
4612
  messageCount: integer("message_count").default(0),
4613
4613
  lastMessageAt: text("last_message_at"),
4614
+ claudeSessionId: text("claude_session_id"),
4614
4615
  createdAt: text("created_at").notNull(),
4615
4616
  updatedAt: text("updated_at").notNull()
4616
4617
  });
@@ -296775,13 +296776,26 @@ var require_dist18 = __commonJS((exports) => {
296775
296776
  });
296776
296777
 
296777
296778
  // server/services/channels/slack-channel.ts
296778
- var import_bolt, SlackChannel;
296779
+ import { readFileSync as readFileSync7 } from "fs";
296780
+ import { basename as basename2 } from "path";
296781
+ var import_bolt, SUPPORTED_MIME_TYPES, MAX_FILE_SIZE, SlackChannel;
296779
296782
  var init_slack_channel = __esm(() => {
296780
296783
  init_drizzle_orm();
296781
296784
  init_db2();
296782
296785
  init_logger3();
296783
296786
  init_settings();
296784
296787
  import_bolt = __toESM(require_dist18(), 1);
296788
+ SUPPORTED_MIME_TYPES = new Set([
296789
+ "image/png",
296790
+ "image/jpeg",
296791
+ "image/gif",
296792
+ "image/webp",
296793
+ "application/pdf",
296794
+ "text/plain",
296795
+ "text/csv",
296796
+ "text/markdown"
296797
+ ]);
296798
+ MAX_FILE_SIZE = 10 * 1024 * 1024;
296785
296799
  SlackChannel = class SlackChannel {
296786
296800
  type = "slack";
296787
296801
  connectionId;
@@ -296925,8 +296939,9 @@ var init_slack_channel = __esm(() => {
296925
296939
  async handleMessage(message) {
296926
296940
  if (message.bot_id || message.subtype === "bot_message")
296927
296941
  return;
296928
- const content = message.text;
296929
- if (!content)
296942
+ const content = message.text || "";
296943
+ const files = message.files;
296944
+ if (!content && (!files || files.length === 0))
296930
296945
  return;
296931
296946
  const userId = message.user;
296932
296947
  if (!userId)
@@ -296938,18 +296953,31 @@ var init_slack_channel = __esm(() => {
296938
296953
  senderName = userInfo.user?.real_name || userInfo.user?.name;
296939
296954
  }
296940
296955
  } catch {}
296956
+ let attachments;
296957
+ if (files && files.length > 0) {
296958
+ const downloaded = await this.downloadSlackFiles(files);
296959
+ if (downloaded.length > 0) {
296960
+ attachments = downloaded;
296961
+ }
296962
+ }
296941
296963
  const incomingMessage = {
296942
296964
  channelType: "slack",
296943
296965
  connectionId: this.connectionId,
296944
296966
  senderId: userId,
296945
296967
  senderName,
296946
296968
  content,
296947
- timestamp: new Date(parseFloat(message.ts) * 1000)
296969
+ attachments,
296970
+ timestamp: new Date(parseFloat(message.ts) * 1000),
296971
+ metadata: files?.length ? {
296972
+ files: files.map((f) => ({ name: f.name, type: f.mimetype, size: f.size }))
296973
+ } : undefined
296948
296974
  };
296949
296975
  log2.messaging.info("Slack message received", {
296950
296976
  connectionId: this.connectionId,
296951
296977
  from: userId,
296952
- contentLength: content.length
296978
+ contentLength: content.length,
296979
+ fileCount: files?.length ?? 0,
296980
+ attachmentCount: attachments?.length ?? 0
296953
296981
  });
296954
296982
  try {
296955
296983
  await this.events?.onMessage(incomingMessage);
@@ -296960,6 +296988,110 @@ var init_slack_channel = __esm(() => {
296960
296988
  });
296961
296989
  }
296962
296990
  }
296991
+ async downloadSlackFiles(files) {
296992
+ const results = [];
296993
+ for (const file of files) {
296994
+ try {
296995
+ const attachment = await this.downloadSlackFile(file);
296996
+ if (attachment) {
296997
+ results.push(attachment);
296998
+ }
296999
+ } catch (err) {
297000
+ log2.messaging.warn("Failed to download Slack file", {
297001
+ connectionId: this.connectionId,
297002
+ fileName: file.name,
297003
+ error: String(err)
297004
+ });
297005
+ }
297006
+ }
297007
+ return results;
297008
+ }
297009
+ async downloadSlackFile(file) {
297010
+ if (!file.url_private_download) {
297011
+ log2.messaging.warn("Slack file has no download URL", {
297012
+ fileName: file.name,
297013
+ fileId: file.id
297014
+ });
297015
+ return null;
297016
+ }
297017
+ if (file.size > MAX_FILE_SIZE) {
297018
+ log2.messaging.warn("Slack file too large, skipping", {
297019
+ fileName: file.name,
297020
+ size: file.size,
297021
+ maxSize: MAX_FILE_SIZE
297022
+ });
297023
+ return null;
297024
+ }
297025
+ if (!SUPPORTED_MIME_TYPES.has(file.mimetype)) {
297026
+ log2.messaging.warn("Unsupported Slack file type, skipping", {
297027
+ fileName: file.name,
297028
+ mimeType: file.mimetype
297029
+ });
297030
+ return null;
297031
+ }
297032
+ let response = await fetch(file.url_private_download, {
297033
+ headers: {
297034
+ Authorization: `Bearer ${this.botToken}`
297035
+ },
297036
+ redirect: "manual"
297037
+ });
297038
+ if (response.status >= 300 && response.status < 400) {
297039
+ const location = response.headers.get("location");
297040
+ if (location) {
297041
+ response = await fetch(location);
297042
+ }
297043
+ }
297044
+ if (!response.ok) {
297045
+ log2.messaging.warn("Failed to download Slack file", {
297046
+ fileName: file.name,
297047
+ status: response.status
297048
+ });
297049
+ return null;
297050
+ }
297051
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
297052
+ if (contentType && contentType !== file.mimetype) {
297053
+ log2.messaging.warn("Slack file content-type mismatch", {
297054
+ fileName: file.name,
297055
+ expected: file.mimetype,
297056
+ actual: contentType
297057
+ });
297058
+ }
297059
+ const isText = file.mimetype.startsWith("text/");
297060
+ const type = file.mimetype.startsWith("image/") ? "image" : isText ? "text" : "document";
297061
+ if (isText) {
297062
+ const text3 = await response.text();
297063
+ return {
297064
+ mediaType: file.mimetype,
297065
+ data: text3,
297066
+ filename: file.name,
297067
+ type
297068
+ };
297069
+ }
297070
+ const buffer = Buffer.from(await response.arrayBuffer());
297071
+ if (file.mimetype === "application/pdf" && buffer.length >= 4) {
297072
+ const header = buffer.subarray(0, 5).toString("ascii");
297073
+ if (!header.startsWith("%PDF-")) {
297074
+ log2.messaging.warn("Downloaded file is not a valid PDF", {
297075
+ fileName: file.name,
297076
+ header: buffer.subarray(0, 16).toString("hex"),
297077
+ size: buffer.length
297078
+ });
297079
+ return null;
297080
+ }
297081
+ }
297082
+ log2.messaging.info("Downloaded Slack file", {
297083
+ fileName: file.name,
297084
+ mimeType: file.mimetype,
297085
+ size: buffer.length,
297086
+ type
297087
+ });
297088
+ return {
297089
+ mediaType: file.mimetype,
297090
+ data: buffer.toString("base64"),
297091
+ filename: file.name,
297092
+ type
297093
+ };
297094
+ }
296963
297095
  async handleSlashCommand(command, respond) {
296964
297096
  log2.messaging.info("Slack slash command received", {
296965
297097
  connectionId: this.connectionId,
@@ -297025,6 +297157,31 @@ var init_slack_channel = __esm(() => {
297025
297157
  if (!channelId) {
297026
297158
  throw new Error("Failed to open DM channel");
297027
297159
  }
297160
+ if (metadata?.filePath) {
297161
+ const filePath = metadata.filePath;
297162
+ try {
297163
+ const fileData = readFileSync7(filePath);
297164
+ const filename = basename2(filePath);
297165
+ await this.app.client.files.uploadV2({
297166
+ channel_id: channelId,
297167
+ file: fileData,
297168
+ filename,
297169
+ initial_comment: content || undefined
297170
+ });
297171
+ log2.messaging.info("Slack file uploaded", {
297172
+ connectionId: this.connectionId,
297173
+ to: recipientId,
297174
+ filename
297175
+ });
297176
+ return true;
297177
+ } catch (fileErr) {
297178
+ log2.messaging.error("Failed to upload Slack file", {
297179
+ connectionId: this.connectionId,
297180
+ filePath,
297181
+ error: String(fileErr)
297182
+ });
297183
+ }
297184
+ }
297028
297185
  if (content.length <= 4000) {
297029
297186
  const messageOptions = {
297030
297187
  channel: channelId,
@@ -1136411,6 +1136568,7 @@ A message has arrived:
1136411
1136568
  **Channel**: ${context.channel}
1136412
1136569
  **From**: ${context.sender}${context.senderName ? ` (${context.senderName})` : ""}
1136413
1136570
  **Content**: ${context.content}
1136571
+ ${context.hasAttachments ? `**Attachments**: ${context.attachmentNames?.join(", ") || "file(s) attached"}` : ""}
1136414
1136572
  ${context.metadata?.subject ? `**Subject**: ${context.metadata.subject}` : ""}
1136415
1136573
  ${context.metadata?.threadId ? `**Thread ID**: ${context.metadata.threadId}` : ""}
1136416
1136574
 
@@ -1136441,7 +1136599,7 @@ ${formattingGuide}`;
1136441
1136599
  function getObserveOnlySystemPrompt(channelType, context) {
1136442
1136600
  return `## Observe-Only Mode
1136443
1136601
 
1136444
- You are OBSERVING a message (read-only mode):
1136602
+ You are the user's observer \u2014 your core job is to prevent things from falling through the cracks. When you see something actionable, make it visible by creating a Fulcrum task. Memories are secondary \u2014 useful for tracking context that doesn't warrant a task.
1136445
1136603
 
1136446
1136604
  **Channel**: ${context.channel}
1136447
1136605
  **From**: ${context.sender}${context.senderName ? ` (${context.senderName})` : ""}
@@ -1136455,20 +1136613,35 @@ ${context.metadata?.isGroup ? `**Group Chat**: yes` : ""}
1136455
1136613
 
1136456
1136614
  ## Available Tools
1136457
1136615
 
1136458
- - \`memory_store\` - Store ephemeral observations with tags
1136459
- - \`memory_file_read\` - Read the master memory file
1136460
- - \`memory_file_update\` - Update the master memory file (for important persistent observations)
1136616
+ ### Task tools
1136617
+ - \`list_tasks\` - Search existing tasks (use to check for duplicates before creating)
1136618
+ - \`create_task\` - Create a new task with title, description, tags, and dueDate
1136619
+ - \`update_task\` - Update an existing task's title or description
1136620
+ - \`add_task_link\` - Add a URL link to a task
1136621
+ - \`add_task_tag\` - Add a tag to an existing task
1136622
+ - \`set_task_due_date\` - Set or update a task's due date
1136623
+
1136624
+ ### Memory tools
1136625
+ - \`memory_store\` - Store observations with tags
1136626
+ - \`memory_search\` - Search existing memories to avoid duplicates
1136627
+ - \`memory_list\` - List existing memories by tag
1136628
+ - \`memory_file_read\` - Read the master memory file (read-only in observer mode)
1136629
+
1136630
+ ### Notification tools
1136631
+ - \`send_notification\` - Send a notification to the user (desktop, sound, Slack, etc.)
1136461
1136632
 
1136462
1136633
  ## Your Task
1136463
1136634
 
1136464
- Silently analyze this message and decide if it needs to be tracked:
1136635
+ Silently analyze this message and take the appropriate action:
1136465
1136636
 
1136466
- 1. **Actionable requests directed at the user** (deadlines, meetings, tasks) \u2192 Store a memory with tag \`actionable\`
1136467
- 2. **Important information** (confirmations, updates about ongoing matters) \u2192 Store a memory with tag \`monitoring\`
1136468
- 3. **Important persistent observations** (learning someone's name, recurring topics, key relationships) \u2192 Update the memory file with \`memory_file_update\`
1136637
+ 1. **Actionable requests directed at the user** (deadlines, meetings, tasks, to-dos, follow-ups) \u2192 Use \`list_tasks\` to check for duplicates, then \`create_task\` with a clear title, description, relevant tags, and dueDate if mentioned. Tag with \`from:${context.channel}\`. After creating a task, use \`send_notification\` to alert the user (e.g., title: "New task from ${context.channel}", message: the task title).
1136638
+ 2. **Updates about existing matters** (confirmations, status changes, new details on known topics) \u2192 Use \`list_tasks\` to find the related task, then \`update_task\` or \`add_task_link\` as appropriate.
1136639
+ 3. **Important persistent observations** (learning someone's name, recurring topics, key relationships) \u2192 \`memory_store\` with tag \`persistent\`
1136469
1136640
  4. **Casual messages, spam, or irrelevant content** \u2192 Do nothing
1136470
1136641
 
1136471
- Use \`memory_store\` for transient observations. Use \`memory_file_update\` only for broadly useful, long-term knowledge.
1136642
+ When creating tasks, write the title as a clear action item (e.g., "Arrange cow rental for parade" not "Message about cow rental"). Include the sender and channel context in the description.
1136643
+
1136644
+ **Always use \`memory_store\`** for non-task observations \u2014 never write to MEMORY.md directly. The hourly sweep reviews stored memories and promotes important patterns to the memory file.
1136472
1136645
  Include the source channel as the \`source\` field (e.g., "channel:${context.channel}").
1136473
1136646
 
1136474
1136647
  ## Security Warning
@@ -1136498,7 +1136671,7 @@ You are performing your hourly sweep.
1136498
1136671
  ## Your Task
1136499
1136672
 
1136500
1136673
  1. **Review actionable memories** - use \`memory_search\` to find memories tagged \`actionable\` or \`monitoring\` and check for:
1136501
- - Items that have been resolved (delete the memory or remove the tag)
1136674
+ - Items that have been resolved \u2192 delete with \`memory_delete\`
1136502
1136675
  - Patterns or connections between tracked items
1136503
1136676
  - Items that should be linked to tasks
1136504
1136677
 
@@ -1136511,14 +1136684,24 @@ You are performing your hourly sweep.
1136511
1136684
  - Store memories with tag \`actionable\` for missed items
1136512
1136685
  - Take action if still relevant
1136513
1136686
 
1136514
- 4. **Update your records** - delete resolved memories or update their tags
1136687
+ 4. **Clean up memories** - use \`memory_list\` and \`memory_delete\` to:
1136688
+ - Delete resolved or outdated memories
1136689
+ - Remove duplicates
1136690
+
1136691
+ 5. **Curate MEMORY.md** - read with \`memory_file_read\` and check if it needs updates:
1136692
+ - Remove genuinely stale, duplicate, or outdated entries
1136693
+ - Move ephemeral observations (one-time events, transient status) to \`memory_store\` with appropriate tags, then remove from the file
1136694
+ - **Promote recurring patterns**: search \`memory_store\` for memories tagged \`persistent\` \u2014 if a pattern appears consistently, add it to MEMORY.md and delete the individual memories
1136695
+ - Keep: user preferences, project conventions, recurring patterns, key relationships
1136696
+ - Rewrite the cleaned file with \`memory_file_update\` if changes are needed
1136697
+ - Do NOT remove content just to reduce size \u2014 only remove what is genuinely stale, duplicate, or ephemeral
1136515
1136698
 
1136516
1136699
  ## Output
1136517
1136700
 
1136518
1136701
  After completing your sweep, provide a brief summary of:
1136519
1136702
  - Memories reviewed and actions taken
1136520
1136703
  - Tasks updated or created
1136521
- - Any patterns noticed
1136704
+ - MEMORY.md changes (stale items removed, ephemeral items migrated to memory_store, patterns promoted from memory_store)
1136522
1136705
  - Items requiring user attention`;
1136523
1136706
  }
1136524
1136707
  function getRitualSystemPrompt(type) {
@@ -1136529,6 +1136712,10 @@ function getRitualSystemPrompt(type) {
1136529
1136712
 
1136530
1136713
  You are performing your morning ritual.
1136531
1136714
 
1136715
+ ## Memory Check
1136716
+
1136717
+ Before composing your briefing, read MEMORY.md with \`memory_file_read\` for context about ongoing matters, preferences, and recent patterns.
1136718
+
1136532
1136719
  ## Output Channels
1136533
1136720
 
1136534
1136721
  Use the \`list_messaging_channels\` tool to discover which messaging channels are available and connected.
@@ -1136540,6 +1136727,15 @@ Then use the \`message\` tool to send your briefing \u2014 just specify \`channe
1136540
1136727
 
1136541
1136728
  You are performing your evening ritual.
1136542
1136729
 
1136730
+ ## Memory Maintenance
1136731
+
1136732
+ Before composing your summary, curate MEMORY.md if changes are needed:
1136733
+ 1. Read MEMORY.md with \`memory_file_read\`
1136734
+ 2. Remove genuinely stale, duplicate, or outdated content
1136735
+ 3. Move ephemeral items to \`memory_store\` with appropriate tags
1136736
+ 4. Search \`memory_store\` for memories tagged \`persistent\` \u2014 promote recurring patterns into MEMORY.md and delete the individual memories
1136737
+ 5. Rewrite the file with \`memory_file_update\` only if changes were made
1136738
+
1136543
1136739
  ## Output Channels
1136544
1136740
 
1136545
1136741
  Use the \`list_messaging_channels\` tool to discover which messaging channels are available and connected.
@@ -1136557,40 +1136753,35 @@ WhatsApp does NOT render full Markdown. Keep formatting simple:
1136557
1136753
  case "slack":
1136558
1136754
  return `## Slack Formatting & Block Kit
1136559
1136755
 
1136560
- Your response will be structured as JSON with two fields:
1136756
+ Wrap your entire response in \`<slack-response>\` XML tags containing a JSON object with:
1136561
1136757
 
1136562
1136758
  - **body** (required): Plain text message shown in notifications and as fallback
1136563
1136759
  - **blocks** (optional): Array of Slack Block Kit blocks for rich formatting
1136760
+ - **filePath** (optional): Absolute path to a file on disk to upload as an attachment
1136761
+
1136762
+ Example:
1136763
+ <slack-response>
1136764
+ {"body": "Here are your open tasks", "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": "*Open Tasks:*\\n\u2022 Task 1\\n\u2022 Task 2"}}]}
1136765
+ </slack-response>
1136766
+
1136767
+ To send a file (image, document, etc.) you created:
1136768
+ <slack-response>
1136769
+ {"body": "Here's the generated image", "filePath": "/absolute/path/to/file.png"}
1136770
+ </slack-response>
1136564
1136771
 
1136565
1136772
  ### Block Kit Blocks
1136566
1136773
 
1136567
1136774
  **Section Block** - Main content:
1136568
- \`\`\`json
1136569
- { "type": "section", "text": { "type": "mrkdwn", "text": "*Bold* and _italic_" } }
1136570
- \`\`\`
1136775
+ \`{"type": "section", "text": {"type": "mrkdwn", "text": "*Bold* and _italic_"}}\`
1136571
1136776
 
1136572
1136777
  **Section with Fields** - Multi-column layout:
1136573
- \`\`\`json
1136574
- { "type": "section", "fields": [
1136575
- { "type": "mrkdwn", "text": "*Status:*\\nIn Progress" },
1136576
- { "type": "mrkdwn", "text": "*Due:*\\nToday" }
1136577
- ] }
1136578
- \`\`\`
1136778
+ \`{"type": "section", "fields": [{"type": "mrkdwn", "text": "*Status:*\\nIn Progress"}, {"type": "mrkdwn", "text": "*Due:*\\nToday"}]}\`
1136579
1136779
 
1136580
- **Header Block** - Large text headers:
1136581
- \`\`\`json
1136582
- { "type": "header", "text": { "type": "plain_text", "text": "Task Summary", "emoji": true } }
1136583
- \`\`\`
1136780
+ **Header Block**: \`{"type": "header", "text": {"type": "plain_text", "text": "Title", "emoji": true}}\`
1136584
1136781
 
1136585
- **Divider Block** - Horizontal rule:
1136586
- \`\`\`json
1136587
- { "type": "divider" }
1136588
- \`\`\`
1136782
+ **Divider Block**: \`{"type": "divider"}\`
1136589
1136783
 
1136590
- **Context Block** - Small muted text:
1136591
- \`\`\`json
1136592
- { "type": "context", "elements": [{ "type": "mrkdwn", "text": "Last updated 5 min ago" }] }
1136593
- \`\`\`
1136784
+ **Context Block**: \`{"type": "context", "elements": [{"type": "mrkdwn", "text": "Small muted text"}]}\`
1136594
1136785
 
1136595
1136786
  ### mrkdwn Syntax
1136596
1136787
  - *bold* with single asterisks
@@ -1136605,7 +1136796,9 @@ Your response will be structured as JSON with two fields:
1136605
1136796
  - **Lists/Status**: Use section blocks with bullet points or fields
1136606
1136797
  - **Structured Data**: Use fields for key-value pairs side by side
1136607
1136798
  - **Headers**: Use header blocks for major sections
1136608
- - **Simple Responses**: Just set body to your plain text reply, omit blocks`;
1136799
+ - **Simple Responses**: Just set body to your plain text, omit blocks
1136800
+
1136801
+ **IMPORTANT**: Always wrap your response in \`<slack-response>\` tags with valid JSON inside.`;
1136609
1136802
  case "discord":
1136610
1136803
  return `## Discord Formatting
1136611
1136804
 
@@ -1138553,7 +1138746,7 @@ function s_({ prompt: X, options: Q }) {
1138553
1138746
  let L6 = TI(import.meta.url), q6 = tz(L6, "..");
1138554
1138747
  B = tz(q6, "cli.js");
1138555
1138748
  }
1138556
- process.env.CLAUDE_AGENT_SDK_VERSION = "0.2.34";
1138749
+ process.env.CLAUDE_AGENT_SDK_VERSION = "0.2.37";
1138557
1138750
  let { abortController: z = F6(), additionalDirectories: K = [], agent: V, agents: L, allowedTools: U = [], betas: F, canUseTool: q2, continue: N, cwd: w, debug: M, debugFile: R, disallowedTools: S = [], tools: C, env: K0, executable: U0 = M6() ? "bun" : "node", executableArgs: s = [], extraArgs: D0 = {}, fallbackModel: q0, enableFileCheckpointing: L1, forkSession: P1, hooks: o1, includePartialMessages: c, persistSession: Q9, maxThinkingTokens: r6, maxTurns: o6, maxBudgetUsd: t6, mcpServers: a6, model: B4, outputFormat: E0, permissionMode: S1 = "default", allowDangerouslySkipPermissions: s6 = false, permissionPromptToolName: az, plugins: sz, resume: ez, resumeSessionAt: XK, sessionId: QK, stderr: $K, strictMcpConfig: YK } = J, G7 = E0?.type === "json_schema" ? E0.schema : undefined, V6 = K0;
1138558
1138751
  if (!V6)
1138559
1138752
  V6 = { ...process.env };
@@ -1147055,8 +1147248,10 @@ You have access to Fulcrum's MCP tools. Use them proactively to help users.
1147055
1147248
  - Use \`search\` with \`memoryTags: ["actionable"]\` to review tracked items
1147056
1147249
 
1147057
1147250
  **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"]\`
1147251
+ - \`search\` - Cross-entity FTS5 full-text search across tasks, projects, messages, events, memories, conversations, and gmail
1147252
+ - Filter by entity type: \`entities: ["tasks", "projects", "messages", "events", "memories", "conversations", "gmail"]\`
1147253
+ - **Gmail is opt-in** \u2014 not included in default searches to avoid latency/rate-limit impact. Must be explicitly requested via \`entities: ["gmail"]\`
1147254
+ - Gmail-specific filters: \`gmailFrom\`, \`gmailTo\`, \`gmailAfter\`, \`gmailBefore\`
1147060
1147255
  - Entity-specific filters: \`taskStatus\`, \`projectStatus\`, \`messageChannel\`, \`messageDirection\`, \`eventFrom\`, \`eventTo\`, \`memoryTags\`, \`conversationRole\`, \`conversationProvider\`, \`conversationProjectId\`
1147061
1147256
  - Conversations search indexes AI assistant chat messages (excludes system prompts) with session context
1147062
1147257
  - Results sorted by relevance score with BM25 ranking
@@ -1147065,8 +1147260,11 @@ You have access to Fulcrum's MCP tools. Use them proactively to help users.
1147065
1147260
  - \`memory_file_read\` - Read the master memory file (MEMORY.md)
1147066
1147261
  - \`memory_file_update\` - Update the file (whole or by section heading)
1147067
1147262
 
1147068
- **Ephemeral Memories:**
1147263
+ **Memory Management:**
1147069
1147264
  - \`memory_store\` - Store individual knowledge snippets with optional tags
1147265
+ - \`memory_search\` - Full-text search across memories (FTS5: AND, OR, NOT, "phrases", prefix*)
1147266
+ - \`memory_list\` - List memories with optional tag filter and pagination
1147267
+ - \`memory_delete\` - Delete a memory by ID (for cleanup of resolved/stale items)
1147070
1147268
 
1147071
1147269
  **Google Account & Gmail Tools:**
1147072
1147270
  - \`list_google_accounts\` - List all Google accounts with calendar/Gmail status
@@ -1147398,9 +1147596,9 @@ Fulcrum is your digital concierge - a personal command center where you track ev
1147398
1147596
  - list_projects, create_project
1147399
1147597
  - execute_command (run any CLI command)
1147400
1147598
  - send_notification
1147401
- - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations)
1147599
+ - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations; gmail is opt-in via \`entities: ["gmail"]\`)
1147402
1147600
  - memory_file_read, memory_file_update (master memory file - always in prompt)
1147403
- - memory_store (ephemeral knowledge snippets with tags)
1147601
+ - memory_store, memory_search, memory_list, memory_delete (persistent knowledge with tags)
1147404
1147602
  - message (send to WhatsApp/Discord/Telegram/Slack/Gmail - user-only, concierge mode)
1147405
1147603
  - memory_store with tag \`actionable\` (track things needing attention - concierge mode)
1147406
1147604
  - search with memoryTags filter (find tracked items by tag)
@@ -1147409,7 +1147607,7 @@ Fulcrum is your digital concierge - a personal command center where you track ev
1147409
1147607
  }
1147410
1147608
 
1147411
1147609
  // server/services/memory-file-service.ts
1147412
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync14 } from "fs";
1147610
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync14 } from "fs";
1147413
1147611
  import { join as join13 } from "path";
1147414
1147612
  function getMemoryFilePath() {
1147415
1147613
  return join13(getFulcrumDir(), MEMORY_FILENAME);
@@ -1147418,7 +1147616,7 @@ function readMemoryFile() {
1147418
1147616
  const filePath = getMemoryFilePath();
1147419
1147617
  if (!existsSync14(filePath))
1147420
1147618
  return "";
1147421
- return readFileSync8(filePath, "utf-8");
1147619
+ return readFileSync9(filePath, "utf-8");
1147422
1147620
  }
1147423
1147621
  function writeMemoryFile(content) {
1147424
1147622
  const filePath = getMemoryFilePath();
@@ -1147597,7 +1147795,7 @@ function addMessage(sessionId, message) {
1147597
1147795
  return db2.select().from(chatMessages).where(eq(chatMessages.id, id)).get();
1147598
1147796
  }
1147599
1147797
  function getMessages(sessionId) {
1147600
- return db2.select().from(chatMessages).where(eq(chatMessages.sessionId, sessionId)).orderBy(chatMessages.createdAt).all();
1147798
+ return db2.select().from(chatMessages).where(eq(chatMessages.sessionId, sessionId)).orderBy(chatMessages.createdAt, sql`rowid`).all();
1147601
1147799
  }
1147602
1147800
  function buildBaselinePrompt(condensed = false) {
1147603
1147801
  const settings = getSettings();
@@ -1147612,7 +1147810,12 @@ ${knowledge}`;
1147612
1147810
 
1147613
1147811
  ## Master Memory File
1147614
1147812
 
1147615
- This is your persistent memory. Update it with \`memory_file_update\` when you learn broadly useful information about the user, their preferences, projects, or recurring patterns.
1147813
+ This is your persistent memory (MEMORY.md), injected into every conversation.
1147814
+
1147815
+ **What belongs here:** user preferences, project conventions, recurring patterns, key relationships, important decisions.
1147816
+ **What does NOT belong:** ephemeral observations, one-time events, transient status \u2014 use \`memory_store\` with appropriate tags instead.
1147817
+
1147818
+ Update with \`memory_file_update\` only for broadly useful, long-term knowledge. The hourly sweep automatically curates this file.
1147616
1147819
 
1147617
1147820
  ${memoryFileContent}`;
1147618
1147821
  }
@@ -1147867,19 +1148070,22 @@ async function* streamMessage(sessionId, userMessage, modelIdOrOptions, editorCo
1147867
1148070
  yield { type: "error", data: { message: "Session not found" } };
1147868
1148071
  return;
1147869
1148072
  }
1147870
- addMessage(sessionId, {
1147871
- role: "user",
1147872
- content: userMessage,
1147873
- sessionId
1147874
- });
1148073
+ if (!options.ephemeral) {
1148074
+ addMessage(sessionId, {
1148075
+ role: "user",
1148076
+ content: userMessage,
1148077
+ sessionId
1148078
+ });
1148079
+ }
1147875
1148080
  const settings = getSettings();
1147876
1148081
  const port = settings.server.port;
1147877
1148082
  const effectiveModelId = options.modelId ?? settings.assistant.model;
1147878
1148083
  let state = sessionState.get(sessionId);
1147879
1148084
  if (!state) {
1147880
- state = {};
1148085
+ state = { claudeSessionId: options.ephemeral ? undefined : session3.claudeSessionId ?? undefined };
1147881
1148086
  sessionState.set(sessionId, state);
1147882
1148087
  }
1148088
+ const resumeSessionId = options.ephemeral ? undefined : state.claudeSessionId;
1147883
1148089
  const tempFiles = [];
1147884
1148090
  try {
1147885
1148091
  log2.assistant.debug("Starting assistant query", {
@@ -1147927,8 +1148133,15 @@ User message: ${userMessage}`;
1147927
1148133
  {
1147928
1148134
  const tempDir = await mkdtemp(join14(tmpdir3(), "fulcrum-doc-"));
1147929
1148135
  const tempPath = join14(tempDir, attachment.filename || "document.pdf");
1147930
- await writeFile3(tempPath, Buffer.from(attachment.data, "base64"));
1148136
+ const docBuffer = Buffer.from(attachment.data, "base64");
1148137
+ await writeFile3(tempPath, docBuffer);
1147931
1148138
  tempFiles.push(tempPath);
1148139
+ log2.assistant.info("Wrote document attachment to temp file", {
1148140
+ sessionId,
1148141
+ path: tempPath,
1148142
+ size: docBuffer.length,
1148143
+ header: docBuffer.subarray(0, 8).toString("ascii")
1148144
+ });
1147932
1148145
  parts.push(`[Attached document: ${tempPath}]`);
1147933
1148146
  }
1147934
1148147
  break;
@@ -1147951,13 +1148164,14 @@ ${attachment.data}`);
1147951
1148164
  sessionId,
1147952
1148165
  requestedModelId: effectiveModelId,
1147953
1148166
  resolvedModel: MODEL_MAP[effectiveModelId],
1147954
- resumeSessionId: state.claudeSessionId ?? null
1148167
+ resumeSessionId: resumeSessionId ?? null,
1148168
+ ephemeral: options.ephemeral ?? false
1147955
1148169
  });
1147956
1148170
  const result = s_({
1147957
1148171
  prompt: fullPrompt,
1147958
1148172
  options: {
1147959
1148173
  model: MODEL_MAP[effectiveModelId],
1147960
- resume: state.claudeSessionId,
1148174
+ resume: resumeSessionId,
1147961
1148175
  includePartialMessages: true,
1147962
1148176
  pathToClaudeCodeExecutable: getClaudeCodePathForSdk(),
1147963
1148177
  mcpServers: {
@@ -1147971,6 +1148185,7 @@ ${attachment.data}`);
1147971
1148185
  permissionMode: "bypassPermissions",
1147972
1148186
  allowDangerouslySkipPermissions: true,
1147973
1148187
  settingSources: ["user"],
1148188
+ ...options.ephemeral && { persistSession: false, maxTurns: 3 },
1147974
1148189
  ...options.outputFormat && { outputFormat: options.outputFormat }
1147975
1148190
  }
1147976
1148191
  });
@@ -1147996,7 +1148211,10 @@ ${attachment.data}`);
1147996
1148211
  }
1147997
1148212
  } else if (message.type === "assistant") {
1147998
1148213
  const assistantMsg = message;
1147999
- state.claudeSessionId = assistantMsg.session_id;
1148214
+ if (!options.ephemeral) {
1148215
+ state.claudeSessionId = assistantMsg.session_id;
1148216
+ db2.update(chatSessions).set({ claudeSessionId: assistantMsg.session_id }).where(eq(chatSessions.id, sessionId)).run();
1148217
+ }
1148000
1148218
  const textContent = assistantMsg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1148001
1148219
  if (textContent) {
1148002
1148220
  if (textContent.length > currentText.length) {
@@ -1148030,7 +1148248,10 @@ ${attachment.data}`);
1148030
1148248
  const resultMsg = message;
1148031
1148249
  if (resultMsg.subtype?.startsWith("error_")) {
1148032
1148250
  const errors2 = resultMsg.errors || ["Unknown error"];
1148033
- state.claudeSessionId = undefined;
1148251
+ if (!options.ephemeral) {
1148252
+ state.claudeSessionId = undefined;
1148253
+ db2.update(chatSessions).set({ claudeSessionId: null }).where(eq(chatSessions.id, sessionId)).run();
1148254
+ }
1148034
1148255
  yield { type: "error", data: { message: errors2.join(", ") } };
1148035
1148256
  }
1148036
1148257
  if (resultMsg.structured_output) {
@@ -1148043,7 +1148264,7 @@ ${attachment.data}`);
1148043
1148264
  });
1148044
1148265
  }
1148045
1148266
  }
1148046
- if (currentText.trim()) {
1148267
+ if (!options.ephemeral && currentText.trim()) {
1148047
1148268
  addMessage(sessionId, {
1148048
1148269
  role: "assistant",
1148049
1148270
  content: currentText,
@@ -1148057,7 +1148278,10 @@ ${attachment.data}`);
1148057
1148278
  } catch (err) {
1148058
1148279
  const errorMsg = err instanceof Error ? err.message : String(err);
1148059
1148280
  log2.assistant.error("Assistant stream error", { sessionId, error: errorMsg });
1148060
- state.claudeSessionId = undefined;
1148281
+ if (!options.ephemeral) {
1148282
+ state.claudeSessionId = undefined;
1148283
+ db2.update(chatSessions).set({ claudeSessionId: null }).where(eq(chatSessions.id, sessionId)).run();
1148284
+ }
1148061
1148285
  yield { type: "error", data: { message: errorMsg } };
1148062
1148286
  } finally {
1148063
1148287
  for (const tempPath of tempFiles) {
@@ -1149955,8 +1150179,51 @@ ${contextualMessage}`;
1149955
1150179
  }
1149956
1150180
  const parsed = JSON.parse(jsonText);
1149957
1150181
  if (parsed.actions && Array.isArray(parsed.actions)) {
1150182
+ const settings2 = getSettings();
1150183
+ const fulcrumPort = settings2.server?.port ?? 7777;
1149958
1150184
  for (const action of parsed.actions) {
1149959
- if (action.type === "store_memory" && action.content) {
1150185
+ if (action.type === "create_task" && action.title) {
1150186
+ try {
1150187
+ const resp = await fetch(`http://localhost:${fulcrumPort}/api/tasks`, {
1150188
+ method: "POST",
1150189
+ headers: { "Content-Type": "application/json" },
1150190
+ body: JSON.stringify({
1150191
+ title: action.title,
1150192
+ description: action.description || null,
1150193
+ status: "TO_DO",
1150194
+ tags: action.tags,
1150195
+ dueDate: action.dueDate || null
1150196
+ })
1150197
+ });
1150198
+ if (!resp.ok) {
1150199
+ log2.messaging.warn("Observer failed to create task via OpenCode", {
1150200
+ sessionId,
1150201
+ status: resp.status,
1150202
+ title: action.title
1150203
+ });
1150204
+ } else {
1150205
+ log2.messaging.info("Observer created task via OpenCode", {
1150206
+ sessionId,
1150207
+ title: action.title
1150208
+ });
1150209
+ try {
1150210
+ await fetch(`http://localhost:${fulcrumPort}/api/config/notifications/send`, {
1150211
+ method: "POST",
1150212
+ headers: { "Content-Type": "application/json" },
1150213
+ body: JSON.stringify({
1150214
+ title: `New task from ${options.channelType}`,
1150215
+ message: action.title
1150216
+ })
1150217
+ });
1150218
+ } catch {}
1150219
+ }
1150220
+ } catch (err) {
1150221
+ log2.messaging.warn("Observer task creation error via OpenCode", {
1150222
+ sessionId,
1150223
+ error: err instanceof Error ? err.message : String(err)
1150224
+ });
1150225
+ }
1150226
+ } else if (action.type === "store_memory" && action.content) {
1149960
1150227
  const source = action.source || `channel:${options.channelType}`;
1149961
1150228
  await storeMemory({
1149962
1150229
  content: action.content,
@@ -1149988,38 +1150255,59 @@ ${contextualMessage}`;
1149988
1150255
  yield { type: "error", data: { message: errorMsg } };
1149989
1150256
  }
1149990
1150257
  }
1149991
- var OPENCODE_DEFAULT_PORT = 4096, opencodeClient = null, OBSERVER_SYSTEM_PROMPT = `You are an observer processing messages from external channels. Your ONLY job is to identify important information worth remembering.
1150258
+ var OPENCODE_DEFAULT_PORT = 4096, opencodeClient = null, OBSERVER_SYSTEM_PROMPT = `You are the user's observer \u2014 your core job is to prevent things from falling through the cracks. When you see something actionable, create a task. Memories are secondary \u2014 useful for context that doesn't warrant a task.
1149992
1150259
 
1149993
1150260
  IMPORTANT: You have NO tools. Instead, respond with a JSON object describing what actions to take.
1149994
1150261
 
1149995
1150262
  Response format (respond with ONLY this JSON, no other text):
1149996
1150263
  {
1149997
1150264
  "actions": [
1150265
+ {
1150266
+ "type": "create_task",
1150267
+ "title": "Clear action item title",
1150268
+ "description": "Details including sender and context",
1150269
+ "tags": ["from:whatsapp", "errand"],
1150270
+ "dueDate": "2025-02-18"
1150271
+ },
1149998
1150272
  {
1149999
1150273
  "type": "store_memory",
1150000
1150274
  "content": "The fact or information to store",
1150001
- "tags": ["tag1", "tag2"],
1150275
+ "tags": ["persistent"],
1150002
1150276
  "source": "channel:whatsapp"
1150003
1150277
  }
1150004
1150278
  ]
1150005
1150279
  }
1150006
1150280
 
1150007
- If the message contains nothing worth remembering (casual chat, greetings, spam, etc.), respond with:
1150281
+ If the message contains nothing worth tracking (casual chat, greetings, spam, etc.), respond with:
1150008
1150282
  {"actions": []}
1150009
1150283
 
1150010
- Guidelines for what to store:
1150011
- - Important dates, deadlines, appointments
1150012
- - Decisions or agreements
1150013
- - Contact information
1150014
- - Project updates or status changes
1150015
- - Action items or requests
1150016
- - Key facts or data points
1150284
+ ## Action types
1150285
+
1150286
+ ### create_task (preferred for actionable items)
1150287
+ Use for: deadlines, meetings, to-dos, follow-ups, requests directed at the user.
1150288
+ Fields: title (required, imperative action item), description, tags (array), dueDate (YYYY-MM-DD if mentioned).
1150289
+ Write titles as clear action items (e.g., "Arrange cow rental for parade" not "Message about cow rental").
1150290
+
1150291
+ ### store_memory (for non-task observations)
1150292
+ Use for: learning someone's name, recurring patterns, key relationships, context updates.
1150293
+ Fields: content (required), tags (array), source (e.g., "channel:whatsapp").
1150017
1150294
 
1150018
- Do NOT store:
1150295
+ ## Guidelines
1150296
+
1150297
+ Create a task for:
1150298
+ - Deadlines, appointments, meetings
1150299
+ - Action items or requests directed at the user
1150300
+ - Follow-ups or reminders
1150301
+
1150302
+ Store a memory for:
1150303
+ - Contact details, names, relationships
1150304
+ - Project context or status updates
1150305
+ - Patterns worth remembering
1150306
+
1150307
+ Do nothing for:
1150019
1150308
  - Casual greetings or small talk
1150020
1150309
  - Spam or promotional content
1150021
- - Messages you don't understand
1150022
- - Trivially obvious information`;
1150310
+ - Messages you don't understand`;
1150023
1150311
  var init_opencode_channel_service = __esm(() => {
1150024
1150312
  init_dist10();
1150025
1150313
  init_logger3();
@@ -1150102,6 +1150390,8 @@ async function handleIncomingMessage(msg) {
1150102
1150390
  sender: msg.senderId,
1150103
1150391
  senderName: msg.senderName,
1150104
1150392
  content,
1150393
+ hasAttachments: (msg.attachments?.length ?? 0) > 0,
1150394
+ attachmentNames: msg.attachments?.map((a) => a.filename),
1150105
1150395
  metadata: {
1150106
1150396
  subject: msg.metadata?.subject,
1150107
1150397
  threadId: msg.metadata?.threadId,
@@ -1150110,24 +1150400,46 @@ async function handleIncomingMessage(msg) {
1150110
1150400
  };
1150111
1150401
  const systemPrompt = getMessagingSystemPrompt(msg.channelType, context);
1150112
1150402
  const isSlack = msg.channelType === "slack";
1150113
- const stream2 = _deps.streamMessage(session3.id, content, {
1150403
+ const stream2 = _deps.streamMessage(session3.id, content || "(file attached)", {
1150114
1150404
  systemPromptAdditions: systemPrompt,
1150115
- ...isSlack && { outputFormat: { type: "json_schema", schema: SLACK_RESPONSE_SCHEMA } }
1150405
+ ...msg.attachments?.length && { attachments: msg.attachments }
1150116
1150406
  });
1150117
1150407
  let responseText = "";
1150118
- let structuredOutput = null;
1150408
+ let hasError = false;
1150119
1150409
  for await (const event of stream2) {
1150120
1150410
  if (event.type === "error") {
1150121
1150411
  const errorMsg = event.data.message;
1150122
1150412
  log2.messaging.error("Assistant error handling message", { error: errorMsg });
1150413
+ hasError = true;
1150123
1150414
  } else if (event.type === "message:complete") {
1150124
- responseText = event.data.content;
1150125
- } else if (event.type === "structured_output") {
1150126
- structuredOutput = event.data;
1150415
+ const text3 = event.data.content;
1150416
+ if (!text3.startsWith("API Error:") && !text3.startsWith("Error:")) {
1150417
+ responseText = text3;
1150418
+ } else {
1150419
+ log2.messaging.warn("Suppressed API error from assistant response", {
1150420
+ errorPreview: text3.slice(0, 200)
1150421
+ });
1150422
+ hasError = true;
1150423
+ }
1150127
1150424
  }
1150128
1150425
  }
1150129
- if (isSlack && structuredOutput?.body?.trim()) {
1150130
- await sendResponse(msg, structuredOutput.body, structuredOutput.blocks ? { blocks: structuredOutput.blocks } : undefined);
1150426
+ if (hasError && !responseText.trim()) {
1150427
+ responseText = "Sorry, I ran into an issue processing that. Could you try again or rephrase your message?";
1150428
+ }
1150429
+ if (isSlack && responseText.trim()) {
1150430
+ const parsed = parseSlackResponse(responseText);
1150431
+ if (parsed) {
1150432
+ const metadata = {};
1150433
+ if (parsed.blocks)
1150434
+ metadata.blocks = parsed.blocks;
1150435
+ if (parsed.filePath)
1150436
+ metadata.filePath = parsed.filePath;
1150437
+ await sendResponse(msg, parsed.body, Object.keys(metadata).length > 0 ? metadata : undefined);
1150438
+ } else {
1150439
+ await sendResponse(msg, responseText, {
1150440
+ blocks: [{ type: "section", text: { type: "mrkdwn", text: responseText } }]
1150441
+ });
1150442
+ }
1150131
1150443
  } else if (responseText.trim()) {
1150132
1150444
  await sendResponse(msg, responseText);
1150133
1150445
  }
@@ -1150253,6 +1150565,24 @@ Started: ${new Date(mapping.createdAt).toLocaleString()}
1150253
1150565
  Last active: ${new Date(mapping.lastMessageAt).toLocaleString()}`;
1150254
1150566
  await sendResponse(msg, statusText);
1150255
1150567
  }
1150568
+ function parseSlackResponse(text3) {
1150569
+ const match3 = text3.match(/<slack-response>([\s\S]*?)<\/slack-response>/);
1150570
+ if (!match3)
1150571
+ return null;
1150572
+ try {
1150573
+ const parsed = JSON.parse(match3[1]);
1150574
+ if (typeof parsed.body === "string" && parsed.body.trim()) {
1150575
+ return {
1150576
+ body: parsed.body,
1150577
+ ...Array.isArray(parsed.blocks) && { blocks: parsed.blocks },
1150578
+ ...typeof parsed.filePath === "string" && parsed.filePath.trim() && { filePath: parsed.filePath }
1150579
+ };
1150580
+ }
1150581
+ return null;
1150582
+ } catch {
1150583
+ return null;
1150584
+ }
1150585
+ }
1150256
1150586
  async function sendResponse(originalMsg, content, metadata) {
1150257
1150587
  if (originalMsg.channelType === "email") {
1150258
1150588
  log2.messaging.info("Skipping email response \u2014 sending disabled, use Gmail drafts", {
@@ -1150359,7 +1150689,8 @@ async function processObserveOnlyMessage(msg) {
1150359
1150689
  const stream2 = _deps.streamMessage(session3.id, msg.content, {
1150360
1150690
  systemPromptAdditions: systemPrompt,
1150361
1150691
  modelId: observerModelId,
1150362
- securityTier: "observer"
1150692
+ securityTier: "observer",
1150693
+ ephemeral: true
1150363
1150694
  });
1150364
1150695
  for await (const event of stream2) {
1150365
1150696
  if (event.type === "error") {
@@ -1150380,7 +1150711,7 @@ async function processObserveOnlyMessage(msg) {
1150380
1150711
  recordObserverFailure();
1150381
1150712
  }
1150382
1150713
  }
1150383
- var _deps, SLACK_RESPONSE_SCHEMA, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150714
+ var _deps, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150384
1150715
  var init_message_handler = __esm(() => {
1150385
1150716
  init_logger3();
1150386
1150717
  init_channel_manager();
@@ -1150392,21 +1150723,6 @@ var init_message_handler = __esm(() => {
1150392
1150723
  streamMessage: (...args) => streamMessage(...args),
1150393
1150724
  streamOpencodeObserverMessage: (...args) => streamOpencodeObserverMessage(...args)
1150394
1150725
  };
1150395
- SLACK_RESPONSE_SCHEMA = {
1150396
- type: "object",
1150397
- properties: {
1150398
- body: {
1150399
- type: "string",
1150400
- description: "Plain text message (shown in notifications and as fallback)"
1150401
- },
1150402
- blocks: {
1150403
- type: "array",
1150404
- description: "Slack Block Kit blocks for rich formatting",
1150405
- items: { type: "object", additionalProperties: true }
1150406
- }
1150407
- },
1150408
- required: ["body"]
1150409
- };
1150410
1150726
  OBSERVER_CIRCUIT_BREAKER = {
1150411
1150727
  failureCount: 0,
1150412
1150728
  failureThreshold: 3,
@@ -1150687,7 +1151003,10 @@ async function sendMessageToChannel(channel, body, options) {
1150687
1151003
  return { success: false, error: "Slack channel not active" };
1150688
1151004
  }
1150689
1151005
  try {
1150690
- const msgMetadata = options?.slackBlocks ? { blocks: options.slackBlocks } : undefined;
1151006
+ const msgMetadata = options?.slackBlocks || options?.filePath ? {
1151007
+ ...options.slackBlocks && { blocks: options.slackBlocks },
1151008
+ ...options.filePath && { filePath: options.filePath }
1151009
+ } : undefined;
1150691
1151010
  const success = await slackChannel.sendMessage(resolvedTo, body, msgMetadata);
1150692
1151011
  if (success) {
1150693
1151012
  log2.messaging.info("Sent Slack message", { to: resolvedTo, hasBlocks: !!options?.slackBlocks });
@@ -1188385,13 +1188704,13 @@ var require_core = __commonJS((exports) => {
1188385
1188704
  return metaOpts;
1188386
1188705
  }
1188387
1188706
  var noLogs = { log() {}, warn() {}, error() {} };
1188388
- function getLogger(logger6) {
1188389
- if (logger6 === false)
1188707
+ function getLogger(logger7) {
1188708
+ if (logger7 === false)
1188390
1188709
  return noLogs;
1188391
- if (logger6 === undefined)
1188710
+ if (logger7 === undefined)
1188392
1188711
  return console;
1188393
- if (logger6.log && logger6.warn && logger6.error)
1188394
- return logger6;
1188712
+ if (logger7.log && logger7.warn && logger7.error)
1188713
+ return logger7;
1188395
1188714
  throw new Error("logger must implement log, warn and error methods");
1188396
1188715
  }
1188397
1188716
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -1194018,13 +1194337,13 @@ var require_core3 = __commonJS((exports) => {
1194018
1194337
  return metaOpts;
1194019
1194338
  }
1194020
1194339
  var noLogs = { log() {}, warn() {}, error() {} };
1194021
- function getLogger(logger6) {
1194022
- if (logger6 === false)
1194340
+ function getLogger(logger7) {
1194341
+ if (logger7 === false)
1194023
1194342
  return noLogs;
1194024
- if (logger6 === undefined)
1194343
+ if (logger7 === undefined)
1194025
1194344
  return console;
1194026
- if (logger6.log && logger6.warn && logger6.error)
1194027
- return logger6;
1194345
+ if (logger7.log && logger7.warn && logger7.error)
1194346
+ return logger7;
1194028
1194347
  throw new Error("logger must implement log, warn and error methods");
1194029
1194348
  }
1194030
1194349
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -1200416,10 +1200735,10 @@ class CaldavAccountManager {
1200416
1200735
  }
1200417
1200736
  async startAll() {
1200418
1200737
  const accounts = db2.select().from(caldavAccounts).where(eq(caldavAccounts.enabled, true)).all();
1200419
- logger6.info("Starting all CalDAV accounts", { count: accounts.length });
1200738
+ logger7.info("Starting all CalDAV accounts", { count: accounts.length });
1200420
1200739
  for (const account2 of accounts) {
1200421
1200740
  await this.startAccount(account2.id).catch((err) => {
1200422
- logger6.error("Failed to start account", {
1200741
+ logger7.error("Failed to start account", {
1200423
1200742
  accountId: account2.id,
1200424
1200743
  name: account2.name,
1200425
1200744
  error: err instanceof Error ? err.message : String(err)
@@ -1200432,7 +1200751,7 @@ class CaldavAccountManager {
1200432
1200751
  this.stopAccount(accountId);
1200433
1200752
  }
1200434
1200753
  this.connections.clear();
1200435
- logger6.info("All CalDAV accounts stopped");
1200754
+ logger7.info("All CalDAV accounts stopped");
1200436
1200755
  }
1200437
1200756
  async startAccount(accountId) {
1200438
1200757
  const account2 = db2.select().from(caldavAccounts).where(eq(caldavAccounts.id, accountId)).get();
@@ -1200440,7 +1200759,7 @@ class CaldavAccountManager {
1200440
1200759
  throw new Error(`Account not found: ${accountId}`);
1200441
1200760
  }
1200442
1200761
  if (!account2.enabled) {
1200443
- logger6.info("Account is disabled, skipping", { accountId, name: account2.name });
1200762
+ logger7.info("Account is disabled, skipping", { accountId, name: account2.name });
1200444
1200763
  return;
1200445
1200764
  }
1200446
1200765
  this.stopAccount(accountId);
@@ -1200456,11 +1200775,11 @@ class CaldavAccountManager {
1200456
1200775
  try {
1200457
1200776
  conn.client = await this.connect(account2);
1200458
1200777
  this.scheduleSync(accountId, account2.syncIntervalMinutes ?? 15);
1200459
- logger6.info("Started CalDAV account", { accountId, name: account2.name });
1200778
+ logger7.info("Started CalDAV account", { accountId, name: account2.name });
1200460
1200779
  } catch (err) {
1200461
1200780
  conn.lastSyncError = err instanceof Error ? err.message : String(err);
1200462
1200781
  this.updateAccountError(accountId, conn.lastSyncError);
1200463
- logger6.error("Failed to connect account", { accountId, error: conn.lastSyncError });
1200782
+ logger7.error("Failed to connect account", { accountId, error: conn.lastSyncError });
1200464
1200783
  this.scheduleRetry(accountId);
1200465
1200784
  }
1200466
1200785
  }
@@ -1200502,7 +1200821,7 @@ class CaldavAccountManager {
1200502
1200821
  for (const [accountId, conn] of this.connections) {
1200503
1200822
  if (conn.client && !conn.isSyncing) {
1200504
1200823
  promises.push(this.syncAccount(accountId).catch((err) => {
1200505
- logger6.error("Sync failed for account", {
1200824
+ logger7.error("Sync failed for account", {
1200506
1200825
  accountId,
1200507
1200826
  error: err instanceof Error ? err.message : String(err)
1200508
1200827
  });
@@ -1200562,7 +1200881,7 @@ class CaldavAccountManager {
1200562
1200881
  defaultAccountType: "caldav"
1200563
1200882
  });
1200564
1200883
  await client4.login();
1200565
- logger6.info("Connected to CalDAV server (Basic)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200884
+ logger7.info("Connected to CalDAV server (Basic)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200566
1200885
  return client4;
1200567
1200886
  }
1200568
1200887
  async connectOAuth(account2) {
@@ -1200597,9 +1200916,9 @@ class CaldavAccountManager {
1200597
1200916
  credentials.expiration = newTokens.expiration;
1200598
1200917
  try {
1200599
1200918
  db2.update(caldavAccounts).set({ oauthTokens: newTokens, updatedAt: new Date().toISOString() }).where(eq(caldavAccounts.id, accountId)).run();
1200600
- logger6.info("Persisted refreshed OAuth tokens", { accountId });
1200919
+ logger7.info("Persisted refreshed OAuth tokens", { accountId });
1200601
1200920
  } catch (err) {
1200602
- logger6.error("Failed to persist refreshed OAuth tokens", {
1200921
+ logger7.error("Failed to persist refreshed OAuth tokens", {
1200603
1200922
  accountId,
1200604
1200923
  error: err instanceof Error ? err.message : String(err)
1200605
1200924
  });
@@ -1200610,7 +1200929,7 @@ class CaldavAccountManager {
1200610
1200929
  defaultAccountType: "caldav"
1200611
1200930
  });
1200612
1200931
  await client4.login();
1200613
- logger6.info("Connected to CalDAV server (Google OAuth)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200932
+ logger7.info("Connected to CalDAV server (Google OAuth)", { accountId: account2.id, serverUrl: account2.serverUrl });
1200614
1200933
  return client4;
1200615
1200934
  }
1200616
1200935
  scheduleSync(accountId, intervalMinutes) {
@@ -1200622,7 +1200941,7 @@ class CaldavAccountManager {
1200622
1200941
  }
1200623
1200942
  conn.syncInterval = setInterval(() => {
1200624
1200943
  this.syncAccount(accountId).catch((err) => {
1200625
- logger6.error("Periodic sync failed", {
1200944
+ logger7.error("Periodic sync failed", {
1200626
1200945
  accountId,
1200627
1200946
  error: err instanceof Error ? err.message : String(err)
1200628
1200947
  });
@@ -1200635,7 +1200954,7 @@ class CaldavAccountManager {
1200635
1200954
  return;
1200636
1200955
  conn.retryCount++;
1200637
1200956
  const delay2 = Math.min(1000 * Math.pow(2, conn.retryCount), MAX_RETRY_DELAY);
1200638
- logger6.info("Scheduling account retry", { accountId, retryCount: conn.retryCount, delayMs: delay2 });
1200957
+ logger7.info("Scheduling account retry", { accountId, retryCount: conn.retryCount, delayMs: delay2 });
1200639
1200958
  setTimeout(async () => {
1200640
1200959
  const account2 = db2.select().from(caldavAccounts).where(eq(caldavAccounts.id, accountId)).get();
1200641
1200960
  if (!account2 || !account2.enabled)
@@ -1200651,7 +1200970,7 @@ class CaldavAccountManager {
1200651
1200970
  } catch (err) {
1200652
1200971
  conn.lastSyncError = err instanceof Error ? err.message : String(err);
1200653
1200972
  this.updateAccountError(accountId, conn.lastSyncError);
1200654
- logger6.error("Account retry failed", { accountId, error: conn.lastSyncError });
1200973
+ logger7.error("Account retry failed", { accountId, error: conn.lastSyncError });
1200655
1200974
  this.scheduleRetry(accountId);
1200656
1200975
  }
1200657
1200976
  }, delay2);
@@ -1200709,10 +1201028,10 @@ class CaldavAccountManager {
1200709
1201028
  if (!seenUrls.has(local.remoteUrl)) {
1200710
1201029
  db2.delete(caldavEvents2).where(eq(caldavEvents2.calendarId, local.id)).run();
1200711
1201030
  db2.delete(caldavCalendars).where(eq(caldavCalendars.id, local.id)).run();
1200712
- logger6.info("Removed deleted calendar", { displayName: local.displayName, accountId });
1201031
+ logger7.info("Removed deleted calendar", { displayName: local.displayName, accountId });
1200713
1201032
  }
1200714
1201033
  }
1200715
- logger6.info("Account sync complete", { accountId, calendars: remoteCalendars.length });
1201034
+ logger7.info("Account sync complete", { accountId, calendars: remoteCalendars.length });
1200716
1201035
  }
1200717
1201036
  async syncCalendarEvents(calendarId, remoteCal, client4) {
1200718
1201037
  const { caldavEvents: caldavEvents2 } = await Promise.resolve().then(() => (init_db2(), exports_db));
@@ -1200779,13 +1201098,13 @@ class CaldavAccountManager {
1200779
1201098
  }
1200780
1201099
  }
1200781
1201100
  }
1200782
- var logger6, GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", MAX_RETRY_DELAY, accountManager;
1201101
+ var logger7, GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", MAX_RETRY_DELAY, accountManager;
1200783
1201102
  var init_caldav_account_manager = __esm(() => {
1200784
1201103
  init_tsdav_esm();
1200785
1201104
  init_drizzle_orm();
1200786
1201105
  init_db2();
1200787
1201106
  init_logger3();
1200788
- logger6 = createLogger("CalDAV:AccountManager");
1201107
+ logger7 = createLogger("CalDAV:AccountManager");
1200789
1201108
  MAX_RETRY_DELAY = 5 * 60 * 1000;
1200790
1201109
  accountManager = new CaldavAccountManager;
1200791
1201110
  });
@@ -1200808,7 +1201127,7 @@ class GoogleCalendarManager {
1200808
1201127
  try {
1200809
1201128
  await this.syncAccount(accountId);
1200810
1201129
  } catch (err) {
1200811
- logger7.error("Initial Google Calendar sync failed", {
1201130
+ logger8.error("Initial Google Calendar sync failed", {
1200812
1201131
  accountId,
1200813
1201132
  error: err instanceof Error ? err.message : String(err)
1200814
1201133
  });
@@ -1200816,13 +1201135,13 @@ class GoogleCalendarManager {
1200816
1201135
  const intervalMs = (account2.syncIntervalMinutes ?? 15) * 60 * 1000;
1200817
1201136
  conn.syncInterval = setInterval(() => {
1200818
1201137
  this.syncAccount(accountId).catch((err) => {
1200819
- logger7.error("Periodic Google Calendar sync failed", {
1201138
+ logger8.error("Periodic Google Calendar sync failed", {
1200820
1201139
  accountId,
1200821
1201140
  error: err instanceof Error ? err.message : String(err)
1200822
1201141
  });
1200823
1201142
  });
1200824
1201143
  }, intervalMs);
1200825
- logger7.info("Started Google Calendar account", { accountId, name: account2.name });
1201144
+ logger8.info("Started Google Calendar account", { accountId, name: account2.name });
1200826
1201145
  }
1200827
1201146
  stopAccount(accountId) {
1200828
1201147
  const conn = this.connections.get(accountId);
@@ -1200840,10 +1201159,10 @@ class GoogleCalendarManager {
1200840
1201159
  }
1200841
1201160
  async startAll() {
1200842
1201161
  const accounts = db2.select().from(googleAccounts).where(eq(googleAccounts.calendarEnabled, true)).all();
1200843
- logger7.info("Starting Google Calendar accounts", { count: accounts.length });
1201162
+ logger8.info("Starting Google Calendar accounts", { count: accounts.length });
1200844
1201163
  for (const account2 of accounts) {
1200845
1201164
  await this.startAccount(account2.id).catch((err) => {
1200846
- logger7.error("Failed to start Google Calendar account", {
1201165
+ logger8.error("Failed to start Google Calendar account", {
1200847
1201166
  accountId: account2.id,
1200848
1201167
  error: err instanceof Error ? err.message : String(err)
1200849
1201168
  });
@@ -1200905,7 +1201224,7 @@ class GoogleCalendarManager {
1200905
1201224
  if (!seenRemoteUrls.has(local.remoteUrl)) {
1200906
1201225
  db2.delete(caldavEvents).where(eq(caldavEvents.calendarId, local.id)).run();
1200907
1201226
  db2.delete(caldavCalendars).where(eq(caldavCalendars.id, local.id)).run();
1200908
- logger7.info("Removed deleted Google calendar", {
1201227
+ logger8.info("Removed deleted Google calendar", {
1200909
1201228
  displayName: local.displayName,
1200910
1201229
  accountId
1200911
1201230
  });
@@ -1200919,7 +1201238,7 @@ class GoogleCalendarManager {
1200919
1201238
  if (conn) {
1200920
1201239
  conn.lastSyncError = null;
1200921
1201240
  }
1200922
- logger7.info("Google Calendar sync complete", {
1201241
+ logger8.info("Google Calendar sync complete", {
1200923
1201242
  accountId,
1200924
1201243
  calendars: remoteCalendars.length
1200925
1201244
  });
@@ -1201136,14 +1201455,14 @@ class GoogleCalendarManager {
1201136
1201455
  return this.connections.get(accountId)?.lastSyncError ?? null;
1201137
1201456
  }
1201138
1201457
  }
1201139
- var import_googleapis3, logger7, googleCalendarManager;
1201458
+ var import_googleapis3, logger8, googleCalendarManager;
1201140
1201459
  var init_google_calendar_manager = __esm(() => {
1201141
1201460
  init_drizzle_orm();
1201142
1201461
  init_db2();
1201143
1201462
  init_google_oauth();
1201144
1201463
  init_logger3();
1201145
1201464
  import_googleapis3 = __toESM(require_src12(), 1);
1201146
- logger7 = createLogger("Google:Calendar");
1201465
+ logger8 = createLogger("Google:Calendar");
1201147
1201466
  googleCalendarManager = new GoogleCalendarManager;
1201148
1201467
  });
1201149
1201468
 
@@ -1201157,12 +1201476,12 @@ async function executeAllRules() {
1201157
1201476
  const rules = db2.select().from(caldavCopyRules).where(eq(caldavCopyRules.enabled, true)).all();
1201158
1201477
  if (rules.length === 0)
1201159
1201478
  return;
1201160
- logger8.info("Executing copy rules", { count: rules.length });
1201479
+ logger9.info("Executing copy rules", { count: rules.length });
1201161
1201480
  for (const rule of rules) {
1201162
1201481
  try {
1201163
1201482
  await executeSingleRule(rule.id);
1201164
1201483
  } catch (err) {
1201165
- logger8.error("Copy rule failed", {
1201484
+ logger9.error("Copy rule failed", {
1201166
1201485
  ruleId: rule.id,
1201167
1201486
  error: err instanceof Error ? err.message : String(err)
1201168
1201487
  });
@@ -1201176,7 +1201495,7 @@ async function executeSingleRule(ruleId) {
1201176
1201495
  const sourceCal = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, rule.sourceCalendarId)).get();
1201177
1201496
  const destCal = db2.select().from(caldavCalendars).where(eq(caldavCalendars.id, rule.destCalendarId)).get();
1201178
1201497
  if (!sourceCal || !destCal) {
1201179
- logger8.warn("Copy rule references missing calendar", {
1201498
+ logger9.warn("Copy rule references missing calendar", {
1201180
1201499
  ruleId,
1201181
1201500
  sourceExists: !!sourceCal,
1201182
1201501
  destExists: !!destCal
@@ -1201186,7 +1201505,7 @@ async function executeSingleRule(ruleId) {
1201186
1201505
  const isGoogleDest = !!destCal.googleAccountId;
1201187
1201506
  const destClient = destCal.accountId ? accountManager.getClient(destCal.accountId) : null;
1201188
1201507
  if (!isGoogleDest && !destClient) {
1201189
- logger8.warn("Destination account not connected", {
1201508
+ logger9.warn("Destination account not connected", {
1201190
1201509
  ruleId,
1201191
1201510
  destCalendarId: destCal.id,
1201192
1201511
  destAccountId: destCal.accountId
@@ -1201272,7 +1201591,7 @@ async function executeSingleRule(ruleId) {
1201272
1201591
  }).run();
1201273
1201592
  created++;
1201274
1201593
  } catch (err) {
1201275
- logger8.error("Failed to copy event", {
1201594
+ logger9.error("Failed to copy event", {
1201276
1201595
  ruleId,
1201277
1201596
  sourceEventId: sourceEvent.id,
1201278
1201597
  error: err instanceof Error ? err.message : String(err)
@@ -1201336,7 +1201655,7 @@ async function executeSingleRule(ruleId) {
1201336
1201655
  db2.update(caldavCopiedEvents).set({ sourceEtag: sourceEvent.etag, updatedAt: now }).where(eq(caldavCopiedEvents.id, existingCopy.id)).run();
1201337
1201656
  updated++;
1201338
1201657
  } catch (err) {
1201339
- logger8.error("Failed to update copied event", {
1201658
+ logger9.error("Failed to update copied event", {
1201340
1201659
  ruleId,
1201341
1201660
  sourceEventId: sourceEvent.id,
1201342
1201661
  destEventId: existingCopy.destEventId,
@@ -1201347,11 +1201666,11 @@ async function executeSingleRule(ruleId) {
1201347
1201666
  }
1201348
1201667
  db2.update(caldavCopyRules).set({ lastExecutedAt: now, updatedAt: now }).where(eq(caldavCopyRules.id, ruleId)).run();
1201349
1201668
  if (created > 0 || updated > 0) {
1201350
- logger8.info("Copy rule executed", { ruleId, created, updated });
1201669
+ logger9.info("Copy rule executed", { ruleId, created, updated });
1201351
1201670
  }
1201352
1201671
  return { created, updated };
1201353
1201672
  }
1201354
- var logger8;
1201673
+ var logger9;
1201355
1201674
  var init_copy_engine = __esm(() => {
1201356
1201675
  init_drizzle_orm();
1201357
1201676
  init_db2();
@@ -1201359,7 +1201678,7 @@ var init_copy_engine = __esm(() => {
1201359
1201678
  init_ical_helpers();
1201360
1201679
  init_caldav_account_manager();
1201361
1201680
  init_google_calendar_manager();
1201362
- logger8 = createLogger("CalDAV:CopyEngine");
1201681
+ logger9 = createLogger("CalDAV:CopyEngine");
1201363
1201682
  });
1201364
1201683
 
1201365
1201684
  // node_modules/@hono/node-server/dist/index.mjs
@@ -1208351,6 +1208670,8 @@ async function updateTaskStatus(taskId, newStatus, newPosition) {
1208351
1208670
  // server/services/search-service.ts
1208352
1208671
  init_db2();
1208353
1208672
  init_drizzle_orm();
1208673
+ init_schema();
1208674
+ init_logger3();
1208354
1208675
  var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories", "conversations"];
1208355
1208676
  async function search(options) {
1208356
1208677
  const entities = options.entities?.length ? options.entities : ALL_ENTITIES;
@@ -1208369,6 +1208690,8 @@ async function search(options) {
1208369
1208690
  return searchMemories2(options.query, { tags: options.memoryTags }, limit);
1208370
1208691
  case "conversations":
1208371
1208692
  return searchConversations(options.query, { role: options.conversationRole, provider: options.conversationProvider, projectId: options.conversationProjectId }, limit);
1208693
+ case "gmail":
1208694
+ return searchGmail(options.query, { from: options.gmailFrom, to: options.gmailTo, after: options.gmailAfter, before: options.gmailBefore }, limit);
1208372
1208695
  }
1208373
1208696
  });
1208374
1208697
  const resultSets = await Promise.all(searches);
@@ -1208572,6 +1208895,55 @@ async function searchConversations(query, filters, limit) {
1208572
1208895
  }
1208573
1208896
  }));
1208574
1208897
  }
1208898
+ var logger6 = createLogger("SearchService");
1208899
+ async function searchGmail(query, filters, limit) {
1208900
+ const accounts = db2.select().from(googleAccounts).where(eq(googleAccounts.gmailEnabled, true)).all();
1208901
+ if (!accounts.length)
1208902
+ return [];
1208903
+ const parts = [query];
1208904
+ if (filters.from)
1208905
+ parts.push(`from:${filters.from}`);
1208906
+ if (filters.to)
1208907
+ parts.push(`to:${filters.to}`);
1208908
+ if (filters.after)
1208909
+ parts.push(`after:${filters.after}`);
1208910
+ if (filters.before)
1208911
+ parts.push(`before:${filters.before}`);
1208912
+ const gmailQuery = parts.join(" ");
1208913
+ const { listMessages: listMessages2 } = await Promise.resolve().then(() => (init_gmail_service(), exports_gmail_service));
1208914
+ const results = await Promise.allSettled(accounts.map(async (account) => {
1208915
+ const messages2 = await listMessages2(account.id, {
1208916
+ query: gmailQuery,
1208917
+ maxResults: limit
1208918
+ });
1208919
+ return messages2.map((msg) => ({
1208920
+ entityType: "gmail",
1208921
+ id: msg.id,
1208922
+ title: msg.subject || "(no subject)",
1208923
+ snippet: msg.snippet || "",
1208924
+ score: 0.9,
1208925
+ metadata: {
1208926
+ threadId: msg.threadId,
1208927
+ from: msg.from,
1208928
+ to: msg.to,
1208929
+ date: msg.date,
1208930
+ labels: msg.labels,
1208931
+ accountId: account.id,
1208932
+ accountEmail: account.email,
1208933
+ gmailLink: `https://mail.google.com/mail/u/0/#inbox/${msg.id}`
1208934
+ }
1208935
+ }));
1208936
+ }));
1208937
+ const allResults = [];
1208938
+ for (const result of results) {
1208939
+ if (result.status === "fulfilled") {
1208940
+ allResults.push(...result.value);
1208941
+ } else {
1208942
+ logger6.warn("Gmail search failed for an account", { error: String(result.reason) });
1208943
+ }
1208944
+ }
1208945
+ return allResults.slice(0, limit);
1208946
+ }
1208575
1208947
  function reindexTaskFTS(taskId) {
1208576
1208948
  db2.run(sql`
1208577
1208949
  UPDATE tasks SET updated_at = updated_at WHERE id = ${taskId}
@@ -1208643,7 +1209015,7 @@ var ALLOWED_MIME_TYPES = [
1208643
1209015
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1208644
1209016
  "text/csv"
1208645
1209017
  ];
1208646
- var MAX_FILE_SIZE = 50 * 1024 * 1024;
1209018
+ var MAX_FILE_SIZE2 = 50 * 1024 * 1024;
1208647
1209019
  function getTaskUploadsDir(taskId) {
1208648
1209020
  return path8.join(getFulcrumDir(), "uploads", "tasks", taskId);
1208649
1209021
  }
@@ -1209300,8 +1209672,8 @@ app2.post("/:id/attachments", async (c) => {
1209300
1209672
  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
1209301
1209673
  return c.json({ error: `File type not allowed: ${file.type}` }, 400);
1209302
1209674
  }
1209303
- if (file.size > MAX_FILE_SIZE) {
1209304
- return c.json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB` }, 400);
1209675
+ if (file.size > MAX_FILE_SIZE2) {
1209676
+ return c.json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE2 / 1024 / 1024}MB` }, 400);
1209305
1209677
  }
1209306
1209678
  const uploadDir = getTaskUploadsDir(taskId);
1209307
1209679
  if (!fs11.existsSync(uploadDir)) {
@@ -1211891,7 +1212263,7 @@ init_drizzle_orm();
1211891
1212263
  init_nanoid();
1211892
1212264
  init_logger3();
1211893
1212265
  init_settings();
1211894
- import { existsSync as existsSync22, readFileSync as readFileSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, mkdtempSync, rmSync as rmSync6 } from "fs";
1212266
+ import { existsSync as existsSync22, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, mkdtempSync, rmSync as rmSync6 } from "fs";
1211895
1212267
  import { join as join22 } from "path";
1211896
1212268
  import { tmpdir as tmpdir4 } from "os";
1211897
1212269
  import { execSync as execSync6 } from "child_process";
@@ -1211968,9 +1212340,9 @@ function fetchCopierYamlFromGit(gitUrl) {
1211968
1212340
  const yamlAltPath = join22(tempDir, "copier.yaml");
1211969
1212341
  let content = null;
1211970
1212342
  if (existsSync22(yamlPath)) {
1211971
- content = readFileSync12(yamlPath, "utf-8");
1212343
+ content = readFileSync13(yamlPath, "utf-8");
1211972
1212344
  } else if (existsSync22(yamlAltPath)) {
1211973
- content = readFileSync12(yamlAltPath, "utf-8");
1212345
+ content = readFileSync13(yamlAltPath, "utf-8");
1211974
1212346
  }
1211975
1212347
  if (!content) {
1211976
1212348
  rmSync6(tempDir, { recursive: true, force: true });
@@ -1211992,10 +1212364,10 @@ async function fetchCopierYaml(source) {
1211992
1212364
  const yamlPath = join22(templatePath, "copier.yml");
1211993
1212365
  const yamlAltPath = join22(templatePath, "copier.yaml");
1211994
1212366
  if (existsSync22(yamlPath)) {
1211995
- return { content: readFileSync12(yamlPath, "utf-8"), templatePath };
1212367
+ return { content: readFileSync13(yamlPath, "utf-8"), templatePath };
1211996
1212368
  }
1211997
1212369
  if (existsSync22(yamlAltPath)) {
1211998
- return { content: readFileSync12(yamlAltPath, "utf-8"), templatePath };
1212370
+ return { content: readFileSync13(yamlAltPath, "utf-8"), templatePath };
1211999
1212371
  }
1212000
1212372
  throw new Error("copier.yml not found in template directory");
1212001
1212373
  }
@@ -1212956,20 +1213328,20 @@ var VERSION4 = "7.0.6";
1212956
1213328
  var noop2 = () => {};
1212957
1213329
  var consoleWarn = console.warn.bind(console);
1212958
1213330
  var consoleError = console.error.bind(console);
1212959
- function createLogger2(logger6 = {}) {
1212960
- if (typeof logger6.debug !== "function") {
1212961
- logger6.debug = noop2;
1213331
+ function createLogger2(logger7 = {}) {
1213332
+ if (typeof logger7.debug !== "function") {
1213333
+ logger7.debug = noop2;
1212962
1213334
  }
1212963
- if (typeof logger6.info !== "function") {
1212964
- logger6.info = noop2;
1213335
+ if (typeof logger7.info !== "function") {
1213336
+ logger7.info = noop2;
1212965
1213337
  }
1212966
- if (typeof logger6.warn !== "function") {
1212967
- logger6.warn = consoleWarn;
1213338
+ if (typeof logger7.warn !== "function") {
1213339
+ logger7.warn = consoleWarn;
1212968
1213340
  }
1212969
- if (typeof logger6.error !== "function") {
1212970
- logger6.error = consoleError;
1213341
+ if (typeof logger7.error !== "function") {
1213342
+ logger7.error = consoleError;
1212971
1213343
  }
1212972
- return logger6;
1213344
+ return logger7;
1212973
1213345
  }
1212974
1213346
  var userAgentTrail = `octokit-core.js/${VERSION4} ${getUserAgent2()}`;
1212975
1213347
 
@@ -1215875,7 +1216247,7 @@ var github_default = app11;
1215875
1216247
  init_db2();
1215876
1216248
  init_pty_instance();
1215877
1216249
  init_dtach_service();
1215878
- import { readdirSync as readdirSync8, readFileSync as readFileSync13, readlinkSync as readlinkSync3, existsSync as existsSync23 } from "fs";
1216250
+ import { readdirSync as readdirSync8, readFileSync as readFileSync14, readlinkSync as readlinkSync3, existsSync as existsSync23 } from "fs";
1215879
1216251
  import { execSync as execSync8 } from "child_process";
1215880
1216252
  import { homedir as homedir7 } from "os";
1215881
1216253
  import { join as join23 } from "path";
@@ -1216160,7 +1216532,7 @@ function findAllAgentProcesses(agentFilter) {
1216160
1216532
  for (const pidStr of procDirs) {
1216161
1216533
  const pid = parseInt(pidStr, 10);
1216162
1216534
  try {
1216163
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216535
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216164
1216536
  if (combinedPattern.test(cmdline)) {
1216165
1216537
  const agent = detectAgentType(cmdline);
1216166
1216538
  if (agent && (!agentFilter || agentFilter.includes(agent))) {
@@ -1216216,7 +1216588,7 @@ function getProcessCwd(pid) {
1216216
1216588
  }
1216217
1216589
  function getProcessMemoryMB(pid) {
1216218
1216590
  try {
1216219
- const status = readFileSync13(`/proc/${pid}/status`, "utf-8");
1216591
+ const status = readFileSync14(`/proc/${pid}/status`, "utf-8");
1216220
1216592
  const match3 = status.match(/VmRSS:\s+(\d+)\s+kB/);
1216221
1216593
  return match3 ? parseInt(match3[1], 10) / 1024 : 0;
1216222
1216594
  } catch {
@@ -1216231,10 +1216603,10 @@ function getProcessMemoryMB(pid) {
1216231
1216603
  function getProcessStartTime(pid) {
1216232
1216604
  if (!isMacOS2) {
1216233
1216605
  try {
1216234
- const stat2 = readFileSync13(`/proc/${pid}/stat`, "utf-8");
1216606
+ const stat2 = readFileSync14(`/proc/${pid}/stat`, "utf-8");
1216235
1216607
  const fields = stat2.split(" ");
1216236
1216608
  const starttime = parseInt(fields[21], 10);
1216237
- const uptime = parseFloat(readFileSync13("/proc/uptime", "utf-8").split(" ")[0]);
1216609
+ const uptime = parseFloat(readFileSync14("/proc/uptime", "utf-8").split(" ")[0]);
1216238
1216610
  const clockTicks = 100;
1216239
1216611
  const bootTime = Math.floor(Date.now() / 1000) - uptime;
1216240
1216612
  return Math.floor(bootTime + starttime / clockTicks);
@@ -1216309,7 +1216681,7 @@ monitoringRoutes.get("/claude-instances", (c) => {
1216309
1216681
  for (const pidStr of procDirs) {
1216310
1216682
  const pid = parseInt(pidStr, 10);
1216311
1216683
  try {
1216312
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216684
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216313
1216685
  if (cmdline.includes(socketPath)) {
1216314
1216686
  foundPids.push(pid);
1216315
1216687
  }
@@ -1216403,7 +1216775,7 @@ monitoringRoutes.post("/claude-instances/:pid/kill-pid", (c) => {
1216403
1216775
  return c.json({ error: "Invalid PID" }, 400);
1216404
1216776
  }
1216405
1216777
  try {
1216406
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216778
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216407
1216779
  const agent = detectAgentType(cmdline);
1216408
1216780
  if (!agent) {
1216409
1216781
  return c.json({ error: "Process is not a recognized AI agent" }, 400);
@@ -1216429,7 +1216801,7 @@ function getTotalMemory() {
1216429
1216801
  }
1216430
1216802
  }
1216431
1216803
  try {
1216432
- const meminfo = readFileSync13("/proc/meminfo", "utf-8");
1216804
+ const meminfo = readFileSync14("/proc/meminfo", "utf-8");
1216433
1216805
  const match3 = meminfo.match(/MemTotal:\s+(\d+)/);
1216434
1216806
  return match3 ? parseInt(match3[1], 10) * 1024 : 0;
1216435
1216807
  } catch {
@@ -1216645,7 +1217017,7 @@ monitoringRoutes.get("/docker-stats", async (c) => {
1216645
1217017
  });
1216646
1217018
  function getProcessEnv(pid) {
1216647
1217019
  try {
1216648
- const environ = readFileSync13(`/proc/${pid}/environ`, "utf-8");
1217020
+ const environ = readFileSync14(`/proc/${pid}/environ`, "utf-8");
1216649
1217021
  const env = {};
1216650
1217022
  for (const entry of environ.split("\x00")) {
1216651
1217023
  const idx = entry.indexOf("=");
@@ -1216660,7 +1217032,7 @@ function getProcessEnv(pid) {
1216660
1217032
  }
1216661
1217033
  function getParentPid(pid) {
1216662
1217034
  try {
1216663
- const stat2 = readFileSync13(`/proc/${pid}/stat`, "utf-8");
1217035
+ const stat2 = readFileSync14(`/proc/${pid}/stat`, "utf-8");
1216664
1217036
  const match3 = stat2.match(/^\d+ \([^)]+\) \S+ (\d+)/);
1216665
1217037
  return match3 ? parseInt(match3[1], 10) : null;
1216666
1217038
  } catch {
@@ -1216675,7 +1217047,7 @@ function findFulcrumInstances() {
1216675
1217047
  for (const pidStr of procDirs) {
1216676
1217048
  const pid = parseInt(pidStr, 10);
1216677
1217049
  try {
1216678
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8").replace(/\0/g, " ");
1217050
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8").replace(/\0/g, " ");
1216679
1217051
  const env = getProcessEnv(pid);
1216680
1217052
  const parentPid = getParentPid(pid);
1216681
1217053
  const cmdParts = cmdline.trim().split(/\s+/);
@@ -1216791,7 +1217163,7 @@ async function getClaudeOAuthToken() {
1216791
1217163
  const primaryPath = join23(homedir7(), ".claude", ".credentials.json");
1216792
1217164
  try {
1216793
1217165
  if (existsSync23(primaryPath)) {
1216794
- const content = readFileSync13(primaryPath, "utf-8");
1217166
+ const content = readFileSync14(primaryPath, "utf-8");
1216795
1217167
  const config = JSON.parse(content);
1216796
1217168
  if (config.claudeAiOauth && typeof config.claudeAiOauth === "object") {
1216797
1217169
  const token = config.claudeAiOauth.accessToken;
@@ -1218385,7 +1218757,7 @@ init_logger3();
1218385
1218757
  import { execSync as execSync10 } from "child_process";
1218386
1218758
  import { homedir as homedir10, platform as platform2 } from "os";
1218387
1218759
  import { join as join33 } from "path";
1218388
- import { existsSync as existsSync27, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11, readFileSync as readFileSync15, unlinkSync as unlinkSync8 } from "fs";
1218760
+ import { existsSync as existsSync27, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync8 } from "fs";
1218389
1218761
  var USER_UNIT_DIR = join33(homedir10(), ".config/systemd/user");
1218390
1218762
  var systemdAvailable = null;
1218391
1218763
  function isSystemdAvailable() {
@@ -1218613,11 +1218985,11 @@ function getTimer(name, scope) {
1218613
1218985
  let workingDirectory = null;
1218614
1218986
  if (scope === "user" && unitPath) {
1218615
1218987
  try {
1218616
- timerContent = readFileSync15(unitPath, "utf-8");
1218988
+ timerContent = readFileSync16(unitPath, "utf-8");
1218617
1218989
  } catch {}
1218618
1218990
  const servicePath = unitPath.replace(".timer", ".service");
1218619
1218991
  try {
1218620
- serviceContent = readFileSync15(servicePath, "utf-8");
1218992
+ serviceContent = readFileSync16(servicePath, "utf-8");
1218621
1218993
  const execMatch = serviceContent.match(/^ExecStart=(.+)$/m);
1218622
1218994
  if (execMatch) {
1218623
1218995
  command = execMatch[1];
@@ -1218822,8 +1219194,8 @@ function updateTimer(name, updates) {
1218822
1219194
  if (!existsSync27(timerPath)) {
1218823
1219195
  throw new Error(`Timer ${timerName} not found`);
1218824
1219196
  }
1218825
- let timerContent = readFileSync15(timerPath, "utf-8");
1218826
- let serviceContent = existsSync27(servicePath) ? readFileSync15(servicePath, "utf-8") : "";
1219197
+ let timerContent = readFileSync16(timerPath, "utf-8");
1219198
+ let serviceContent = existsSync27(servicePath) ? readFileSync16(servicePath, "utf-8") : "";
1218827
1219199
  if (updates.description !== undefined) {
1218828
1219200
  timerContent = timerContent.replace(/^Description=.*$/m, `Description=${updates.description}`);
1218829
1219201
  serviceContent = serviceContent.replace(/^Description=.*$/m, `Description=${updates.description}`);
@@ -1219797,7 +1220169,7 @@ var ALLOWED_MIME_TYPES2 = [
1219797
1220169
  "application/gzip",
1219798
1220170
  "application/x-tar"
1219799
1220171
  ];
1219800
- var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
1220172
+ var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
1219801
1220173
  function sanitizeFilename2(filename) {
1219802
1220174
  return filename.replace(/[^a-zA-Z0-9._-]/g, "_");
1219803
1220175
  }
@@ -1220784,8 +1221156,8 @@ app19.post("/:id/attachments", async (c) => {
1220784
1221156
  if (!ALLOWED_MIME_TYPES2.includes(file.type)) {
1220785
1221157
  return c.json({ error: `File type not allowed: ${file.type}` }, 400);
1220786
1221158
  }
1220787
- if (file.size > MAX_FILE_SIZE2) {
1220788
- return c.json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE2 / 1024 / 1024}MB` }, 400);
1221159
+ if (file.size > MAX_FILE_SIZE3) {
1221160
+ return c.json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE3 / 1024 / 1024}MB` }, 400);
1220789
1221161
  }
1220790
1221162
  const uploadDir = getProjectUploadsDir(projectId);
1220791
1221163
  if (!fs17.existsSync(uploadDir)) {
@@ -1251838,6 +1252210,27 @@ var toolRegistry = [
1251838
1252210
  keywords: ["memory", "store", "save", "remember", "knowledge", "persist", "fact"],
1251839
1252211
  defer_loading: false
1251840
1252212
  },
1252213
+ {
1252214
+ name: "memory_search",
1252215
+ description: "Search persistent memories using full-text search (FTS5)",
1252216
+ category: "memory",
1252217
+ keywords: ["memory", "search", "find", "query", "fts", "recall", "knowledge"],
1252218
+ defer_loading: false
1252219
+ },
1252220
+ {
1252221
+ name: "memory_list",
1252222
+ description: "List persistent memories with optional tag filter",
1252223
+ category: "memory",
1252224
+ keywords: ["memory", "list", "browse", "tags", "filter", "all"],
1252225
+ defer_loading: false
1252226
+ },
1252227
+ {
1252228
+ name: "memory_delete",
1252229
+ description: "Delete a persistent memory by ID",
1252230
+ category: "memory",
1252231
+ keywords: ["memory", "delete", "remove", "clean", "resolved", "outdated"],
1252232
+ defer_loading: false
1252233
+ },
1251841
1252234
  {
1251842
1252235
  name: "memory_file_read",
1251843
1252236
  description: "Read the master memory file (MEMORY.md)",
@@ -1251854,9 +1252247,9 @@ var toolRegistry = [
1251854
1252247
  },
1251855
1252248
  {
1251856
1252249
  name: "search",
1251857
- description: "Search across all Fulcrum entities (tasks, projects, messages, events, memories) using full-text search",
1252250
+ description: "Search across all Fulcrum entities (tasks, projects, messages, events, memories, gmail) using full-text search",
1251858
1252251
  category: "core",
1251859
- keywords: ["search", "find", "query", "fts", "full-text", "task", "project", "message", "event", "memory", "recall", "knowledge"],
1252252
+ keywords: ["search", "find", "query", "fts", "full-text", "task", "project", "message", "event", "memory", "recall", "knowledge", "gmail", "email", "google"],
1251860
1252253
  defer_loading: false
1251861
1252254
  },
1251862
1252255
  {
@@ -1251938,7 +1252331,7 @@ var registerCoreTools = (server2, _client) => {
1251938
1252331
  };
1251939
1252332
 
1251940
1252333
  // cli/src/mcp/tools/tasks.ts
1251941
- import { basename as basename3 } from "path";
1252334
+ import { basename as basename4 } from "path";
1251942
1252335
 
1251943
1252336
  // shared/date-utils.ts
1251944
1252337
  function getTodayInTimezone(timezone) {
@@ -1251959,7 +1252352,7 @@ function getTodayInTimezone(timezone) {
1251959
1252352
  }
1251960
1252353
 
1251961
1252354
  // cli/src/mcp/tools/tasks.ts
1251962
- var registerTaskTools = (server2, client3) => {
1252355
+ function registerListTasks(server2, client3) {
1251963
1252356
  server2.tool("list_tasks", "List all Fulcrum tasks with flexible filtering. Supports text search across title/tags/project, multi-tag filtering (OR logic), multi-status filtering, date range, and overdue detection.", {
1251964
1252357
  status: exports_external.optional(TaskStatusSchema2).describe("Filter by single task status (use statuses for multiple)"),
1251965
1252358
  statuses: exports_external.optional(exports_external.array(TaskStatusSchema2)).describe("Filter by multiple statuses (OR logic)"),
@@ -1252051,20 +1252444,8 @@ var registerTaskTools = (server2, client3) => {
1252051
1252444
  return handleToolError(err);
1252052
1252445
  }
1252053
1252446
  });
1252054
- server2.tool("get_task", "Get details of a specific task by ID, including dependencies and attachments", {
1252055
- id: exports_external.string().describe("Task ID (UUID)")
1252056
- }, async ({ id }) => {
1252057
- try {
1252058
- const [task, dependencies, attachments] = await Promise.all([
1252059
- client3.getTask(id),
1252060
- client3.getTaskDependencies(id),
1252061
- client3.listTaskAttachments(id)
1252062
- ]);
1252063
- return formatSuccess({ ...task, dependencies, attachments });
1252064
- } catch (err) {
1252065
- return handleToolError(err);
1252066
- }
1252067
- });
1252447
+ }
1252448
+ function registerCreateTask(server2, client3) {
1252068
1252449
  server2.tool("create_task", "Create a new task. For worktree tasks, provide repoPath to create a git worktree. For non-worktree tasks, omit repoPath. When tags are provided, returns all existing tags for reference.", {
1252069
1252450
  title: exports_external.string().describe("Task title"),
1252070
1252451
  repoPath: exports_external.optional(exports_external.string()).describe("Absolute path to the git repository (optional for non-worktree tasks)"),
@@ -1252089,7 +1252470,7 @@ var registerTaskTools = (server2, client3) => {
1252089
1252470
  dueDate
1252090
1252471
  }) => {
1252091
1252472
  try {
1252092
- const repoName = repoPath ? basename3(repoPath) : null;
1252473
+ const repoName = repoPath ? basename4(repoPath) : null;
1252093
1252474
  const effectiveBaseBranch = baseBranch ?? "main";
1252094
1252475
  const task = await client3.createTask({
1252095
1252476
  title,
@@ -1252125,6 +1252506,8 @@ var registerTaskTools = (server2, client3) => {
1252125
1252506
  return handleToolError(err);
1252126
1252507
  }
1252127
1252508
  });
1252509
+ }
1252510
+ function registerUpdateTask(server2, client3) {
1252128
1252511
  server2.tool("update_task", "Update task metadata (title or description)", {
1252129
1252512
  id: exports_external.string().describe("Task ID"),
1252130
1252513
  title: exports_external.optional(exports_external.string()).describe("New title"),
@@ -1252142,6 +1252525,82 @@ var registerTaskTools = (server2, client3) => {
1252142
1252525
  return handleToolError(err);
1252143
1252526
  }
1252144
1252527
  });
1252528
+ }
1252529
+ function registerAddTaskLink(server2, client3) {
1252530
+ server2.tool("add_task_link", "Add a URL link to a task (for documentation, related PRs, design files, etc.)", {
1252531
+ taskId: exports_external.string().describe("Task ID"),
1252532
+ url: exports_external.string().url().describe("URL to add"),
1252533
+ label: exports_external.optional(exports_external.string()).describe("Display label (auto-detected if not provided)")
1252534
+ }, async ({ taskId, url: url2, label }) => {
1252535
+ try {
1252536
+ const link = await client3.addTaskLink(taskId, url2, label);
1252537
+ return formatSuccess(link);
1252538
+ } catch (err) {
1252539
+ return handleToolError(err);
1252540
+ }
1252541
+ });
1252542
+ }
1252543
+ function registerAddTaskTag(server2, client3) {
1252544
+ server2.tool("add_task_tag", "Add a tag to a task for categorization. Returns similar existing tags to help catch typos.", {
1252545
+ taskId: exports_external.string().describe("Task ID"),
1252546
+ tag: exports_external.string().describe("Tag to add")
1252547
+ }, async ({ taskId, tag }) => {
1252548
+ try {
1252549
+ const result = await client3.addTaskTag(taskId, tag);
1252550
+ const allTasks = await client3.listTasks();
1252551
+ const existingTags = new Set;
1252552
+ for (const t of allTasks) {
1252553
+ if (t.tags) {
1252554
+ for (const tg of t.tags) {
1252555
+ existingTags.add(tg);
1252556
+ }
1252557
+ }
1252558
+ }
1252559
+ const tagLower = tag.toLowerCase();
1252560
+ const similarTags = Array.from(existingTags).filter((tg) => tg !== tag && (tg.toLowerCase().includes(tagLower) || tagLower.includes(tg.toLowerCase())));
1252561
+ return formatSuccess({
1252562
+ ...result,
1252563
+ similarTags: similarTags.length > 0 ? similarTags : undefined
1252564
+ });
1252565
+ } catch (err) {
1252566
+ return handleToolError(err);
1252567
+ }
1252568
+ });
1252569
+ }
1252570
+ function registerSetTaskDueDate(server2, client3) {
1252571
+ server2.tool("set_task_due_date", "Set or clear the due date for a task", {
1252572
+ taskId: exports_external.string().describe("Task ID"),
1252573
+ dueDate: exports_external.nullable(exports_external.string()).describe("Due date in YYYY-MM-DD format, or null to clear")
1252574
+ }, async ({ taskId, dueDate }) => {
1252575
+ try {
1252576
+ const result = await client3.setTaskDueDate(taskId, dueDate);
1252577
+ return formatSuccess(result);
1252578
+ } catch (err) {
1252579
+ return handleToolError(err);
1252580
+ }
1252581
+ });
1252582
+ }
1252583
+ var registerTaskTools = (server2, client3) => {
1252584
+ registerListTasks(server2, client3);
1252585
+ registerCreateTask(server2, client3);
1252586
+ registerUpdateTask(server2, client3);
1252587
+ registerAddTaskLink(server2, client3);
1252588
+ registerAddTaskTag(server2, client3);
1252589
+ registerSetTaskDueDate(server2, client3);
1252590
+ server2.tool("get_task", "Get details of a specific task by ID, including dependencies and attachments", {
1252591
+ id: exports_external.string().describe("Task ID (UUID)")
1252592
+ }, async ({ id }) => {
1252593
+ try {
1252594
+ const [task, dependencies, attachments] = await Promise.all([
1252595
+ client3.getTask(id),
1252596
+ client3.getTaskDependencies(id),
1252597
+ client3.listTaskAttachments(id)
1252598
+ ]);
1252599
+ return formatSuccess({ ...task, dependencies, attachments });
1252600
+ } catch (err) {
1252601
+ return handleToolError(err);
1252602
+ }
1252603
+ });
1252145
1252604
  server2.tool("delete_task", "Delete a task and optionally its linked git worktree", {
1252146
1252605
  id: exports_external.string().describe("Task ID"),
1252147
1252606
  deleteWorktree: exports_external.optional(exports_external.boolean()).describe("Also delete the linked git worktree (default: false)")
@@ -1252166,18 +1252625,6 @@ var registerTaskTools = (server2, client3) => {
1252166
1252625
  return handleToolError(err);
1252167
1252626
  }
1252168
1252627
  });
1252169
- server2.tool("add_task_link", "Add a URL link to a task (for documentation, related PRs, design files, etc.)", {
1252170
- taskId: exports_external.string().describe("Task ID"),
1252171
- url: exports_external.string().url().describe("URL to add"),
1252172
- label: exports_external.optional(exports_external.string()).describe("Display label (auto-detected if not provided)")
1252173
- }, async ({ taskId, url: url2, label }) => {
1252174
- try {
1252175
- const link = await client3.addTaskLink(taskId, url2, label);
1252176
- return formatSuccess(link);
1252177
- } catch (err) {
1252178
- return handleToolError(err);
1252179
- }
1252180
- });
1252181
1252628
  server2.tool("remove_task_link", "Remove a URL link from a task", {
1252182
1252629
  taskId: exports_external.string().describe("Task ID"),
1252183
1252630
  linkId: exports_external.string().describe("Link ID to remove")
@@ -1252199,31 +1252646,6 @@ var registerTaskTools = (server2, client3) => {
1252199
1252646
  return handleToolError(err);
1252200
1252647
  }
1252201
1252648
  });
1252202
- server2.tool("add_task_tag", "Add a tag to a task for categorization. Returns similar existing tags to help catch typos.", {
1252203
- taskId: exports_external.string().describe("Task ID"),
1252204
- tag: exports_external.string().describe("Tag to add")
1252205
- }, async ({ taskId, tag }) => {
1252206
- try {
1252207
- const result = await client3.addTaskTag(taskId, tag);
1252208
- const allTasks = await client3.listTasks();
1252209
- const existingTags = new Set;
1252210
- for (const t of allTasks) {
1252211
- if (t.tags) {
1252212
- for (const tg of t.tags) {
1252213
- existingTags.add(tg);
1252214
- }
1252215
- }
1252216
- }
1252217
- const tagLower = tag.toLowerCase();
1252218
- const similarTags = Array.from(existingTags).filter((tg) => tg !== tag && (tg.toLowerCase().includes(tagLower) || tagLower.includes(tg.toLowerCase())));
1252219
- return formatSuccess({
1252220
- ...result,
1252221
- similarTags: similarTags.length > 0 ? similarTags : undefined
1252222
- });
1252223
- } catch (err) {
1252224
- return handleToolError(err);
1252225
- }
1252226
- });
1252227
1252649
  server2.tool("remove_task_tag", "Remove a tag from a task", {
1252228
1252650
  taskId: exports_external.string().describe("Task ID"),
1252229
1252651
  tag: exports_external.string().describe("Tag to remove")
@@ -1252235,17 +1252657,6 @@ var registerTaskTools = (server2, client3) => {
1252235
1252657
  return handleToolError(err);
1252236
1252658
  }
1252237
1252659
  });
1252238
- server2.tool("set_task_due_date", "Set or clear the due date for a task", {
1252239
- taskId: exports_external.string().describe("Task ID"),
1252240
- dueDate: exports_external.nullable(exports_external.string()).describe("Due date in YYYY-MM-DD format, or null to clear")
1252241
- }, async ({ taskId, dueDate }) => {
1252242
- try {
1252243
- const result = await client3.setTaskDueDate(taskId, dueDate);
1252244
- return formatSuccess(result);
1252245
- } catch (err) {
1252246
- return handleToolError(err);
1252247
- }
1252248
- });
1252249
1252660
  server2.tool("get_task_dependencies", "Get the dependencies and dependents of a task, and whether it is blocked", {
1252250
1252661
  taskId: exports_external.string().describe("Task ID")
1252251
1252662
  }, async ({ taskId }) => {
@@ -1252404,6 +1252815,14 @@ var registerTaskTools = (server2, client3) => {
1252404
1252815
  }
1252405
1252816
  });
1252406
1252817
  };
1252818
+ var registerTaskObserverTools = (server2, client3) => {
1252819
+ registerListTasks(server2, client3);
1252820
+ registerCreateTask(server2, client3);
1252821
+ registerUpdateTask(server2, client3);
1252822
+ registerAddTaskLink(server2, client3);
1252823
+ registerAddTaskTag(server2, client3);
1252824
+ registerSetTaskDueDate(server2, client3);
1252825
+ };
1252407
1252826
 
1252408
1252827
  // cli/src/mcp/tools/repositories.ts
1252409
1252828
  var registerRepositoryTools = (server2, client3) => {
@@ -1253293,8 +1253712,9 @@ var registerAssistantTools = (server2, client3) => {
1253293
1253712
  subject: exports_external.optional(exports_external.string()).describe("Email subject (for Gmail channel only)"),
1253294
1253713
  replyToMessageId: exports_external.optional(exports_external.string()).describe("Message ID to reply to (for threading)"),
1253295
1253714
  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."),
1253715
+ filePath: exports_external.optional(exports_external.string()).describe("Absolute path to a local file to upload alongside the message (Slack only). Use for sending images, documents, etc."),
1253296
1253716
  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.")
1253297
- }, async ({ channel, body, subject, replyToMessageId, slack_blocks, googleAccountId }) => {
1253717
+ }, async ({ channel, body, subject, replyToMessageId, slack_blocks, filePath, googleAccountId }) => {
1253298
1253718
  try {
1253299
1253719
  if (channel === "gmail") {
1253300
1253720
  let accountId = googleAccountId;
@@ -1253317,7 +1253737,8 @@ var registerAssistantTools = (server2, client3) => {
1253317
1253737
  body,
1253318
1253738
  subject,
1253319
1253739
  replyToMessageId,
1253320
- slackBlocks: slack_blocks
1253740
+ slackBlocks: slack_blocks,
1253741
+ filePath
1253321
1253742
  });
1253322
1253743
  return formatSuccess(result);
1253323
1253744
  } catch (err) {
@@ -1253588,7 +1254009,7 @@ var MEMORY_SOURCES = [
1253588
1254009
  ];
1253589
1254010
 
1253590
1254011
  // cli/src/mcp/tools/memory.ts
1253591
- var registerMemoryTools = (server2, client3) => {
1254012
+ function registerMemoryStoreTool(server2, client3) {
1253592
1254013
  server2.tool("memory_store", "Store a piece of knowledge in persistent memory. Use this to remember facts, preferences, decisions, patterns, or any information that should persist across conversations.", {
1253593
1254014
  content: exports_external.string().describe("The memory content to store. Be specific and self-contained."),
1253594
1254015
  tags: exports_external.optional(exports_external.array(exports_external.string())).describe('Optional tags for categorization (e.g., ["preference", "architecture", "decision"])'),
@@ -1253601,10 +1254022,61 @@ var registerMemoryTools = (server2, client3) => {
1253601
1254022
  return handleToolError(err);
1253602
1254023
  }
1253603
1254024
  });
1254025
+ }
1254026
+ function registerMemorySearchTool(server2, client3) {
1254027
+ server2.tool("memory_search", 'Search persistent memories using full-text search (FTS5). Supports boolean operators (AND, OR, NOT), phrase matching ("quoted"), and prefix matching (term*).', {
1254028
+ query: exports_external.string().describe('Search query. Supports FTS5 syntax: AND, OR, NOT, "phrases", prefix*'),
1254029
+ tags: exports_external.optional(exports_external.array(exports_external.string())).describe('Filter by tags (e.g., ["actionable", "preference"])'),
1254030
+ limit: exports_external.optional(exports_external.number()).describe("Max results to return (default: 20)")
1254031
+ }, async ({ query, tags: tags2, limit: limit2 }) => {
1254032
+ try {
1254033
+ const result = await client3.searchMemories({ query, tags: tags2, limit: limit2 });
1254034
+ return formatSuccess(result);
1254035
+ } catch (err) {
1254036
+ return handleToolError(err);
1254037
+ }
1254038
+ });
1254039
+ }
1254040
+ function registerMemoryListTool(server2, client3) {
1254041
+ server2.tool("memory_list", "List all persistent memories, optionally filtered by tags. Returns memories sorted by creation date (newest first).", {
1254042
+ tags: exports_external.optional(exports_external.array(exports_external.string())).describe('Filter by tags (e.g., ["actionable"])'),
1254043
+ limit: exports_external.optional(exports_external.number()).describe("Max results to return (default: 50)"),
1254044
+ offset: exports_external.optional(exports_external.number()).describe("Offset for pagination")
1254045
+ }, async ({ tags: tags2, limit: limit2, offset }) => {
1254046
+ try {
1254047
+ const result = await client3.listMemories({ tags: tags2, limit: limit2, offset });
1254048
+ return formatSuccess(result);
1254049
+ } catch (err) {
1254050
+ return handleToolError(err);
1254051
+ }
1254052
+ });
1254053
+ }
1254054
+ function registerMemoryDeleteTool(server2, client3) {
1254055
+ server2.tool("memory_delete", "Delete a persistent memory by ID. Use this to clean up resolved or outdated memories.", {
1254056
+ id: exports_external.string().describe("The ID of the memory to delete")
1254057
+ }, async ({ id }) => {
1254058
+ try {
1254059
+ const result = await client3.deleteMemory(id);
1254060
+ return formatSuccess(result);
1254061
+ } catch (err) {
1254062
+ return handleToolError(err);
1254063
+ }
1254064
+ });
1254065
+ }
1254066
+ var registerMemoryTools = (server2, client3) => {
1254067
+ registerMemoryStoreTool(server2, client3);
1254068
+ registerMemorySearchTool(server2, client3);
1254069
+ registerMemoryListTool(server2, client3);
1254070
+ registerMemoryDeleteTool(server2, client3);
1254071
+ };
1254072
+ var registerMemoryObserverTools = (server2, client3) => {
1254073
+ registerMemoryStoreTool(server2, client3);
1254074
+ registerMemorySearchTool(server2, client3);
1254075
+ registerMemoryListTool(server2, client3);
1253604
1254076
  };
1253605
1254077
 
1253606
1254078
  // cli/src/mcp/tools/memory-file.ts
1253607
- var registerMemoryFileTools = (server2, client3) => {
1254079
+ var registerMemoryFileReadTool = (server2, client3) => {
1253608
1254080
  server2.tool("memory_file_read", "Read the master memory file (MEMORY.md). This file contains persistent knowledge, user preferences, and instructions that are included in every conversation.", {}, async () => {
1253609
1254081
  try {
1253610
1254082
  const result = await client3.readMemoryFile();
@@ -1253613,6 +1254085,9 @@ var registerMemoryFileTools = (server2, client3) => {
1253613
1254085
  return handleToolError(err);
1253614
1254086
  }
1253615
1254087
  });
1254088
+ };
1254089
+ var registerMemoryFileTools = (server2, client3) => {
1254090
+ registerMemoryFileReadTool(server2, client3);
1253616
1254091
  server2.tool("memory_file_update", "Update the master memory file. Provide full content to replace the entire file, or specify a section heading to update just that section. The memory file is included in every conversation, so keep it organized and concise.", {
1253617
1254092
  content: exports_external.string().describe("The content to write. If section is specified, this replaces only that section body."),
1253618
1254093
  section: exports_external.optional(exports_external.string()).describe('Optional markdown heading (e.g., "## Preferences") to update a specific section. If omitted, replaces the entire file.')
@@ -1253645,9 +1254120,9 @@ var registerMessagingTools = (server2, client3) => {
1253645
1254120
  };
1253646
1254121
 
1253647
1254122
  // cli/src/mcp/tools/search.ts
1253648
- var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
1254123
+ var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations", "gmail"]);
1253649
1254124
  var registerSearchTools = (server2, client3) => {
1253650
- 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.', {
1254125
+ 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).', {
1253651
1254126
  query: exports_external.string().describe('FTS5 search query. Supports: AND, OR, NOT operators, "quoted phrases", prefix* matching. Example: "kubernetes deployment" OR k8s'),
1253652
1254127
  entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories, conversations"),
1253653
1254128
  limit: exports_external.optional(exports_external.number()).describe("Maximum results per entity type (default: 10)"),
@@ -1253660,8 +1254135,12 @@ var registerSearchTools = (server2, client3) => {
1253660
1254135
  memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags"),
1253661
1254136
  conversationRole: exports_external.optional(exports_external.string()).describe('Filter conversations by role (e.g., "user", "assistant")'),
1253662
1254137
  conversationProvider: exports_external.optional(exports_external.string()).describe('Filter conversations by provider (e.g., "claude", "opencode")'),
1253663
- conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID")
1253664
- }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId }) => {
1254138
+ conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID"),
1254139
+ gmailFrom: exports_external.optional(exports_external.string()).describe('Filter Gmail results by sender (e.g., "user@example.com")'),
1254140
+ gmailTo: exports_external.optional(exports_external.string()).describe('Filter Gmail results by recipient (e.g., "user@example.com")'),
1254141
+ gmailAfter: exports_external.optional(exports_external.string()).describe("Filter Gmail results after this date (YYYY/MM/DD format)"),
1254142
+ gmailBefore: exports_external.optional(exports_external.string()).describe("Filter Gmail results before this date (YYYY/MM/DD format)")
1254143
+ }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId, gmailFrom, gmailTo, gmailAfter, gmailBefore }) => {
1253665
1254144
  try {
1253666
1254145
  const results = await client3.search({
1253667
1254146
  query,
@@ -1253676,7 +1254155,11 @@ var registerSearchTools = (server2, client3) => {
1253676
1254155
  memoryTags,
1253677
1254156
  conversationRole,
1253678
1254157
  conversationProvider,
1253679
- conversationProjectId
1254158
+ conversationProjectId,
1254159
+ gmailFrom,
1254160
+ gmailTo,
1254161
+ gmailAfter,
1254162
+ gmailBefore
1253680
1254163
  });
1253681
1254164
  return formatSuccess(results);
1253682
1254165
  } catch (err) {
@@ -1253707,7 +1254190,7 @@ function registerTools(server2, client3) {
1253707
1254190
  registerSearchTools(server2, client3);
1253708
1254191
  }
1253709
1254192
  // cli/src/utils/server.ts
1253710
- import { existsSync as existsSync31, readFileSync as readFileSync17, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, cpSync } from "fs";
1254193
+ import { existsSync as existsSync31, readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, cpSync } from "fs";
1253711
1254194
  import { join as join37 } from "path";
1253712
1254195
  import { homedir as homedir13 } from "os";
1253713
1254196
  var DEFAULT_PORT = 7777;
@@ -1253731,7 +1254214,7 @@ function expandPath2(p3) {
1253731
1254214
  function readSettingsFile(path14) {
1253732
1254215
  try {
1253733
1254216
  if (existsSync31(path14)) {
1253734
- const content = readFileSync17(path14, "utf-8");
1254217
+ const content = readFileSync18(path14, "utf-8");
1253735
1254218
  return JSON.parse(content);
1253736
1254219
  }
1253737
1254220
  } catch {}
@@ -1253771,8 +1254254,8 @@ function discoverServerUrl(urlOverride, portOverride) {
1253771
1254254
  }
1253772
1254255
 
1253773
1254256
  // cli/src/client.ts
1253774
- import { readFileSync as readFileSync18 } from "fs";
1253775
- import { basename as basename4 } from "path";
1254257
+ import { readFileSync as readFileSync19 } from "fs";
1254258
+ import { basename as basename5 } from "path";
1253776
1254259
 
1253777
1254260
  class FulcrumClient {
1253778
1254261
  baseUrl;
@@ -1254034,8 +1254517,8 @@ class FulcrumClient {
1254034
1254517
  return this.fetch(`/api/tasks/${taskId}/attachments`);
1254035
1254518
  }
1254036
1254519
  async uploadTaskAttachment(taskId, filePath) {
1254037
- const fileContent = readFileSync18(filePath);
1254038
- const filename = basename4(filePath);
1254520
+ const fileContent = readFileSync19(filePath);
1254521
+ const filename = basename5(filePath);
1254039
1254522
  const formData = new FormData;
1254040
1254523
  const blob3 = new Blob([fileContent]);
1254041
1254524
  formData.append("file", blob3, filename);
@@ -1254121,8 +1254604,8 @@ class FulcrumClient {
1254121
1254604
  return this.fetch(`/api/projects/${projectId}/attachments`);
1254122
1254605
  }
1254123
1254606
  async uploadProjectAttachment(projectId, filePath) {
1254124
- const fileContent = readFileSync18(filePath);
1254125
- const filename = basename4(filePath);
1254607
+ const fileContent = readFileSync19(filePath);
1254608
+ const filename = basename5(filePath);
1254126
1254609
  const formData = new FormData;
1254127
1254610
  const blob3 = new Blob([fileContent]);
1254128
1254611
  formData.append("file", blob3, filename);
@@ -1254516,6 +1254999,14 @@ class FulcrumClient {
1254516
1254999
  params.set("conversationProvider", input.conversationProvider);
1254517
1255000
  if (input.conversationProjectId)
1254518
1255001
  params.set("conversationProjectId", input.conversationProjectId);
1255002
+ if (input.gmailFrom)
1255003
+ params.set("gmailFrom", input.gmailFrom);
1255004
+ if (input.gmailTo)
1255005
+ params.set("gmailTo", input.gmailTo);
1255006
+ if (input.gmailAfter)
1255007
+ params.set("gmailAfter", input.gmailAfter);
1255008
+ if (input.gmailBefore)
1255009
+ params.set("gmailBefore", input.gmailBefore);
1254519
1255010
  return this.fetch(`/api/search?${params.toString()}`);
1254520
1255011
  }
1254521
1255012
  }
@@ -1254531,7 +1255022,7 @@ mcpRoutes.all("/", async (c) => {
1254531
1255022
  });
1254532
1255023
  const server2 = new McpServer({
1254533
1255024
  name: "fulcrum",
1254534
- version: "3.2.1"
1255025
+ version: "3.4.0"
1254535
1255026
  });
1254536
1255027
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254537
1255028
  registerTools(server2, client3);
@@ -1254554,8 +1255045,10 @@ mcpObserverRoutes.all("/", async (c) => {
1254554
1255045
  version: "2.12.0"
1254555
1255046
  });
1254556
1255047
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254557
- registerMemoryTools(server2, client3);
1254558
- registerMemoryFileTools(server2, client3);
1255048
+ registerMemoryObserverTools(server2, client3);
1255049
+ registerMemoryFileReadTool(server2, client3);
1255050
+ registerTaskObserverTools(server2, client3);
1255051
+ registerNotificationTools(server2, client3);
1254559
1255052
  await server2.connect(transport);
1254560
1255053
  return transport.handleRequest(c.req.raw);
1254561
1255054
  });
@@ -1255616,7 +1256109,8 @@ app23.post("/send", async (c) => {
1255616
1256109
  const result = await sendMessageToChannel(body.channel, body.body, {
1255617
1256110
  subject: body.subject,
1255618
1256111
  replyToMessageId: body.replyToMessageId,
1255619
- slackBlocks: body.slackBlocks
1256112
+ slackBlocks: body.slackBlocks,
1256113
+ filePath: body.filePath
1255620
1256114
  });
1255621
1256115
  if (result.success) {
1255622
1256116
  log2.messaging.info("Message sent via API", {
@@ -1255855,7 +1256349,7 @@ init_logger3();
1255855
1256349
  init_ical_helpers();
1255856
1256350
  init_caldav_account_manager();
1255857
1256351
  init_google_calendar_manager();
1255858
- var logger9 = createLogger("CalDAV");
1256352
+ var logger10 = createLogger("CalDAV");
1255859
1256353
  function migrateFromSettings() {
1255860
1256354
  const settings = getSettings();
1255861
1256355
  const caldavSettings = settings.caldav;
@@ -1255871,7 +1256365,7 @@ function migrateFromSettings() {
1255871
1256365
  return;
1255872
1256366
  return;
1255873
1256367
  }
1255874
- logger9.info("Migrating CalDAV credentials from settings.json to database", {
1256368
+ logger10.info("Migrating CalDAV credentials from settings.json to database", {
1255875
1256369
  authType: caldavSettings.authType,
1255876
1256370
  orphanedCalendars: orphanedCalendars.length
1255877
1256371
  });
@@ -1255896,28 +1256390,28 @@ function migrateFromSettings() {
1255896
1256390
  for (const cal of orphanedCalendars) {
1255897
1256391
  db2.update(caldavCalendars).set({ accountId, updatedAt: now }).where(eq(caldavCalendars.id, cal.id)).run();
1255898
1256392
  }
1255899
- logger9.info("Migration complete", { accountId, migratedCalendars: orphanedCalendars.length });
1256393
+ logger10.info("Migration complete", { accountId, migratedCalendars: orphanedCalendars.length });
1255900
1256394
  }
1255901
1256395
  async function startCaldavSync() {
1255902
1256396
  const settings = getSettings();
1255903
1256397
  if (!settings.caldav?.enabled) {
1255904
- logger9.info("CalDAV disabled, skipping sync start");
1256398
+ logger10.info("CalDAV disabled, skipping sync start");
1255905
1256399
  return;
1255906
1256400
  }
1255907
1256401
  migrateFromSettings();
1255908
1256402
  await accountManager.startAll();
1255909
1256403
  await accountManager.syncAll().catch((err) => {
1255910
- logger9.error("Initial sync failed", { error: err instanceof Error ? err.message : String(err) });
1256404
+ logger10.error("Initial sync failed", { error: err instanceof Error ? err.message : String(err) });
1255911
1256405
  });
1255912
1256406
  await executeCopyRules().catch((err) => {
1255913
- logger9.error("Copy rules execution failed after sync", {
1256407
+ logger10.error("Copy rules execution failed after sync", {
1255914
1256408
  error: err instanceof Error ? err.message : String(err)
1255915
1256409
  });
1255916
1256410
  });
1255917
1256411
  }
1255918
1256412
  function stopCaldavSync() {
1255919
1256413
  accountManager.stopAll();
1255920
- logger9.info("CalDAV sync stopped");
1256414
+ logger10.info("CalDAV sync stopped");
1255921
1256415
  }
1255922
1256416
  function getCaldavStatus() {
1255923
1256417
  const accounts = accountManager.getStatus();
@@ -1255960,7 +1256454,7 @@ async function createAccount2(input) {
1255960
1256454
  updatedAt: now
1255961
1256455
  };
1255962
1256456
  db2.insert(caldavAccounts).values(account2).run();
1255963
- logger9.info("Created CalDAV account", { id, name: input.name });
1256457
+ logger10.info("Created CalDAV account", { id, name: input.name });
1255964
1256458
  return account2;
1255965
1256459
  }
1255966
1256460
  async function updateAccount(id, updates) {
@@ -1255988,7 +1256482,7 @@ async function deleteAccount(id) {
1255988
1256482
  }
1255989
1256483
  db2.delete(caldavCalendars).where(eq(caldavCalendars.accountId, id)).run();
1255990
1256484
  db2.delete(caldavAccounts).where(eq(caldavAccounts.id, id)).run();
1255991
- logger9.info("Deleted CalDAV account", { id });
1256485
+ logger10.info("Deleted CalDAV account", { id });
1255992
1256486
  }
1255993
1256487
  async function enableAccount(id) {
1255994
1256488
  db2.update(caldavAccounts).set({ enabled: true, updatedAt: new Date().toISOString() }).where(eq(caldavAccounts.id, id)).run();
@@ -1256024,7 +1256518,7 @@ async function testAccountConnection(config3) {
1256024
1256518
  async function syncAccount(id) {
1256025
1256519
  await accountManager.syncAccount(id);
1256026
1256520
  await executeCopyRules().catch((err) => {
1256027
- logger9.error("Copy rules failed after account sync", {
1256521
+ logger10.error("Copy rules failed after account sync", {
1256028
1256522
  error: err instanceof Error ? err.message : String(err)
1256029
1256523
  });
1256030
1256524
  });
@@ -1256130,7 +1256624,7 @@ function listCalendars(accountId) {
1256130
1256624
  async function syncCalendars2() {
1256131
1256625
  await accountManager.syncAll();
1256132
1256626
  await executeCopyRules().catch((err) => {
1256133
- logger9.error("Copy rules failed after sync", {
1256627
+ logger10.error("Copy rules failed after sync", {
1256134
1256628
  error: err instanceof Error ? err.message : String(err)
1256135
1256629
  });
1256136
1256630
  });
@@ -1256221,7 +1256715,7 @@ async function createEvent(input) {
1256221
1256715
  updatedAt: now
1256222
1256716
  };
1256223
1256717
  db2.insert(caldavEvents).values(event).run();
1256224
- logger9.info("Created CalDAV event", { id, summary: input.summary });
1256718
+ logger10.info("Created CalDAV event", { id, summary: input.summary });
1256225
1256719
  return event;
1256226
1256720
  }
1256227
1256721
  async function updateEvent(id, updates) {
@@ -1256271,7 +1256765,7 @@ async function updateEvent(id, updates) {
1256271
1256765
  rawIcal: updatedIcal,
1256272
1256766
  updatedAt: now
1256273
1256767
  }).where(eq(caldavEvents.id, id)).run();
1256274
- logger9.info("Updated CalDAV event", { id, summary: updates.summary ?? event.summary });
1256768
+ logger10.info("Updated CalDAV event", { id, summary: updates.summary ?? event.summary });
1256275
1256769
  return db2.select().from(caldavEvents).where(eq(caldavEvents.id, id)).get();
1256276
1256770
  }
1256277
1256771
  async function deleteEvent(id) {
@@ -1256295,7 +1256789,7 @@ async function deleteEvent(id) {
1256295
1256789
  }
1256296
1256790
  });
1256297
1256791
  db2.delete(caldavEvents).where(eq(caldavEvents.id, id)).run();
1256298
- logger9.info("Deleted CalDAV event", { id, summary: event.summary });
1256792
+ logger10.info("Deleted CalDAV event", { id, summary: event.summary });
1256299
1256793
  }
1256300
1256794
  function listCopyRules() {
1256301
1256795
  return db2.select().from(caldavCopyRules).all();
@@ -1256314,7 +1256808,7 @@ function createCopyRule(input) {
1256314
1256808
  updatedAt: now
1256315
1256809
  };
1256316
1256810
  db2.insert(caldavCopyRules).values(rule).run();
1256317
- logger9.info("Created copy rule", { id, source: input.sourceCalendarId, dest: input.destCalendarId });
1256811
+ logger10.info("Created copy rule", { id, source: input.sourceCalendarId, dest: input.destCalendarId });
1256318
1256812
  return rule;
1256319
1256813
  }
1256320
1256814
  function updateCopyRule(id, updates) {
@@ -1256327,7 +1256821,7 @@ function updateCopyRule(id, updates) {
1256327
1256821
  function deleteCopyRule(id) {
1256328
1256822
  db2.delete(caldavCopiedEvents).where(eq(caldavCopiedEvents.ruleId, id)).run();
1256329
1256823
  db2.delete(caldavCopyRules).where(eq(caldavCopyRules.id, id)).run();
1256330
- logger9.info("Deleted copy rule", { id });
1256824
+ logger10.info("Deleted copy rule", { id });
1256331
1256825
  }
1256332
1256826
  async function executeCopyRule(ruleId) {
1256333
1256827
  const { executeSingleRule: executeSingleRule2 } = await Promise.resolve().then(() => (init_copy_engine(), exports_copy_engine));
@@ -1256770,13 +1257264,13 @@ init_db2();
1256770
1257264
  init_settings();
1256771
1257265
  init_google_oauth();
1256772
1257266
  init_logger3();
1256773
- var logger10 = createLogger("GoogleOAuth:Routes");
1257267
+ var logger11 = createLogger("GoogleOAuth:Routes");
1256774
1257268
  var app25 = new Hono2;
1256775
1257269
  app25.get("/authorize", (c) => {
1256776
1257270
  const accountName = c.req.query("accountName");
1256777
1257271
  const accountId = c.req.query("accountId");
1256778
1257272
  const clientOrigin = c.req.query("origin");
1256779
- logger10.info("OAuth authorize request", {
1257273
+ logger11.info("OAuth authorize request", {
1256780
1257274
  accountName,
1256781
1257275
  accountId,
1256782
1257276
  clientOrigin,
@@ -1256800,7 +1257294,7 @@ app25.get("/authorize", (c) => {
1256800
1257294
  accountName: accountName || "Google Account",
1256801
1257295
  redirectUri
1256802
1257296
  });
1256803
- logger10.info("OAuth authorize: generating auth URL", {
1257297
+ logger11.info("OAuth authorize: generating auth URL", {
1256804
1257298
  baseUrl,
1256805
1257299
  redirectUri,
1256806
1257300
  stateJson: state,
@@ -1256809,18 +1257303,18 @@ app25.get("/authorize", (c) => {
1256809
1257303
  const authUrl = generateAuthUrl(client4, redirectUri, state);
1256810
1257304
  try {
1256811
1257305
  const parsed = new URL(authUrl);
1256812
- logger10.info("OAuth authorize: generated auth URL", {
1257306
+ logger11.info("OAuth authorize: generated auth URL", {
1256813
1257307
  authUrl,
1256814
1257308
  redirect_uri_in_url: parsed.searchParams.get("redirect_uri"),
1256815
1257309
  client_id_in_url: parsed.searchParams.get("client_id"),
1256816
1257310
  scope_in_url: parsed.searchParams.get("scope")
1256817
1257311
  });
1256818
1257312
  } catch {
1256819
- logger10.info("OAuth authorize: generated auth URL (unparseable)", { authUrl });
1257313
+ logger11.info("OAuth authorize: generated auth URL (unparseable)", { authUrl });
1256820
1257314
  }
1256821
1257315
  return c.json({ authUrl });
1256822
1257316
  } catch (err) {
1256823
- logger10.error("OAuth authorize failed", {
1257317
+ logger11.error("OAuth authorize failed", {
1256824
1257318
  error: err instanceof Error ? err.message : String(err)
1256825
1257319
  });
1256826
1257320
  return c.json({ error: err instanceof Error ? err.message : "Failed to generate auth URL" }, 400);
@@ -1256831,7 +1257325,7 @@ app25.get("/callback", async (c) => {
1256831
1257325
  const error46 = c.req.query("error");
1256832
1257326
  const stateParam = c.req.query("state");
1256833
1257327
  const fullUrl = c.req.url;
1256834
- logger10.info("OAuth callback received", {
1257328
+ logger11.info("OAuth callback received", {
1256835
1257329
  hasCode: !!code,
1256836
1257330
  codePrefix: code?.slice(0, 20) + "...",
1256837
1257331
  error: error46,
@@ -1256842,11 +1257336,11 @@ app25.get("/callback", async (c) => {
1256842
1257336
  refererHeader: c.req.header("referer")
1256843
1257337
  });
1256844
1257338
  if (error46) {
1256845
- logger10.warn("OAuth callback: Google returned error", { error: error46 });
1257339
+ logger11.warn("OAuth callback: Google returned error", { error: error46 });
1256846
1257340
  return c.html(`<html><body><h2>Authorization Failed</h2><p>${error46}</p><script>setTimeout(()=>window.close(),3000)</script></body></html>`);
1256847
1257341
  }
1256848
1257342
  if (!code) {
1256849
- logger10.warn("OAuth callback: missing authorization code");
1257343
+ logger11.warn("OAuth callback: missing authorization code");
1256850
1257344
  return c.html("<html><body><h2>Missing authorization code</h2><script>setTimeout(()=>window.close(),3000)</script></body></html>", 400);
1256851
1257345
  }
1256852
1257346
  let state = {
@@ -1256858,9 +1257352,9 @@ app25.get("/callback", async (c) => {
1256858
1257352
  state = JSON.parse(stateParam);
1256859
1257353
  }
1256860
1257354
  } catch {
1256861
- logger10.warn("OAuth callback: failed to parse state param", { stateParam });
1257355
+ logger11.warn("OAuth callback: failed to parse state param", { stateParam });
1256862
1257356
  }
1256863
- logger10.info("OAuth callback: parsed state", {
1257357
+ logger11.info("OAuth callback: parsed state", {
1256864
1257358
  accountId: state.accountId,
1256865
1257359
  accountName: state.accountName,
1256866
1257360
  redirectUri: state.redirectUri
@@ -1256871,7 +1257365,7 @@ app25.get("/callback", async (c) => {
1256871
1257365
  throw new Error("Missing redirect URI in state \u2014 cannot match token exchange");
1256872
1257366
  }
1256873
1257367
  const client4 = createOAuth2Client();
1256874
- logger10.info("OAuth callback: exchanging code for tokens", {
1257368
+ logger11.info("OAuth callback: exchanging code for tokens", {
1256875
1257369
  redirectUri,
1256876
1257370
  clientId: client4._clientId,
1256877
1257371
  codePrefix: code.slice(0, 20) + "..."
@@ -1256893,7 +1257387,7 @@ app25.get("/callback", async (c) => {
1256893
1257387
  email: email4 ?? undefined,
1256894
1257388
  updatedAt: now
1256895
1257389
  }).where(eq(googleAccounts.id, state.accountId)).run();
1256896
- logger10.info("Re-authorized Google account", {
1257390
+ logger11.info("Re-authorized Google account", {
1256897
1257391
  accountId: state.accountId,
1256898
1257392
  email: email4
1256899
1257393
  });
@@ -1256913,7 +1257407,7 @@ app25.get("/callback", async (c) => {
1256913
1257407
  createdAt: now,
1256914
1257408
  updatedAt: now
1256915
1257409
  }).run();
1256916
- logger10.info("Created Google account", {
1257410
+ logger11.info("Created Google account", {
1256917
1257411
  accountId,
1256918
1257412
  name: state.accountName,
1256919
1257413
  email: email4
@@ -1256921,7 +1257415,7 @@ app25.get("/callback", async (c) => {
1256921
1257415
  }
1256922
1257416
  return c.html("<html><body><h2>Authorization Successful</h2><p>You can close this window.</p><script>setTimeout(()=>window.close(),2000)</script></body></html>");
1256923
1257417
  } catch (err) {
1256924
- logger10.error("OAuth callback failed", {
1257418
+ logger11.error("OAuth callback failed", {
1256925
1257419
  error: err instanceof Error ? err.message : String(err)
1256926
1257420
  });
1256927
1257421
  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);
@@ -1256936,7 +1257430,7 @@ init_google_calendar_manager();
1256936
1257430
  init_logger3();
1256937
1257431
  init_settings();
1256938
1257432
  init_channel_manager();
1256939
- var logger11 = createLogger("Google:CalendarService");
1257433
+ var logger12 = createLogger("Google:CalendarService");
1256940
1257434
  function listGoogleAccounts() {
1256941
1257435
  return db2.select().from(googleAccounts).all();
1256942
1257436
  }
@@ -1256956,19 +1257450,19 @@ async function deleteGoogleAccount(id) {
1256956
1257450
  }
1256957
1257451
  db2.delete(caldavCalendars).where(eq(caldavCalendars.googleAccountId, id)).run();
1256958
1257452
  db2.delete(googleAccounts).where(eq(googleAccounts.id, id)).run();
1256959
- logger11.info("Deleted Google account", { accountId: id });
1257453
+ logger12.info("Deleted Google account", { accountId: id });
1256960
1257454
  }
1256961
1257455
  async function enableGoogleCalendar(id) {
1256962
1257456
  const now = new Date().toISOString();
1256963
1257457
  db2.update(googleAccounts).set({ calendarEnabled: true, updatedAt: now }).where(eq(googleAccounts.id, id)).run();
1256964
1257458
  await googleCalendarManager.startAccount(id);
1256965
- logger11.info("Enabled Google Calendar for account", { accountId: id });
1257459
+ logger12.info("Enabled Google Calendar for account", { accountId: id });
1256966
1257460
  }
1256967
1257461
  async function disableGoogleCalendar(id) {
1256968
1257462
  const now = new Date().toISOString();
1256969
1257463
  db2.update(googleAccounts).set({ calendarEnabled: false, updatedAt: now }).where(eq(googleAccounts.id, id)).run();
1256970
1257464
  googleCalendarManager.stopAccount(id);
1256971
- logger11.info("Disabled Google Calendar for account", { accountId: id });
1257465
+ logger12.info("Disabled Google Calendar for account", { accountId: id });
1256972
1257466
  }
1256973
1257467
  async function enableGmail(id) {
1256974
1257468
  const now = new Date().toISOString();
@@ -1256978,7 +1257472,7 @@ async function enableGmail(id) {
1256978
1257472
  updateSettingByPath("channels.email.googleAccountId", id);
1256979
1257473
  await stopEmailChannel();
1256980
1257474
  await startEmailChannel();
1256981
- logger11.info("Enabled Gmail for account", { accountId: id });
1257475
+ logger12.info("Enabled Gmail for account", { accountId: id });
1256982
1257476
  }
1256983
1257477
  async function disableGmail(id) {
1256984
1257478
  const now = new Date().toISOString();
@@ -1256986,7 +1257480,7 @@ async function disableGmail(id) {
1256986
1257480
  await stopEmailChannel();
1256987
1257481
  updateSettingByPath("channels.email.enabled", false);
1256988
1257482
  updateSettingByPath("channels.email.googleAccountId", null);
1256989
- logger11.info("Disabled Gmail for account", { accountId: id });
1257483
+ logger12.info("Disabled Gmail for account", { accountId: id });
1256990
1257484
  }
1256991
1257485
  async function syncGoogleCalendar(id) {
1256992
1257486
  await googleCalendarManager.syncAccount(id);
@@ -1257240,6 +1257734,10 @@ app28.get("/", async (c) => {
1257240
1257734
  const conversationRole = c.req.query("conversationRole") || undefined;
1257241
1257735
  const conversationProvider = c.req.query("conversationProvider") || undefined;
1257242
1257736
  const conversationProjectId = c.req.query("conversationProjectId") || undefined;
1257737
+ const gmailFrom = c.req.query("gmailFrom") || undefined;
1257738
+ const gmailTo = c.req.query("gmailTo") || undefined;
1257739
+ const gmailAfter = c.req.query("gmailAfter") || undefined;
1257740
+ const gmailBefore = c.req.query("gmailBefore") || undefined;
1257243
1257741
  try {
1257244
1257742
  const results = await search({
1257245
1257743
  query: query.trim(),
@@ -1257254,7 +1257752,11 @@ app28.get("/", async (c) => {
1257254
1257752
  memoryTags,
1257255
1257753
  conversationRole,
1257256
1257754
  conversationProvider,
1257257
- conversationProjectId
1257755
+ conversationProjectId,
1257756
+ gmailFrom,
1257757
+ gmailTo,
1257758
+ gmailAfter,
1257759
+ gmailBefore
1257258
1257760
  });
1257259
1257761
  return c.json(results);
1257260
1257762
  } catch (err) {
@@ -1257480,6 +1257982,7 @@ init_logger3();
1257480
1257982
  init_settings();
1257481
1257983
  init_assistant_service();
1257482
1257984
  init_session_mapper();
1257985
+ init_memory_file_service();
1257483
1257986
  var HOURLY_INTERVAL = 60 * 60 * 1000;
1257484
1257987
  var hourlyIntervalId = null;
1257485
1257988
  var morningTimeoutId = null;
@@ -1257519,10 +1258022,14 @@ async function runHourlySweep() {
1257519
1258022
  const lastSweep = getLastSweepRun("hourly");
1257520
1258023
  const actionableMemoryCount = countActionableMemories();
1257521
1258024
  const openTaskCount = countOpenTasks();
1258025
+ const memoryFileContent = readMemoryFile();
1258026
+ const memoryFileLineCount = memoryFileContent.trim() ? memoryFileContent.split(`
1258027
+ `).length : 0;
1257522
1258028
  log2.assistant.info("Running hourly sweep", {
1257523
1258029
  runId: run2.id,
1257524
1258030
  actionableMemories: actionableMemoryCount,
1257525
- openTasks: openTaskCount
1258031
+ openTasks: openTaskCount,
1258032
+ memoryFileLines: memoryFileLineCount
1257526
1258033
  });
1257527
1258034
  const { session: session3 } = getOrCreateSession(SWEEP_SESSION_PREFIX, "sweep-agent", "Assistant Sweep");
1257528
1258035
  const prompt = `Perform your hourly sweep. Last sweep: ${lastSweep?.completedAt ?? "never"}`;