@datasynx/agentic-crm 1.3.0 → 1.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.
Files changed (29) hide show
  1. package/README.md +10 -0
  2. package/dist/{attachments-rLa96rOK.js → attachments-BddHbCt8.js} +51 -32
  3. package/dist/{attachments-D207gXfN.js.map → attachments-BddHbCt8.js.map} +1 -1
  4. package/dist/{attachments-D207gXfN.js → attachments-Co3kXIvu.js} +46 -31
  5. package/dist/{attachments-rLa96rOK.js.map → attachments-Co3kXIvu.js.map} +1 -1
  6. package/dist/{attachments-CX2GAtsw.cjs → attachments-Dbe7Bidz.cjs} +46 -31
  7. package/dist/{attachments-CX2GAtsw.cjs.map → attachments-Dbe7Bidz.cjs.map} +1 -1
  8. package/dist/attachments-YQKYmg6N.js +2 -0
  9. package/dist/cli.js +61 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/daemon/worker.js +1 -1
  12. package/dist/{gmail-sync-DIbrPnTK.js → gmail-sync-BHLa8v51.js} +2 -2
  13. package/dist/{gmail-sync-DIbrPnTK.js.map → gmail-sync-BHLa8v51.js.map} +1 -1
  14. package/dist/{gmail-sync-BpSVESSe.cjs → gmail-sync-CodrUNR4.cjs} +2 -2
  15. package/dist/{gmail-sync-BpSVESSe.cjs.map → gmail-sync-CodrUNR4.cjs.map} +1 -1
  16. package/dist/{gmail-sync-B4Iu3AQb.js → gmail-sync-SvECok5p.js} +2 -2
  17. package/dist/{gmail-sync-B4Iu3AQb.js.map → gmail-sync-SvECok5p.js.map} +1 -1
  18. package/dist/imap-o6PRuBvm.js +270 -0
  19. package/dist/imap-o6PRuBvm.js.map +1 -0
  20. package/dist/{index-DMTVVYwr.d.cts → index-Dspvybo0.d.cts} +22 -22
  21. package/dist/index-Dspvybo0.d.cts.map +1 -0
  22. package/dist/index.d.cts +22 -22
  23. package/dist/index.d.cts.map +1 -1
  24. package/dist/mcp.cjs +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/dist/{server-BhNLrnAD.js → server-uqXUhF4H.js} +3 -3
  27. package/dist/{server-BhNLrnAD.js.map → server-uqXUhF4H.js.map} +1 -1
  28. package/package.json +4 -1
  29. package/dist/index-DMTVVYwr.d.cts.map +0 -1
