From 5e593806f111ef3e3efa24175218527980603750 Mon Sep 17 00:00:00 2001 From: Waren Gonzaga Date: Thu, 28 May 2026 11:56:04 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=98=95=20chore:=20apply=20biome=20format?= =?UTF-8?q?ting=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- package.json | 6 +----- src/cli.ts | 5 +++-- tests/engine.test.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index f92c68e..dd8569d 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,7 @@ "bin": { "summary-engine": "./dist/cli.js" }, - "files": [ - "dist", - "README.md", - "LICENSE" - ], + "files": ["dist", "README.md", "LICENSE"], "engines": { "node": "^22.0.0 || ^24.0.0 || ^26.0.0", "bun": ">=1.0.0" diff --git a/src/cli.ts b/src/cli.ts index c237de2..1034c67 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; +import { createInterface } from "node:readline/promises"; import { buildTitle, extractSummary } from "./engine.js"; import { discordPreset, telegramPreset, whatsappPreset } from "./presets.js"; import type { SummaryEngineConfig } from "./types.js"; @@ -57,7 +57,8 @@ async function main(): Promise { const rl = createInterface({ input, output }); const summary = await rl.question("Issue details: "); const customer = (await rl.question("Customer name (optional): ")).trim() || customerArg; - const platform = (await rl.question("Platform (telegram|whatsapp|discord): ")).trim() || platformArg; + const platform = + (await rl.question("Platform (telegram|whatsapp|discord): ")).trim() || platformArg; rl.close(); const interactivePreset = resolvePreset(platform); diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 5f75535..ec487ba 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -8,7 +8,7 @@ describe("summary extraction", () => { "we enabled 2FA for security, but OTP emails are not arriving for two users in our workspace, and both are now locked out of the platform."; expect(extractSummary(summary, telegramPreset)).toBe( - "OTP emails are not arriving for two users in our workspace" + "OTP emails are not arriving for two users in our workspace", ); }); @@ -26,9 +26,9 @@ describe("title building", () => { { summary: "I need help with my account login, I can't login for some reason, please help me.", - customerName: "Acme Corp" + customerName: "Acme Corp", }, - telegramPreset + telegramPreset, ); expect(title).toBe("[Telegram] Can't login"); @@ -38,9 +38,9 @@ describe("title building", () => { const title = buildTitle( { summary: "help", - customerName: "Acme Corp" + customerName: "Acme Corp", }, - whatsappPreset + whatsappPreset, ); expect(title).toBe("[WhatsApp] Acme Corp - Support Request"); @@ -50,9 +50,9 @@ describe("title building", () => { const title = buildTitle( { summary: "help", - customerName: "Unknown Company" + customerName: "Unknown Company", }, - telegramPreset + telegramPreset, ); expect(title).toBe("[Telegram] New Support Ticket"); From f5918a9693f81d3e5a74ca9a3fc72052c61b1877 Mon Sep 17 00:00:00 2001 From: Waren Gonzaga Date: Thu, 28 May 2026 11:56:09 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A7=20update:=20tune=20extraction?= =?UTF-8?q?=20and=20scoring=20rules=20for=20better=20summaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/defaults.ts | 81 +++++++++++++++++++++++++++++++++++++++++++------ src/engine.ts | 49 ++++++++++++++++++++++-------- 2 files changed, 109 insertions(+), 21 deletions(-) diff --git a/src/defaults.ts b/src/defaults.ts index c00d6ce..bfff05f 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -17,7 +17,7 @@ export const defaultConfig: SummaryEngineConfig = { "need support", "please help", "can you help", - "assistance" + "assistance", ], issueKeywords: [ "login", @@ -42,7 +42,38 @@ export const defaultConfig: SummaryEngineConfig = { "timeout", "locked", "access", - "account" + "account", + "crash", + "broken", + "upload", + "download", + "webhook", + "api", + "integration", + "notification", + "email", + "sso", + "mfa", + "display", + "slow", + "delayed", + "subscription", + "charge", + "refund", + "502", + "403", + "500", + "503", + "404", + "connection", + "database", + "query", + "queries", + "stale", + "missing", + "empty", + "failing", + "expired", ], rootCausePatterns: [ /\bnot\s+arriv(ing|ed)?\b/i, @@ -55,7 +86,19 @@ export const defaultConfig: SummaryEngineConfig = { /\bfailed\b/i, /\berror\b/i, /\btimeout\b/i, - /\binvalid\b/i + /\binvalid\b/i, + /\bnot\s+work(ing|ed|s)?\b/i, + /\bnot\s+render(ing|ed|s)?\b/i, + /\bnot\s+load(ing|ed|s)?\b/i, + /\bnot\s+show(ing|n)?\b/i, + /\bnot\s+display(ing|ed)?\b/i, + /\bnot\s+send(ing)?\b/i, + /\b(time[sd]?|timing)\s+out\b/i, + /\bcrash(es|ed|ing)?\b/i, + /\bbroken\b/i, + /\bincorrect(ly)?\b/i, + /\b(returns?|returning)\s+(no\s+results?|empty|null|404|403|500|502|503)\b/i, + /\bfailing\b/i, ], impactOnlyPatterns: [ /\blocked\s+out\b/i, @@ -63,29 +106,49 @@ export const defaultConfig: SummaryEngineConfig = { /\bcannot\s+access\b/i, /\bunable\s+to\s+access\b/i, /\bfor\s+urgent\s+tasks\b/i, - /\bbusiness\s+impact\b/i + /\bbusiness\s+impact\b/i, + ], + contextOnlyPatterns: [ + /^we\s+enabled\b/i, + /^we\s+updated\b/i, + /^for\s+security\b/i, + /^after\s+/i, + /^hope\s+you.re\s+doing\s+well\b/i, + /^we.ve\s+been\s+using\b/i, + /^i\s+love\b/i, + /^great\s+product\b/i, + /^this\s+is\s+\w+\s+from\b/i, ], - contextOnlyPatterns: [/^we\s+enabled\b/i, /^we\s+updated\b/i, /^for\s+security\b/i, /^after\s+/i], leadingFillerPatterns: [ /^(hi|hello|hey)(\s+(team|support|there))?[\s,.:!\-]*/i, /^(please|pls)\s+/i, /^(i have an issue with|i have an issue|i have problem with|i have a problem with)\s+/i, /^(i need help with|i need help|need help with|need help)\s+/i, /^(issue[:\-]\s*)/i, - /^(problem[:\-]\s*)/i + /^(problem[:\-]\s*)/i, + /^(i\s+just\s+wanted\s+to\s+(reach\s+out|let\s+you\s+know|report|inform\s+you)(\s+(because|that))?)\s*/i, + /^(just\s+wanted\s+to\s+(reach\s+out|let\s+you\s+know|report)(\s+that)?)\s*/i, + /^(i\s+wanted\s+to\s+(report|let\s+you\s+know|reach\s+out|inform\s+you)(\s+that)?)\s*/i, + /^(i('m|\s+am)\s+experiencing)\s+/i, + /^(we('re|\s+are)\s+experiencing)\s+/i, + /^(we('re|\s+are)\s+facing(\s+issues?\s+with)?)\s+/i, + /^(i('m|\s+am)\s+having\s+(trouble|issues?|problems?)\s+with)\s+/i, + /^(we\s+noticed\s+that)\s+/i, + /^(a\s+)?(critical\s+|major\s+|minor\s+|serious\s+)?(bug|issue|problem|error)\s+(where|that)\s+/i, + /^(this\s+is\s+ridiculous|this\s+is\s+unacceptable)[.!,\s]*/i, ], trailingFillerPatterns: [ /[\s,.-]*(please|pls)\s+help(\s+me)?[\s,.-]*$/i, /[\s,.-]*(can\s+you\s+help(\s+me)?)[\s,.-]*$/i, - /[\s,.-]*(need\s+help|support\s+please)[\s,.-]*$/i + /[\s,.-]*(need\s+help|support\s+please)[\s,.-]*$/i, ], conjunctionStripPatterns: [/^\s*(but|and|also)\s+/i], - causalSplitPattern: /\b(because|since|as|while|even though|although|so that)\b/i + causalSplitPattern: /\b(because|since|as|while|even though|although|so that)\b/i, }; export function withPrefix(config: SummaryEngineConfig, prefix: string): SummaryEngineConfig { return { ...config, - prefix + prefix, }; } diff --git a/src/engine.ts b/src/engine.ts index 2558ecc..e85b88d 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -61,9 +61,13 @@ function compactSummary(input: string, config: SummaryEngineConfig): string { let text = input.trim(); text = text + .replace(/^\s*(a|an)\s+subset\s+of\s+users\s+(are|were|have|had)\s+/i, "") .replace(/^\s*(a|an)\s+subset\s+of\s+users\s+/i, "") + .replace(/^\s*(some|many|several)\s+users\s+(are|were|have|had)\s+/i, "") .replace(/^\s*(some|many|several)\s+users\s+/i, "") - .replace(/^\s*users\s+/i, ""); + .replace(/^\s*users\s+(are|were|have|had)\s+/i, "") + .replace(/^\s*users\s+/i, "") + .replace(/^\s*our\s+team\s+(is|are|was|were|has|have)\s+/i, ""); const causalSplit = text.split(config.causalSplitPattern); if (causalSplit[0]) { @@ -86,7 +90,15 @@ function scoreClause(input: string, config: SummaryEngineConfig): number { } } - if (/\b(cannot|can't|unable|failed|error|timeout|locked)\b/.test(lowered)) { + if ( + /\b(cannot|can't|unable|fail(ed|ing|s)?|error|timeout|locked|crash(es|ed|ing)?|broken|invalid|incorrect)\b/.test( + lowered, + ) + ) { + score += 3; + } + + if (/\b(time[sd]?|timing)\s+out\b/.test(lowered)) { score += 3; } @@ -133,7 +145,7 @@ function extractSummaryInternal(summary: string, config: SummaryEngineConfig): s for (const segment of segments) { const clauses = segment - .split(/[,|]+|\s+-\s+|\s+and\s+/i) + .split(/[,|]+|\s+-\s+|\s+(?:and|but)\s+/i) .map((clause) => cleanClause(clause, config)) .filter(Boolean); @@ -142,6 +154,19 @@ function extractSummaryInternal(summary: string, config: SummaryEngineConfig): s } else { candidates.push(cleanClause(segment, config)); } + + const causalParts = segment.split(config.causalSplitPattern); + if (causalParts.length >= 3) { + for (let i = 2; i < causalParts.length; i += 2) { + const tail = causalParts[i]?.trim(); + if (tail) { + const cleaned = cleanClause(stripLeadingFiller(tail, config), config); + if (cleaned) { + candidates.push(cleaned); + } + } + } + } } const filtered = candidates.filter((candidate) => { @@ -156,7 +181,7 @@ function extractSummaryInternal(summary: string, config: SummaryEngineConfig): s const best = filtered .map((candidate) => ({ candidate, - score: scoreClause(candidate, config) + score: scoreClause(candidate, config), })) .sort((a, b) => b.score - a.score)[0]?.candidate; @@ -180,7 +205,7 @@ function normalizeCustomer(customerName: string | undefined): string { export function createEngine(overrides: Partial = {}): SummaryEngine { const config: SummaryEngineConfig = { ...defaultConfig, - ...overrides + ...overrides, }; return { @@ -201,22 +226,22 @@ export function createEngine(overrides: Partial = {}): Summ if (normalizedCustomer && !isUnknown) { return truncateAtWordBoundary( `${config.prefix} ${normalizeText(input.customerName ?? "")} - ${config.fallbackSuffix}`, - config.maxTitleLength + config.maxTitleLength, ); } return `${config.prefix} ${config.fallbackNewTicket}`; - } + }, }; } -export function extractSummary(summary: string, config?: Partial): string | null { +export function extractSummary( + summary: string, + config?: Partial, +): string | null { return createEngine(config).extractSummary(summary); } -export function buildTitle( - input: BuildTitleInput, - config?: Partial -): string { +export function buildTitle(input: BuildTitleInput, config?: Partial): string { return createEngine(config).buildTitle(input); } From 3818a4f7a86864fe197e21075952e7dabcb82c32 Mon Sep 17 00:00:00 2001 From: Waren Gonzaga Date: Thu, 28 May 2026 11:56:13 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20CLI=20usage=20?= =?UTF-8?q?examples=20to=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index bee5be1..238d898 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,74 @@ Creates a reusable engine instance with shared config. ## CLI Preview +Use the CLI to preview title generation with different inputs and platform presets. + +### Flags + +| Flag | Description | Default | +| --- | --- | --- | +| `--summary` | The support message text | _(interactive prompt)_ | +| `--customer` | Customer or company name | `Unknown Company` | +| `--platform` | Platform preset (`telegram`, `whatsapp`, `discord`) | `telegram` | + +When `--summary` is omitted the CLI starts an interactive prompt. + +### Examples + +**Telegram — login issue** + ```bash bun run preview --summary "I need help with my account login, I can't login for some reason" --customer "Acme Corp" --platform telegram +# Title preview: +# [Telegram] Can't login +``` + +**WhatsApp — password reset** + +```bash +bun run preview --summary "A subset of users cannot complete password reset because reset links intermittently return token expired." --customer "Acme Corp" --platform whatsapp +# Title preview: +# [WhatsApp] Cannot complete password reset +``` + +**Discord — OTP delivery failure** + +```bash +bun run preview --summary "we enabled 2FA for security, but OTP emails are not arriving for two users in our workspace, and both are now locked out of the platform." --customer "TechStart Inc" --platform discord +# Title preview: +# [Discord] OTP emails are not arriving for two users in our workspace +``` + +**Telegram — noisy message with buried issue** + +```bash +bun run preview --summary "Hey there! Hope you're doing well. I just wanted to reach out because our team is unable to upload files larger than 10MB. The upload just times out after a few minutes." --customer "MediaGroup" --platform telegram +# Title preview: +# [Telegram] Unable to upload files larger than 10MB +``` + +**Discord — verbose bug report** + +```bash +bun run preview --summary "I'm experiencing a critical bug where the app crashes whenever I open the settings page on mobile" --customer "MobileFirst" --platform discord +# Title preview: +# [Discord] The app crashes whenever I open the settings page on mobile +``` + +**WhatsApp — generic help (fallback)** + +```bash +bun run preview --summary "help" --customer "Acme Corp" --platform whatsapp +# Title preview: +# [WhatsApp] Acme Corp - Support Request +``` + +**Telegram — unknown customer (fallback)** + +```bash +bun run preview --summary "help" --customer "Unknown" --platform telegram +# Title preview: +# [Telegram] New Support Ticket ``` ## Scripts