@knowsuchagency/fulcrum 2.15.2 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -253,7 +253,7 @@ Both plugins include an MCP server with 60+ tools:
253
253
  | **Notifications** | Send notifications to enabled channels |
254
254
  | **Backup & Restore** | Snapshot database and settings; auto-safety-backup on restore |
255
255
  | **Settings** | View and update configuration; manage notification channels |
256
- | **Search** | Unified full-text search across tasks, projects, messages, events, and memories |
256
+ | **Search** | Unified full-text search across tasks, projects, messages, events, memories, and conversations |
257
257
  | **Memory** | Read/update master memory file; store ephemeral knowledge with tags |
258
258
  | **Calendar** | Manage CalDAV accounts, sync calendars, configure event copy rules |
259
259
  | **Gmail** | List Google accounts, manage Gmail drafts (create, update, delete) |
package/bin/fulcrum.js CHANGED
@@ -1695,6 +1695,12 @@ class FulcrumClient {
1695
1695
  params.set("eventTo", input.eventTo);
1696
1696
  if (input.memoryTags?.length)
1697
1697
  params.set("memoryTags", input.memoryTags.join(","));
1698
+ if (input.conversationRole)
1699
+ params.set("conversationRole", input.conversationRole);
1700
+ if (input.conversationProvider)
1701
+ params.set("conversationProvider", input.conversationProvider);
1702
+ if (input.conversationProjectId)
1703
+ params.set("conversationProjectId", input.conversationProjectId);
1698
1704
  return this.fetch(`/api/search?${params.toString()}`);
1699
1705
  }
1700
1706
  }
