@knowsuchagency/fulcrum 2.15.3 → 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.3"
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.3",
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.3",
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.3",
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)
@@ -1214393,7 +1214394,7 @@ async function updateTaskStatus(taskId, newStatus, newPosition) {
1214393
1214394
  // server/services/search-service.ts
1214394
1214395
  init_db2();
1214395
1214396
  init_drizzle_orm();
1214396
- var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories"];
1214397
+ var ALL_ENTITIES = ["tasks", "projects", "messages", "events", "memories", "conversations"];
1214397
1214398
  async function search(options) {
1214398
1214399
  const entities = options.entities?.length ? options.entities : ALL_ENTITIES;
1214399
1214400
  const limit = options.limit ?? 10;
@@ -1214409,6 +1214410,8 @@ async function search(options) {
1214409
1214410
  return searchEvents(options.query, { from: options.eventFrom, to: options.eventTo }, limit);
1214410
1214411
  case "memories":
1214411
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);
1214412
1214415
  }
1214413
1214416
  });
1214414
1214417
  const resultSets = await Promise.all(searches);
@@ -1214575,6 +1214578,43 @@ async function searchMemories(query, filters, limit) {
1214575
1214578
  }
1214576
1214579
  }));
1214577
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
+ }
1214578
1214618
  function reindexTaskFTS(taskId) {
1214579
1214619
  db2.run(sql`
1214580
1214620
  UPDATE tasks SET updated_at = updated_at WHERE id = ${taskId}
@@ -1259594,11 +1259634,11 @@ var registerMemoryFileTools = (server, client) => {
1259594
1259634
  };
1259595
1259635
 
1259596
1259636
  // cli/src/mcp/tools/search.ts
1259597
- var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories"]);
1259637
+ var EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations"]);
1259598
1259638
  var registerSearchTools = (server, client) => {
1259599
- 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.', {
1259600
1259640
  query: exports_external.string().describe('FTS5 search query. Supports: AND, OR, NOT operators, "quoted phrases", prefix* matching. Example: "kubernetes deployment" OR k8s'),
1259601
- 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"),
1259602
1259642
  limit: exports_external.optional(exports_external.number()).describe("Maximum results per entity type (default: 10)"),
1259603
1259643
  taskStatus: exports_external.optional(exports_external.array(exports_external.string())).describe('Filter tasks by status (e.g., ["IN_PROGRESS", "TO_DO"])'),
1259604
1259644
  projectStatus: exports_external.optional(exports_external.enum(["active", "archived"])).describe("Filter projects by status"),
@@ -1259606,8 +1259646,11 @@ var registerSearchTools = (server, client) => {
1259606
1259646
  messageDirection: exports_external.optional(exports_external.enum(["incoming", "outgoing"])).describe("Filter messages by direction"),
1259607
1259647
  eventFrom: exports_external.optional(exports_external.string()).describe("Filter calendar events starting from this date (ISO 8601)"),
1259608
1259648
  eventTo: exports_external.optional(exports_external.string()).describe("Filter calendar events up to this date (ISO 8601)"),
1259609
- memoryTags: exports_external.optional(exports_external.array(exports_external.string())).describe("Filter memories by tags")
1259610
- }, 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 }) => {
1259611
1259654
  try {
1259612
1259655
  const results = await client.search({
1259613
1259656
  query,
@@ -1259619,7 +1259662,10 @@ var registerSearchTools = (server, client) => {
1259619
1259662
  messageDirection,
1259620
1259663
  eventFrom,
1259621
1259664
  eventTo,
1259622
- memoryTags
1259665
+ memoryTags,
1259666
+ conversationRole,
1259667
+ conversationProvider,
1259668
+ conversationProjectId
1259623
1259669
  });
1259624
1259670
  return formatSuccess(results);
1259625
1259671
  } catch (err) {
@@ -1260443,6 +1260489,12 @@ class FulcrumClient {
1260443
1260489
  params.set("eventTo", input.eventTo);
1260444
1260490
  if (input.memoryTags?.length)
1260445
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);
1260446
1260498
  return this.fetch(`/api/search?${params.toString()}`);
1260447
1260499
  }
1260448
1260500
  }
@@ -1260458,7 +1260510,7 @@ mcpRoutes.all("/", async (c) => {
1260458
1260510
  });
1260459
1260511
  const server = new McpServer({
1260460
1260512
  name: "fulcrum",
1260461
- version: "2.15.3"
1260513
+ version: "2.16.0"
1260462
1260514
  });
1260463
1260515
  const client = new FulcrumClient(`http://localhost:${port}`);
1260464
1260516
  registerTools(server, client);
@@ -1263092,6 +1263144,9 @@ app28.get("/", async (c) => {
1263092
1263144
  const eventTo = c.req.query("eventTo") || undefined;
1263093
1263145
  const memoryTagsParam = c.req.query("memoryTags");
1263094
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;
1263095
1263150
  try {
1263096
1263151
  const results = await search({
1263097
1263152
  query: query2.trim(),
@@ -1263103,7 +1263158,10 @@ app28.get("/", async (c) => {
1263103
1263158
  messageDirection,
1263104
1263159
  eventFrom,
1263105
1263160
  eventTo,
1263106
- memoryTags
1263161
+ memoryTags,
1263162
+ conversationRole,
1263163
+ conversationProvider,
1263164
+ conversationProjectId
1263107
1263165
  });
1263108
1263166
  return c.json(results);
1263109
1263167
  } catch (err) {