@knowsuchagency/fulcrum 3.3.0 → 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 };
@@ -1147067,8 +1147260,11 @@ You have access to Fulcrum's MCP tools. Use them proactively to help users.
1147067
1147260
  - \`memory_file_read\` - Read the master memory file (MEMORY.md)
1147068
1147261
  - \`memory_file_update\` - Update the file (whole or by section heading)
1147069
1147262
 
1147070
- **Ephemeral Memories:**
1147263
+ **Memory Management:**
1147071
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)
1147072
1147268
 
1147073
1147269
  **Google Account & Gmail Tools:**
1147074
1147270
  - \`list_google_accounts\` - List all Google accounts with calendar/Gmail status
@@ -1147402,7 +1147598,7 @@ Fulcrum is your digital concierge - a personal command center where you track ev
1147402
1147598
  - send_notification
1147403
1147599
  - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations; gmail is opt-in via \`entities: ["gmail"]\`)
1147404
1147600
  - memory_file_read, memory_file_update (master memory file - always in prompt)
1147405
- - memory_store (ephemeral knowledge snippets with tags)
1147601
+ - memory_store, memory_search, memory_list, memory_delete (persistent knowledge with tags)
1147406
1147602
  - message (send to WhatsApp/Discord/Telegram/Slack/Gmail - user-only, concierge mode)
1147407
1147603
  - memory_store with tag \`actionable\` (track things needing attention - concierge mode)
1147408
1147604
  - search with memoryTags filter (find tracked items by tag)
@@ -1147411,7 +1147607,7 @@ Fulcrum is your digital concierge - a personal command center where you track ev
1147411
1147607
  }
1147412
1147608
 
1147413
1147609
  // server/services/memory-file-service.ts
1147414
- 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";
1147415
1147611
  import { join as join13 } from "path";
1147416
1147612
  function getMemoryFilePath() {
1147417
1147613
  return join13(getFulcrumDir(), MEMORY_FILENAME);
@@ -1147420,7 +1147616,7 @@ function readMemoryFile() {
1147420
1147616
  const filePath = getMemoryFilePath();
1147421
1147617
  if (!existsSync14(filePath))
1147422
1147618
  return "";
1147423
- return readFileSync8(filePath, "utf-8");
1147619
+ return readFileSync9(filePath, "utf-8");
1147424
1147620
  }
1147425
1147621
  function writeMemoryFile(content) {
1147426
1147622
  const filePath = getMemoryFilePath();
@@ -1147614,7 +1147810,12 @@ ${knowledge}`;
1147614
1147810
 
1147615
1147811
  ## Master Memory File
1147616
1147812
 
1147617
- 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.
1147618
1147819
 
1147619
1147820
  ${memoryFileContent}`;
1147620
1147821
  }
@@ -1147869,19 +1148070,22 @@ async function* streamMessage(sessionId, userMessage, modelIdOrOptions, editorCo
1147869
1148070
  yield { type: "error", data: { message: "Session not found" } };
1147870
1148071
  return;
1147871
1148072
  }
1147872
- addMessage(sessionId, {
1147873
- role: "user",
1147874
- content: userMessage,
1147875
- sessionId
1147876
- });
1148073
+ if (!options.ephemeral) {
1148074
+ addMessage(sessionId, {
1148075
+ role: "user",
1148076
+ content: userMessage,
1148077
+ sessionId
1148078
+ });
1148079
+ }
1147877
1148080
  const settings = getSettings();
1147878
1148081
  const port = settings.server.port;
1147879
1148082
  const effectiveModelId = options.modelId ?? settings.assistant.model;
1147880
1148083
  let state = sessionState.get(sessionId);
1147881
1148084
  if (!state) {
1147882
- state = {};
1148085
+ state = { claudeSessionId: options.ephemeral ? undefined : session3.claudeSessionId ?? undefined };
1147883
1148086
  sessionState.set(sessionId, state);
1147884
1148087
  }
1148088
+ const resumeSessionId = options.ephemeral ? undefined : state.claudeSessionId;
1147885
1148089
  const tempFiles = [];
1147886
1148090
  try {
1147887
1148091
  log2.assistant.debug("Starting assistant query", {
@@ -1147929,8 +1148133,15 @@ User message: ${userMessage}`;
1147929
1148133
  {
1147930
1148134
  const tempDir = await mkdtemp(join14(tmpdir3(), "fulcrum-doc-"));
1147931
1148135
  const tempPath = join14(tempDir, attachment.filename || "document.pdf");
1147932
- await writeFile3(tempPath, Buffer.from(attachment.data, "base64"));
1148136
+ const docBuffer = Buffer.from(attachment.data, "base64");
1148137
+ await writeFile3(tempPath, docBuffer);
1147933
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
+ });
1147934
1148145
  parts.push(`[Attached document: ${tempPath}]`);
1147935
1148146
  }
1147936
1148147
  break;
@@ -1147953,13 +1148164,14 @@ ${attachment.data}`);
1147953
1148164
  sessionId,
1147954
1148165
  requestedModelId: effectiveModelId,
1147955
1148166
  resolvedModel: MODEL_MAP[effectiveModelId],
1147956
- resumeSessionId: state.claudeSessionId ?? null
1148167
+ resumeSessionId: resumeSessionId ?? null,
1148168
+ ephemeral: options.ephemeral ?? false
1147957
1148169
  });
1147958
1148170
  const result = s_({
1147959
1148171
  prompt: fullPrompt,
1147960
1148172
  options: {
1147961
1148173
  model: MODEL_MAP[effectiveModelId],
1147962
- resume: state.claudeSessionId,
1148174
+ resume: resumeSessionId,
1147963
1148175
  includePartialMessages: true,
1147964
1148176
  pathToClaudeCodeExecutable: getClaudeCodePathForSdk(),
1147965
1148177
  mcpServers: {
@@ -1147973,6 +1148185,7 @@ ${attachment.data}`);
1147973
1148185
  permissionMode: "bypassPermissions",