@@ -39,7 +39,7 @@ async function syncAllCustomers() {
39
39
  const credPath = path.join(DATA_DIR, ".agentic", "gmail-credentials.json");
40
40
  if (fs.existsSync(tokenPath) && fs.existsSync(credPath)) {
41
41
  const { getGmailAuth } = await import("../gmail-auth-OComS92L.js");
42
- const { syncGmail } = await import("../gmail-sync-B4Iu3AQb.js");
42
+ const { syncGmail } = await import("../gmail-sync-SvECok5p.js");
43
43
  const auth = await getGmailAuth(credPath, tokenPath);
44
44
  await syncWithBackoff(async () => {
45
45
  const result = await syncGmail({
@@ -177,7 +177,7 @@ async function syncGmail(opts) {
177
177
  const emailSummary = await summarizeEmail(subject, body, from);
178
178
  let attachmentLinks = [];
179
179
  if (includeAttachments) try {
180
- const { processMessageAttachments } = await import("./attachments-D207gXfN.js");
180
+ const { processMessageAttachments } = await import("./attachments-Co3kXIvu.js");
181
181
  attachmentLinks = (await processMessageAttachments({
182
182
  gmail: gmail$1,
183
183
  dataDir: opts.dataDir,
@@ -245,4 +245,4 @@ function sleep(ms) {
245
245
  //#endregion
246
246
  export { syncGmail };
247
247
 
248
- //# sourceMappingURL=gmail-sync-DIbrPnTK.js.map
248
+ //# sourceMappingURL=gmail-sync-BHLa8v51.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gmail-sync-DIbrPnTK.js","names":["gmail","gmailApi"],"sources":["../src/schemas/agent-config.ts","../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const AgentConfigSchema = z.object({\n slug: z.string().min(1),\n channel: z.enum([\"telegram\"]),\n wakeOn: z.array(z.enum([\"email\", \"calendar\"])).default([\"email\"]),\n createdAt: z.string(),\n lastWake: z.string().nullable().default(null),\n telegramChatId: z.string().optional(),\n});\n\nexport type AgentConfig = z.infer<typeof AgentConfigSchema>;\n","// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gmail-sync-BHLa8v51.js","names":["gmail","gmailApi"],"sources":["../src/schemas/agent-config.ts","../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const AgentConfigSchema = z.object({\n slug: z.string().min(1),\n channel: z.enum([\"telegram\"]),\n wakeOn: z.array(z.enum([\"email\", \"calendar\"])).default([\"email\"]),\n createdAt: z.string(),\n lastWake: z.string().nullable().default(null),\n telegramChatId: z.string().optional(),\n});\n\nexport type AgentConfig = z.infer<typeof AgentConfigSchema>;\n","// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
@@ -181,7 +181,7 @@ async function syncGmail(opts) {
181
181
  const emailSummary = await summarizeEmail(subject, body, from);
182
182
  let attachmentLinks = [];
183
183
  if (includeAttachments) try {
184
- const { processMessageAttachments } = await Promise.resolve().then(() => require("./attachments-CX2GAtsw.cjs"));
184
+ const { processMessageAttachments } = await Promise.resolve().then(() => require("./attachments-Dbe7Bidz.cjs"));
185
185
  attachmentLinks = (await processMessageAttachments({
186
186
  gmail,
187
187
  dataDir: opts.dataDir,
@@ -249,4 +249,4 @@ function sleep(ms) {
249
249
  //#endregion
250
250
  exports.syncGmail = syncGmail;
251
251
 
252
- //# sourceMappingURL=gmail-sync-BpSVESSe.cjs.map
252
+ //# sourceMappingURL=gmail-sync-CodrUNR4.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"gmail-sync-BpSVESSe.cjs","names":["z","summarizeEmail","readInteractions","appendInteraction"],"sources":["../src/schemas/agent-config.ts","../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const AgentConfigSchema = z.object({\n slug: z.string().min(1),\n channel: z.enum([\"telegram\"]),\n wakeOn: z.array(z.enum([\"email\", \"calendar\"])).default([\"email\"]),\n createdAt: z.string(),\n lastWake: z.string().nullable().default(null),\n telegramChatId: z.string().optional(),\n});\n\nexport type AgentConfig = z.infer<typeof AgentConfigSchema>;\n","// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gmail-sync-CodrUNR4.cjs","names":["z","summarizeEmail","readInteractions","appendInteraction"],"sources":["../src/schemas/agent-config.ts","../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const AgentConfigSchema = z.object({\n slug: z.string().min(1),\n channel: z.enum([\"telegram\"]),\n wakeOn: z.array(z.enum([\"email\", \"calendar\"])).default([\"email\"]),\n createdAt: z.string(),\n lastWake: z.string().nullable().default(null),\n telegramChatId: z.string().optional(),\n});\n\nexport type AgentConfig = z.infer<typeof AgentConfigSchema>;\n","// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
@@ -167,7 +167,7 @@ async function syncGmail(opts) {
167
167
  const emailSummary = await summarizeEmail(subject, body, from);
168
168
  let attachmentLinks = [];
169
169
  if (includeAttachments) try {
170
- const { processMessageAttachments } = await import("./attachments-rLa96rOK.js");
170
+ const { processMessageAttachments } = await import("./attachments-YQKYmg6N.js");
171
171
  attachmentLinks = (await processMessageAttachments({
172
172
  gmail: gmail$1,
173
173
  dataDir: opts.dataDir,
@@ -235,4 +235,4 @@ function sleep(ms) {
235
235
  //#endregion
236
236
  export { syncGmail };
237
237
 
238
- //# sourceMappingURL=gmail-sync-B4Iu3AQb.js.map
238
+ //# sourceMappingURL=gmail-sync-SvECok5p.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gmail-sync-B4Iu3AQb.js","names":["gmail","gmailApi"],"sources":["../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gmail-sync-SvECok5p.js","names":["gmail","gmailApi"],"sources":["../src/core/agent-notifier.ts","../src/sync/gmail-sync.ts"],"sourcesContent":["// src/core/agent-notifier.ts\n// Sends a Telegram wake notification when a new inbound email from a customer\n// domain is detected and an agent config exists for that customer slug.\n// All errors are swallowed — this is a notification feature and must never\n// crash the core loop.\n\nimport fs from \"fs\";\nimport https from \"https\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\nimport { AgentConfigSchema, type AgentConfig } from \"../schemas/agent-config.js\";\nimport { summarizeEmail } from \"./llm.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface WakeContext {\n trigger: \"email\" | \"calendar\";\n subject: string;\n from: string;\n snippet: string;\n}\n\n// ─── Agent config helpers ─────────────────────────────────────────────────────\n\nfunction agentConfigPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n}\n\nfunction readAgentConfig(dataDir: string, slug: string): AgentConfig | null {\n const p = agentConfigPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n const result = AgentConfigSchema.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nfunction writeLastWake(dataDir: string, slug: string, config: AgentConfig): void {\n const p = agentConfigPath(dataDir, slug);\n try {\n const updated: AgentConfig = { ...config, lastWake: new Date().toISOString() };\n writeJsonFile(p, updated);\n } catch {\n // non-fatal — just a housekeeping write\n }\n}\n\n// ─── Telegram transport ───────────────────────────────────────────────────────\n\nfunction sendTelegramMessage(token: string, chatId: string, text: string): Promise<void> {\n const body = JSON.stringify({ chat_id: chatId, text, parse_mode: \"Markdown\" });\n return new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n// ─── Message builder ──────────────────────────────────────────────────────────\n\nfunction buildWakeMessage(\n slug: string,\n subject: string,\n summary: string,\n nextSteps: string[]\n): string {\n const suggestedAction = nextSteps[0] ?? \"Follow up within 24h\";\n return (\n `📧 New email from **${slug}**: ${subject}\\n` +\n `${summary}\\n\\n` +\n `💡 Suggested action: ${suggestedAction}`\n );\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget notification: reads the agent config for `slug`, summarises\n * the inbound email with the LLM, and sends a Telegram message.\n *\n * Silently returns (no throw) when:\n * - no agent config exists for the slug\n * - TELEGRAM_BOT_TOKEN env var is not set\n * - no chat id is available (neither in config nor in TELEGRAM_CHAT_ID env var)\n * - any HTTPS / LLM error occurs\n */\nexport async function notifyAgentWake(\n dataDir: string,\n slug: string,\n context: WakeContext\n): Promise<void> {\n try {\n // 1. Read agent config — bail silently if not found\n const config = readAgentConfig(dataDir, slug);\n if (!config) return;\n\n // 2. Check for Telegram token — bail silently if absent\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n if (!token) return;\n\n // 3. Determine chat id — config takes precedence, fallback to env var\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"];\n if (!chatId) return;\n\n // 4. Summarise the email (LLM, with fallback built into summarizeEmail itself)\n const emailSummary = await summarizeEmail(context.subject, context.snippet, context.from);\n\n // 5. Build and send the Telegram message\n const text = buildWakeMessage(\n slug,\n context.subject,\n emailSummary.summary,\n emailSummary.nextSteps\n );\n await sendTelegramMessage(token, chatId, text);\n\n // 6. Update lastWake on success\n writeLastWake(dataDir, slug, config);\n } catch {\n // Swallow all errors — this is a notification feature, never crashes core loop\n }\n}\n","// src/sync/gmail-sync.ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { gmail as gmailApi, type gmail_v1 } from \"@googleapis/gmail\";\nimport type { OAuth2Client } from \"google-auth-library\";\nimport { readInteractions, appendInteraction } from \"../fs/interactions-writer.js\";\nimport { notifyAgentWake } from \"../core/agent-notifier.js\";\nimport { logger } from \"../core/logger.js\";\n\ninterface SyncOptions {\n slug: string;\n dataDir: string;\n auth: OAuth2Client;\n query: string;\n since?: Date;\n maxPages?: number;\n /** Download, convert and index email attachments (default true). */\n includeAttachments?: boolean;\n /** Per-attachment size cap in bytes. */\n maxAttachmentBytes?: number;\n}\n\n/**\n * Retry a function with exponential backoff on any error.\n * Delays: 1s, 2s, 4s, 8s … (2^attempt seconds), up to maxRetries retries.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n const delayMs = 1000 * Math.pow(2, attempt);\n await sleep(delayMs);\n attempt++;\n }\n }\n}\n\nexport async function syncGmail(opts: SyncOptions): Promise<{ synced: number; skipped: number }> {\n const gmail = gmailApi({ version: \"v1\", auth: opts.auth });\n const maxPages = opts.maxPages ?? 5;\n const includeAttachments = opts.includeAttachments ?? true;\n\n let q = opts.query;\n if (opts.since) {\n const after = Math.floor(opts.since.getTime() / 1000);\n q += ` after:${after}`;\n }\n\n // Collect all message stubs across pages (Task A — pagination)\n const allMessages: Array<{ id?: string | null; threadId?: string | null }> = [];\n let pageToken: string | undefined = undefined;\n let pagesFetched = 0;\n\n do {\n const listResp: { data: gmail_v1.Schema$ListMessagesResponse } =\n await gmail.users.messages.list({\n userId: \"me\",\n q,\n maxResults: 200,\n ...(pageToken ? { pageToken } : {}),\n });\n const pageMessages = listResp.data.messages ?? [];\n allMessages.push(...pageMessages);\n pageToken = listResp.data.nextPageToken ?? undefined;\n pagesFetched++;\n } while (pageToken && pagesFetched < maxPages);\n\n // Read existing interactions once before the loop — avoids O(messages) file reads\n let existingContent = await readInteractions(opts.dataDir, opts.slug);\n\n let synced = 0;\n let skipped = 0;\n\n for (const msg of allMessages) {\n if (!msg.id) continue;\n\n const source = `gmail://thread/${msg.threadId ?? msg.id}`;\n\n if (existingContent.includes(source)) {\n skipped++;\n continue;\n }\n\n // Rate limiting ~10 req/s\n await sleep(100);\n\n // Task B — exponential backoff retry on any error\n let msgData: gmail_v1.Schema$Message;\n try {\n const detail = await retryWithBackoff(() =>\n gmail.users.messages.get({\n userId: \"me\",\n id: msg.id!,\n // \"full\" exposes payload.parts so attachments can be downloaded;\n // fall back to lighter \"metadata\" when attachment sync is disabled.\n ...(includeAttachments\n ? { format: \"full\" }\n : { format: \"metadata\", metadataHeaders: [\"Subject\", \"From\", \"Date\"] }),\n })\n );\n msgData = detail.data;\n } catch (err) {\n logger.warn(\"gmail-sync\", \"skipping message after retries\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n skipped++;\n continue;\n }\n\n const headers = msgData.payload?.headers ?? [];\n const subject = headers.find((h) => h.name === \"Subject\")?.value ?? \"(no subject)\";\n const from = headers.find((h) => h.name === \"From\")?.value ?? \"\";\n const dateStr = headers.find((h) => h.name === \"Date\")?.value;\n const date = dateStr\n ? new Date(dateStr).toISOString().slice(0, 10)\n : new Date().toISOString().slice(0, 10);\n const snippet = msgData.snippet ?? \"\";\n\n // Extract the full inline body (plain preferred, else HTML->Markdown) so\n // summaries and search cover the whole message, not just the snippet.\n const { extractEmailBodyMarkdown } = await import(\"./email-body.js\");\n const body = (await extractEmailBodyMarkdown(msgData.payload ?? undefined)) || snippet;\n\n // LLM summary — non-blocking fallback to raw body/snippet if no API key or error\n const { summarizeEmail } = await import(\"../core/llm.js\");\n const emailSummary = await summarizeEmail(subject, body, from);\n\n // Download, convert and index attachments before logging the interaction so\n // the entry can link to the generated Markdown. Failures here are swallowed.\n let attachmentLinks: string[] = [];\n if (includeAttachments) {\n try {\n const { processMessageAttachments } = await import(\"./attachments.js\");\n const saved = await processMessageAttachments({\n gmail,\n dataDir: opts.dataDir,\n slug: opts.slug,\n messageId: msg.id,\n source,\n payload: msgData.payload ?? undefined,\n date,\n ...(opts.maxAttachmentBytes !== undefined\n ? { maxBytes: opts.maxAttachmentBytes }\n : {}),\n });\n attachmentLinks = saved.map((a) => a.markdownName);\n } catch (err) {\n logger.warn(\"gmail-sync\", \"attachment processing failed\", {\n messageId: msg.id,\n error: (err as Error).message,\n });\n }\n }\n\n await appendInteraction(opts.dataDir, opts.slug, {\n date,\n type: \"Email\",\n direction: detectDirection(from),\n with: from,\n subject,\n summary: emailSummary.summary,\n nextSteps: emailSummary.nextSteps,\n ...(attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {}),\n sourceRef: source,\n synced: new Date().toISOString(),\n });\n\n // Append to in-memory string so within-batch duplicates are detected\n existingContent += source;\n\n // Index the full email (subject + body) into LanceDB for semantic search,\n // chunked so long threads stay searchable (non-blocking).\n const { indexInLanceDB } = await import(\"../core/lancedb.js\");\n const { chunkText } = await import(\"../core/chunk.js\");\n const bodyChunks = chunkText(`${subject}\\n${body}`);\n for (let i = 0; i < bodyChunks.length; i++) {\n const ref = i === 0 ? source : `${source}#${i}`;\n await indexInLanceDB(opts.dataDir, opts.slug, bodyChunks[i]!, ref, {\n date,\n type: \"Email\",\n }).catch((err: unknown) => {\n logger.error(\"gmail-sync\", \"LanceDB index failed\", { error: (err as Error).message });\n });\n }\n\n // Agent wake: notify if an agent config exists for this customer (fire-and-forget)\n if (agentConfigExists(opts.dataDir, opts.slug)) {\n notifyAgentWake(opts.dataDir, opts.slug, {\n trigger: \"email\",\n subject,\n from,\n snippet,\n }).catch(() => {\n // Notification is non-blocking; swallow all errors\n });\n }\n\n synced++;\n }\n\n return { synced, skipped };\n}\n\nfunction agentConfigExists(dataDir: string, slug: string): boolean {\n const configPath = path.join(dataDir, \".agentic\", \"agents\", `${slug}.agent.json`);\n return fs.existsSync(configPath);\n}\n\nfunction detectDirection(_from: string): \"inbound\" | \"outbound\" {\n return \"inbound\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ import { i as listCustomerSlugs } from "./customer-dir-CkMMXhb0.js";
2
+ import { i as readInteractions, n as appendInteraction } from "./interactions-writer-B8XAzdqR.js";
3
+ import { n as logger } from "./logger-Dyl4VcLO.js";
4
+ import { t as chunkText } from "./chunk-BhUZmQg5.js";
5
+ import { r as persistAttachment } from "./attachments-BddHbCt8.js";
6
+ import { n as htmlToMarkdown } from "./html-BaeOCZKE.js";
7
+ import path from "path";
8
+ import fs from "fs";
9
+ import matter from "gray-matter";
10
+ //#region src/sync/email-ingest.ts
11
+ /**
12
+ * Ingest one normalized email into a customer: convert + index its
13
+ * attachments, summarize it, append the interaction (with attachment links),
14
+ * and index the full body (chunked) for semantic search. Caller is responsible
15
+ * for deduplication (skip messages whose sourceRef is already logged).
16
+ */
17
+ async function ingestEmail(dataDir, slug, msg, options = {}) {
18
+ const includeAttachments = options.includeAttachments ?? true;
19
+ const maxBytes = options.maxAttachmentBytes ?? 26214400;
20
+ const attachmentLinks = [];
21
+ if (includeAttachments) for (const att of msg.attachments) {
22
+ if (att.content.length > maxBytes) {
23
+ logger.warn("email-ingest", "skipping oversized attachment", {
24
+ filename: att.filename,
25
+ bytes: att.content.length
26
+ });
27
+ continue;
28
+ }
29
+ try {
30
+ const saved = await persistAttachment({
31
+ dataDir,
32
+ slug,
33
+ messageId: msg.messageId,
34
+ source: msg.sourceRef,
35
+ date: msg.date,
36
+ filename: att.filename,
37
+ mimeType: att.mimeType,
38
+ buffer: att.content
39
+ });
40
+ attachmentLinks.push(saved.markdownName);
41
+ } catch (err) {
42
+ logger.warn("email-ingest", "attachment failed", {
43
+ filename: att.filename,
44
+ error: err.message
45
+ });
46
+ }
47
+ }
48
+ const { summarizeEmail } = await import("./llm-DSX1-wFu.js");
49
+ const summary = await summarizeEmail(msg.subject, msg.bodyMarkdown, msg.from);
50
+ await appendInteraction(dataDir, slug, {
51
+ date: msg.date,
52
+ type: "Email",
53
+ direction: options.direction ?? "inbound",
54
+ with: msg.from,
55
+ subject: msg.subject,
56
+ summary: summary.summary,
57
+ nextSteps: summary.nextSteps,
58
+ ...attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {},
59
+ sourceRef: msg.sourceRef,
60
+ synced: (/* @__PURE__ */ new Date()).toISOString()
61
+ });
62
+ const { indexInLanceDB } = await import("./lancedb-CswQEE5K.js");
63
+ const bodyChunks = chunkText(`${msg.subject}\n${msg.bodyMarkdown}`);
64
+ for (let i = 0; i < bodyChunks.length; i++) {
65
+ const ref = i === 0 ? msg.sourceRef : `${msg.sourceRef}#${i}`;
66
+ await indexInLanceDB(dataDir, slug, bodyChunks[i], ref, {
67
+ date: msg.date,
68
+ type: "Email"
69
+ }).catch((err) => {
70
+ logger.error("email-ingest", "LanceDB index failed", { error: err.message });
71
+ });
72
+ }
73
+ return {
74
+ attachments: attachmentLinks.length,
75
+ chunks: bodyChunks.length
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region src/sync/email-router.ts
80
+ /** The domain part of an email address, lowercased (empty string if malformed). */
81
+ function domainOf(email) {
82
+ const at = email.lastIndexOf("@");
83
+ return at >= 0 ? email.slice(at + 1).trim().toLowerCase() : "";
84
+ }
85
+ /** Read just the routing-relevant fields from a customer's main_facts (tolerant). */
86
+ function readRoutingFields(dataDir, slug) {
87
+ const file = path.join(dataDir, "customers", slug, "main_facts.md");
88
+ if (!fs.existsSync(file)) return {};
89
+ try {
90
+ const data = matter(fs.readFileSync(file, "utf-8")).data;
91
+ return {
92
+ domain: typeof data["domain"] === "string" ? data["domain"] : void 0,
93
+ email: typeof data["email"] === "string" ? data["email"] : void 0,
94
+ primary_contact: typeof data["primary_contact"] === "string" ? data["primary_contact"] : void 0
95
+ };
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+ /**
101
+ * Build the routing table from every customer's main_facts. A customer is
102
+ * identified by its `domain`, `email`, and `primary_contact` (when it looks
103
+ * like an email). Customers without any identifier are still listed (empty
104
+ * arrays) so callers can see them, but they never match.
105
+ */
106
+ function buildRoutingTable(dataDir) {
107
+ return listCustomerSlugs(dataDir).map((slug) => {
108
+ const facts = readRoutingFields(dataDir, slug);
109
+ const domains = /* @__PURE__ */ new Set();
110
+ const emails = /* @__PURE__ */ new Set();
111
+ if (facts.domain) domains.add(facts.domain.trim().toLowerCase());
112
+ for (const candidate of [facts.email, facts.primary_contact]) if (candidate && candidate.includes("@")) {
113
+ const addr = candidate.trim().toLowerCase();
114
+ emails.add(addr);
115
+ const d = domainOf(addr);
116
+ if (d) domains.add(d);
117
+ }
118
+ return {
119
+ slug,
120
+ domains: [...domains],
121
+ emails: [...emails]
122
+ };
123
+ });
124
+ }
125
+ /**
126
+ * Route a message to a customer slug by matching any of its addresses
127
+ * (from/to/cc) against the routing table. Exact email matches win over domain
128
+ * matches. Returns the matched slug, or null when nothing matches (the message
129
+ * is "unrouted").
130
+ */
131
+ function routeMessage(addresses, table) {
132
+ const addrs = addresses.map((a) => a.trim().toLowerCase()).filter((a) => a.includes("@"));
133
+ if (addrs.length === 0) return null;
134
+ const domains = new Set(addrs.map(domainOf).filter(Boolean));
135
+ for (const c of table) if (c.emails.some((e) => addrs.includes(e))) return c.slug;
136
+ for (const c of table) if (c.domains.some((d) => domains.has(d))) return c.slug;
137
+ return null;
138
+ }
139
+ //#endregion
140
+ //#region src/sync/connectors/imap.ts
141
+ /** Build a real ImapFlow client. Loaded lazily so the dep stays off hot paths. */
142
+ async function defaultClientFactory(config) {
143
+ const { ImapFlow } = await import("imapflow");
144
+ const auth = config.auth.accessToken ? {
145
+ user: config.auth.user,
146
+ accessToken: config.auth.accessToken
147
+ } : {
148
+ user: config.auth.user,
149
+ pass: config.auth.pass ?? ""
150
+ };
151
+ return new ImapFlow({
152
+ host: config.host,
153
+ port: config.port ?? 993,
154
+ secure: config.secure ?? true,
155
+ auth,
156
+ logger: false
157
+ });
158
+ }
159
+ /** Normalize extracted email fields into the provider-independent email shape. */
160
+ async function normalizeParsedEmail(parsed, ctx) {
161
+ const toAddresses = (parsed.toAddresses ?? []).map((a) => a.toLowerCase()).filter((a) => a.includes("@"));
162
+ const plain = (parsed.text ?? "").trim();
163
+ const bodyMarkdown = plain ? plain : parsed.html ? (await htmlToMarkdown(parsed.html)).trim() : "";
164
+ return {
165
+ messageId: (parsed.messageId ?? "").replace(/[<>]/g, "").trim() || `uid-${ctx.uid}`,
166
+ from: parsed.fromText ?? "",
167
+ toAddresses,
168
+ subject: parsed.subject ?? "(no subject)",
169
+ date: (parsed.date ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10),
170
+ bodyMarkdown,
171
+ attachments: (parsed.attachments ?? []).filter((a) => a.filename).map((a) => ({
172
+ filename: a.filename,
173
+ mimeType: a.contentType ?? "application/octet-stream",
174
+ content: a.content
175
+ })),
176
+ sourceRef: `imap://${ctx.user}@${ctx.host}/${ctx.mailbox}/${ctx.uid}`
177
+ };
178
+ }
179
+ /** Flatten mailparser's AddressObject | AddressObject[] | undefined to addresses. */
180
+ function flattenAddresses(field) {
181
+ if (!field) return [];
182
+ return (Array.isArray(field) ? field : [field]).flatMap((o) => o.value ?? []).map((a) => (a.address ?? "").toLowerCase()).filter((a) => a.includes("@"));
183
+ }
184
+ /**
185
+ * Sync a whole IMAP mailbox (any provider). Each message is parsed, routed to a
186
+ * customer — by a fixed `slug` or auto-routed by sender/recipient domain — and
187
+ * ingested through the shared pipeline (attachments→Markdown, summary, index).
188
+ * Messages that match no customer are counted as `unrouted` and skipped.
189
+ */
190
+ async function syncImapMailbox(opts) {
191
+ const result = {
192
+ synced: 0,
193
+ skipped: 0,
194
+ unrouted: 0
195
+ };
196
+ const mailbox = opts.config.mailbox ?? "INBOX";
197
+ const { simpleParser } = await import("mailparser");
198
+ const client = opts.clientFactory ? opts.clientFactory(opts.config) : await defaultClientFactory(opts.config);
199
+ const table = opts.slug ? null : buildRoutingTable(opts.dataDir);
200
+ const dedupCache = /* @__PURE__ */ new Map();
201
+ const seen = async (slug, sourceRef) => {
202
+ let content = dedupCache.get(slug);
203
+ if (content === void 0) {
204
+ content = await readInteractions(opts.dataDir, slug).catch(() => "");
205
+ dedupCache.set(slug, content);
206
+ }
207
+ return content.includes(sourceRef);
208
+ };
209
+ await client.connect();
210
+ const lock = await client.getMailboxLock(mailbox);
211
+ try {
212
+ const range = opts.since ? { since: opts.since } : { all: true };
213
+ for await (const message of client.fetch(range, {
214
+ uid: true,
215
+ source: true
216
+ })) try {
217
+ const parsed = await simpleParser(message.source);
218
+ const msg = await normalizeParsedEmail({
219
+ messageId: parsed.messageId,
220
+ fromText: parsed.from?.text,
221
+ toAddresses: [...flattenAddresses(parsed.to), ...flattenAddresses(parsed.cc)],
222
+ subject: parsed.subject,
223
+ date: parsed.date,
224
+ text: parsed.text,
225
+ html: parsed.html,
226
+ attachments: parsed.attachments
227
+ }, {
228
+ user: opts.config.auth.user,
229
+ host: opts.config.host,
230
+ mailbox,
231
+ uid: message.uid
232
+ });
233
+ let slug = opts.slug ?? null;
234
+ if (!slug && table) slug = routeMessage([(msg.from.match(/<([^>]+)>/)?.[1] ?? msg.from).toLowerCase(), ...msg.toAddresses].filter((a) => domainOf(a)), table);
235
+ if (!slug) {
236
+ result.unrouted++;
237
+ continue;
238
+ }
239
+ if (await seen(slug, msg.sourceRef)) {
240
+ result.skipped++;
241
+ continue;
242
+ }
243
+ await ingestEmail(opts.dataDir, slug, msg, {
244
+ ...opts.includeAttachments !== void 0 ? { includeAttachments: opts.includeAttachments } : {},
245
+ ...opts.maxAttachmentBytes !== void 0 ? { maxAttachmentBytes: opts.maxAttachmentBytes } : {},
246
+ direction: directionFor(msg, opts.config.auth.user)
247
+ });
248
+ dedupCache.set(slug, (dedupCache.get(slug) ?? "") + msg.sourceRef);
249
+ result.synced++;
250
+ } catch (err) {
251
+ logger.warn("imap-sync", "message failed", {
252
+ uid: message.uid,
253
+ error: err.message
254
+ });
255
+ result.skipped++;
256
+ }
257
+ } finally {
258
+ lock.release();
259
+ await client.logout().catch(() => void 0);
260
+ }
261
+ return result;
262
+ }
263
+ /** Inbound unless the mailbox owner is the sender. */
264
+ function directionFor(msg, user) {
265
+ return (msg.from.match(/<([^>]+)>/)?.[1] ?? msg.from).toLowerCase() === user.toLowerCase() ? "outbound" : "inbound";
266
+ }
267
+ //#endregion
268
+ export { syncImapMailbox };
269
+
270
+ //# sourceMappingURL=imap-o6PRuBvm.js.map