@@ -46142,9 +46148,9 @@ var init_memory_file = __esm(() => {
46142
46148
 
46143
46149
  // cli/src/mcp/tools/search.ts
46144
46150
  var EntityTypeSchema, registerSearchTools = (server, client) => {
46145
- server.tool("search", 'Search across all Fulcrum entities (tasks, projects, messages, calendar events, memories) using full-text search. Supports boolean operators (AND, OR, NOT), phrase matching ("quoted phrases"), and prefix matching (term*). Returns results ranked by relevance.', {
46151
+ server.tool("search", 'Search across all Fulcrum entities (tasks, projects, messages, calendar events, memories, conversations) using full-text search. Supports boolean operators (AND, OR, NOT), phrase matching ("quoted phrases"), and prefix matching (term*). Returns results ranked by relevance.', {
46146
46152
  query: exports_external.string().describe('FTS5 search query. Supports: AND, OR, NOT operators, "quoted phrases", prefix* matching. Example: "kubernetes deployment" OR k8s'),
46147
- entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories"),
46153
+ entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories, conversations"),
46148
46154
  limit: exports_external.optional(exports_external.number()).describe("Maximum results per entity type (default: 10)"),
46149
46155
  taskStatus: exports_external.optional(exports_external.array(exports_external.string())).describe('Filter tasks by status (e.g., ["IN_PROGRESS", "TO_DO"])'),
46150
46156
  projectStatus: exports_external.optional(exports_external.enum(["active", "archived"])).describe("Filter projects by status"),
@@ -46152,8 +46158,11 @@ var EntityTypeSchema, registerSearchTools = (server, client) => {
46152
46158
  messageDirection: exports_external.optional(exports_external.enum(["incoming", "outgoing"])).describe("Filter messages by direction"),
46153
46159
  eventFrom: exports_external.optional(exports_external.string()).describe("Filter calendar events starting from this date (ISO 8601)"),
46154
46160
  eventTo: exports_external.optional(exports_external.string()).describe("Filter calendar events up to this date (ISO 8601)"),
46155
- memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags")
46156
- }, async ({ query, entities, limit, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags }) => {
46161
+ memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags"),
46162
+ conversationRole: exports_external.optional(exports_external.string()).describe('Filter conversations by role (e.g., "user", "assistant")'),
46163
+ conversationProvider: exports_external.optional(exports_external.string()).describe('Filter conversations by provider (e.g., "claude", "opencode")'),
46164
+ conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID")
46165
+ }, async ({ query, entities, limit, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId }) => {
46157
46166
  try {
46158
46167
  const results = await client.search({
46159
46168
  query,
@@ -46165,7 +46174,10 @@ var EntityTypeSchema, registerSearchTools = (server, client) => {
46165
46174
  messageDirection,
46166
46175
  eventFrom,
46167
46176
  eventTo,
46168
- memoryTags
46177
+ memoryTags,
46178
+ conversationRole,
46179
+ conversationProvider,
46180
+ conversationProjectId
46169
46181
  });
46170
46182
  return formatSuccess(results);
46171
46183
  } catch (err) {
@@ -46176,7 +46188,7 @@ var EntityTypeSchema, registerSearchTools = (server, client) => {
46176
46188
  var init_search = __esm(() => {
46177
46189
  init_zod2();
46178
46190
  init_utils();
46179
- EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories"]);
46191
+ EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
46180
46192
  });
46181
46193
 
46182
46194
  // cli/src/mcp/tools/index.ts
@@ -46235,7 +46247,7 @@ async function runMcpServer(urlOverride, portOverride) {
46235
46247
  const client = new FulcrumClient(urlOverride, portOverride);
46236
46248
  const server = new McpServer({
46237
46249
  name: "fulcrum",
46238
- version: "2.15.2"
46250
+ version: "2.16.0"
46239
46251
  });
46240
46252
  registerTools(server, client);
46241
46253
  const transport = new StdioServerTransport;
@@ -48584,7 +48596,7 @@ var marketplace_default = `{
48584
48596
  "name": "fulcrum",
48585
48597
  "source": "./",
48586
48598
  "description": "Task orchestration for Claude Code",
48587
- "version": "2.15.2",
48599
+ "version": "2.16.0",
48588
48600
  "skills": [
48589
48601
  "./skills/fulcrum"
48590
48602
  ],
@@ -49772,7 +49784,7 @@ function compareVersions(v1, v2) {
49772
49784
  var package_default = {
49773
49785
  name: "@knowsuchagency/fulcrum",
49774
49786
  private: true,
49775
- version: "2.15.2",
49787
+ version: "2.16.0",
49776
49788
  description: "Harness Attention. Orchestrate Agents. Ship.",
49777
49789
  license: "PolyForm-Perimeter-1.0.0",
49778
49790
  type: "module",
@@ -0,0 +1,37 @@
1
+ -- FTS5 full-text search for chat messages (AI assistant conversations)
2
+ -- Indexes content and denormalized session_title for search
3
+ -- Excludes system role messages from index (system prompts pollute results)
4
+ CREATE VIRTUAL TABLE `chat_messages_fts` USING fts5(
5
+ content,
6
+ session_title,
7
+ content=chat_messages,
8
+ content_rowid=rowid
9
+ );--> statement-breakpoint
10
+ CREATE TRIGGER chat_messages_fts_ai AFTER INSERT ON `chat_messages` BEGIN
11
+ INSERT INTO chat_messages_fts(rowid, content, session_title)
12
+ SELECT new.rowid, new.content,
13
+ COALESCE((SELECT title FROM chat_sessions WHERE id = new.session_id), '')
14
+ WHERE new.role != 'system';
15
+ END;--> statement-breakpoint
16
+ CREATE TRIGGER chat_messages_fts_ad AFTER DELETE ON `chat_messages` BEGIN
17
+ INSERT INTO chat_messages_fts(chat_messages_fts, rowid, content, session_title)
18
+ SELECT 'delete', old.rowid, old.content,
19
+ COALESCE((SELECT title FROM chat_sessions WHERE id = old.session_id), '')
20
+ WHERE old.role != 'system';
21
+ END;--> statement-breakpoint
22
+ CREATE TRIGGER chat_messages_fts_au AFTER UPDATE ON `chat_messages` BEGIN
23
+ INSERT INTO chat_messages_fts(chat_messages_fts, rowid, content, session_title)
24
+ SELECT 'delete', old.rowid, old.content,
25
+ COALESCE((SELECT title FROM chat_sessions WHERE id = old.session_id), '')
26
+ WHERE old.role != 'system';
27
+ INSERT INTO chat_messages_fts(rowid, content, session_title)
28
+ SELECT new.rowid, new.content,
29
+ COALESCE((SELECT title FROM chat_sessions WHERE id = new.session_id), '')
30
+ WHERE new.role != 'system';
31
+ END;--> statement-breakpoint
32
+ -- Backfill chat_messages_fts from existing data (excluding system messages)
33
+ INSERT INTO chat_messages_fts(rowid, content, session_title)
34
+ SELECT m.rowid, m.content,
35
+ COALESCE((SELECT title FROM chat_sessions WHERE id = m.session_id), '')
36
+ FROM chat_messages m
37
+ WHERE m.role != 'system';
@@ -414,6 +414,13 @@
414
414
  "when": 1771152800000,
415
415
  "tag": "0058_unified_search_fts",
416
416
  "breakpoints": true
417
+ },
418
+ {
419
+ "idx": 59,
420
+ "version": "6",
421
+ "when": 1771239200000,
422
+ "tag": "0059_chat_messages_fts",
423
+ "breakpoints": true
417
424
  }
418
425
  ]
419
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowsuchagency/fulcrum",
3
- "version": "2.15.2",
3
+ "version": "2.16.0",
4
4
  "description": "Harness Attention. Orchestrate Agents. Ship.",
5
5
  "license": "PolyForm-Perimeter-1.0.0",
6
6
  "repository": {
package/server/index.js CHANGED
@@ -71229,9 +71229,10 @@ You have access to Fulcrum's MCP tools. Use them proactively to help users.
71229
71229
  - Use \`search\` with \`memoryTags: ["actionable"]\` to review tracked items
71230
71230
 
71231
71231
  **Unified Search:**
71232
- - \`search\` - Cross-entity FTS5 full-text search across tasks, projects, messages, events, and memories
71233
- - Filter by entity type: \`entities: ["tasks", "projects", "messages", "events", "memories"]\`
71234
- - Entity-specific filters: \`taskStatus\`, \`projectStatus\`, \`messageChannel\`, \`messageDirection\`, \`eventFrom\`, \`eventTo\`, \`memoryTags\`
71232
+ - \`search\` - Cross-entity FTS5 full-text search across tasks, projects, messages, events, memories, and conversations
71233
+ - Filter by entity type: \`entities: ["tasks", "projects", "messages", "events", "memories", "conversations"]\`
71234
+ - Entity-specific filters: \`taskStatus\`, \`projectStatus\`, \`messageChannel\`, \`messageDirection\`, \`eventFrom\`, \`eventTo\`, \`memoryTags\`, \`conversationRole\`, \`conversationProvider\`, \`conversationProjectId\`
71235
+ - Conversations search indexes AI assistant chat messages (excludes system prompts) with session context
71235
71236
  - Results sorted by relevance score with BM25 ranking
71236
71237
 
71237
71238
  **Memory File (Persistent Knowledge):**
@@ -71562,7 +71563,7 @@ Fulcrum is your digital concierge - a personal command center where you track ev
71562
71563
  - list_projects, create_project
71563
71564
  - execute_command (run any CLI command)
71564
71565
  - send_notification
71565
- - search (unified FTS5 search across tasks, projects, messages, events, memories)
71566
+ - search (unified FTS5 search across tasks, projects, messages, events, memories, conversations)
71566
71567
  - memory_file_read, memory_file_update (master memory file - always in prompt)
71567
71568
  - memory_store (ephemeral knowledge snippets with tags)
71568
71569
  - message (send to email/WhatsApp - concierge mode)
@@ -1200858,7 +1200859,7 @@ var init_slack = __esm(() => {
1200858
1200859
  });
1200859
1200860
 
1200860
1200861
  // server/services/channels/session-mapper.ts
1200861
- function getOrCreateSession(connectionId, channelUserId, channelUserName, sessionKey) {
1200862
+ function getOrCreateSession(connectionId, channelUserId, channelUserName, sessionKey, channelType) {
1200862
1200863
  const lookupKey = sessionKey ?? channelUserId;
1200863
1200864
  const now = new Date().toISOString();
1200864
1200865
  const existingMapping = db2.select().from(messagingSessionMappings).where(and(eq(messagingSessionMappings.connectionId, connectionId), eq(messagingSessionMappings.channelUserId, lookupKey))).get();
@@ -1200879,7 +1200880,7 @@ function getOrCreateSession(connectionId, channelUserId, channelUserName, sessio
1200879
1200880
  });
1200880
1200881
  }
1200881
1200882
  const sessionId = nanoid();
1200882
- const sessionTitle = channelUserName ? `Chat with ${channelUserName}` : `Chat ${channelUserId}`;
1200883
+ const sessionTitle = channelType ? `${channelType.charAt(0).toUpperCase() + channelType.slice(1)} Chat` : channelUserName ? `Chat with ${channelUserName}` : `Chat ${channelUserId}`;
1200883
1200884
  const newSession = {
1200884
1200885
  id: sessionId,
1200885
1200886
  title: sessionTitle,
@@ -1200920,12 +1200921,12 @@ function getOrCreateSession(connectionId, channelUserId, channelUserName, sessio
1200920
1200921
  });
1200921
1200922
  return { mapping, session: session3, isNew: true };
1200922
1200923
  }
1200923
- function resetSession(connectionId, channelUserId, channelUserName, sessionKey) {
1200924
+ function resetSession(connectionId, channelUserId, channelUserName, sessionKey, channelType) {
1200924
1200925
  const now = new Date().toISOString();
1200925
1200926
  const lookupKey = sessionKey ?? channelUserId;
1200926
1200927
  const existingMapping = db2.select().from(messagingSessionMappings).where(and(eq(messagingSessionMappings.connectionId, connectionId), eq(messagingSessionMappings.channelUserId, lookupKey))).get();
1200927
1200928
  const sessionId = nanoid();
1200928
- const sessionTitle = channelUserName ? `Chat with ${channelUserName}` : `Chat ${channelUserId}`;
1200929
+ const sessionTitle = channelType ? `${channelType.charAt(0).toUpperCase() + channelType.slice(1)} Chat` : channelUserName ? `Chat with ${channelUserName}` : `Chat ${channelUserId}`;
1200929
1200930
  const newSession = {
1200930
1200931
  id: sessionId,
1200931
1200932
  title: sessionTitle,
@@ -1200967,11 +1200968,43 @@ function resetSession(connectionId, channelUserId, channelUserName, sessionKey)
1200967
1200968
  function listSessionMappings(connectionId) {
1200968
1200969
  return db2.select().from(messagingSessionMappings).where(eq(messagingSessionMappings.connectionId, connectionId)).all();
1200969
1200970
  }
1200971
+ function migrateSessionTitles() {
1200972
+ const rows = db2.select({
1200973
+ sessionId: messagingSessionMappings.sessionId,
1200974
+ connectionId: messagingSessionMappings.connectionId,
1200975
+ sessionTitle: chatSessions.title
1200976
+ }).from(messagingSessionMappings).innerJoin(chatSessions, eq(messagingSessionMappings.sessionId, chatSessions.id)).where(or(like(chatSessions.title, "Chat with %"), like(chatSessions.title, "Chat %"))).all();
1200977
+ if (rows.length === 0)
1200978
+ return;
1200979
+ const dbConnections = db2.select({ id: messagingConnections.id, channelType: messagingConnections.channelType }).from(messagingConnections).all();
1200980
+ const dbConnectionMap = new Map(dbConnections.map((c) => [c.id, c.channelType]));
1200981
+ let updated = 0;
1200982
+ for (const row of rows) {
1200983
+ const channelType = CONNECTION_ID_TO_CHANNEL[row.connectionId] ?? dbConnectionMap.get(row.connectionId);
1200984
+ if (!channelType)
1200985
+ continue;
1200986
+ const newTitle = `${channelType.charAt(0).toUpperCase() + channelType.slice(1)} Chat`;
1200987
+ if (row.sessionTitle !== newTitle) {
1200988
+ db2.update(chatSessions).set({ title: newTitle }).where(eq(chatSessions.id, row.sessionId)).run();
1200989
+ updated++;
1200990
+ }
1200991
+ }
1200992
+ if (updated > 0) {
1200993
+ log2.messaging.info("Migrated channel session titles", { updated, total: rows.length });
1200994
+ }
1200995
+ }
1200996
+ var CONNECTION_ID_TO_CHANNEL;
1200970
1200997
  var init_session_mapper = __esm(() => {
1200971
1200998
  init_nanoid();
1200972
1200999
  init_drizzle_orm();
1200973
1201000
  init_db2();
1200974
1201001
  init_logger3();
1201002
+ CONNECTION_ID_TO_CHANNEL = {
1201003
+ "slack-channel": "slack",
1201004
+ "discord-channel": "discord",
1201005
+ "telegram-channel": "telegram",
1201006
+ "email-channel": "email"
1201007
+ };
1200975
1201008
  });
1200976
1201009
 
1200977
1201010
  // server/services/channels/system-prompts.ts
@@ -1201547,7 +1201580,7 @@ async function handleIncomingMessage(msg) {
1201547
1201580
  return;
1201548
1201581
  }
1201549
1201582
  const emailThreadId = msg.channelType === "email" ? msg.metadata?.threadId : undefined;
1201550
- const { session: session3 } = getOrCreateSession(msg.connectionId, msg.senderId, msg.senderName, emailThreadId);
1201583
+ const { session: session3 } = getOrCreateSession(msg.connectionId, msg.senderId, msg.senderName, emailThreadId, msg.channelType);
1201551
1201584
  log2.messaging.info("Routing message to assistant", {
1201552
1201585
  connectionId: msg.connectionId,
1201553
1201586
  senderId: msg.senderId,
@@ -1201585,7 +1201618,7 @@ async function handleIncomingMessage(msg) {
1201585
1201618
  }
1201586
1201619
  }
1201587
1201620
  async function handleResetCommand(msg) {
1201588
- resetSession(msg.connectionId, msg.senderId, msg.senderName);
1201621
+ resetSession(msg.connectionId, msg.senderId, msg.senderName, undefined, msg.channelType);
1201589
1201622
  if (msg.channelType === "slack") {
1201590
1201623
  const blocks = [
1201591
1201624
  {
@@ -1201659,7 +1201692,7 @@ Just send any message and I'll do my best to help!`;
1201659
1201692
  }
1201660
1201693
  async function handleStatusCommand(msg) {
1201661
1201694
  const emailThreadId = msg.channelType === "email" ? msg.metadata?.threadId : undefined;
1201662
- const { session: session3, mapping } = getOrCreateSession(msg.connectionId, msg.senderId, msg.senderName, emailThreadId);
1201695
+ const { session: session3, mapping } = getOrCreateSession(msg.connectionId, msg.senderId, msg.senderName, emailThreadId, msg.channelType);
1201663
1201696
  if (msg.channelType === "slack") {
1201664
1201697
  const blocks = [
1201665
1201698
  {
@@ -1201753,7 +1201786,7 @@ function splitMessage(content, maxLength) {
1201753
1201786
  }
1201754
1201787
  async function processObserveOnlyMessage(msg) {
1201755
1201788
  const observeSessionKey = `observe-${msg.connectionId}`;
1201756
- const { session: session3 } = getOrCreateSession(msg.connectionId, observeSessionKey, "Observer");
1201789
+ const { session: session3 } = getOrCreateSession(msg.connectionId, observeSessionKey, "Observer", undefined, msg.channelType);
1201757
1201790
  const settings = getSettings();
1201758
1201791
  const observerProvider = settings.assistant.observerProvider ?? settings.assistant.provider;
1201759
1201792
  if (observerProvider === "opencode") {
@@ -1202097,6 +1202130,7 @@ var init_channels = __esm(() => {
1202097
1202130
  init_telegram();
1202098
1202131
  init_slack();
1202099
1202132
  init_message_handler();
1202133
+ init_session_mapper();
1202100
1202134
  init_channel_manager();
1202101
1202135
  init_message_handler();
1202102
1202136
  init_whatsapp();
@@ -1202105,6 +1202139,7 @@ var init_channels = __esm(() => {
1202105
1202139
  init_slack();
1202106
1202140
  init_email3();
1202107
1202141
  init_session_mapper();
1202142
+ migrateSessionTitles();
1202108
1202143
  });
1202109
1202144
 
1202110
1202145
  // node_modules/cross-fetch/dist/node-ponyfill.js
@@ -1214359,7 +1214394,7 @@ async function updateTaskStatus(taskId, newStatus, newPosition) {
1214359
1214394
  // server/services/search-service.ts
1214360
1214395
  init_db2();
1214361
1214396
  init_drizzle_orm();
1214362
- var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories"];
1214397
+ var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories", "conversations"];
1214363
1214398
  async function search(options) {
1214364
1214399
  const entities = options.entities?.length ? options.entities : ALL_ENTITIES;
1214365
1214400
  const limit = options.limit ?? 10;
@@ -1214375,6 +1214410,8 @@ async function search(options) {
1214375
1214410
  return searchEvents(options.query, { from: options.eventFrom, to: options.eventTo }, limit);
1214376
1214411
  case "memories":
1214377
1214412
  return searchMemories(options.query, { tags: options.memoryTags }, limit);
1214413
+ case "conversations":
1214414
+ return searchConversations(options.query, { role: options.conversationRole, provider: options.conversationProvider, projectId: options.conversationProjectId }, limit);
1214378
1214415
  }
1214379
1214416
  });
1214380
1214417
  const resultSets = await Promise.all(searches);
@@ -1214541,6 +1214578,43 @@ async function searchMemories(query, filters, limit) {
1214541
1214578
  }
1214542
1214579
  }));
1214543
1214580
  }
1214581
+ async function searchConversations(query, filters, limit) {
1214582
+ const roleFilter = filters.role ? sql`AND m.role = ${filters.role}` : sql`AND m.role != 'system'`;
1214583
+ const providerFilter = filters.provider ? sql`AND s.provider = ${filters.provider}` : sql``;
1214584
+ const projectFilter = filters.projectId ? sql`AND s.project_id = ${filters.projectId}` : sql``;
1214585
+ const rows = db2.all(sql`SELECT m.id, m.content, m.role, m.session_id as "sessionId",
1214586
+ s.title as "sessionTitle", s.provider, s.project_id as "projectId",
1214587
+ m.created_at as "createdAt",
1214588
+ bm25(chat_messages_fts, 5.0, 2.0) as rank
1214589
+ FROM chat_messages_fts fts
1214590
+ JOIN chat_messages m ON m.rowid = fts.rowid
1214591
+ JOIN chat_sessions s ON s.id = m.session_id
1214592
+ WHERE chat_messages_fts MATCH ${query}
1214593
+ ${roleFilter}
1214594
+ ${providerFilter}
1214595
+ ${projectFilter}
1214596
+ ORDER BY bm25(chat_messages_fts, 5.0, 2.0)
1214597
+ LIMIT ${limit}`);
1214598
+ if (!rows.length)
1214599
+ return [];
1214600
+ const minRank = Math.min(...rows.map((r) => r.rank));
1214601
+ const maxRank = Math.max(...rows.map((r) => r.rank));
1214602
+ const range2 = maxRank - minRank || 1;
1214603
+ return rows.map((r) => ({
1214604
+ entityType: "conversation",
1214605
+ id: r.id,
1214606
+ title: r.sessionTitle || "Untitled conversation",
1214607
+ snippet: r.content.slice(0, 200),
1214608
+ score: 1 - (r.rank - minRank) / range2,
1214609
+ metadata: {
1214610
+ sessionId: r.sessionId,
1214611
+ role: r.role,
1214612
+ provider: r.provider,
1214613
+ projectId: r.projectId || undefined,
1214614
+ createdAt: r.createdAt
1214615
+ }
1214616
+ }));
1214617
+ }
1214544
1214618
  function reindexTaskFTS(taskId) {
1214545
1214619
  db2.run(sql`
1214546
1214620
  UPDATE tasks SET updated_at = updated_at WHERE id = ${taskId}
@@ -1259560,11 +1259634,11 @@ var registerMemoryFileTools = (server, client) => {
1259560
1259634
  };
1259561
1259635
 
1259562
1259636
  // cli/src/mcp/tools/search.ts
1259563
- var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories"]);
1259637
+ var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
1259564
1259638
  var registerSearchTools = (server, client) => {
1259565
- server.tool("search", 'Search across all Fulcrum entities (tasks, projects, messages, calendar events, memories) using full-text search. Supports boolean operators (AND, OR, NOT), phrase matching ("quoted phrases"), and prefix matching (term*). Returns results ranked by relevance.', {
1259639
+ server.tool("search", 'Search across all Fulcrum entities (tasks, projects, messages, calendar events, memories, conversations) using full-text search. Supports boolean operators (AND, OR, NOT), phrase matching ("quoted phrases"), and prefix matching (term*). Returns results ranked by relevance.', {
1259566
1259640
  query: exports_external.string().describe('FTS5 search query. Supports: AND, OR, NOT operators, "quoted phrases", prefix* matching. Example: "kubernetes deployment" OR k8s'),
1259567
- entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories"),
1259641
+ entities: exports_external.optional(exports_external.array(EntityTypeSchema)).describe("Entity types to search. Defaults to all: tasks, projects, messages, events, memories, conversations"),
1259568
1259642
  limit: exports_external.optional(exports_external.number()).describe("Maximum results per entity type (default: 10)"),
1259569
1259643
  taskStatus: exports_external.optional(exports_external.array(exports_external.string())).describe('Filter tasks by status (e.g., ["IN_PROGRESS", "TO_DO"])'),
1259570
1259644
  projectStatus: exports_external.optional(exports_external.enum(["active", "archived"])).describe("Filter projects by status"),
@@ -1259572,8 +1259646,11 @@ var registerSearchTools = (server, client) => {
1259572
1259646
  messageDirection: exports_external.optional(exports_external.enum(["incoming", "outgoing"])).describe("Filter messages by direction"),
1259573
1259647
  eventFrom: exports_external.optional(exports_external.string()).describe("Filter calendar events starting from this date (ISO 8601)"),
1259574
1259648
  eventTo: exports_external.optional(exports_external.string()).describe("Filter calendar events up to this date (ISO 8601)"),
1259575
- memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags")
1259576
- }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags }) => {
1259649
+ memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags"),
1259650
+ conversationRole: exports_external.optional(exports_external.string()).describe('Filter conversations by role (e.g., "user", "assistant")'),
1259651
+ conversationProvider: exports_external.optional(exports_external.string()).describe('Filter conversations by provider (e.g., "claude", "opencode")'),
1259652
+ conversationProjectId: exports_external.optional(exports_external.string()).describe("Filter conversations by project ID")
1259653
+ }, async ({ query, entities, limit: limit2, taskStatus, projectStatus, messageChannel, messageDirection, eventFrom, eventTo, memoryTags, conversationRole, conversationProvider, conversationProjectId }) => {
1259577
1259654
  try {
1259578
1259655
  const results = await client.search({
1259579
1259656
  query,
@@ -1259585,7 +1259662,10 @@ var registerSearchTools = (server, client) => {
1259585
1259662
  messageDirection,
1259586
1259663
  eventFrom,
1259587
1259664
  eventTo,
1259588
- memoryTags
1259665
+ memoryTags,
1259666
+ conversationRole,
1259667
+ conversationProvider,
1259668
+ conversationProjectId
1259589
1259669
  });
1259590
1259670
  return formatSuccess(results);
1259591
1259671
  } catch (err) {
@@ -1260409,6 +1260489,12 @@ class FulcrumClient {
1260409
1260489
  params.set("eventTo", input.eventTo);
1260410
1260490
  if (input.memoryTags?.length)
1260411
1260491
  params.set("memoryTags", input.memoryTags.join(","));
1260492
+ if (input.conversationRole)
1260493
+ params.set("conversationRole", input.conversationRole);
1260494
+ if (input.conversationProvider)
1260495
+ params.set("conversationProvider", input.conversationProvider);
1260496
+ if (input.conversationProjectId)
1260497
+ params.set("conversationProjectId", input.conversationProjectId);
1260412
1260498
  return this.fetch(`/api/search?${params.toString()}`);
1260413
1260499
  }
1260414
1260500
  }
@@ -1260424,7 +1260510,7 @@ mcpRoutes.all("/", async (c) => {
1260424
1260510
  });
1260425
1260511
  const server = new McpServer({
1260426
1260512
  name: "fulcrum",
1260427
- version: "2.15.2"
1260513
+ version: "2.16.0"
1260428
1260514
  });
1260429
1260515
  const client = new FulcrumClient(`http://localhost:${port}`);
1260430
1260516
  registerTools(server, client);
@@ -1263058,6 +1263144,9 @@ app28.get("/", async (c) => {
1263058
1263144
  const eventTo = c.req.query("eventTo") || undefined;
1263059
1263145
  const memoryTagsParam = c.req.query("memoryTags");
1263060
1263146
  const memoryTags = memoryTagsParam ? memoryTagsParam.split(",").map((t) => t.trim()) : undefined;
1263147
+ const conversationRole = c.req.query("conversationRole") || undefined;
1263148
+ const conversationProvider = c.req.query("conversationProvider") || undefined;
1263149
+ const conversationProjectId = c.req.query("conversationProjectId") || undefined;
1263061
1263150
  try {
1263062
1263151
  const results = await search({
1263063
1263152
  query: query2.trim(),
@@ -1263069,7 +1263158,10 @@ app28.get("/", async (c) => {
1263069
1263158
  messageDirection,
1263070
1263159
  eventFrom,
1263071
1263160
  eventTo,
1263072
- memoryTags
1263161
+ memoryTags,
1263162
+ conversationRole,
1263163
+ conversationProvider,
1263164
+ conversationProjectId
1263073
1263165
  });
1263074
1263166
  return c.json(results);
1263075
1263167
  } catch (err) {
@@ -1263528,13 +1263620,16 @@ async function checkDatabaseAvailable() {
1263528
1263620
  encoding: "utf-8"
1263529
1263621
  }).trim();
1263530
1263622
  if (result) {
1263531
- const pid = parseInt(result.split(`
1263532
- `)[0], 10);
1263533
- return {
1263534
- available: false,
1263535
- error: `Database is already in use by process ${pid}: ${dbPath}`,
1263536
- pid
1263537
- };
1263623
+ const myPid = process.pid;
1263624
+ const otherPids = result.split(`
1263625
+ `).map(Number).filter((pid) => pid !== myPid);
1263626
+ if (otherPids.length > 0) {
1263627
+ return {
1263628
+ available: false,
1263629
+ error: `Database is already in use by process ${otherPids[0]}: ${dbPath}`,
1263630
+ pid: otherPids[0]
1263631
+ };
1263632
+ }
1263538
1263633
  }
1263539
1263634
  } catch {}
1263540
1263635
  return { available: true };