1147974
1148186
  allowDangerouslySkipPermissions: true,
1147975
1148187
  settingSources: ["user"],
1148188
+ ...options.ephemeral && { persistSession: false, maxTurns: 3 },
1147976
1148189
  ...options.outputFormat && { outputFormat: options.outputFormat }
1147977
1148190
  }
1147978
1148191
  });
@@ -1147998,7 +1148211,10 @@ ${attachment.data}`);
1147998
1148211
  }
1147999
1148212
  } else if (message.type === "assistant") {
1148000
1148213
  const assistantMsg = message;
1148001
- 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
+ }
1148002
1148218
  const textContent = assistantMsg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1148003
1148219
  if (textContent) {
1148004
1148220
  if (textContent.length > currentText.length) {
@@ -1148032,7 +1148248,10 @@ ${attachment.data}`);
1148032
1148248
  const resultMsg = message;
1148033
1148249
  if (resultMsg.subtype?.startsWith("error_")) {
1148034
1148250
  const errors2 = resultMsg.errors || ["Unknown error"];
1148035
- 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
+ }
1148036
1148255
  yield { type: "error", data: { message: errors2.join(", ") } };
1148037
1148256
  }
1148038
1148257
  if (resultMsg.structured_output) {
@@ -1148045,7 +1148264,7 @@ ${attachment.data}`);
1148045
1148264
  });
1148046
1148265
  }
1148047
1148266
  }
1148048
- if (currentText.trim()) {
1148267
+ if (!options.ephemeral && currentText.trim()) {
1148049
1148268
  addMessage(sessionId, {
1148050
1148269
  role: "assistant",
1148051
1148270
  content: currentText,
@@ -1148059,7 +1148278,10 @@ ${attachment.data}`);
1148059
1148278
  } catch (err) {
1148060
1148279
  const errorMsg = err instanceof Error ? err.message : String(err);
1148061
1148280
  log2.assistant.error("Assistant stream error", { sessionId, error: errorMsg });
1148062
- 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
+ }
1148063
1148285
  yield { type: "error", data: { message: errorMsg } };
1148064
1148286
  } finally {
1148065
1148287
  for (const tempPath of tempFiles) {
@@ -1149957,8 +1150179,51 @@ ${contextualMessage}`;
1149957
1150179
  }
1149958
1150180
  const parsed = JSON.parse(jsonText);
1149959
1150181
  if (parsed.actions && Array.isArray(parsed.actions)) {
1150182
+ const settings2 = getSettings();
1150183
+ const fulcrumPort = settings2.server?.port ?? 7777;
1149960
1150184
  for (const action of parsed.actions) {
1149961
- 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) {
1149962
1150227
  const source = action.source || `channel:${options.channelType}`;
1149963
1150228
  await storeMemory({
1149964
1150229
  content: action.content,
@@ -1149990,38 +1150255,59 @@ ${contextualMessage}`;
1149990
1150255
  yield { type: "error", data: { message: errorMsg } };
1149991
1150256
  }
1149992
1150257
  }
1149993
- 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.
1149994
1150259
 
1149995
1150260
  IMPORTANT: You have NO tools. Instead, respond with a JSON object describing what actions to take.
1149996
1150261
 
1149997
1150262
  Response format (respond with ONLY this JSON, no other text):
1149998
1150263
  {
1149999
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
+ },
1150000
1150272
  {
1150001
1150273
  "type": "store_memory",
1150002
1150274
  "content": "The fact or information to store",
1150003
- "tags": ["tag1", "tag2"],
1150275
+ "tags": ["persistent"],
1150004
1150276
  "source": "channel:whatsapp"
1150005
1150277
  }
1150006
1150278
  ]
1150007
1150279
  }
1150008
1150280
 
1150009
- 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:
1150010
1150282
  {"actions": []}
1150011
1150283
 
1150012
- Guidelines for what to store:
1150013
- - Important dates, deadlines, appointments
1150014
- - Decisions or agreements
1150015
- - Contact information
1150016
- - Project updates or status changes
1150017
- - Action items or requests
1150018
- - 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").
1150019
1150294
 
1150020
- 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:
1150021
1150308
  - Casual greetings or small talk
1150022
1150309
  - Spam or promotional content
