Conversation
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds email-based subscription suggestions with IMAP scanning, parsing, persistence, review, and CLI commands. Splits the MCP server into modular handlers, tools, security, and transport modules. Also centralizes formatting/date helpers and reorganizes subscription workflows. ChangesEmail subscription suggestions
MCP server modularization
CLI utilities and workflow refactors
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (9)
apps/subtrack/src/mcp/security.ts (1)
13-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNaming nit: this is a fixed-window limiter, not a token bucket. Tokens only refill in a single burst once
elapsed >= windowMs, so up to2 * maxTokensrequests can pass across a window boundary. Fine for a single-user stdio server; consider renaming the comment/class or refilling proportionally if smoother limiting is ever needed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/mcp/security.ts` around lines 13 - 34, Rename the “token-bucket” terminology in the comment and RateLimiter class to describe the implemented fixed-window behavior, updating references to RateLimiter as needed. Preserve the existing bursty refill logic; do not add proportional refilling.apps/subtrack/src/stats.ts (2)
10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueImport
getFileSizeto eliminate duplicated logic.You can import
getFileSizehere as well to replace the manualstatSynctry/catch block below.♻️ Proposed fix
-import { formatBytes } from "./format.ts" +import { formatBytes, getFileSize } from "./format.ts"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/stats.ts` at line 10, Update the stats logic using the manual statSync try/catch to import and reuse getFileSize, replacing the duplicated file-size retrieval while preserving the existing behavior and output.
62-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
getFileSizeto replace this block.Since
getFileSizeencapsulates this exacttry/catchandstatSyncfallback behavior, you can replace this block with a single function call.♻️ Proposed fix
- // DB size - let dbSizeBytes = 0 - try { - dbSizeBytes = statSync(getDbPath()).size - } catch { /* ignore */ } + // DB size + const dbSizeBytes = getFileSize(getDbPath())🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/stats.ts` around lines 62 - 66, Replace the local dbSizeBytes initialization and statSync try/catch block with a call to getFileSize, preserving the existing database path argument and resulting byte-size value. Update the surrounding stats flow without changing its behavior.apps/subtrack/src/config.ts (1)
117-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared IMAP-defaults merge helper.
Each case rebuilds
config.imapwith the same four fallback defaults (port ?? 993,tls ?? true, etc.), duplicated four times. A small helper reduces drift risk if defaults ever change.♻️ Proposed refactor
+function mergeImapConfig(config: SubtrackConfig, patch: Partial<ImapConfig>): ImapConfig { + return { + host: patch.host ?? config.imap?.host ?? "", + port: patch.port ?? config.imap?.port ?? 993, + tls: patch.tls ?? config.imap?.tls ?? true, + username: patch.username ?? config.imap?.username ?? "", + } +} + case "imapHost": if (!value) { consola.error("imapHost must not be empty"); return false } - config.imap = { ...config.imap, host: value, port: config.imap?.port ?? 993, tls: config.imap?.tls ?? true, username: config.imap?.username ?? "" } + config.imap = mergeImapConfig(config, { host: value }) break🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/config.ts` around lines 117 - 140, Extract the repeated IMAP fallback construction from the imapHost, imapPort, imapTls, and imapUsername branches into a shared helper that merges partial updates with the established defaults for host, port, tls, and username. Update each branch to use this helper while preserving its existing validation and field-specific values.apps/subtrack/src/suggest/email-parser.ts (1)
182-195: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMIME header decoder only supports base64 (
?B?) words, always as UTF-8.Quoted-printable (
?Q?) encoded-word subjects/from headers pass through undecoded, and the declared charset is ignored (bytes are always interpreted as UTF-8). Given the parsers explicitly target Japanese subject keywords elsewhere in this feature, non-UTF8-charset or Q-encoded Japanese subjects will silently fail to decode, degrading match rates without crashing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/suggest/email-parser.ts` around lines 182 - 195, The decodeMimeHeader function only handles base64 encoded-words and always decodes bytes as UTF-8. Extend it to parse the declared charset and support both B and Q encodings, including Q rules for underscores and hexadecimal escapes, then convert the decoded bytes using the declared charset with an appropriate fallback while preserving undecodable text safely.apps/subtrack/src/suggest/parser/index.ts (1)
25-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect the comment about
genericParser.The comment states that
genericParsernever returns null, but its implementation ingeneric.tsactually does return null if it cannot extract a price from the email.♻️ Proposed refactor
/** * Try each parser in order and return the first matching result. - * Returns null only if all parsers return null (which genericParser never does). + * Returns null if all parsers, including the generic fallback, return null. */🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/suggest/parser/index.ts` around lines 25 - 29, Update the documentation above parseEmail to accurately state that genericParser may return null when it cannot extract a price, while preserving the description of parser ordering and the all-parsers-null behavior.apps/subtrack/src/suggest/types.ts (1)
54-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove duplicated
ImapConfigtype.This type is already defined and exported in
apps/subtrack/src/types.ts. You should import it from there to prevent duplication and ensure consistency.♻️ Proposed refactor
-/** IMAP connection settings. */ -export type ImapConfig = { - host: string - port: number - tls: boolean - username: string -} +export type { ImapConfig } from "../types.ts"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/suggest/types.ts` around lines 54 - 60, Remove the local ImapConfig declaration in the suggest types module and import the existing exported ImapConfig from the shared types module instead. Update any references to use that imported type, preserving the current fields and behavior.apps/subtrack/src/suggest/parser/credit-card.ts (1)
86-95: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoffImprove currency detection by capturing the symbol.
The current heuristic uses the presence of a decimal point to distinguish between USD and JPY. This is unreliable because whole-dollar amounts (e.g.,
$10) will be incorrectly interpreted as 10 JPY, and other currencies like£10.00will be interpreted as USD.Consider updating the
amountPatternregexes to capture the currency symbol in a separate group, and map the captured symbol to the correct currency and scaling factor (similar to how it is handled inreceipt.ts).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/suggest/parser/credit-card.ts` around lines 86 - 95, Update the amountPattern regexes and parsing logic in the credit-card parser to capture the currency symbol separately, then map that symbol to the appropriate currency and scaling factor instead of using isDecimal. Ensure whole-dollar USD amounts and symbols such as £ are classified correctly, following the existing approach in receipt.ts, and update the amount validation flow to use the mapped value.apps/subtrack/src/suggest/interactor.ts (1)
144-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant dynamic imports duplicate already-statically-imported members.
inputandconfirmare already imported statically at the top of the file (line 11); re-importing them dynamically here (159, 161) just shadows the outer bindings with the same module reference. Likewise,dismissSuggestion(144) could be added to the existing static"../db.ts"import instead of a fresh dynamic import.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/subtrack/src/suggest/interactor.ts` around lines 144 - 161, Remove the redundant dynamic imports of input and inquirerConfirm inside editSuggestion, reusing the existing static bindings. Add dismissSuggestion to the existing static ../db.ts import and update the dismissal case to use that binding instead of dynamically importing ../db/suggestions.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/subtrack/src/date-utils.ts`:
- Around line 122-129: Update the "bi-weekly" branch in the current-period date
range calculation to subtract 13 days from d, producing a 14-day inclusive
window ending today. Preserve the existing date formatting and to value; do not
alter getPreviousPeriodDateRange unless needed for this minimum length
correction.
In `@apps/subtrack/src/mcp/handlers.ts`:
- Around line 226-270: The handleCompare function must apply the requested
period instead of only echoing it. Thread period into calcSubTotal and
calcPreviousTotals, or reuse the existing date-range calculation logic, so
monthly, quarterly, and yearly requests produce their corresponding totals while
preserving the current response structure.
In `@apps/subtrack/src/subscription/core.ts`:
- Around line 125-143: Validate the converted price in handleClone before
constructing or saving newData: when flags.price is provided, convert it to a
number and reject non-finite or NaN values with the existing command
error-handling behavior. Preserve sub.price when the flag is omitted, and only
assign a validated numeric value to the cloned subscription.
In `@apps/subtrack/src/subscription/edit.ts`:
- Around line 48-53: Update the hasFlags predicate in the subscription edit flow
to include flags.notes so --notes alone enters the flag-based update path and
reaches the existing notes handling. Also add notes to the interactive checkbox
choices so notes can be selected and edited interactively, preserving the
behavior of all existing choices.
- Around line 67-69: Update the edit flag handling before updateSubscription to
validate currency, cycle, and status using the existing isValidCurrency,
isValidCycle, and isValidStatus helpers, matching resolveAddOptions behavior;
only assign validated values to newData and reject invalid inputs before
persistence.
In `@apps/subtrack/src/suggest/email-parser.ts`:
- Around line 38-46: Update parseEml, extractTextBody, extractFromMultipart, and
decodeBase64Body to recognize both LF and CRLF blank-line separators by
replacing LF-only boundary detection and splitting with equivalent optional-CR
handling. Preserve the existing header, body, multipart, and base64 parsing
behavior after the separator is identified.
In `@apps/subtrack/src/suggest/imap.ts`:
- Around line 38-95: Update the IMAP scan flow around client.connect(),
getMailboxLock(), and the outer cleanup so logout runs only after a successful
connection, preserving the original connection error when connect fails. Honor
signal?.aborted before and during connection or mailbox-lock acquisition, using
the client/library’s supported abort or timeout mechanism so an aborted scan
cannot remain stuck in either operation.
- Around line 48-65: Update the searchQuery definition in the imap suggestion
flow to use ImapFlow’s typed SearchObject format: set since to sinceDate and
place the subject alternatives in the required nested or structure. Remove the
explicit any annotation and preserve all existing English and Japanese subject
terms.
In `@apps/subtrack/src/suggest/interactor.ts`:
- Around line 155-225: Update billingDay construction in editSuggestion to
validate the parsed emailDate before calling getDate, matching the add path’s
isNaN(d.getTime()) handling. Pass a valid day to writeSubscription and use the
existing null fallback when suggestion.emailDate is absent or malformed.
In `@apps/subtrack/src/suggest/parser/bank.ts`:
- Around line 102-111: Update the date extraction logic in the parser around the
dateMatch handling to distinguish unified dates captured in dateMatch[1] from
Generic Bank’s separate year, month, and day groups. Parse and normalize the
unified YYYY/MM/DD value directly, preserve the separated-group path for Generic
Bank, and remove the dangling dateMatch expression from the fallback branch.
In `@apps/subtrack/src/suggest/parser/generic.ts`:
- Around line 78-91: The parsers inconsistently scale monetary amounts because
they rely on decimal formatting instead of the resolved currency. In
apps/subtrack/src/suggest/parser/generic.ts#L78-L91, scale the resolved amount
by 100 for fractional currencies and 1 for zero-decimal currencies; apply the
same currency-based scaling in
apps/subtrack/src/suggest/parser/receipt.ts#L133-L137 and
apps/subtrack/src/suggest/parser/credit-card.ts#L87-L91, replacing each
isDecimal-based decision while preserving existing rounding and validation.
In `@apps/subtrack/src/suggest/parser/wallet.ts`:
- Around line 65-67: Update the tag sanitization in the wallet name parsing flow
around nameMatch and the name length check to repeatedly strip HTML-like tags
until the value is unchanged, or use the project’s established HTML-stripping
utility. Ensure the final persisted name contains no removable tags before
applying the existing empty and 100-character validation.
In `@apps/subtrack/src/suggest/scan.ts`:
- Around line 93-98: Update the connectAndSearch call in the scan flow to pass
sinceDays: 7 explicitly, preserving the existing abort signal and result
handling so the scan window matches the documented seven-day behavior.
- Around line 85-99: Update scanEmails so its timeout bounds the entire
connectAndSearch operation, including connection and mailbox-lock setup; ensure
the timeout rejects or aborts promptly even when those stages do not consume
controller.signal. Preserve the existing successful scan and no-results
behavior, and use the existing timeout configuration rather than allowing
ImapFlow’s default connection timeout to exceed it.
---
Nitpick comments:
In `@apps/subtrack/src/config.ts`:
- Around line 117-140: Extract the repeated IMAP fallback construction from the
imapHost, imapPort, imapTls, and imapUsername branches into a shared helper that
merges partial updates with the established defaults for host, port, tls, and
username. Update each branch to use this helper while preserving its existing
validation and field-specific values.
In `@apps/subtrack/src/mcp/security.ts`:
- Around line 13-34: Rename the “token-bucket” terminology in the comment and
RateLimiter class to describe the implemented fixed-window behavior, updating
references to RateLimiter as needed. Preserve the existing bursty refill logic;
do not add proportional refilling.
In `@apps/subtrack/src/stats.ts`:
- Line 10: Update the stats logic using the manual statSync try/catch to import
and reuse getFileSize, replacing the duplicated file-size retrieval while
preserving the existing behavior and output.
- Around line 62-66: Replace the local dbSizeBytes initialization and statSync
try/catch block with a call to getFileSize, preserving the existing database
path argument and resulting byte-size value. Update the surrounding stats flow
without changing its behavior.
In `@apps/subtrack/src/suggest/email-parser.ts`:
- Around line 182-195: The decodeMimeHeader function only handles base64
encoded-words and always decodes bytes as UTF-8. Extend it to parse the declared
charset and support both B and Q encodings, including Q rules for underscores
and hexadecimal escapes, then convert the decoded bytes using the declared
charset with an appropriate fallback while preserving undecodable text safely.
In `@apps/subtrack/src/suggest/interactor.ts`:
- Around line 144-161: Remove the redundant dynamic imports of input and
inquirerConfirm inside editSuggestion, reusing the existing static bindings. Add
dismissSuggestion to the existing static ../db.ts import and update the
dismissal case to use that binding instead of dynamically importing
../db/suggestions.ts.
In `@apps/subtrack/src/suggest/parser/credit-card.ts`:
- Around line 86-95: Update the amountPattern regexes and parsing logic in the
credit-card parser to capture the currency symbol separately, then map that
symbol to the appropriate currency and scaling factor instead of using
isDecimal. Ensure whole-dollar USD amounts and symbols such as £ are classified
correctly, following the existing approach in receipt.ts, and update the amount
validation flow to use the mapped value.
In `@apps/subtrack/src/suggest/parser/index.ts`:
- Around line 25-29: Update the documentation above parseEmail to accurately
state that genericParser may return null when it cannot extract a price, while
preserving the description of parser ordering and the all-parsers-null behavior.
In `@apps/subtrack/src/suggest/types.ts`:
- Around line 54-60: Remove the local ImapConfig declaration in the suggest
types module and import the existing exported ImapConfig from the shared types
module instead. Update any references to use that imported type, preserving the
current fields and behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e603bcd7-459a-4941-8d47-0aab193012e2
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (43)
apps/subtrack/package.jsonapps/subtrack/src/backup.tsapps/subtrack/src/cleanup.tsapps/subtrack/src/commands/index.tsapps/subtrack/src/commands/suggest.tsapps/subtrack/src/compare.tsapps/subtrack/src/config.tsapps/subtrack/src/date-utils.tsapps/subtrack/src/db.tsapps/subtrack/src/db/schema.tsapps/subtrack/src/db/suggestions.tsapps/subtrack/src/format.tsapps/subtrack/src/maintenance.tsapps/subtrack/src/mcp.tsapps/subtrack/src/mcp/handlers.tsapps/subtrack/src/mcp/index.tsapps/subtrack/src/mcp/security.tsapps/subtrack/src/mcp/server.tsapps/subtrack/src/mcp/tools.tsapps/subtrack/src/mcp/types.tsapps/subtrack/src/notifications/banner.tsapps/subtrack/src/payment.tsapps/subtrack/src/stats.tsapps/subtrack/src/subscription.tsapps/subtrack/src/subscription/add.tsapps/subtrack/src/subscription/core.tsapps/subtrack/src/subscription/edit.tsapps/subtrack/src/suggest/email-parser.tsapps/subtrack/src/suggest/imap.tsapps/subtrack/src/suggest/interactor.tsapps/subtrack/src/suggest/matcher.tsapps/subtrack/src/suggest/parser/bank.tsapps/subtrack/src/suggest/parser/credit-card.tsapps/subtrack/src/suggest/parser/generic.tsapps/subtrack/src/suggest/parser/index.tsapps/subtrack/src/suggest/parser/receipt.tsapps/subtrack/src/suggest/parser/wallet.tsapps/subtrack/src/suggest/scan.tsapps/subtrack/src/suggest/suggest.tsapps/subtrack/src/suggest/types.tsapps/subtrack/src/types.tsapps/subtrack/src/upcoming.tsapps/subtrack/src/usage-total.ts
| const hasFlags = | ||
| flags.name !== undefined || flags.price !== undefined || | ||
| flags.currency !== undefined || flags.cycle !== undefined || | ||
| flags.tags !== undefined || flags.status !== undefined || | ||
| flags.billingDay !== undefined || | ||
| flags.paymentMethod !== undefined |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
--notes alone is silently ignored; notes can't be edited interactively either.
hasFlags (Lines 48-53) doesn't include flags.notes. When --notes is the only flag supplied, hasFlags is false, so execution falls through to interactive mode — but the interactive checkbox (Lines 107-116) has no notes option, so the notes-handling code at Lines 77-80 is unreachable in that case. Net effect: editing only notes is a no-op.
Add flags.notes to the hasFlags predicate (and consider adding a notes choice to the interactive checkbox for parity).
🐛 Proposed fix for the hasFlags predicate
const hasFlags =
flags.name !== undefined || flags.price !== undefined ||
flags.currency !== undefined || flags.cycle !== undefined ||
flags.tags !== undefined || flags.status !== undefined ||
flags.billingDay !== undefined ||
+ flags.notes !== undefined ||
flags.paymentMethod !== undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hasFlags = | |
| flags.name !== undefined || flags.price !== undefined || | |
| flags.currency !== undefined || flags.cycle !== undefined || | |
| flags.tags !== undefined || flags.status !== undefined || | |
| flags.billingDay !== undefined || | |
| flags.paymentMethod !== undefined | |
| const hasFlags = | |
| flags.name !== undefined || flags.price !== undefined || | |
| flags.currency !== undefined || flags.cycle !== undefined || | |
| flags.tags !== undefined || flags.status !== undefined || | |
| flags.billingDay !== undefined || | |
| flags.notes !== undefined || | |
| flags.paymentMethod !== undefined |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/subscription/edit.ts` around lines 48 - 53, Update the
hasFlags predicate in the subscription edit flow to include flags.notes so
--notes alone enters the flag-based update path and reaches the existing notes
handling. Also add notes to the interactive checkbox choices so notes can be
selected and edited interactively, preserving the behavior of all existing
choices.
| try { | ||
| const emails = await connectAndSearch(imapConfig, password, { signal: controller.signal }) | ||
|
|
||
| if (emails.length === 0) { | ||
| consola.info("No relevant emails found in the last 30 days.") | ||
| return 0 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scan window defaults to 30 days, not the documented 7 days.
connectAndSearch is called without sinceDays, so it falls back to its default of 30 (per imap.ts), while the PR describes a 7-day scan window. Consider passing sinceDays: 7 explicitly here to match the intended behavior.
🐛 Proposed fix
- const emails = await connectAndSearch(imapConfig, password, { signal: controller.signal })
+ const emails = await connectAndSearch(imapConfig, password, { sinceDays: 7, signal: controller.signal })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const emails = await connectAndSearch(imapConfig, password, { signal: controller.signal }) | |
| if (emails.length === 0) { | |
| consola.info("No relevant emails found in the last 30 days.") | |
| return 0 | |
| try { | |
| const emails = await connectAndSearch(imapConfig, password, { sinceDays: 7, signal: controller.signal }) | |
| if (emails.length === 0) { | |
| consola.info("No relevant emails found in the last 30 days.") | |
| return 0 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/suggest/scan.ts` around lines 93 - 98, Update the
connectAndSearch call in the scan flow to pass sinceDays: 7 explicitly,
preserving the existing abort signal and result handling so the scan window
matches the documented seven-day behavior.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/subtrack/src/__tests__/untested-commands.test.ts`:
- Around line 236-247: Ensure the fake timers configured in this test are always
restored by wrapping the setup and notify flow after vi.useFakeTimers() in a
try/finally block, placing vi.useRealTimers() in the finally clause. Keep the
existing test behavior unchanged while covering failures from insertSub, the
dynamic import, or handleNotify.
In `@apps/subtrack/src/suggest/parser/wallet.ts`:
- Around line 69-79: The amount conversion currently depends on decimal
formatting rather than the resolved currency. In the wallet parsing flow,
resolve currency from symbol before calculating amount, then scale USD, EUR, and
GBP values by 100 while leaving JPY values unscaled, preserving validation after
conversion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7aee9d38-7fb7-4811-b7b9-f3e829602cb6
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
apps/subtrack/package.jsonapps/subtrack/src/__tests__/untested-commands.test.tsapps/subtrack/src/commands/index.tsapps/subtrack/src/commands/suggest.tsapps/subtrack/src/config.tsapps/subtrack/src/db.tsapps/subtrack/src/db/schema.tsapps/subtrack/src/db/suggestions.tsapps/subtrack/src/notifications/banner.tsapps/subtrack/src/payment.tsapps/subtrack/src/subscription/add.tsapps/subtrack/src/subscription/core.tsapps/subtrack/src/suggest/email-parser.tsapps/subtrack/src/suggest/imap.tsapps/subtrack/src/suggest/interactor.tsapps/subtrack/src/suggest/matcher.tsapps/subtrack/src/suggest/parser/bank.tsapps/subtrack/src/suggest/parser/credit-card.tsapps/subtrack/src/suggest/parser/generic.tsapps/subtrack/src/suggest/parser/index.tsapps/subtrack/src/suggest/parser/receipt.tsapps/subtrack/src/suggest/parser/wallet.tsapps/subtrack/src/suggest/scan.tsapps/subtrack/src/suggest/suggest.tsapps/subtrack/src/suggest/types.tsapps/subtrack/src/types.tsapps/subtrack/src/upcoming.tsapps/subtrack/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (21)
- apps/subtrack/package.json
- apps/subtrack/src/commands/index.ts
- apps/subtrack/src/notifications/banner.ts
- apps/subtrack/src/db.ts
- apps/subtrack/src/upcoming.ts
- apps/subtrack/src/suggest/imap.ts
- apps/subtrack/src/db/schema.ts
- apps/subtrack/src/commands/suggest.ts
- apps/subtrack/src/subscription/add.ts
- apps/subtrack/src/subscription/core.ts
- apps/subtrack/src/suggest/interactor.ts
- apps/subtrack/src/suggest/parser/receipt.ts
- apps/subtrack/src/payment.ts
- apps/subtrack/src/suggest/types.ts
- apps/subtrack/src/suggest/suggest.ts
- apps/subtrack/src/suggest/parser/index.ts
- apps/subtrack/src/suggest/matcher.ts
- apps/subtrack/src/suggest/scan.ts
- apps/subtrack/src/config.ts
- apps/subtrack/src/suggest/email-parser.ts
- apps/subtrack/src/db/suggestions.ts
| const symbol = amountMatch[1] | ||
| const rawAmount = amountMatch[2].replace(/,/g, "") | ||
| const isDecimal = rawAmount.includes(".") | ||
| const amount = isDecimal | ||
| ? Math.round(parseFloat(rawAmount) * 100) | ||
| : parseInt(rawAmount, 10) | ||
|
|
||
| if (isNaN(amount) || amount <= 0 || amount > 99999999) continue | ||
|
|
||
| const currencyMap: Record<string, string> = { "$": "USD", "¥": "JPY", "€": "EUR", "£": "GBP" } | ||
| const currency = symbol ? (currencyMap[symbol] ?? "USD") : "USD" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Scale by resolved currency, not decimal formatting.
$10 is stored as 10 USD units, while $10.00 is stored as 1000. Resolve currency first, then apply a 100 multiplier for USD/EUR/GBP and 1 for JPY.
Proposed fix
const symbol = amountMatch[1]
const rawAmount = amountMatch[2].replace(/,/g, "")
-const isDecimal = rawAmount.includes(".")
-const amount = isDecimal
- ? Math.round(parseFloat(rawAmount) * 100)
- : parseInt(rawAmount, 10)
+const currencyMap: Record<string, string> = { "$": "USD", "¥": "JPY", "€": "EUR", "£": "GBP" }
+const currency = symbol ? (currencyMap[symbol] ?? "USD") : "USD"
+const amount = Math.round(
+ parseFloat(rawAmount) * (currency === "JPY" ? 1 : 100),
+)
if (isNaN(amount) || amount <= 0 || amount > 99999999) continue
-const currencyMap: Record<string, string> = { "$": "USD", "¥": "JPY", "€": "EUR", "£": "GBP" }
-const currency = symbol ? (currencyMap[symbol] ?? "USD") : "USD"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const symbol = amountMatch[1] | |
| const rawAmount = amountMatch[2].replace(/,/g, "") | |
| const isDecimal = rawAmount.includes(".") | |
| const amount = isDecimal | |
| ? Math.round(parseFloat(rawAmount) * 100) | |
| : parseInt(rawAmount, 10) | |
| if (isNaN(amount) || amount <= 0 || amount > 99999999) continue | |
| const currencyMap: Record<string, string> = { "$": "USD", "¥": "JPY", "€": "EUR", "£": "GBP" } | |
| const currency = symbol ? (currencyMap[symbol] ?? "USD") : "USD" | |
| const symbol = amountMatch[1] | |
| const rawAmount = amountMatch[2].replace(/,/g, "") | |
| const currencyMap: Record<string, string> = { "$": "USD", "¥": "JPY", "€": "EUR", "£": "GBP" } | |
| const currency = symbol ? (currencyMap[symbol] ?? "USD") : "USD" | |
| const amount = Math.round( | |
| parseFloat(rawAmount) * (currency === "JPY" ? 1 : 100), | |
| ) | |
| if (isNaN(amount) || amount <= 0 || amount > 99999999) continue |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/suggest/parser/wallet.ts` around lines 69 - 79, The amount
conversion currently depends on decimal formatting rather than the resolved
currency. In the wallet parsing flow, resolve currency from symbol before
calculating amount, then scale USD, EUR, and GBP values by 100 while leaving JPY
values unscaled, preserving validation after conversion.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
pnpm-workspace.yaml (1)
22-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBound the new override ranges to their compatible major versions.
These open-ended
>=ranges can select a future major release during a lockfile refresh. Use ranges such as>=2.1.2 <3.0.0, or pin the exact patched versions, to preserve the intended compatibility boundary. pnpm applies overrides to the dependency graph, so these ranges control future resolutions. (pnpm.io)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpm-workspace.yaml` around lines 22 - 24, Bound the dependency override ranges for brace-expansion, js-yaml, and fast-uri to their compatible major versions by adding upper bounds such as <3.0.0, <5.0.0, and <4.0.0 respectively, or pin each to an exact patched version. Preserve the existing minimum versions while preventing future major releases during lockfile resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/subtrack/src/suggest/email-parser.ts`:
- Around line 138-141: Update extractFromMultipart so each selected MIME part is
decoded according to its own Content-Transfer-Encoding header before assigning
textPart and passing it to suggestion parsers. Handle base64 and
quoted-printable appropriately, while preserving plain 7bit/8bit payloads and
the existing header/body extraction behavior.
- Around line 182-203: Update decodeMimeHeader so Base64 encoded-word bytes are
decoded using the declared charset rather than always UTF-8, preserving an
ISO-2022-JP-aware mapping and fallback for unsupported charsets. Apply the same
raw-byte, charset-aware conversion to the Q-encoded path, ensuring values such
as ISO-8859-1 encoded words decode correctly.
In `@apps/subtrack/src/suggest/parser/wallet.ts`:
- Around line 80-84: Update the amount parsing in the wallet suggestion parser
so JPY uses numeric parsing without truncation, then reject non-integer JPY
amounts before accepting the candidate. Preserve the existing positive and
maximum amount checks and the non-JPY conversion behavior.
---
Nitpick comments:
In `@pnpm-workspace.yaml`:
- Around line 22-24: Bound the dependency override ranges for brace-expansion,
js-yaml, and fast-uri to their compatible major versions by adding upper bounds
such as <3.0.0, <5.0.0, and <4.0.0 respectively, or pin each to an exact patched
version. Preserve the existing minimum versions while preventing future major
releases during lockfile resolution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c3822b6b-28aa-4bce-b189-77dcee301aa6
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
.github/workflows/dependency-review.ymlapps/subtrack/src/__tests__/untested-commands.test.tsapps/subtrack/src/config.tsapps/subtrack/src/date-utils.tsapps/subtrack/src/mcp/handlers.tsapps/subtrack/src/mcp/security.tsapps/subtrack/src/payment.tsapps/subtrack/src/stats.tsapps/subtrack/src/subscription/core.tsapps/subtrack/src/subscription/edit.tsapps/subtrack/src/suggest/email-parser.tsapps/subtrack/src/suggest/imap.tsapps/subtrack/src/suggest/interactor.tsapps/subtrack/src/suggest/parser/bank.tsapps/subtrack/src/suggest/parser/credit-card.tsapps/subtrack/src/suggest/parser/generic.tsapps/subtrack/src/suggest/parser/index.tsapps/subtrack/src/suggest/parser/receipt.tsapps/subtrack/src/suggest/parser/wallet.tsapps/subtrack/src/suggest/types.tspnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (16)
- apps/subtrack/src/tests/untested-commands.test.ts
- apps/subtrack/src/suggest/parser/credit-card.ts
- apps/subtrack/src/stats.ts
- apps/subtrack/src/suggest/parser/index.ts
- apps/subtrack/src/suggest/parser/bank.ts
- apps/subtrack/src/suggest/imap.ts
- apps/subtrack/src/date-utils.ts
- apps/subtrack/src/suggest/types.ts
- apps/subtrack/src/mcp/security.ts
- apps/subtrack/src/subscription/edit.ts
- apps/subtrack/src/config.ts
- apps/subtrack/src/payment.ts
- apps/subtrack/src/suggest/interactor.ts
- apps/subtrack/src/suggest/parser/receipt.ts
- apps/subtrack/src/suggest/parser/generic.ts
- apps/subtrack/src/mcp/handlers.ts
| // Extract content after the header block (CRLF or LF) | ||
| const contentStart = part.search(/\r?\n\r?\n/) | ||
| if (contentStart !== -1) { | ||
| textPart = part.slice(contentStart + (part[contentStart] === '\r' ? 4 : 2)).trim() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Decode each selected MIME part by its own transfer encoding.
A text/plain multipart part with Content-Transfer-Encoding: base64 is selected here, but extractFromMultipart later applies decodeQp unconditionally. Its encoded payload reaches all suggestion parsers instead of readable text.
Also applies to: 152-154
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/suggest/email-parser.ts` around lines 138 - 141, Update
extractFromMultipart so each selected MIME part is decoded according to its own
Content-Transfer-Encoding header before assigning textPart and passing it to
suggestion parsers. Handle base64 and quoted-printable appropriately, while
preserving plain 7bit/8bit payloads and the existing header/body extraction
behavior.
| /** Minimal MIME encoded-word decoder for Subject/From headers. | ||
| * Supports both Base64 (?B?) and Quoted-printable (?Q?) encoding, | ||
| * respecting the declared charset. */ | ||
| function decodeMimeHeader(header: string | null): string | null { | ||
| if (!header) return null | ||
| return header.replace( | ||
| /=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g, | ||
| (_, charset: string, encoding: string, encoded: string) => { | ||
| try { | ||
| if (encoding.toUpperCase() === "B") { | ||
| return Buffer.from(encoded, "base64").toString("utf-8") | ||
| } | ||
| // Q-encoding: replace _ with space, decode =FF hex escapes | ||
| const qDecoded = encoded | ||
| .replace(/_/g, " ") | ||
| .replace(/=([0-9A-Fa-f]{2})/g, (__, hex) => String.fromCharCode(parseInt(hex, 16))) | ||
| // Try to decode using the declared charset; fall back to utf-8 | ||
| try { | ||
| return Buffer.from(qDecoded, "latin1").toString(charset.toLowerCase() === "iso-2022-jp" ? "utf-8" : charset as BufferEncoding) | ||
| } catch { | ||
| return qDecoded | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the repository's declared Node.js target and locate MIME-decoding tests.
fd -HI 'package.json' 'tsconfig*.json' '.nvmrc' '.node-version' . -x sh -c 'echo "--- $1"; sed -n "1,160p" "$1"' _ {}
rg -n -i --glob '*.{test,spec}.{ts,tsx}' 'parseEmailContent|decodeMimeHeader|iso-2022-jp|iso-8859|encoded-word' apps/subtrackRepository: nazozokc/subtrack
Length of output: 3522
🏁 Script executed:
#!/bin/bash
set -u
echo "## email-parser locations"
fd -HI 'email-parser.ts' . -x sh -c 'echo "--- $1"; wc -l "$1"; ast-grep outline "$1" | sed -n "1,220p"' _ {}
echo "## relevant email-parser section"
file="$(fd -HI 'email-parser.ts' . | head -n 1)"
if [ -n "${file:-}" ]; then
sed -n '1,260p' "$file" | cat -n
fi
echo "## MIME header usages/tests"
rg -n -i 'decodeMimeHeader|parseEmailContent|encoded-word|encodedWord|iso-8859|iso-2022-jp|latin1' . --glob '*.{ts,tsx,mts,cts,json,yml,yaml,md}' | sed -n '1,220p'
echo "## behavioral probe of current implementation for ISO-8859-1 Base64"
node - <<'JS'
const inputs = [
'=?ISO-8859-1?B?6Q==?=', // ISO-8859-1 0xE9 = é
'=?iso-2022-jp?B?GyRBOA?=?=', // ISO-2022-JP Shift-JIS representation for あ
]
const decoder = new TextDecoder('utf-8', { fatal: false })
for (const header of inputs) {
const decoded = header.replace(
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
(_, charset, encoding, encoded) => {
if (encoding.toUpperCase() === 'B') {
return Buffer.from(encoded, 'base64').toString('utf-8')
}
const qDecoded = encoded.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
try {
return Buffer.from(qDecoded, 'latin1').toString(charset.toLowerCase() === 'iso-2022-jp' ? 'utf-8' : charset)
} catch {
return qDecoded
}
}
)
console.log(JSON.stringify({ header, decoded, ok: header === decoded }))
}
JSRepository: nazozokc/subtrack
Length of output: 9604
🏁 Script executed:
#!/bin/bash
set -u
echo "## MIME decoder behavior variants"
node - <<'JS'
const cases = [
['ISO-8859-1 Base64', '=?ISO-8859-1?B?6Q==?=', 'é'],
['ISO-2022-JP Base64', '=?ISO-2022-JP?B?GyRBOA?=?=', 'あ'],
['ISO-8859-1 Q', '=?ISO-8859-1?Q?E9_a=42?=', 'é a*'],
]
function current(header) {
return header.replace(
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
(_, charset, encoding, encoded) => {
if (encoding.toUpperCase() === 'B') return Buffer.from(encoded, 'base64').toString('utf-8')
const q = encoded.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
try {
return Buffer.from(q, 'latin1').toString(charset.toLowerCase() === 'iso-2022-jp' ? 'utf-8' : charset)
} catch {
return q
}
}
)
}
function suggestedTextDecoder(header) {
const dec = new TextDecoder(encodingLabelFromCharset, { fatal: false })
function encodingLabelFromCharset(charset) {
switch (charset.toLowerCase()) {
case 'iso-2022-jp': return 'iso-2022-jp'
case 'iso-8859-7': return 'windows-1253'
default: return charset
}
}
return header.replace(
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
(_, charset, encoding, encoded) => {
const label = encodingLabelFromCharset(charset)
if (encoding.toUpperCase() === 'B') {
return dec.decode(Buffer.from(encoded, 'base64'))
}
const q = encoded.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
return Buffer.from(q, 'latin1')
}
)
}
function suggestedBufferDecoder(header) {
return header.replace(
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
(_, charset, encoding, encoded) => {
if (encoding.toUpperCase() === 'B') {
return Buffer.from(encoded, 'base64').toString(charset.toLowerCase() === 'iso-8859-7' ? 'windows-1253' : charset)
}
const q = encoded.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
return Buffer.from(q, 'latin1').toString(charset.toLowerCase() === 'iso-8859-7' ? 'windows-1253' : charset)
}
)
}
for (const [name, header, expected] of cases) {
console.log(JSON.stringify({ name, header, expected, currentCurrent: current(header), suggestedBufferDecoder, decoded: suggestedBufferDecoder(header) }))
}
JS
echo "## current Node charset support"
node - <<'JS'
const dec = new TextDecoder('iso-2022-jp', { fatal: false })
const b64 = Buffer.from('GyRBOA==', 'base64')
for (const enc of ['latin1', 'utf-8', 'iso-2022-jp']) {
try {
console.log(JSON.stringify({ encoding: enc, result: dec.decode(Buffer.from(b64, enc)) }))
} catch (e) {
console.log(JSON.stringify({ encoding: enc, error: String(e.message || e) }))
}
}
console.log(JSON.stringify({ BufferSupported: Buffer.from('e9', 'hex').toString('iso-8859-1') }))
JS
echo "## package runtime context"
sed -n '1,70p' package.json apps/subtrack/package.json pnpm-workspace.yaml 2>/dev/null | cat -nRepository: nazozokc/subtrack
Length of output: 3911
Honor the declared charset in MIME encoded-word decoding.
decodeMimeHeader() always decodes Base64 as UTF-8, corrupting values like =?ISO-8859-1?B?6Q==?= and invalidates the current catch fallback. Decode the raw bytes with the declared charset (with an ISO-2022-JP-aware mapping if needed) and apply the same charset-aware handling to the Q path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/suggest/email-parser.ts` around lines 182 - 203, Update
decodeMimeHeader so Base64 encoded-word bytes are decoded using the declared
charset rather than always UTF-8, preserving an ISO-2022-JP-aware mapping and
fallback for unsupported charsets. Apply the same raw-byte, charset-aware
conversion to the Q-encoded path, ensuring values such as ISO-8859-1 encoded
words decode correctly.
| const amount = currency === "JPY" | ||
| ? parseInt(rawAmount, 10) | ||
| : Math.round(parseFloat(rawAmount) * 100) | ||
|
|
||
| if (isNaN(amount) || amount <= 0 || amount > 99999999) continue |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Reject fractional JPY values instead of truncating them.
parseInt("1.50", 10) stores ¥1.50 as 1. Parse numerically, then require an integer for JPY before returning the candidate.
Proposed fix
- const amount = currency === "JPY"
- ? parseInt(rawAmount, 10)
- : Math.round(parseFloat(rawAmount) * 100)
+ const numericAmount = Number(rawAmount)
+ const amount = currency === "JPY"
+ ? numericAmount
+ : Math.round(numericAmount * 100)
- if (isNaN(amount) || amount <= 0 || amount > 99999999) continue
+ if (
+ !Number.isFinite(amount) ||
+ amount <= 0 ||
+ amount > 99999999 ||
+ (currency === "JPY" && !Number.isInteger(amount))
+ ) continue📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const amount = currency === "JPY" | |
| ? parseInt(rawAmount, 10) | |
| : Math.round(parseFloat(rawAmount) * 100) | |
| if (isNaN(amount) || amount <= 0 || amount > 99999999) continue | |
| const numericAmount = Number(rawAmount) | |
| const amount = currency === "JPY" | |
| ? numericAmount | |
| : Math.round(numericAmount * 100) | |
| if ( | |
| !Number.isFinite(amount) || | |
| amount <= 0 || | |
| amount > 99999999 || | |
| (currency === "JPY" && !Number.isInteger(amount)) | |
| ) continue |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/subtrack/src/suggest/parser/wallet.ts` around lines 80 - 84, Update the
amount parsing in the wallet suggestion parser so JPY uses numeric parsing
without truncation, then reject non-integer JPY amounts before accepting the
candidate. Preserve the existing positive and maximum amount checks and the
non-JPY conversion behavior.
Summary
Suggest subscriptions by scanning email inbox via IMAP. Scans bank/credit-card/receipt/wallet payment emails and generates
suggestionsfor review.What was done
imapflow, search recent inbox emails (last 7 days, max 50)subtrack suggestlaunches inquirer flow to review candidates (add/edit/skip/quit) with duplicate detectionlist/summary/payment/upcomingshowing pending suggestion count and upcoming paymentssubtrack config set imapHost/imapPort/imapTls/imapUsername, password viaSUBTRACK_IMAP_PASSWORDenv varsuggest list --jsonwhen empty, regex escaping in generic parser,subscription/add.tstype narrowingimapflow(MIT)Testing
pnpm test: 396/397 pass (1 pre-existing notify failure)tsc --noEmit: 0 errorspnpm build: 496 kB, 18 chunksCommands
subtrack suggestsubtrack suggest list [--all] [--json]subtrack suggest view <id>subtrack suggest add <id>subtrack suggest dismiss [id|--all]subtrack suggest scanSummary by CodeRabbit
suggestcommand for suggestion management.