@skillrecordings/cli 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/skill.mjs CHANGED
@@ -1,8 +1,3 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- // Load secrets via 1Password/age encryption before importing CLI
4
- // This sets DATABASE_URL and other env vars from encrypted .env.encrypted
5
- await import('../dist/preload.js')
6
-
7
- // Now import the main CLI with secrets available
8
3
  await import('../dist/index.js')
package/bin/skill.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- import '../preload'
3
2
  import '../src/index'
@@ -3560,7 +3560,7 @@ ${message.body}
3560
3560
  ---
3561
3561
  Write your response:`;
3562
3562
  if (useAgentMode && appId) {
3563
- const { runSupportAgent } = await import("./config-523F4OBH.js");
3563
+ const { runSupportAgent } = await import("./config-HLXR42M4.js");
3564
3564
  await log("debug", "draft using agent mode", {
3565
3565
  workflow: "pipeline",
3566
3566
  step: "draft",
@@ -7028,6 +7028,17 @@ async function runThreadPipeline(input, options = {}) {
7028
7028
  }
7029
7029
 
7030
7030
  export {
7031
+ FrontApiError,
7032
+ createChannelsClient,
7033
+ createContactsClient,
7034
+ createConversationsClient,
7035
+ createDraftsClient,
7036
+ createInboxesClient,
7037
+ createMessagesClient,
7038
+ createTagsClient,
7039
+ createTeammatesClient,
7040
+ createTemplatesClient,
7041
+ createInstrumentedBaseClient,
7031
7042
  createInstrumentedFrontClient,
7032
7043
  computeMessageSignals,
7033
7044
  computeThreadSignals,
@@ -7085,4 +7096,4 @@ export {
7085
7096
  runPipeline,
7086
7097
  runThreadPipeline
7087
7098
  };
7088
- //# sourceMappingURL=chunk-TFLLCHUX.js.map
7099
+ //# sourceMappingURL=chunk-54VQQSWT.js.map
@@ -0,0 +1,198 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-WFANXVQG.js";
4
+
5
+ // ../core/src/faq/filters.ts
6
+ init_esm_shims();
7
+ var NOISE_SENDER_DOMAINS = [
8
+ // Transcription services
9
+ "castingwords.com",
10
+ // Email/Marketing services
11
+ "postmarkapp.com",
12
+ "convertkit.com",
13
+ "kit.com",
14
+ "kit-mail6.com",
15
+ "convertkit-mail4.com",
16
+ "mailgun.org",
17
+ "sendgrid.net",
18
+ // Cloud services
19
+ "algolia.com",
20
+ "cloudinary.com",
21
+ "mux.com",
22
+ "aws.amazon.com",
23
+ "amazonaws.com",
24
+ "vercel.com",
25
+ // Google services
26
+ "google.com",
27
+ "google.co.uk",
28
+ "googlemail.com",
29
+ // Payment/Billing
30
+ "stripe.com",
31
+ "paddle.com",
32
+ // Training/Enterprise platforms
33
+ "placedelaformation.com",
34
+ "allwyn-lotterysolutions.com",
35
+ "atlassian.net",
36
+ // Email routing/filtering
37
+ "mixmax.com",
38
+ // Security/DMARC
39
+ "dmarc.postmarkapp.com",
40
+ "postmarkdmarc.com",
41
+ "activecampaign.com"
42
+ ];
43
+ var AUTO_REPLY_PATTERNS = [
44
+ // Chinese auto-replies
45
+ /这是.*自动回复/,
46
+ /[你您]好.*我已收到您的邮件/,
47
+ /我已收到您的邮件/,
48
+ /您的邮件我已经收到/,
49
+ /我最近正在休假中/,
50
+ // English auto-replies
51
+ /vacation.*auto.*reply/i,
52
+ /out of office/i,
53
+ /automatic reply/i,
54
+ /auto-reply/i,
55
+ // Calendar notifications
56
+ /has accepted this invitation/i,
57
+ /has declined this invitation/i,
58
+ /invitation from google calendar/i,
59
+ // Email routing tools
60
+ /uses Mixmax to route/i,
61
+ /keeping their inbox clear/i,
62
+ // Service desk auto-responses
63
+ /just confirming that we got your request/i,
64
+ /reply above this line/i,
65
+ // Unsubscribe confirmations
66
+ /you have been unsubscribed/i,
67
+ /successfully unsubscribed/i
68
+ ];
69
+ var SPAM_PATTERNS = [
70
+ // Influencer outreach (Head AI)
71
+ /reaching out from Head/i,
72
+ /AI-powered influencer/i,
73
+ /Partnerships at Head Creator/i,
74
+ // Generic cold outreach - broad patterns
75
+ /collaboration/i,
76
+ // Catches: collab, collaboration, collaborations, etc.
77
+ /partnership/i,
78
+ // Catches: partner, partnership, partnerships, etc.
79
+ /sponsored/i,
80
+ // Sponsored content requests
81
+ /affiliate/i,
82
+ // Affiliate program requests
83
+ // Generic cold outreach - specific patterns
84
+ /I hope this email finds you well.*My name is/i,
85
+ /I'd like to discuss/i,
86
+ /brand collab/i,
87
+ /creator partnership/i,
88
+ // SEO spam
89
+ /increase your organic traffic/i,
90
+ /boost your SEO/i,
91
+ /backlink opportunity/i
92
+ ];
93
+ var LESSON_COMMENT_PATTERN = /^.*writes:[\s\S]*---\s*lesson:/i;
94
+ var SERVICE_NOTIFICATION_PATTERNS = [
95
+ // Transcription services
96
+ /We've just finished your transcription/i,
97
+ /We just finished up transcribing order/i,
98
+ // DMARC/Email reports
99
+ /emails were sent using.*between/i,
100
+ /DMARC Compliance/i,
101
+ /DMARC aligned/i,
102
+ /dmarc.postmarkapp.com/i,
103
+ // Usage reports
104
+ /Weekly report.*Plan usage/i,
105
+ /Usage Report/i,
106
+ /monthly invoice is available/i,
107
+ // Payment notifications
108
+ /Your payment was successful/i,
109
+ /We received your payment/i,
110
+ // Security notifications
111
+ /new sign-in.*device/i,
112
+ /unrecognized device/i,
113
+ /We found some security gaps/i,
114
+ // Google Workspace
115
+ /Google Workspace.*invoice/i,
116
+ /Google Analytics.*tips/i,
117
+ /Dear Administrator/i
118
+ ];
119
+ function isNoiseSenderDomain(email) {
120
+ const domain = email.split("@")[1]?.toLowerCase() ?? "";
121
+ return NOISE_SENDER_DOMAINS.some(
122
+ (noiseDomain) => domain === noiseDomain || domain.endsWith(`.${noiseDomain}`)
123
+ );
124
+ }
125
+ function isAutoReply(text) {
126
+ return AUTO_REPLY_PATTERNS.some((pattern) => pattern.test(text));
127
+ }
128
+ function isSpam(text) {
129
+ return SPAM_PATTERNS.some((pattern) => pattern.test(text));
130
+ }
131
+ function isLessonComment(text) {
132
+ return LESSON_COMMENT_PATTERN.test(text);
133
+ }
134
+ function isServiceNotification(text) {
135
+ return SERVICE_NOTIFICATION_PATTERNS.some((pattern) => pattern.test(text));
136
+ }
137
+ function shouldFilter(text, senderEmail) {
138
+ if (senderEmail && isNoiseSenderDomain(senderEmail)) {
139
+ return { filtered: true, reason: "sender_domain" };
140
+ }
141
+ if (isLessonComment(text)) {
142
+ return { filtered: true, reason: "lesson_comment" };
143
+ }
144
+ if (isAutoReply(text)) {
145
+ return { filtered: true, reason: "auto_reply" };
146
+ }
147
+ if (isSpam(text)) {
148
+ return { filtered: true, reason: "spam" };
149
+ }
150
+ if (isServiceNotification(text)) {
151
+ return { filtered: true, reason: "service_notification" };
152
+ }
153
+ return { filtered: false };
154
+ }
155
+ function createFilterStats() {
156
+ return {
157
+ total: 0,
158
+ filtered: 0,
159
+ passed: 0,
160
+ byReason: {}
161
+ };
162
+ }
163
+ function updateFilterStats(stats, result) {
164
+ stats.total++;
165
+ if (result.filtered) {
166
+ stats.filtered++;
167
+ if (result.reason) {
168
+ stats.byReason[result.reason] = (stats.byReason[result.reason] ?? 0) + 1;
169
+ }
170
+ } else {
171
+ stats.passed++;
172
+ }
173
+ }
174
+ function formatFilterStats(stats) {
175
+ const lines = [
176
+ `\u{1F4CA} Filter Statistics:`,
177
+ ` Total processed: ${stats.total}`,
178
+ ` Filtered out: ${stats.filtered} (${(stats.filtered / stats.total * 100).toFixed(1)}%)`,
179
+ ` Passed: ${stats.passed} (${(stats.passed / stats.total * 100).toFixed(1)}%)`
180
+ ];
181
+ if (Object.keys(stats.byReason).length > 0) {
182
+ lines.push(` By reason:`);
183
+ for (const [reason, count] of Object.entries(stats.byReason).sort(
184
+ (a, b) => b[1] - a[1]
185
+ )) {
186
+ lines.push(` - ${reason}: ${count}`);
187
+ }
188
+ }
189
+ return lines.join("\n");
190
+ }
191
+
192
+ export {
193
+ shouldFilter,
194
+ createFilterStats,
195
+ updateFilterStats,
196
+ formatFilterStats
197
+ };
198
+ //# sourceMappingURL=chunk-5ACOI6Z3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/faq/filters.ts"],"sourcesContent":["/**\n * FAQ Preprocessing Filters\n *\n * Filters to remove noise before clustering. These patterns were identified\n * by auditing Phase 0 cluster quality (see docs/cluster-quality-diagnosis.md).\n *\n * Issue: #110\n *\n * @module faq/filters\n */\n\n/**\n * Sender domains that should be filtered out before clustering.\n * These are service providers, not customer support requests.\n */\nexport const NOISE_SENDER_DOMAINS = [\n // Transcription services\n 'castingwords.com',\n\n // Email/Marketing services\n 'postmarkapp.com',\n 'convertkit.com',\n 'kit.com',\n 'kit-mail6.com',\n 'convertkit-mail4.com',\n 'mailgun.org',\n 'sendgrid.net',\n\n // Cloud services\n 'algolia.com',\n 'cloudinary.com',\n 'mux.com',\n 'aws.amazon.com',\n 'amazonaws.com',\n 'vercel.com',\n\n // Google services\n 'google.com',\n 'google.co.uk',\n 'googlemail.com',\n\n // Payment/Billing\n 'stripe.com',\n 'paddle.com',\n\n // Training/Enterprise platforms\n 'placedelaformation.com',\n 'allwyn-lotterysolutions.com',\n 'atlassian.net',\n\n // Email routing/filtering\n 'mixmax.com',\n\n // Security/DMARC\n 'dmarc.postmarkapp.com',\n 'postmarkdmarc.com',\n 'activecampaign.com',\n] as const\n\n/**\n * Auto-reply patterns that indicate noise, not real support requests.\n */\nexport const AUTO_REPLY_PATTERNS = [\n // Chinese auto-replies\n /这是.*自动回复/,\n /[你您]好.*我已收到您的邮件/,\n /我已收到您的邮件/,\n /您的邮件我已经收到/,\n /我最近正在休假中/,\n\n // English auto-replies\n /vacation.*auto.*reply/i,\n /out of office/i,\n /automatic reply/i,\n /auto-reply/i,\n\n // Calendar notifications\n /has accepted this invitation/i,\n /has declined this invitation/i,\n /invitation from google calendar/i,\n\n // Email routing tools\n /uses Mixmax to route/i,\n /keeping their inbox clear/i,\n\n // Service desk auto-responses\n /just confirming that we got your request/i,\n /reply above this line/i,\n\n // Unsubscribe confirmations\n /you have been unsubscribed/i,\n /successfully unsubscribed/i,\n] as const\n\n/**\n * Spam/outreach patterns - cold emails to support addresses\n */\nexport const SPAM_PATTERNS = [\n // Influencer outreach (Head AI)\n /reaching out from Head/i,\n /AI-powered influencer/i,\n /Partnerships at Head Creator/i,\n\n // Generic cold outreach - broad patterns\n /collaboration/i, // Catches: collab, collaboration, collaborations, etc.\n /partnership/i, // Catches: partner, partnership, partnerships, etc.\n /sponsored/i, // Sponsored content requests\n /affiliate/i, // Affiliate program requests\n\n // Generic cold outreach - specific patterns\n /I hope this email finds you well.*My name is/i,\n /I'd like to discuss/i,\n /brand collab/i,\n /creator partnership/i,\n\n // SEO spam\n /increase your organic traffic/i,\n /boost your SEO/i,\n /backlink opportunity/i,\n] as const\n\n/**\n * Lesson comment pattern - these are egghead lesson comments, not support\n */\nexport const LESSON_COMMENT_PATTERN = /^.*writes:[\\s\\S]*---\\s*lesson:/i\n\n/**\n * Service notification patterns - automated emails from service providers\n */\nexport const SERVICE_NOTIFICATION_PATTERNS = [\n // Transcription services\n /We've just finished your transcription/i,\n /We just finished up transcribing order/i,\n\n // DMARC/Email reports\n /emails were sent using.*between/i,\n /DMARC Compliance/i,\n /DMARC aligned/i,\n /dmarc.postmarkapp.com/i,\n\n // Usage reports\n /Weekly report.*Plan usage/i,\n /Usage Report/i,\n /monthly invoice is available/i,\n\n // Payment notifications\n /Your payment was successful/i,\n /We received your payment/i,\n\n // Security notifications\n /new sign-in.*device/i,\n /unrecognized device/i,\n /We found some security gaps/i,\n\n // Google Workspace\n /Google Workspace.*invoice/i,\n /Google Analytics.*tips/i,\n /Dear Administrator/i,\n] as const\n\n/**\n * Marketing reply indicators - replies to outbound marketing emails\n */\nexport const MARKETING_REPLY_PATTERNS = [\n // Price objection replies (from TotalTypeScript campaigns)\n /What's holding you back from buying/i,\n /Total TypeScript Core Volume Pre-Release/i,\n /workshop.*discount.*limited time/i,\n] as const\n\n/**\n * Check if a message should be filtered based on sender domain.\n */\nexport function isNoiseSenderDomain(email: string): boolean {\n const domain = email.split('@')[1]?.toLowerCase() ?? ''\n return NOISE_SENDER_DOMAINS.some(\n (noiseDomain) =>\n domain === noiseDomain || domain.endsWith(`.${noiseDomain}`)\n )\n}\n\n/**\n * Check if a message matches any auto-reply pattern.\n */\nexport function isAutoReply(text: string): boolean {\n return AUTO_REPLY_PATTERNS.some((pattern) => pattern.test(text))\n}\n\n/**\n * Check if a message matches any spam/outreach pattern.\n */\nexport function isSpam(text: string): boolean {\n return SPAM_PATTERNS.some((pattern) => pattern.test(text))\n}\n\n/**\n * Check if a message is an egghead lesson comment.\n */\nexport function isLessonComment(text: string): boolean {\n return LESSON_COMMENT_PATTERN.test(text)\n}\n\n/**\n * Check if a message is a service notification.\n */\nexport function isServiceNotification(text: string): boolean {\n return SERVICE_NOTIFICATION_PATTERNS.some((pattern) => pattern.test(text))\n}\n\n/**\n * Check if a message is a reply to a marketing email.\n * These are detected by checking if the quoted/forwarded content matches marketing patterns.\n */\nexport function isMarketingReply(text: string): boolean {\n return MARKETING_REPLY_PATTERNS.some((pattern) => pattern.test(text))\n}\n\n/**\n * Filter result with reason for transparency/debugging.\n */\nexport interface FilterResult {\n /** Whether the message should be filtered */\n filtered: boolean\n /** Reason for filtering (if filtered) */\n reason?:\n | 'sender_domain'\n | 'auto_reply'\n | 'spam'\n | 'lesson_comment'\n | 'service_notification'\n | 'marketing_reply'\n}\n\n/**\n * Check if a message should be filtered before clustering.\n *\n * @param text - Message text\n * @param senderEmail - Sender email address\n * @returns FilterResult with filtered status and reason\n */\nexport function shouldFilter(text: string, senderEmail?: string): FilterResult {\n // Check sender domain first (cheapest check)\n if (senderEmail && isNoiseSenderDomain(senderEmail)) {\n return { filtered: true, reason: 'sender_domain' }\n }\n\n // Check lesson comment pattern\n if (isLessonComment(text)) {\n return { filtered: true, reason: 'lesson_comment' }\n }\n\n // Check auto-reply patterns\n if (isAutoReply(text)) {\n return { filtered: true, reason: 'auto_reply' }\n }\n\n // Check spam patterns\n if (isSpam(text)) {\n return { filtered: true, reason: 'spam' }\n }\n\n // Check service notification patterns\n if (isServiceNotification(text)) {\n return { filtered: true, reason: 'service_notification' }\n }\n\n // Don't filter marketing replies by default - they might still be useful\n // for understanding customer objections, just not for FAQ extraction\n // Uncomment to enable:\n // if (isMarketingReply(text)) {\n // return { filtered: true, reason: 'marketing_reply' }\n // }\n\n return { filtered: false }\n}\n\n/**\n * Filter statistics for logging/monitoring.\n */\nexport interface FilterStats {\n total: number\n filtered: number\n passed: number\n byReason: Record<string, number>\n}\n\n/**\n * Create empty filter stats.\n */\nexport function createFilterStats(): FilterStats {\n return {\n total: 0,\n filtered: 0,\n passed: 0,\n byReason: {},\n }\n}\n\n/**\n * Update filter stats with a result.\n */\nexport function updateFilterStats(\n stats: FilterStats,\n result: FilterResult\n): void {\n stats.total++\n if (result.filtered) {\n stats.filtered++\n if (result.reason) {\n stats.byReason[result.reason] = (stats.byReason[result.reason] ?? 0) + 1\n }\n } else {\n stats.passed++\n }\n}\n\n/**\n * Format filter stats for display.\n */\nexport function formatFilterStats(stats: FilterStats): string {\n const lines = [\n `📊 Filter Statistics:`,\n ` Total processed: ${stats.total}`,\n ` Filtered out: ${stats.filtered} (${((stats.filtered / stats.total) * 100).toFixed(1)}%)`,\n ` Passed: ${stats.passed} (${((stats.passed / stats.total) * 100).toFixed(1)}%)`,\n ]\n\n if (Object.keys(stats.byReason).length > 0) {\n lines.push(` By reason:`)\n for (const [reason, count] of Object.entries(stats.byReason).sort(\n (a, b) => b[1] - a[1]\n )) {\n lines.push(` - ${reason}: ${count}`)\n }\n }\n\n return lines.join('\\n')\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,3 @@
1
- import {
2
- createTool
3
- } from "./chunk-F4EM72IH.js";
4
1
  import {
5
2
  Redis2,
6
3
  traceMemoryCite,
@@ -21,6 +18,9 @@ import {
21
18
  import {
22
19
  queryVectors
23
20
  } from "./chunk-H3D6VCME.js";
21
+ import {
22
+ createTool
23
+ } from "./chunk-F4EM72IH.js";
24
24
  import {
25
25
  __export,
26
26
  init_esm_shims
@@ -22336,7 +22336,6 @@ export {
22336
22336
  getDb,
22337
22337
  closeDb,
22338
22338
  database,
22339
- IntegrationClient,
22340
22339
  getApp,
22341
22340
  getRedis,
22342
22341
  getOutcomeHistory,
@@ -22345,4 +22344,4 @@ export {
22345
22344
  DEFAULT_AGENT_MODEL,
22346
22345
  runSupportAgent
22347
22346
  };
22348
- //# sourceMappingURL=chunk-HCFBOGCL.js.map
22347
+ //# sourceMappingURL=chunk-EGBS3VJI.js.map
@@ -3,13 +3,13 @@ import {
3
3
  SUPPORT_AGENT_PROMPT,
4
4
  agentTools,
5
5
  runSupportAgent
6
- } from "./chunk-HCFBOGCL.js";
7
- import "./chunk-F4EM72IH.js";
6
+ } from "./chunk-EGBS3VJI.js";
8
7
  import "./chunk-KEV3QKXP.js";
9
8
  import "./chunk-MLNDSBZ4.js";
10
9
  import "./chunk-ZNF7XD2S.js";
11
10
  import "./chunk-H3D6VCME.js";
12
11
  import "./chunk-MG37YDAK.js";
12
+ import "./chunk-F4EM72IH.js";
13
13
  import "./chunk-WFANXVQG.js";
14
14
  export {
15
15
  DEFAULT_AGENT_MODEL,
@@ -17,4 +17,4 @@ export {
17
17
  agentTools,
18
18
  runSupportAgent
19
19
  };
20
- //# sourceMappingURL=config-523F4OBH.js.map
20
+ //# sourceMappingURL=config-HLXR42M4.js.map
@@ -0,0 +1,353 @@
1
+ import {
2
+ shouldFilter
3
+ } from "./chunk-5ACOI6Z3.js";
4
+ import {
5
+ init_esm_shims
6
+ } from "./chunk-WFANXVQG.js";
7
+
8
+ // ../core/src/faq/duckdb-source.ts
9
+ init_esm_shims();
10
+ var INBOX_TO_APP = {
11
+ inb_1bwzr: "epic-react",
12
+ // KCD Support (Epic React is the main product)
13
+ inb_3srbb: "total-typescript",
14
+ inb_1c77r: "egghead",
15
+ inb_jqs11: "epic-ai",
16
+ inb_3pqh3: "pro-tailwind",
17
+ inb_2odqf: "just-javascript",
18
+ inb_4bj7r: "ai-hero",
19
+ inb_3bkef: "testing-accessibility",
20
+ inb_jqs2t: "epic-web",
21
+ inb_1zh3b: "egghead",
22
+ inb_43olj: "pro-nextjs"
23
+ };
24
+ var APP_TO_INBOX = Object.entries(
25
+ INBOX_TO_APP
26
+ ).reduce(
27
+ (acc, [inboxId, appSlug]) => {
28
+ if (!acc[appSlug]) {
29
+ acc[appSlug] = [];
30
+ }
31
+ acc[appSlug].push(inboxId);
32
+ return acc;
33
+ },
34
+ {}
35
+ );
36
+ function toFrontMessage(row) {
37
+ const createdAt = toJSDate(row.created_at);
38
+ const createdAtTimestamp = createdAt ? Math.floor(createdAt.getTime() / 1e3) : 0;
39
+ return {
40
+ id: row.id,
41
+ type: "email",
42
+ is_inbound: row.is_inbound,
43
+ is_draft: false,
44
+ error_type: null,
45
+ version: null,
46
+ created_at: createdAtTimestamp,
47
+ subject: "",
48
+ blurb: row.body_text?.slice(0, 100) ?? "",
49
+ body: row.body_html ?? row.body_text ?? "",
50
+ text: row.body_text ?? "",
51
+ author: row.author_email ? {
52
+ id: row.author_email,
53
+ email: row.author_email,
54
+ first_name: row.author_name?.split(" ")[0] ?? "",
55
+ last_name: row.author_name?.split(" ").slice(1).join(" ") ?? "",
56
+ is_admin: !row.is_inbound,
57
+ is_available: true,
58
+ is_blocked: false
59
+ } : null,
60
+ recipients: [],
61
+ attachments: [],
62
+ metadata: {},
63
+ _links: {
64
+ self: `https://api.frontapp.com/messages/${row.id}`,
65
+ related: {
66
+ conversation: `https://api.frontapp.com/conversations/${row.conversation_id}`
67
+ }
68
+ }
69
+ };
70
+ }
71
+ function toFrontConversation(row) {
72
+ const createdAt = toJSDate(row.created_at);
73
+ const createdAtTimestamp = createdAt ? Math.floor(createdAt.getTime() / 1e3) : 0;
74
+ return {
75
+ id: row.id,
76
+ subject: row.subject ?? "",
77
+ status: row.status === "archived" ? "archived" : row.status,
78
+ assignee: row.assignee_email ? {
79
+ id: row.assignee_email,
80
+ email: row.assignee_email,
81
+ first_name: "",
82
+ last_name: ""
83
+ } : null,
84
+ recipient: row.customer_email ? {
85
+ handle: row.customer_email,
86
+ role: "from"
87
+ } : null,
88
+ tags: (row.tags ?? []).map((tag) => ({
89
+ id: tag,
90
+ name: tag
91
+ })),
92
+ links: [],
93
+ created_at: createdAtTimestamp,
94
+ is_private: false,
95
+ scheduled_reminders: [],
96
+ metadata: {},
97
+ _links: {
98
+ self: `https://api.frontapp.com/conversations/${row.id}`,
99
+ related: {
100
+ events: "",
101
+ followers: "",
102
+ messages: "",
103
+ comments: "",
104
+ inboxes: ""
105
+ }
106
+ }
107
+ };
108
+ }
109
+ function extractQuestion(messages) {
110
+ const sorted = [...messages].sort((a, b) => a.created_at - b.created_at);
111
+ const firstInbound = sorted.find((m) => m.is_inbound);
112
+ if (!firstInbound) {
113
+ return "";
114
+ }
115
+ const text = firstInbound.text ?? firstInbound.body?.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim() ?? "";
116
+ return text;
117
+ }
118
+ function extractAnswer(messages) {
119
+ const sorted = [...messages].sort((a, b) => b.created_at - a.created_at);
120
+ const lastOutbound = sorted.find((m) => !m.is_inbound);
121
+ if (!lastOutbound) {
122
+ return "";
123
+ }
124
+ return lastOutbound.text ?? lastOutbound.body?.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim() ?? "";
125
+ }
126
+ function isSpamOrNoise(text, senderEmail) {
127
+ if (text.trim().length < 20) return true;
128
+ const result = shouldFilter(text, senderEmail);
129
+ return result.filtered;
130
+ }
131
+ function toJSDate(value) {
132
+ if (value === null || value === void 0) {
133
+ return null;
134
+ }
135
+ if (value instanceof Date) {
136
+ return value;
137
+ }
138
+ if (typeof value === "object" && "micros" in value) {
139
+ const ms = Number(value.micros) / 1e3;
140
+ return new Date(ms);
141
+ }
142
+ if (typeof value === "number") {
143
+ if (value < 1e9) {
144
+ return new Date(value * 1e3);
145
+ }
146
+ return new Date(value);
147
+ }
148
+ if (typeof value === "string") {
149
+ return new Date(value);
150
+ }
151
+ return null;
152
+ }
153
+ async function createDuckDBSource(config) {
154
+ const { DuckDBInstance } = await import("./lib-R6DEEJCP.js");
155
+ const instance = await DuckDBInstance.create(
156
+ config.dbPath
157
+ );
158
+ const connection = await instance.connect();
159
+ const statusFilter = config.statusFilter ?? ["archived"];
160
+ return {
161
+ name: "duckdb-cache",
162
+ async getConversations(options) {
163
+ const { appId, since, limit = 500 } = options;
164
+ let inboxIds = config.inboxIds;
165
+ if (!inboxIds && appId) {
166
+ inboxIds = APP_TO_INBOX[appId];
167
+ if (!inboxIds || inboxIds.length === 0) {
168
+ console.warn(`No inbox mapping found for app: ${appId}`);
169
+ return [];
170
+ }
171
+ }
172
+ const whereClauses = [];
173
+ const params = {};
174
+ if (statusFilter.length > 0) {
175
+ whereClauses.push(
176
+ `status IN (${statusFilter.map((_, i) => `$status_${i}`).join(", ")})`
177
+ );
178
+ statusFilter.forEach((s, i) => {
179
+ params[`status_${i}`] = s;
180
+ });
181
+ }
182
+ if (inboxIds && inboxIds.length > 0) {
183
+ whereClauses.push(
184
+ `inbox_id IN (${inboxIds.map((_, i) => `$inbox_${i}`).join(", ")})`
185
+ );
186
+ inboxIds.forEach((id, i) => {
187
+ params[`inbox_${i}`] = id;
188
+ });
189
+ }
190
+ if (since) {
191
+ whereClauses.push(`last_message_at >= $since::TIMESTAMP`);
192
+ params.since = since.toISOString();
193
+ }
194
+ if (config.minThreadLength !== void 0) {
195
+ whereClauses.push(`thread_depth >= $minThreadLength`);
196
+ params.minThreadLength = config.minThreadLength;
197
+ }
198
+ if (config.maxThreadLength !== void 0) {
199
+ whereClauses.push(`thread_depth <= $maxThreadLength`);
200
+ params.maxThreadLength = config.maxThreadLength;
201
+ }
202
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
203
+ const query = `
204
+ SELECT *
205
+ FROM conversations
206
+ ${whereClause}
207
+ ORDER BY last_message_at DESC
208
+ LIMIT $limit
209
+ `;
210
+ params.limit = limit;
211
+ const reader = await connection.runAndReadAll(query, params);
212
+ const rows = reader.getRowObjectsJS();
213
+ console.log(`DuckDB: Found ${rows.length} conversations matching filters`);
214
+ const results = [];
215
+ let processed = 0;
216
+ for (const row of rows) {
217
+ processed++;
218
+ if (processed % 100 === 0) {
219
+ console.log(`Processing ${processed}/${rows.length}...`);
220
+ }
221
+ const messagesReader = await connection.runAndReadAll(
222
+ `SELECT * FROM messages WHERE conversation_id = $convId ORDER BY created_at ASC`,
223
+ { convId: row.id }
224
+ );
225
+ const messageRows = messagesReader.getRowObjectsJS();
226
+ if (messageRows.length === 0) {
227
+ continue;
228
+ }
229
+ const messages = messageRows.map(toFrontMessage);
230
+ const conversation = toFrontConversation(row);
231
+ const question = extractQuestion(messages);
232
+ const answer = extractAnswer(messages);
233
+ if (!question || !answer) {
234
+ continue;
235
+ }
236
+ const firstInbound = messages.find((m) => m.is_inbound);
237
+ const senderEmail = firstInbound?.author?.email;
238
+ if (isSpamOrNoise(question, senderEmail)) {
239
+ continue;
240
+ }
241
+ const answerFilter = shouldFilter(answer);
242
+ if (answerFilter.filtered && answerFilter.reason === "auto_reply") {
243
+ continue;
244
+ }
245
+ const tagNames = (row.tags ?? []).map((t) => t.toLowerCase());
246
+ if (tagNames.includes("spam") || tagNames.includes("collaboration")) {
247
+ continue;
248
+ }
249
+ const resolvedAppId = appId ?? INBOX_TO_APP[row.inbox_id] ?? "unknown";
250
+ results.push({
251
+ conversationId: row.id,
252
+ question,
253
+ answer,
254
+ subject: row.subject ?? "",
255
+ resolvedAt: toJSDate(row.last_message_at) ?? /* @__PURE__ */ new Date(),
256
+ appId: resolvedAppId,
257
+ wasUnchanged: false,
258
+ // Unknown from cache
259
+ draftSimilarity: void 0,
260
+ tags: row.tags ?? [],
261
+ _raw: {
262
+ conversation,
263
+ messages
264
+ }
265
+ });
266
+ }
267
+ console.log(
268
+ `DuckDB: Extracted ${results.length} conversations with Q&A pairs`
269
+ );
270
+ return results;
271
+ },
272
+ async getMessages(conversationId) {
273
+ const reader = await connection.runAndReadAll(
274
+ `SELECT * FROM messages WHERE conversation_id = $convId ORDER BY created_at ASC`,
275
+ { convId: conversationId }
276
+ );
277
+ const rows = reader.getRowObjectsJS();
278
+ return rows.map(toFrontMessage);
279
+ },
280
+ async getStats() {
281
+ const totalReader = await connection.runAndReadAll(
282
+ `SELECT COUNT(*) as count FROM conversations`
283
+ );
284
+ const totalRows = totalReader.getRowObjectsJS();
285
+ const totalConversations = Number(totalRows[0]?.count ?? 0);
286
+ const filterClauses = [];
287
+ const filterParams = {};
288
+ if (statusFilter.length > 0) {
289
+ filterClauses.push(
290
+ `status IN (${statusFilter.map((_, i) => `$status_${i}`).join(", ")})`
291
+ );
292
+ statusFilter.forEach((s, i) => {
293
+ filterParams[`status_${i}`] = s;
294
+ });
295
+ }
296
+ if (config.inboxIds && config.inboxIds.length > 0) {
297
+ filterClauses.push(
298
+ `inbox_id IN (${config.inboxIds.map((_, i) => `$inbox_${i}`).join(", ")})`
299
+ );
300
+ config.inboxIds.forEach((id, i) => {
301
+ filterParams[`inbox_${i}`] = id;
302
+ });
303
+ }
304
+ const filterWhere = filterClauses.length > 0 ? `WHERE ${filterClauses.join(" AND ")}` : "";
305
+ const filteredReader = await connection.runAndReadAll(
306
+ `SELECT COUNT(*) as count FROM conversations ${filterWhere}`,
307
+ filterParams
308
+ );
309
+ const filteredRows = filteredReader.getRowObjectsJS();
310
+ const filteredConversations = Number(filteredRows[0]?.count ?? 0);
311
+ const messagesReader = await connection.runAndReadAll(
312
+ `SELECT COUNT(*) as count FROM messages`
313
+ );
314
+ const messagesRows = messagesReader.getRowObjectsJS();
315
+ const totalMessages = Number(messagesRows[0]?.count ?? 0);
316
+ const inboxReader = await connection.runAndReadAll(
317
+ `SELECT COUNT(DISTINCT inbox_id) as count FROM conversations`
318
+ );
319
+ const inboxRows = inboxReader.getRowObjectsJS();
320
+ const inboxCount = Number(inboxRows[0]?.count ?? 0);
321
+ const dateReader = await connection.runAndReadAll(
322
+ `SELECT MIN(last_message_at) as oldest, MAX(last_message_at) as newest FROM conversations`
323
+ );
324
+ const dateRows = dateReader.getRowObjectsJS();
325
+ const dateRange = {
326
+ oldest: toJSDate(dateRows[0]?.oldest),
327
+ newest: toJSDate(dateRows[0]?.newest)
328
+ };
329
+ return {
330
+ totalConversations,
331
+ filteredConversations,
332
+ totalMessages,
333
+ inboxCount,
334
+ dateRange
335
+ };
336
+ },
337
+ async close() {
338
+ connection.closeSync();
339
+ }
340
+ };
341
+ }
342
+ function getInboxIdsForApp(appSlug) {
343
+ return APP_TO_INBOX[appSlug];
344
+ }
345
+ function getAppForInbox(inboxId) {
346
+ return INBOX_TO_APP[inboxId];
347
+ }
348
+ export {
349
+ createDuckDBSource,
350
+ getAppForInbox,
351
+ getInboxIdsForApp
352
+ };
353
+ //# sourceMappingURL=duckdb-source-T7EJUOC7.js.map