1150023
- - Messages you don't understand
1150024
- - Trivially obvious information`;
1150310
+ - Messages you don't understand`;
1150025
1150311
  var init_opencode_channel_service = __esm(() => {
1150026
1150312
  init_dist10();
1150027
1150313
  init_logger3();
@@ -1150104,6 +1150390,8 @@ async function handleIncomingMessage(msg) {
1150104
1150390
  sender: msg.senderId,
1150105
1150391
  senderName: msg.senderName,
1150106
1150392
  content,
1150393
+ hasAttachments: (msg.attachments?.length ?? 0) > 0,
1150394
+ attachmentNames: msg.attachments?.map((a) => a.filename),
1150107
1150395
  metadata: {
1150108
1150396
  subject: msg.metadata?.subject,
1150109
1150397
  threadId: msg.metadata?.threadId,
@@ -1150112,24 +1150400,46 @@ async function handleIncomingMessage(msg) {
1150112
1150400
  };
1150113
1150401
  const systemPrompt = getMessagingSystemPrompt(msg.channelType, context);
1150114
1150402
  const isSlack = msg.channelType === "slack";
1150115
- const stream2 = _deps.streamMessage(session3.id, content, {
1150403
+ const stream2 = _deps.streamMessage(session3.id, content || "(file attached)", {
1150116
1150404
  systemPromptAdditions: systemPrompt,
1150117
- ...isSlack && { outputFormat: { type: "json_schema", schema: SLACK_RESPONSE_SCHEMA } }
1150405
+ ...msg.attachments?.length && { attachments: msg.attachments }
1150118
1150406
  });
1150119
1150407
  let responseText = "";
1150120
- let structuredOutput = null;
1150408
+ let hasError = false;
1150121
1150409
  for await (const event of stream2) {
1150122
1150410
  if (event.type === "error") {
1150123
1150411
  const errorMsg = event.data.message;
1150124
1150412
  log2.messaging.error("Assistant error handling message", { error: errorMsg });
1150413
+ hasError = true;
1150125
1150414
  } else if (event.type === "message:complete") {
1150126
- responseText = event.data.content;
1150127
- } else if (event.type === "structured_output") {
1150128
- 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
+ }
1150129
1150424
  }
1150130
1150425
  }
1150131
- if (isSlack && structuredOutput?.body?.trim()) {
1150132
- 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
+ }
1150133
1150443
  } else if (responseText.trim()) {
1150134
1150444
  await sendResponse(msg, responseText);
1150135
1150445
  }
@@ -1150255,6 +1150565,24 @@ Started: ${new Date(mapping.createdAt).toLocaleString()}
1150255
1150565
  Last active: ${new Date(mapping.lastMessageAt).toLocaleString()}`;
1150256
1150566
  await sendResponse(msg, statusText);
1150257
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
+ }
1150258
1150586
  async function sendResponse(originalMsg, content, metadata) {
1150259
1150587
  if (originalMsg.channelType === "email") {
1150260
1150588
  log2.messaging.info("Skipping email response \u2014 sending disabled, use Gmail drafts", {
@@ -1150361,7 +1150689,8 @@ async function processObserveOnlyMessage(msg) {
1150361
1150689
  const stream2 = _deps.streamMessage(session3.id, msg.content, {
1150362
1150690
  systemPromptAdditions: systemPrompt,
1150363
1150691
  modelId: observerModelId,
1150364
- securityTier: "observer"
1150692
+ securityTier: "observer",
1150693
+ ephemeral: true
1150365
1150694
  });
1150366
1150695
  for await (const event of stream2) {
1150367
1150696
  if (event.type === "error") {
@@ -1150382,7 +1150711,7 @@ async function processObserveOnlyMessage(msg) {
1150382
1150711
  recordObserverFailure();
1150383
1150712
  }
1150384
1150713
  }
1150385
- var _deps, SLACK_RESPONSE_SCHEMA, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150714
+ var _deps, OBSERVER_CIRCUIT_BREAKER, COMMANDS;
1150386
1150715
  var init_message_handler = __esm(() => {
1150387
1150716
  init_logger3();
1150388
1150717
  init_channel_manager();
@@ -1150394,21 +1150723,6 @@ var init_message_handler = __esm(() => {
1150394
1150723
  streamMessage: (...args) => streamMessage(...args),
1150395
1150724
  streamOpencodeObserverMessage: (...args) => streamOpencodeObserverMessage(...args)
1150396
1150725
  };
1150397
- SLACK_RESPONSE_SCHEMA = {
1150398
- type: "object",
1150399
- properties: {
1150400
- body: {
1150401
- type: "string",
1150402
- description: "Plain text message (shown in notifications and as fallback)"
1150403
- },
1150404
- blocks: {
1150405
- type: "array",
1150406
- description: "Slack Block Kit blocks for rich formatting",
1150407
- items: { type: "object", additionalProperties: true }
1150408
- }
1150409
- },
1150410
- required: ["body"]
1150411
- };
1150412
1150726
  OBSERVER_CIRCUIT_BREAKER = {
1150413
1150727
  failureCount: 0,
1150414
1150728
  failureThreshold: 3,
@@ -1150689,7 +1151003,10 @@ async function sendMessageToChannel(channel, body, options) {
1150689
1151003
  return { success: false, error: "Slack channel not active" };
1150690
1151004
  }
1150691
1151005
  try {
1150692
- 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;
1150693
1151010
  const success = await slackChannel.sendMessage(resolvedTo, body, msgMetadata);
1150694
1151011
  if (success) {
1150695
1151012
  log2.messaging.info("Sent Slack message", { to: resolvedTo, hasBlocks: !!options?.slackBlocks });
@@ -1208698,7 +1209015,7 @@ var ALLOWED_MIME_TYPES = [
1208698
1209015
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1208699
1209016
  "text/csv"
1208700
1209017
  ];
1208701
- var MAX_FILE_SIZE = 50 * 1024 * 1024;
1209018
+ var MAX_FILE_SIZE2 = 50 * 1024 * 1024;
1208702
1209019
  function getTaskUploadsDir(taskId) {
1208703
1209020
  return path8.join(getFulcrumDir(), "uploads", "tasks", taskId);
1208704
1209021
  }
@@ -1209355,8 +1209672,8 @@ app2.post("/:id/attachments", async (c) => {
1209355
1209672
  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
1209356
1209673
  return c.json({ error: `File type not allowed: ${file.type}` }, 400);
1209357
1209674
  }
1209358
- if (file.size > MAX_FILE_SIZE) {
1209359
- 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);
1209360
1209677
  }
1209361
1209678
  const uploadDir = getTaskUploadsDir(taskId);
1209362
1209679
  if (!fs11.existsSync(uploadDir)) {
@@ -1211946,7 +1212263,7 @@ init_drizzle_orm();
1211946
1212263
  init_nanoid();
1211947
1212264
  init_logger3();
1211948
1212265
  init_settings();
1211949
- 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";
1211950
1212267
  import { join as join22 } from "path";
1211951
1212268
  import { tmpdir as tmpdir4 } from "os";
1211952
1212269
  import { execSync as execSync6 } from "child_process";
@@ -1212023,9 +1212340,9 @@ function fetchCopierYamlFromGit(gitUrl) {
1212023
1212340
  const yamlAltPath = join22(tempDir, "copier.yaml");
1212024
1212341
  let content = null;
1212025
1212342
  if (existsSync22(yamlPath)) {
1212026
- content = readFileSync12(yamlPath, "utf-8");
1212343
+ content = readFileSync13(yamlPath, "utf-8");
1212027
1212344
  } else if (existsSync22(yamlAltPath)) {
1212028
- content = readFileSync12(yamlAltPath, "utf-8");
1212345
+ content = readFileSync13(yamlAltPath, "utf-8");
1212029
1212346
  }
1212030
1212347
  if (!content) {
1212031
1212348
  rmSync6(tempDir, { recursive: true, force: true });
@@ -1212047,10 +1212364,10 @@ async function fetchCopierYaml(source) {
1212047
1212364
  const yamlPath = join22(templatePath, "copier.yml");
1212048
1212365
  const yamlAltPath = join22(templatePath, "copier.yaml");
1212049
1212366
  if (existsSync22(yamlPath)) {
1212050
- return { content: readFileSync12(yamlPath, "utf-8"), templatePath };
1212367
+ return { content: readFileSync13(yamlPath, "utf-8"), templatePath };
1212051
1212368
  }
1212052
1212369
  if (existsSync22(yamlAltPath)) {
1212053
- return { content: readFileSync12(yamlAltPath, "utf-8"), templatePath };
1212370
+ return { content: readFileSync13(yamlAltPath, "utf-8"), templatePath };
1212054
1212371
  }
1212055
1212372
  throw new Error("copier.yml not found in template directory");
1212056
1212373
  }
@@ -1215930,7 +1216247,7 @@ var github_default = app11;
1215930
1216247
  init_db2();
1215931
1216248
  init_pty_instance();
1215932
1216249
  init_dtach_service();
1215933
- 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";
1215934
1216251
  import { execSync as execSync8 } from "child_process";
1215935
1216252
  import { homedir as homedir7 } from "os";
1215936
1216253
  import { join as join23 } from "path";
@@ -1216215,7 +1216532,7 @@ function findAllAgentProcesses(agentFilter) {
1216215
1216532
  for (const pidStr of procDirs) {
1216216
1216533
  const pid = parseInt(pidStr, 10);
1216217
1216534
  try {
1216218
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216535
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216219
1216536
  if (combinedPattern.test(cmdline)) {
1216220
1216537
  const agent = detectAgentType(cmdline);
1216221
1216538
  if (agent && (!agentFilter || agentFilter.includes(agent))) {
@@ -1216271,7 +1216588,7 @@ function getProcessCwd(pid) {
1216271
1216588
  }
1216272
1216589
  function getProcessMemoryMB(pid) {
1216273
1216590
  try {
1216274
- const status = readFileSync13(`/proc/${pid}/status`, "utf-8");
1216591
+ const status = readFileSync14(`/proc/${pid}/status`, "utf-8");
1216275
1216592
  const match3 = status.match(/VmRSS:\s+(\d+)\s+kB/);
1216276
1216593
  return match3 ? parseInt(match3[1], 10) / 1024 : 0;
1216277
1216594
  } catch {
@@ -1216286,10 +1216603,10 @@ function getProcessMemoryMB(pid) {
1216286
1216603
  function getProcessStartTime(pid) {
1216287
1216604
  if (!isMacOS2) {
1216288
1216605
  try {
1216289
- const stat2 = readFileSync13(`/proc/${pid}/stat`, "utf-8");
1216606
+ const stat2 = readFileSync14(`/proc/${pid}/stat`, "utf-8");
1216290
1216607
  const fields = stat2.split(" ");
1216291
1216608
  const starttime = parseInt(fields[21], 10);
1216292
- const uptime = parseFloat(readFileSync13("/proc/uptime", "utf-8").split(" ")[0]);
1216609
+ const uptime = parseFloat(readFileSync14("/proc/uptime", "utf-8").split(" ")[0]);
1216293
1216610
  const clockTicks = 100;
1216294
1216611
  const bootTime = Math.floor(Date.now() / 1000) - uptime;
1216295
1216612
  return Math.floor(bootTime + starttime / clockTicks);
@@ -1216364,7 +1216681,7 @@ monitoringRoutes.get("/claude-instances", (c) => {
1216364
1216681
  for (const pidStr of procDirs) {
1216365
1216682
  const pid = parseInt(pidStr, 10);
1216366
1216683
  try {
1216367
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216684
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216368
1216685
  if (cmdline.includes(socketPath)) {
1216369
1216686
  foundPids.push(pid);
1216370
1216687
  }
@@ -1216458,7 +1216775,7 @@ monitoringRoutes.post("/claude-instances/:pid/kill-pid", (c) => {
1216458
1216775
  return c.json({ error: "Invalid PID" }, 400);
1216459
1216776
  }
1216460
1216777
  try {
1216461
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8");
1216778
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8");
1216462
1216779
  const agent = detectAgentType(cmdline);
1216463
1216780
  if (!agent) {
1216464
1216781
  return c.json({ error: "Process is not a recognized AI agent" }, 400);
@@ -1216484,7 +1216801,7 @@ function getTotalMemory() {
1216484
1216801
  }
1216485
1216802
  }
1216486
1216803
  try {
1216487
- const meminfo = readFileSync13("/proc/meminfo", "utf-8");
1216804
+ const meminfo = readFileSync14("/proc/meminfo", "utf-8");
1216488
1216805
  const match3 = meminfo.match(/MemTotal:\s+(\d+)/);
1216489
1216806
  return match3 ? parseInt(match3[1], 10) * 1024 : 0;
1216490
1216807
  } catch {
@@ -1216700,7 +1217017,7 @@ monitoringRoutes.get("/docker-stats", async (c) => {
1216700
1217017
  });
1216701
1217018
  function getProcessEnv(pid) {
1216702
1217019
  try {
1216703
- const environ = readFileSync13(`/proc/${pid}/environ`, "utf-8");
1217020
+ const environ = readFileSync14(`/proc/${pid}/environ`, "utf-8");
1216704
1217021
  const env = {};
1216705
1217022
  for (const entry of environ.split("\x00")) {
1216706
1217023
  const idx = entry.indexOf("=");
@@ -1216715,7 +1217032,7 @@ function getProcessEnv(pid) {
1216715
1217032
  }
1216716
1217033
  function getParentPid(pid) {
1216717
1217034
  try {
1216718
- const stat2 = readFileSync13(`/proc/${pid}/stat`, "utf-8");
1217035
+ const stat2 = readFileSync14(`/proc/${pid}/stat`, "utf-8");
1216719
1217036
  const match3 = stat2.match(/^\d+ \([^)]+\) \S+ (\d+)/);
1216720
1217037
  return match3 ? parseInt(match3[1], 10) : null;
1216721
1217038
  } catch {
@@ -1216730,7 +1217047,7 @@ function findFulcrumInstances() {
1216730
1217047
  for (const pidStr of procDirs) {
1216731
1217048
  const pid = parseInt(pidStr, 10);
1216732
1217049
  try {
1216733
- const cmdline = readFileSync13(`/proc/${pid}/cmdline`, "utf-8").replace(/\0/g, " ");
1217050
+ const cmdline = readFileSync14(`/proc/${pid}/cmdline`, "utf-8").replace(/\0/g, " ");
1216734
1217051
  const env = getProcessEnv(pid);
1216735
1217052
  const parentPid = getParentPid(pid);
1216736
1217053
  const cmdParts = cmdline.trim().split(/\s+/);
@@ -1216846,7 +1217163,7 @@ async function getClaudeOAuthToken() {
1216846
1217163
  const primaryPath = join23(homedir7(), ".claude", ".credentials.json");
1216847
1217164
  try {
1216848
1217165
  if (existsSync23(primaryPath)) {
1216849
- const content = readFileSync13(primaryPath, "utf-8");
1217166
+ const content = readFileSync14(primaryPath, "utf-8");
1216850
1217167
  const config = JSON.parse(content);
1216851
1217168
  if (config.claudeAiOauth && typeof config.claudeAiOauth === "object") {
1216852
1217169
  const token = config.claudeAiOauth.accessToken;
@@ -1218440,7 +1218757,7 @@ init_logger3();
1218440
1218757
  import { execSync as execSync10 } from "child_process";
1218441
1218758
  import { homedir as homedir10, platform as platform2 } from "os";
1218442
1218759
  import { join as join33 } from "path";
1218443
- 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";
1218444
1218761
  var USER_UNIT_DIR = join33(homedir10(), ".config/systemd/user");
1218445
1218762
  var systemdAvailable = null;
1218446
1218763
  function isSystemdAvailable() {
@@ -1218668,11 +1218985,11 @@ function getTimer(name, scope) {
1218668
1218985
  let workingDirectory = null;
1218669
1218986
  if (scope === "user" && unitPath) {
1218670
1218987
  try {
1218671
- timerContent = readFileSync15(unitPath, "utf-8");
1218988
+ timerContent = readFileSync16(unitPath, "utf-8");
1218672
1218989
  } catch {}
1218673
1218990
  const servicePath = unitPath.replace(".timer", ".service");
1218674
1218991
  try {
1218675
- serviceContent = readFileSync15(servicePath, "utf-8");
1218992
+ serviceContent = readFileSync16(servicePath, "utf-8");
1218676
1218993
  const execMatch = serviceContent.match(/^ExecStart=(.+)$/m);
1218677
1218994
  if (execMatch) {
1218678
1218995
  command = execMatch[1];
@@ -1218877,8 +1219194,8 @@ function updateTimer(name, updates) {
1218877
1219194
  if (!existsSync27(timerPath)) {
1218878
1219195
  throw new Error(`Timer ${timerName} not found`);
1218879
1219196
  }
1218880
- let timerContent = readFileSync15(timerPath, "utf-8");
1218881
- let serviceContent = existsSync27(servicePath) ? readFileSync15(servicePath, "utf-8") : "";
1219197
+ let timerContent = readFileSync16(timerPath, "utf-8");
1219198
+ let serviceContent = existsSync27(servicePath) ? readFileSync16(servicePath, "utf-8") : "";
1218882
1219199
  if (updates.description !== undefined) {
1218883
1219200
  timerContent = timerContent.replace(/^Description=.*$/m, `Description=${updates.description}`);
1218884
1219201
  serviceContent = serviceContent.replace(/^Description=.*$/m, `Description=${updates.description}`);
@@ -1219852,7 +1220169,7 @@ var ALLOWED_MIME_TYPES2 = [
1219852
1220169
  "application/gzip",
1219853
1220170
  "application/x-tar"
1219854
1220171
  ];
1219855
- var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
1220172
+ var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
1219856
1220173
  function sanitizeFilename2(filename) {
1219857
1220174
  return filename.replace(/[^a-zA-Z0-9._-]/g, "_");
1219858
1220175
  }
@@ -1220839,8 +1221156,8 @@ app19.post("/:id/attachments", async (c) => {
1220839
1221156
  if (!ALLOWED_MIME_TYPES2.includes(file.type)) {
1220840
1221157
  return c.json({ error: `File type not allowed: ${file.type}` }, 400);
1220841
1221158
  }
1220842
- if (file.size > MAX_FILE_SIZE2) {
1220843
- 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);
1220844
1221161
  }
1220845
1221162
  const uploadDir = getProjectUploadsDir(projectId);
1220846
1221163
  if (!fs17.existsSync(uploadDir)) {
@@ -1251893,6 +1252210,27 @@ var toolRegistry = [
1251893
1252210
  keywords: ["memory", "store", "save", "remember", "knowledge", "persist", "fact"],
1251894
1252211
  defer_loading: false
1251895
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
+ },
1251896
1252234
  {
1251897
1252235
  name: "memory_file_read",
1251898
1252236
  description: "Read the master memory file (MEMORY.md)",
@@ -1251993,7 +1252331,7 @@ var registerCoreTools = (server2, _client) => {
1251993
1252331
  };
1251994
1252332
 
1251995
1252333
  // cli/src/mcp/tools/tasks.ts
1251996
- import { basename as basename3 } from "path";
1252334
+ import { basename as basename4 } from "path";
1251997
1252335
 
1251998
1252336
  // shared/date-utils.ts
1251999
1252337
  function getTodayInTimezone(timezone) {
@@ -1252014,7 +1252352,7 @@ function getTodayInTimezone(timezone) {
1252014
1252352
  }
1252015
1252353
 
1252016
1252354
  // cli/src/mcp/tools/tasks.ts
1252017
- var registerTaskTools = (server2, client3) => {
1252355
+ function registerListTasks(server2, client3) {
1252018
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.", {
1252019
1252357
  status: exports_external.optional(TaskStatusSchema2).describe("Filter by single task status (use statuses for multiple)"),
1252020
1252358
  statuses: exports_external.optional(exports_external.array(TaskStatusSchema2)).describe("Filter by multiple statuses (OR logic)"),
@@ -1252106,20 +1252444,8 @@ var registerTaskTools = (server2, client3) => {
1252106
1252444
  return handleToolError(err);
1252107
1252445
  }
1252108
1252446
  });
1252109
- server2.tool("get_task", "Get details of a specific task by ID, including dependencies and attachments", {
1252110
- id: exports_external.string().describe("Task ID (UUID)")
1252111
- }, async ({ id }) => {
1252112
- try {
1252113
- const [task, dependencies, attachments] = await Promise.all([
1252114
- client3.getTask(id),
1252115
- client3.getTaskDependencies(id),
1252116
- client3.listTaskAttachments(id)
1252117
- ]);
1252118
- return formatSuccess({ ...task, dependencies, attachments });
1252119
- } catch (err) {
1252120
- return handleToolError(err);
1252121
- }
1252122
- });
1252447
+ }
1252448
+ function registerCreateTask(server2, client3) {
1252123
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.", {
1252124
1252450
  title: exports_external.string().describe("Task title"),
1252125
1252451
  repoPath: exports_external.optional(exports_external.string()).describe("Absolute path to the git repository (optional for non-worktree tasks)"),
@@ -1252144,7 +1252470,7 @@ var registerTaskTools = (server2, client3) => {
1252144
1252470
  dueDate
1252145
1252471
  }) => {
1252146
1252472
  try {
1252147
- const repoName = repoPath ? basename3(repoPath) : null;
1252473
+ const repoName = repoPath ? basename4(repoPath) : null;
1252148
1252474
  const effectiveBaseBranch = baseBranch ?? "main";
1252149
1252475
  const task = await client3.createTask({
1252150
1252476
  title,
@@ -1252180,6 +1252506,8 @@ var registerTaskTools = (server2, client3) => {
1252180
1252506
  return handleToolError(err);
1252181
1252507
  }
1252182
1252508
  });
1252509
+ }
1252510
+ function registerUpdateTask(server2, client3) {
1252183
1252511
  server2.tool("update_task", "Update task metadata (title or description)", {
1252184
1252512
  id: exports_external.string().describe("Task ID"),
1252185
1252513
  title: exports_external.optional(exports_external.string()).describe("New title"),
@@ -1252197,6 +1252525,82 @@ var registerTaskTools = (server2, client3) => {
1252197
1252525
  return handleToolError(err);
1252198
1252526
  }
1252199
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
+ });
1252200
1252604
  server2.tool("delete_task", "Delete a task and optionally its linked git worktree", {
1252201
1252605
  id: exports_external.string().describe("Task ID"),
1252202
1252606
  deleteWorktree: exports_external.optional(exports_external.boolean()).describe("Also delete the linked git worktree (default: false)")
@@ -1252221,18 +1252625,6 @@ var registerTaskTools = (server2, client3) => {
1252221
1252625
  return handleToolError(err);
1252222
1252626
  }
1252223
1252627
  });
1252224
- server2.tool("add_task_link", "Add a URL link to a task (for documentation, related PRs, design files, etc.)", {
1252225
- taskId: exports_external.string().describe("Task ID"),
1252226
- url: exports_external.string().url().describe("URL to add"),
1252227
- label: exports_external.optional(exports_external.string()).describe("Display label (auto-detected if not provided)")
1252228
- }, async ({ taskId, url: url2, label }) => {
1252229
- try {
1252230
- const link = await client3.addTaskLink(taskId, url2, label);
1252231
- return formatSuccess(link);
1252232
- } catch (err) {
1252233
- return handleToolError(err);
1252234
- }
1252235
- });
1252236
1252628
  server2.tool("remove_task_link", "Remove a URL link from a task", {
1252237
1252629
  taskId: exports_external.string().describe("Task ID"),
1252238
1252630
  linkId: exports_external.string().describe("Link ID to remove")
@@ -1252254,31 +1252646,6 @@ var registerTaskTools = (server2, client3) => {
1252254
1252646
  return handleToolError(err);
1252255
1252647
  }
1252256
1252648
  });
1252257
- server2.tool("add_task_tag", "Add a tag to a task for categorization. Returns similar existing tags to help catch typos.", {
1252258
- taskId: exports_external.string().describe("Task ID"),
1252259
- tag: exports_external.string().describe("Tag to add")
1252260
- }, async ({ taskId, tag }) => {
1252261
- try {
1252262
- const result = await client3.addTaskTag(taskId, tag);
1252263
- const allTasks = await client3.listTasks();
1252264
- const existingTags = new Set;
1252265
- for (const t of allTasks) {
1252266
- if (t.tags) {
1252267
- for (const tg of t.tags) {
1252268
- existingTags.add(tg);
1252269
- }
1252270
- }
1252271
- }
1252272
- const tagLower = tag.toLowerCase();
1252273
- const similarTags = Array.from(existingTags).filter((tg) => tg !== tag && (tg.toLowerCase().includes(tagLower) || tagLower.includes(tg.toLowerCase())));
1252274
- return formatSuccess({
1252275
- ...result,
1252276
- similarTags: similarTags.length > 0 ? similarTags : undefined
1252277
- });
1252278
- } catch (err) {
1252279
- return handleToolError(err);
1252280
- }
1252281
- });
1252282
1252649
  server2.tool("remove_task_tag", "Remove a tag from a task", {
1252283
1252650
  taskId: exports_external.string().describe("Task ID"),
1252284
1252651
  tag: exports_external.string().describe("Tag to remove")
@@ -1252290,17 +1252657,6 @@ var registerTaskTools = (server2, client3) => {
1252290
1252657
  return handleToolError(err);
1252291
1252658
  }
1252292
1252659
  });
1252293
- server2.tool("set_task_due_date", "Set or clear the due date for a task", {
1252294
- taskId: exports_external.string().describe("Task ID"),
1252295
- dueDate: exports_external.nullable(exports_external.string()).describe("Due date in YYYY-MM-DD format, or null to clear")
1252296
- }, async ({ taskId, dueDate }) => {
1252297
- try {
1252298
- const result = await client3.setTaskDueDate(taskId, dueDate);
1252299
- return formatSuccess(result);
1252300
- } catch (err) {
1252301
- return handleToolError(err);
1252302
- }
1252303
- });
1252304
1252660
  server2.tool("get_task_dependencies", "Get the dependencies and dependents of a task, and whether it is blocked", {
1252305
1252661
  taskId: exports_external.string().describe("Task ID")
1252306
1252662
  }, async ({ taskId }) => {
@@ -1252459,6 +1252815,14 @@ var registerTaskTools = (server2, client3) => {
1252459
1252815
  }
1252460
1252816
  });
1252461
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
+ };
1252462
1252826
 
1252463
1252827
  // cli/src/mcp/tools/repositories.ts
1252464
1252828
  var registerRepositoryTools = (server2, client3) => {
@@ -1253348,8 +1253712,9 @@ var registerAssistantTools = (server2, client3) => {
1253348
1253712
  subject: exports_external.optional(exports_external.string()).describe("Email subject (for Gmail channel only)"),
1253349
1253713
  replyToMessageId: exports_external.optional(exports_external.string()).describe("Message ID to reply to (for threading)"),
1253350
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."),
1253351
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.")
1253352
- }, async ({ channel, body, subject, replyToMessageId, slack_blocks, googleAccountId }) => {
1253717
+ }, async ({ channel, body, subject, replyToMessageId, slack_blocks, filePath, googleAccountId }) => {
1253353
1253718
  try {
1253354
1253719
  if (channel === "gmail") {
1253355
1253720
  let accountId = googleAccountId;
@@ -1253372,7 +1253737,8 @@ var registerAssistantTools = (server2, client3) => {
1253372
1253737
  body,
1253373
1253738
  subject,
1253374
1253739
  replyToMessageId,
1253375
- slackBlocks: slack_blocks
1253740
+ slackBlocks: slack_blocks,
1253741
+ filePath
1253376
1253742
  });
1253377
1253743
  return formatSuccess(result);
1253378
1253744
  } catch (err) {
@@ -1253643,7 +1254009,7 @@ var MEMORY_SOURCES = [
1253643
1254009
  ];
1253644
1254010
 
1253645
1254011
  // cli/src/mcp/tools/memory.ts
1253646
- var registerMemoryTools = (server2, client3) => {
1254012
+ function registerMemoryStoreTool(server2, client3) {
1253647
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.", {
1253648
1254014
  content: exports_external.string().describe("The memory content to store. Be specific and self-contained."),
1253649
1254015
  tags: exports_external.optional(exports_external.array(exports_external.string())).describe('Optional tags for categorization (e.g., ["preference", "architecture", "decision"])'),
@@ -1253656,10 +1254022,61 @@ var registerMemoryTools = (server2, client3) => {
1253656
1254022
  return handleToolError(err);
1253657
1254023
  }
1253658
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);
1253659
1254076
  };
1253660
1254077
 
1253661
1254078
  // cli/src/mcp/tools/memory-file.ts
1253662
- var registerMemoryFileTools = (server2, client3) => {
1254079
+ var registerMemoryFileReadTool = (server2, client3) => {
1253663
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 () => {
1253664
1254081
  try {
1253665
1254082
  const result = await client3.readMemoryFile();
@@ -1253668,6 +1254085,9 @@ var registerMemoryFileTools = (server2, client3) => {
1253668
1254085
  return handleToolError(err);
1253669
1254086
  }
1253670
1254087
  });
1254088
+ };
1254089
+ var registerMemoryFileTools = (server2, client3) => {
1254090
+ registerMemoryFileReadTool(server2, client3);
1253671
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.", {
1253672
1254092
  content: exports_external.string().describe("The content to write. If section is specified, this replaces only that section body."),
1253673
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.')
@@ -1253770,7 +1254190,7 @@ function registerTools(server2, client3) {
1253770
1254190
  registerSearchTools(server2, client3);
1253771
1254191
  }
1253772
1254192
  // cli/src/utils/server.ts
1253773
- 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";
1253774
1254194
  import { join as join37 } from "path";
1253775
1254195
  import { homedir as homedir13 } from "os";
1253776
1254196
  var DEFAULT_PORT = 7777;
@@ -1253794,7 +1254214,7 @@ function expandPath2(p3) {
1253794
1254214
  function readSettingsFile(path14) {
1253795
1254215
  try {
1253796
1254216
  if (existsSync31(path14)) {
1253797
- const content = readFileSync17(path14, "utf-8");
1254217
+ const content = readFileSync18(path14, "utf-8");
1253798
1254218
  return JSON.parse(content);
1253799
1254219
  }
1253800
1254220
  } catch {}
@@ -1253834,8 +1254254,8 @@ function discoverServerUrl(urlOverride, portOverride) {
1253834
1254254
  }
1253835
1254255
 
1253836
1254256
  // cli/src/client.ts
1253837
- import { readFileSync as readFileSync18 } from "fs";
1253838
- import { basename as basename4 } from "path";
1254257
+ import { readFileSync as readFileSync19 } from "fs";
1254258
+ import { basename as basename5 } from "path";
1253839
1254259
 
1253840
1254260
  class FulcrumClient {
1253841
1254261
  baseUrl;
@@ -1254097,8 +1254517,8 @@ class FulcrumClient {
1254097
1254517
  return this.fetch(`/api/tasks/${taskId}/attachments`);
1254098
1254518
  }
1254099
1254519
  async uploadTaskAttachment(taskId, filePath) {
1254100
- const fileContent = readFileSync18(filePath);
1254101
- const filename = basename4(filePath);
1254520
+ const fileContent = readFileSync19(filePath);
1254521
+ const filename = basename5(filePath);
1254102
1254522
  const formData = new FormData;
1254103
1254523
  const blob3 = new Blob([fileContent]);
1254104
1254524
  formData.append("file", blob3, filename);
@@ -1254184,8 +1254604,8 @@ class FulcrumClient {
1254184
1254604
  return this.fetch(`/api/projects/${projectId}/attachments`);
1254185
1254605
  }
1254186
1254606
  async uploadProjectAttachment(projectId, filePath) {
1254187
- const fileContent = readFileSync18(filePath);
1254188
- const filename = basename4(filePath);
1254607
+ const fileContent = readFileSync19(filePath);
1254608
+ const filename = basename5(filePath);
1254189
1254609
  const formData = new FormData;
1254190
1254610
  const blob3 = new Blob([fileContent]);
1254191
1254611
  formData.append("file", blob3, filename);
@@ -1254602,7 +1255022,7 @@ mcpRoutes.all("/", async (c) => {
1254602
1255022
  });
1254603
1255023
  const server2 = new McpServer({
1254604
1255024
  name: "fulcrum",
1254605
- version: "3.3.0"
1255025
+ version: "3.4.0"
1254606
1255026
  });
1254607
1255027
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254608
1255028
  registerTools(server2, client3);
@@ -1254625,8 +1255045,10 @@ mcpObserverRoutes.all("/", async (c) => {
1254625
1255045
  version: "2.12.0"
1254626
1255046
  });
1254627
1255047
  const client3 = new FulcrumClient(`http://localhost:${port}`);
1254628
- registerMemoryTools(server2, client3);
1254629
- registerMemoryFileTools(server2, client3);
1255048
+ registerMemoryObserverTools(server2, client3);
1255049
+ registerMemoryFileReadTool(server2, client3);
1255050
+ registerTaskObserverTools(server2, client3);
1255051
+ registerNotificationTools(server2, client3);
1254630
1255052
  await server2.connect(transport);
1254631
1255053
  return transport.handleRequest(c.req.raw);
1254632
1255054
  });
@@ -1255687,7 +1256109,8 @@ app23.post("/send", async (c) => {
1255687
1256109
  const result = await sendMessageToChannel(body.channel, body.body, {
1255688
1256110
  subject: body.subject,
1255689
1256111
  replyToMessageId: body.replyToMessageId,
1255690
- slackBlocks: body.slackBlocks
1256112
+ slackBlocks: body.slackBlocks,
1256113
+ filePath: body.filePath
1255691
1256114
  });
1255692
1256115
  if (result.success) {
1255693
1256116
  log2.messaging.info("Message sent via API", {
@@ -1257559,6 +1257982,7 @@ init_logger3();
1257559
1257982
  init_settings();
1257560
1257983
  init_assistant_service();
1257561
1257984
  init_session_mapper();
1257985
+ init_memory_file_service();
1257562
1257986
  var HOURLY_INTERVAL = 60 * 60 * 1000;
1257563
1257987
  var hourlyIntervalId = null;
1257564
1257988
  var morningTimeoutId = null;
@@ -1257598,10 +1258022,14 @@ async function runHourlySweep() {
1257598
1258022
  const lastSweep = getLastSweepRun("hourly");
1257599
1258023
  const actionableMemoryCount = countActionableMemories();
1257600
1258024
  const openTaskCount = countOpenTasks();
1258025
+ const memoryFileContent = readMemoryFile();
1258026
+ const memoryFileLineCount = memoryFileContent.trim() ? memoryFileContent.split(`
1258027
+ `).length : 0;
1257601
1258028
  log2.assistant.info("Running hourly sweep", {
1257602
1258029
  runId: run2.id,
1257603
1258030
  actionableMemories: actionableMemoryCount,
1257604
- openTasks: openTaskCount
1258031
+ openTasks: openTaskCount,
1258032
+ memoryFileLines: memoryFileLineCount
1257605
1258033
  });
1257606
1258034
  const { session: session3 } = getOrCreateSession(SWEEP_SESSION_PREFIX, "sweep-agent", "Assistant Sweep");
1257607
1258035
  const prompt = `Perform your hourly sweep. Last sweep: ${lastSweep?.completedAt ?? "never"}`;