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