@rubytech/create-maxy-code 0.1.140 → 0.1.143
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/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts +6 -4
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +10 -23
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/index.ts +10 -20
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +6 -1
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +6 -1
- package/payload/platform/neo4j/edge-annotations.json +3 -3
- package/payload/platform/neo4j/schema.cypher +81 -119
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +14 -8
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +10 -7
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
- package/payload/platform/plugins/docs/references/neo4j.md +3 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +3 -7
- package/payload/platform/plugins/email/mcp/dist/index.js +7 -46
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +4 -15
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +51 -43
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +223 -229
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +7 -4
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +14 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +69 -84
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +49 -64
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +1 -2
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +12 -13
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +17 -15
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +13 -7
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +101 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +2 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +5 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +12 -10
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +7 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +3 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +47 -39
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +7 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +12 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +59 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +326 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +31 -27
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +72 -49
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +5 -6
- package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +1 -1
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +16 -15
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +8 -8
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +14 -13
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +13 -9
- package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +11 -9
- package/payload/platform/plugins/notion-import/skills/notion-import/references/page-tree.md +12 -12
- package/payload/platform/plugins/scheduling/PLUGIN.md +18 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +93 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts +80 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js +395 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts +16 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js +111 -2
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts +20 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js +63 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js.map +1 -0
- package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/x-import/PLUGIN.md +33 -0
- package/payload/platform/plugins/x-import/references/archive-shape.md +124 -0
- package/payload/platform/plugins/x-import/references/transcript-shape.md +121 -0
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +123 -0
- package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
- package/payload/platform/templates/specialists/agents/librarian.md +4 -4
- package/payload/server/public/assets/{admin-BM9aXUUh.js → admin-_pm0RRTE.js} +1 -1
- package/payload/server/public/assets/{data-Ds2ECtfP.js → data-b6q64Ot4.js} +1 -1
- package/payload/server/public/assets/{graph-BzxObKMM.js → graph-D1SVlBLb.js} +1 -1
- package/payload/server/public/assets/graph-labels-BiDGzVs2.js +1 -0
- package/payload/server/public/assets/{page-BlkBsjP2.js → page-BV6E4J2q.js} +1 -1
- package/payload/server/public/assets/{page-h_SA5SVQ.js → page-DwFigpyR.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +18 -21
- package/payload/server/public/assets/graph-labels-Dwgep_nr.js +0 -1
|
@@ -15,131 +15,212 @@ export function stripAngleBrackets(value) {
|
|
|
15
15
|
}
|
|
16
16
|
return trimmed;
|
|
17
17
|
}
|
|
18
|
+
/** Globally-unique attachmentId for a thread, scoped by account + root messageId. */
|
|
19
|
+
function deriveThreadAttachmentId(accountId, rootMessageId) {
|
|
20
|
+
return createHash("sha256")
|
|
21
|
+
.update(`email:${accountId}:${rootMessageId}`)
|
|
22
|
+
.digest("hex");
|
|
23
|
+
}
|
|
24
|
+
/** Fallback attachmentId for a message with no Message-ID header. */
|
|
25
|
+
function deriveFallbackAttachmentId(data) {
|
|
26
|
+
return createHash("sha256")
|
|
27
|
+
.update(`email-fallback:${data.accountId}:${data.uidValidity}:${data.uid}:${data.mailbox}`)
|
|
28
|
+
.digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function normaliseAddress(addr) {
|
|
31
|
+
return addr.trim().toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
function renderMessageBlock(data, messageId) {
|
|
34
|
+
const headers = [
|
|
35
|
+
`Message-ID: ${messageId}`,
|
|
36
|
+
`From: ${data.fromName ? `${data.fromName} <${data.fromAddress}>` : data.fromAddress}`,
|
|
37
|
+
`To: ${data.toAddresses.join(", ")}`,
|
|
38
|
+
data.ccAddresses.length > 0 ? `Cc: ${data.ccAddresses.join(", ")}` : null,
|
|
39
|
+
`Date: ${data.receivedAt}`,
|
|
40
|
+
`Subject: ${data.subject}`,
|
|
41
|
+
]
|
|
42
|
+
.filter((line) => line !== null)
|
|
43
|
+
.join("\n");
|
|
44
|
+
return `--- BEGIN MESSAGE ---\n${headers}\n\n${data.bodyPreview}\n--- END MESSAGE ---`;
|
|
45
|
+
}
|
|
18
46
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
47
|
+
* Find the thread KD that already contains the given message-id in its
|
|
48
|
+
* messageIds array, scoped to one account. Returns null when no match
|
|
49
|
+
* (the message starts a new thread or is an out-of-order reply whose
|
|
50
|
+
* parent thread has not yet been ingested).
|
|
22
51
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
52
|
+
async function findThreadByMessageId(messageId, accountId) {
|
|
53
|
+
const session = getSession();
|
|
54
|
+
try {
|
|
55
|
+
const result = await session.run(`MATCH (kd:KnowledgeDocument {source: 'email', accountId: $accountId})
|
|
56
|
+
WHERE $messageId IN kd.messageIds
|
|
57
|
+
RETURN kd.attachmentId AS attachmentId, kd.body AS body,
|
|
58
|
+
kd.messageIds AS messageIds,
|
|
59
|
+
kd.messages AS messagesJson,
|
|
60
|
+
kd.firstReceivedAt AS firstReceivedAt,
|
|
61
|
+
kd.lastReceivedAt AS lastReceivedAt
|
|
62
|
+
LIMIT 1`, { messageId, accountId });
|
|
63
|
+
if (result.records.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const record = result.records[0];
|
|
66
|
+
return {
|
|
67
|
+
attachmentId: record.get("attachmentId"),
|
|
68
|
+
body: record.get("body"),
|
|
69
|
+
messageIds: record.get("messageIds"),
|
|
70
|
+
messagesJson: record.get("messagesJson") ?? "[]",
|
|
71
|
+
firstReceivedAt: record.get("firstReceivedAt"),
|
|
72
|
+
lastReceivedAt: record.get("lastReceivedAt"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await session.close();
|
|
28
77
|
}
|
|
29
|
-
return createHash("sha256")
|
|
30
|
-
.update(`uid:${data.uidValidity}:${data.uid}:${data.mailbox}:${data.recipientAddress}`)
|
|
31
|
-
.digest("hex");
|
|
32
78
|
}
|
|
33
79
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
80
|
+
* Ingest one IMAP message into the per-thread :KnowledgeDocument.
|
|
81
|
+
*
|
|
82
|
+
* - If the message's In-Reply-To points to a known thread KD, the
|
|
83
|
+
* message is appended (body grows, messageIds grows, lastReceivedAt
|
|
84
|
+
* advances).
|
|
85
|
+
* - Otherwise, a new thread KD is created with this message as root.
|
|
37
86
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* - Back-fills any orphaned emails whose inReplyTo references this email's messageId
|
|
87
|
+
* Person, address-edge, anchor, and embedding writes always run.
|
|
88
|
+
* Discarded-screening messages skip embedding to keep vector search clean.
|
|
41
89
|
*/
|
|
42
|
-
export async function
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const existing = await checkSession.run(`MATCH (e:Email {messageId: $messageId, accountId: $accountId})
|
|
54
|
-
RETURN e.emailId AS emailId LIMIT 1`, { messageId: normalisedMessageId, accountId: data.accountId });
|
|
55
|
-
if (existing.records.length > 0) {
|
|
56
|
-
const existingEmailId = existing.records[0].get("emailId");
|
|
57
|
-
console.error(`[email-fetch] INFO: messageId ${normalisedMessageId} already exists for account ${data.accountId} — skipping (emailId=${existingEmailId})`);
|
|
58
|
-
return { emailId: existingEmailId, created: false };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
await checkSession.close();
|
|
63
|
-
}
|
|
90
|
+
export async function storeEmailMessage(data) {
|
|
91
|
+
const messageId = stripAngleBrackets(data.messageId) ?? `synthetic:${deriveFallbackAttachmentId(data)}`;
|
|
92
|
+
const inReplyTo = stripAngleBrackets(data.inReplyTo);
|
|
93
|
+
const existing = inReplyTo ? await findThreadByMessageId(inReplyTo, data.accountId) : null;
|
|
94
|
+
const attachmentId = existing
|
|
95
|
+
? existing.attachmentId
|
|
96
|
+
: deriveThreadAttachmentId(data.accountId, messageId);
|
|
97
|
+
// Already ingested? Idempotent skip — caught by messageIds-contains check.
|
|
98
|
+
if (existing && existing.messageIds.includes(messageId)) {
|
|
99
|
+
return { attachmentId, created: false, appended: false };
|
|
64
100
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
101
|
+
// Build new body + metadata (append or create).
|
|
102
|
+
const messageBlock = renderMessageBlock(data, messageId);
|
|
103
|
+
const newBody = existing ? `${existing.body}\n\n${messageBlock}` : messageBlock;
|
|
104
|
+
const newMessageIds = existing ? [...existing.messageIds, messageId] : [messageId];
|
|
105
|
+
const newEnvelope = {
|
|
106
|
+
messageId,
|
|
107
|
+
fromAddress: data.fromAddress,
|
|
108
|
+
fromName: data.fromName,
|
|
109
|
+
toAddresses: data.toAddresses,
|
|
110
|
+
ccAddresses: data.ccAddresses,
|
|
111
|
+
subject: data.subject,
|
|
112
|
+
receivedAt: data.receivedAt,
|
|
113
|
+
uid: data.uid,
|
|
114
|
+
mailbox: data.mailbox,
|
|
115
|
+
};
|
|
116
|
+
const existingMessages = existing
|
|
117
|
+
? JSON.parse(existing.messagesJson)
|
|
118
|
+
: [];
|
|
119
|
+
const newMessages = [...existingMessages, newEnvelope];
|
|
120
|
+
const newLastReceivedAt = existing && data.receivedAt < existing.lastReceivedAt
|
|
121
|
+
? existing.lastReceivedAt
|
|
122
|
+
: data.receivedAt;
|
|
123
|
+
const newFirstReceivedAt = existing && data.receivedAt > existing.firstReceivedAt
|
|
124
|
+
? existing.firstReceivedAt
|
|
125
|
+
: data.receivedAt;
|
|
126
|
+
// Embedding: skip discarded messages so vector search stays clean.
|
|
67
127
|
let embedding = [];
|
|
68
128
|
if (data.screening !== "discarded") {
|
|
69
129
|
const embeddingText = [data.subject, data.bodyPreview].filter(Boolean).join(" — ");
|
|
70
130
|
embedding = await computeEmbedding(embeddingText);
|
|
71
131
|
}
|
|
132
|
+
const nowIso = new Date().toISOString();
|
|
133
|
+
// Collect addresses for Person/edge MERGE. Sender first so :FROM gets a
|
|
134
|
+
// displayName when only the sender carries one.
|
|
135
|
+
const addressRows = [
|
|
136
|
+
{ address: data.fromAddress, displayName: data.fromName, edge: "FROM" },
|
|
137
|
+
...data.toAddresses.map((address) => ({
|
|
138
|
+
address,
|
|
139
|
+
displayName: null,
|
|
140
|
+
edge: "TO",
|
|
141
|
+
})),
|
|
142
|
+
...data.ccAddresses.map((address) => ({
|
|
143
|
+
address,
|
|
144
|
+
displayName: null,
|
|
145
|
+
edge: "CC",
|
|
146
|
+
})),
|
|
147
|
+
]
|
|
148
|
+
.filter((row) => row.address && row.address.length > 0)
|
|
149
|
+
.map((row) => ({ ...row, address: normaliseAddress(row.address) }));
|
|
72
150
|
const session = getSession();
|
|
73
151
|
try {
|
|
74
|
-
|
|
152
|
+
await session.run(`MERGE (kd:KnowledgeDocument {attachmentId: $attachmentId})
|
|
75
153
|
ON CREATE SET
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
MERGE (e)-[:RECEIVED_BY]->(ea)
|
|
95
|
-
RETURN e.fetchedAt = $fetchedAt AS created`, {
|
|
96
|
-
emailId,
|
|
97
|
-
messageId: normalisedMessageId,
|
|
98
|
-
inReplyTo: normalisedInReplyTo,
|
|
99
|
-
toAddresses: data.toAddresses,
|
|
100
|
-
ccAddresses: data.ccAddresses,
|
|
101
|
-
recipientAddress: data.recipientAddress,
|
|
102
|
-
fromAddress: data.fromAddress,
|
|
103
|
-
fromName: data.fromName,
|
|
104
|
-
subject: data.subject,
|
|
105
|
-
bodyPreview: data.bodyPreview.slice(0, 4096),
|
|
106
|
-
receivedAt: data.receivedAt,
|
|
107
|
-
fetchedAt: new Date().toISOString(),
|
|
154
|
+
kd.source = 'email',
|
|
155
|
+
kd.scope = 'admin',
|
|
156
|
+
kd.accountId = $accountId,
|
|
157
|
+
kd.subject = $subject,
|
|
158
|
+
kd.createdAt = $nowIso,
|
|
159
|
+
kd.firstReceivedAt = $firstReceivedAt,
|
|
160
|
+
kd.screening = $screening,
|
|
161
|
+
kd.screeningReason = $screeningReason,
|
|
162
|
+
kd.promptInjectionFlag = $promptInjectionFlag
|
|
163
|
+
SET kd.body = $body,
|
|
164
|
+
kd.messageIds = $messageIds,
|
|
165
|
+
kd.messages = $messagesJson,
|
|
166
|
+
kd.lastReceivedAt = $lastReceivedAt,
|
|
167
|
+
kd.updatedAt = $nowIso
|
|
168
|
+
WITH kd
|
|
169
|
+
MERGE (ea:EmailAccount {accountId: $accountId})
|
|
170
|
+
MERGE (kd)-[:RECEIVED_BY]->(ea)`, {
|
|
171
|
+
attachmentId,
|
|
108
172
|
accountId: data.accountId,
|
|
173
|
+
subject: data.subject,
|
|
174
|
+
body: newBody,
|
|
175
|
+
messageIds: newMessageIds,
|
|
176
|
+
messagesJson: JSON.stringify(newMessages),
|
|
177
|
+
firstReceivedAt: newFirstReceivedAt,
|
|
178
|
+
lastReceivedAt: newLastReceivedAt,
|
|
179
|
+
nowIso,
|
|
109
180
|
screening: data.screening ?? null,
|
|
110
181
|
screeningReason: data.screeningReason ?? null,
|
|
111
182
|
promptInjectionFlag: data.promptInjectionFlag ?? null,
|
|
112
183
|
});
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
// Persons + address edges. UNWIND so one round-trip handles every address.
|
|
185
|
+
if (addressRows.length > 0) {
|
|
186
|
+
await session.run(`UNWIND $rows AS row
|
|
187
|
+
MERGE (p:Person {email: row.address})
|
|
188
|
+
ON CREATE SET p.displayName = row.displayName,
|
|
189
|
+
p.scope = 'admin',
|
|
190
|
+
p.accountId = $accountId,
|
|
191
|
+
p.createdAt = $nowIso
|
|
192
|
+
WITH p, row
|
|
193
|
+
MATCH (kd:KnowledgeDocument {attachmentId: $attachmentId})
|
|
194
|
+
CALL {
|
|
195
|
+
WITH kd, p, row
|
|
196
|
+
WITH kd, p, row WHERE row.edge = 'FROM'
|
|
197
|
+
MERGE (kd)-[:FROM]->(p)
|
|
198
|
+
}
|
|
199
|
+
CALL {
|
|
200
|
+
WITH kd, p, row
|
|
201
|
+
WITH kd, p, row WHERE row.edge = 'TO'
|
|
202
|
+
MERGE (kd)-[:TO]->(p)
|
|
203
|
+
}
|
|
204
|
+
CALL {
|
|
205
|
+
WITH kd, p, row
|
|
206
|
+
WITH kd, p, row WHERE row.edge = 'CC'
|
|
207
|
+
MERGE (kd)-[:CC]->(p)
|
|
208
|
+
}`, { rows: addressRows, attachmentId, accountId: data.accountId, nowIso });
|
|
129
209
|
}
|
|
130
|
-
|
|
131
|
-
|
|
210
|
+
// Embedding written separately so the large vector doesn't go through the
|
|
211
|
+
// main MERGE params (some Neo4j versions reject very-large Cypher params).
|
|
212
|
+
if (embedding.length > 0) {
|
|
213
|
+
await session.run(`MATCH (kd:KnowledgeDocument {attachmentId: $attachmentId})
|
|
214
|
+
SET kd.embedding = $embedding`, { attachmentId, embedding });
|
|
132
215
|
}
|
|
133
|
-
return {
|
|
216
|
+
return { attachmentId, created: !existing, appended: !!existing };
|
|
134
217
|
}
|
|
135
218
|
catch (err) {
|
|
136
|
-
// Constraint violation from email_message_id_account_unique — race condition
|
|
137
|
-
// where two concurrent storeEmail calls passed the pre-check before either
|
|
138
|
-
// MERGE completed. Treat as a graceful duplicate skip.
|
|
139
219
|
if (err instanceof neo4j.Neo4jError &&
|
|
140
220
|
err.code === "Neo.ClientError.Schema.ConstraintValidationFailed") {
|
|
141
|
-
|
|
142
|
-
|
|
221
|
+
// Race: two concurrent writes for the same attachmentId. Treat as duplicate.
|
|
222
|
+
console.error(`[email-fetch] INFO: constraint caught duplicate attachmentId=${attachmentId} accountId=${data.accountId}`);
|
|
223
|
+
return { attachmentId, created: false, appended: false };
|
|
143
224
|
}
|
|
144
225
|
throw err;
|
|
145
226
|
}
|
|
@@ -148,140 +229,58 @@ export async function storeEmail(data) {
|
|
|
148
229
|
}
|
|
149
230
|
}
|
|
150
231
|
/**
|
|
151
|
-
*
|
|
232
|
+
* Ingest a batch of IMAP messages. Returns aggregate counts.
|
|
233
|
+
*
|
|
234
|
+
* Threads are intentionally not reordered — messages are processed in
|
|
235
|
+
* input order. Callers that want chronological transcript shape should
|
|
236
|
+
* pre-sort by `receivedAt` ascending.
|
|
152
237
|
*/
|
|
153
|
-
export async function
|
|
154
|
-
let
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
|
|
238
|
+
export async function storeEmailMessages(messages) {
|
|
239
|
+
let threadsCreated = 0;
|
|
240
|
+
let messagesAppended = 0;
|
|
241
|
+
let duplicates = 0;
|
|
242
|
+
for (const message of messages) {
|
|
243
|
+
const result = await storeEmailMessage(message);
|
|
158
244
|
if (result.created) {
|
|
159
|
-
|
|
245
|
+
threadsCreated++;
|
|
160
246
|
}
|
|
161
|
-
else {
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { created, skipped };
|
|
166
|
-
}
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Thread linking
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
/**
|
|
171
|
-
* Create a REPLY_TO edge from child to parent.
|
|
172
|
-
* Looks up the parent Email by messageId within the same account.
|
|
173
|
-
* Uses MERGE for idempotency — safe to call multiple times.
|
|
174
|
-
*/
|
|
175
|
-
async function linkReplyTo(childEmailId, inReplyTo, accountId) {
|
|
176
|
-
const session = getSession();
|
|
177
|
-
try {
|
|
178
|
-
const result = await session.run(`MATCH (child:Email {emailId: $childEmailId})
|
|
179
|
-
MATCH (parent:Email {messageId: $inReplyTo, accountId: $accountId})
|
|
180
|
-
WHERE child <> parent
|
|
181
|
-
MERGE (child)-[:REPLY_TO]->(parent)
|
|
182
|
-
RETURN parent.emailId AS parentEmailId`, { childEmailId, inReplyTo, accountId });
|
|
183
|
-
if (result.records.length > 0) {
|
|
184
|
-
const parentId = result.records[0].get("parentEmailId");
|
|
185
|
-
console.error(`[email-fetch] INFO: linked REPLY_TO edge: ${childEmailId} → ${parentId}`);
|
|
247
|
+
else if (result.appended) {
|
|
248
|
+
messagesAppended++;
|
|
186
249
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
await session.close();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Back-fill REPLY_TO edges for orphaned emails.
|
|
194
|
-
* When a new email arrives, check if any existing emails have an inReplyTo
|
|
195
|
-
* matching this email's messageId (out-of-order delivery). Create edges
|
|
196
|
-
* for any matches within the same account.
|
|
197
|
-
*/
|
|
198
|
-
async function backfillOrphans(messageId, accountId) {
|
|
199
|
-
const session = getSession();
|
|
200
|
-
try {
|
|
201
|
-
const result = await session.run(`MATCH (parent:Email {messageId: $messageId, accountId: $accountId})
|
|
202
|
-
MATCH (orphan:Email {inReplyTo: $messageId, accountId: $accountId})
|
|
203
|
-
WHERE NOT (orphan)-[:REPLY_TO]->(parent)
|
|
204
|
-
AND orphan <> parent
|
|
205
|
-
MERGE (orphan)-[:REPLY_TO]->(parent)
|
|
206
|
-
RETURN count(orphan) AS linked`, { messageId, accountId });
|
|
207
|
-
const linked = neo4j.integer.toNumber(result.records[0]?.get("linked") ?? 0);
|
|
208
|
-
if (linked > 0) {
|
|
209
|
-
console.error(`[email-fetch] INFO: back-filled ${linked} orphaned REPLY_TO edge(s) for messageId=${messageId}`);
|
|
250
|
+
else {
|
|
251
|
+
duplicates++;
|
|
210
252
|
}
|
|
211
253
|
}
|
|
212
|
-
|
|
213
|
-
await session.close();
|
|
214
|
-
}
|
|
254
|
+
return { threadsCreated, messagesAppended, duplicates };
|
|
215
255
|
}
|
|
216
256
|
// ---------------------------------------------------------------------------
|
|
217
|
-
//
|
|
257
|
+
// Read helpers — used by email-reply, email-read, email-search to look
|
|
258
|
+
// individual messages and threads back out of the KD model.
|
|
218
259
|
// ---------------------------------------------------------------------------
|
|
219
260
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* email-auto-respond.ts manages its own standalone Neo4j connection.
|
|
224
|
-
*
|
|
225
|
-
* Embedding is computed inline — if Ollama is down, stored without one.
|
|
261
|
+
* Look up a single message envelope by its Message-ID. Walks the thread
|
|
262
|
+
* KD's `messages` array (JSON-encoded). Returns null when the thread KD
|
|
263
|
+
* has not been ingested yet.
|
|
226
264
|
*/
|
|
227
|
-
export async function
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
const session = driver.session();
|
|
237
|
-
try {
|
|
238
|
-
// Create the outbound Email node + REPLY_TO edge to original in one query
|
|
239
|
-
await session.run(`MERGE (e:Email {emailId: $emailId})
|
|
240
|
-
ON CREATE SET
|
|
241
|
-
e.messageId = $messageId,
|
|
242
|
-
e.inReplyTo = $inReplyTo,
|
|
243
|
-
e.direction = 'outbound',
|
|
244
|
-
e.toAddresses = $toAddresses,
|
|
245
|
-
e.ccAddresses = [],
|
|
246
|
-
e.fromAddress = $fromAddress,
|
|
247
|
-
e.subject = $subject,
|
|
248
|
-
e.bodyPreview = $bodyPreview,
|
|
249
|
-
e.receivedAt = $sentAt,
|
|
250
|
-
e.fetchedAt = $sentAt,
|
|
251
|
-
e.accountId = $accountId,
|
|
252
|
-
e.scope = 'admin'
|
|
253
|
-
WITH e
|
|
254
|
-
MATCH (original:Email {emailId: $originalEmailId})
|
|
255
|
-
MERGE (e)-[:REPLY_TO]->(original)`, {
|
|
256
|
-
emailId,
|
|
257
|
-
messageId: normalisedMessageId,
|
|
258
|
-
inReplyTo: normalisedInReplyTo,
|
|
259
|
-
toAddresses: data.toAddresses,
|
|
260
|
-
fromAddress: data.fromAddress,
|
|
261
|
-
subject: data.subject,
|
|
262
|
-
bodyPreview: data.bodyPreview.slice(0, 4096),
|
|
263
|
-
sentAt: new Date().toISOString(),
|
|
264
|
-
accountId: data.accountId,
|
|
265
|
-
originalEmailId: data.originalEmailId,
|
|
266
|
-
});
|
|
267
|
-
if (embedding.length > 0) {
|
|
268
|
-
await session.run(`MATCH (e:Email {emailId: $emailId})
|
|
269
|
-
WHERE e.embedding IS NULL
|
|
270
|
-
SET e.embedding = $embedding`, { emailId, embedding });
|
|
271
|
-
}
|
|
272
|
-
return { emailId };
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
await session.close();
|
|
276
|
-
}
|
|
265
|
+
export async function getMessageEnvelope(messageId, accountId) {
|
|
266
|
+
const normalised = stripAngleBrackets(messageId);
|
|
267
|
+
if (!normalised)
|
|
268
|
+
return null;
|
|
269
|
+
const thread = await findThreadByMessageId(normalised, accountId);
|
|
270
|
+
if (!thread)
|
|
271
|
+
return null;
|
|
272
|
+
const messages = JSON.parse(thread.messagesJson);
|
|
273
|
+
return messages.find((m) => m.messageId === normalised) ?? null;
|
|
277
274
|
}
|
|
278
275
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* the thread
|
|
282
|
-
*
|
|
276
|
+
* Given a set of messageIds, locate each one inside its thread KD and
|
|
277
|
+
* report:
|
|
278
|
+
* - threadId — the thread KD's attachmentId
|
|
279
|
+
* - threadDepth — position of the message in the KD's messageIds array
|
|
280
|
+
* (0 = root). Useful for sorting search hits by thread
|
|
281
|
+
* position.
|
|
283
282
|
*
|
|
284
|
-
*
|
|
283
|
+
* Messages whose thread KD has not been ingested are absent from the map.
|
|
285
284
|
*/
|
|
286
285
|
export async function getThreadInfo(messageIds, accountId) {
|
|
287
286
|
if (messageIds.length === 0)
|
|
@@ -293,18 +292,13 @@ export async function getThreadInfo(messageIds, accountId) {
|
|
|
293
292
|
return new Map();
|
|
294
293
|
const session = getSession();
|
|
295
294
|
try {
|
|
296
|
-
//
|
|
297
|
-
//
|
|
295
|
+
// Index-of-mid is computed inline with a list comprehension so the query
|
|
296
|
+
// does not depend on the apoc plugin being installed.
|
|
298
297
|
const result = await session.run(`UNWIND $messageIds AS mid
|
|
299
|
-
MATCH (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
CASE WHEN path IS NULL THEN 0 ELSE length(path) END AS depth,
|
|
304
|
-
CASE WHEN path IS NULL THEN e ELSE ancestor END AS root
|
|
305
|
-
ORDER BY mid, depth DESC
|
|
306
|
-
WITH mid, head(collect(depth)) AS threadDepth, head(collect(root)).emailId AS threadId
|
|
307
|
-
WHERE threadDepth > 0
|
|
298
|
+
MATCH (kd:KnowledgeDocument {source: 'email', accountId: $accountId})
|
|
299
|
+
WHERE mid IN kd.messageIds
|
|
300
|
+
WITH mid, kd.attachmentId AS threadId,
|
|
301
|
+
[i IN range(0, size(kd.messageIds) - 1) WHERE kd.messageIds[i] = mid | i][0] AS threadDepth
|
|
308
302
|
RETURN mid AS messageId, threadDepth, threadId`, { messageIds: normalised, accountId });
|
|
309
303
|
const map = new Map();
|
|
310
304
|
for (const record of result.records) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,7 +31,7 @@ import { resolve } from "node:path";
|
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import { loadCredentials, readPollState, updatePollState } from "../lib/credentials.js";
|
|
33
33
|
import { fetchSinceUid } from "../lib/imap.js";
|
|
34
|
-
import {
|
|
34
|
+
import { storeEmailMessages } from "../lib/graph.js";
|
|
35
35
|
import { closeDriver } from "../lib/neo4j.js";
|
|
36
36
|
import { screenEmail } from "../lib/screening.js";
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -209,9 +209,12 @@ async function main() {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
log("INFO", `classification summary: ${countClean} clean, ${countSuspicious} suspicious, ${countDiscard} discarded`);
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
// Sort by receivedAt ascending so thread transcripts read chronologically
|
|
213
|
+
// (storeEmailMessage appends in input order).
|
|
214
|
+
emailData.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
|
|
215
|
+
// Store in graph as :KnowledgeDocument {source:'email'} per thread.
|
|
216
|
+
const { threadsCreated, messagesAppended, duplicates } = await storeEmailMessages(emailData);
|
|
217
|
+
log("INFO", `stored ${threadsCreated} new threads, ${messagesAppended} messages appended to existing threads, ${duplicates} duplicates skipped`);
|
|
215
218
|
// Update high-water mark
|
|
216
219
|
await updatePollState(accountId, maxUid, uidValidity);
|
|
217
220
|
log("INFO", `high-water mark updated to UID ${maxUid}, uidValidity=${uidValidity}`);
|