@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 +1 -1
- package/bin/fulcrum.js +21 -9
- package/drizzle/0059_chat_messages_fts.sql +37 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/server/index.js +123 -28
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
1263532
|
-
`
|
|
1263533
|
-
|
|
1263534
|
-
|
|
1263535
|
-
|
|
1263536
|
-
|
|
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 };
|